Linux 下的 socket 编程

 Linux 󰈭 7726字

基础知识

socket常用函数

socket

domain指定协议族,type指定socket类型,protocol指定协议(为0时自动选择type类型对应的默认协议)

1int socket(int domain, int type, int protocol);

返回值大于0成功;-1失败,错误类型保存在全局变量errno中。

bind

分配地址族中的特地地址给socket sockfd为socket描述字,通过socket函数的返回值确定 addr指向绑定给sockfd的地址,根据socket创建时协议族的不同而不同。

1int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

返回值等于0成功;-1失败,错误类型保存在全局变量errno中。

sockaddr*可能的若干结构

ipv4
1struct sockaddr_in {
2    sa_family_t    sin_family; 
3    in_port_t      sin_port;   
4    struct in_addr sin_addr;   
5};
6
7struct in_addr {
8    uint32_t       s_addr;     
9};
ipv6
 1struct sockaddr_in6 { 
 2    sa_family_t     sin6_fami一个ly;    
 3    in_port_thtonl()--"Host to Network Long"
 4ntohl()--"Network to Host Long"
 5htons()--"Host to Network Short"
 6ntohs()--"Network to Host Short"       sin6_port;      
 7    uint32_t        sin6_flowinfo;  
 8    struct in6_addr sin6_addr;      
 9    uint32_t        sin6_scope_id;  
10};
11
12struct in6_addr { 
13    unsigned char   s6_addr[16];    
14};

listen

服务器监听指定的套接字,backlog指定socket最大连接个数。

1int listen(int sockfd, int backlog);

返回值等于0成功;-1失败,错误类型保存在全局变量errno中。

connect

客户端调用connect函数与服务器进行连接。

sockfd为客户机的套接字描述符,addr为服务器的套接字地址,addrlen为socket地址的长度。

1int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

客户机一般不指定自己的端口,系统在1024-5000中选一个(即自由端口),5000以上作为公共端口。

accept

当服务器listen到某htonl()–“Host to Network Long” ntohl()–“Network to Host Long” htons()–“Host to Network Short” ntohs()–“Network to Host Short"客户机的connect连接请求,则可以通过accpet函数接受请求,同时建立一个新的套接字,专门用于本次的通信服务。

sockfd为服务器的监听socket描述符,addr为其地址,addrlen为协议地址的长度,返回连接套接字的描述字。

1int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

IO

服务器与客户机像操作文件一样操作socket进行通信。

有许多对IO操作:

  • read/write
  • recv/send
  • readv/writev
  • recvmsg/sendmsg
  • recvfrome/sendto

暂时仅考虑一对IO操作(read/write)

1ssize_t read(int fd, void *buf, size_t count);
2ssize_t write(int fd, const void *buf, size_t count);

close

将套接字引用计数-1

close后进程不再能访问该套接字,但套接字的清除由TCP延后完成。

1int close(int sockfd);

getsockname, getpeername

获取套接字的名字。

1int getsockname(int fd,struct sockaddr* localaddr,int* addrlen);
2int getpeername(int fd,struct sockaddr* peeraddr,int* addrlen);

字节顺序转换

htonl()–“Host to Network Long” ntohl()–“Network to Host Long” htons()–“Host to Network Short” ntohs()–“Network to Host Short”

参考博客

Socket原理讲解

Unit 02

简单的交互示例

client向server发送三条消息hello hello1 hello2

server收到消息后回复一个welcome

server.c

 1#include <stdio.h>
 2#include <stdlib.h>
 3#include <string.h>
 4#include <sys/socket.h>
 5#include <netinet/in.h>
 6#include <arpa/inet.h>
 7#include <unistd.h>
 8#include <sys/types.h>
 9
10#define MAXDATASIZE 128
11#define PORT 3000
12#define BACKLOG 5
13
14int main(int argc,char **argv){
15	int sockfd, new_fd, nbytes, sin_size;
16	char buf[MAXDATASIZE];
17	struct sockaddr_in srvaddr, clientaddr;
18	
19	//1.创建网络端点
20	sockfd=socket(AF_INET,SOCK_STREAM,0);
21	if(sockfd==-1){
22		printf("can;t create socket\n");
23		exit(1);
24	}
25	
26	//套接字选项 增添端口地址可重用选项,使得某台服务器崩溃后可以快速用其余服务器重新提供服务
27	if(argc==2){
28		int on=1;
29		setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
30		printf("reuse addr\n");
31	}
32	
33	//填充地址
34	bzero(&srvaddr,sizeof(srvaddr));
35	srvaddr.sin_family=AF_INET;
36	//htons进行字节顺序转换
37	srvaddr.sin_port=htons(PORT);
38	//服务器上无论有多少个IP发来消息,全部都予以接受
39	srvaddr.sin_addr.s_addr=htonl(INADDR_ANY);
40	
41	//将xxx.xxx.xxx.xxx转换为32位的地址
42	/*
43	if(inet_aton(argv[1],&srvaddr.sin_addr)==-1){
44		printf("addr convert error\n");
45		exit(1);
46	}
47	*/
48	//2.绑定服务器地址和端口
49	if(bind(sockfd,(struct sockaddr *)&srvaddr,sizeof(struct sockaddr))==-1){
50		printf("bind error\n");
51		exit(1);
52	}
53	//3. 监听端口,将主动套接字转换为被动套接字
54	if(listen(sockfd,BACKLOG)==-1){
55		printf("listen error\n");
56		exit(1);
57	}
58	//一般应该在循环中添加一些退出的方法
59	for(;;){
60		//4.接受客户端连接
61		sin_size=sizeof(struct sockaddr_in);
62		if((new_fd=accept(sockfd,(struct sockaddr *)&clientaddr, (socklen_t*)&sin_size))==-1){
63			printf("accept error\n");
64			continue;
65		}
66		//如果成功连接,那么打印出客户机的ip、port
67		printf("client addr:%s %d\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));
68		
69		//5.接收请求 
70		//延时 必须读入一个字符才继续 可以检查读入的客户机数据的情况(但是很麻烦所以我删了)
71		//getchar();		
72        
73		//sockfd属于监听套接字,new_fd属于连接套接字
74		nbytes=read(new_fd,buf,MAXDATASIZE);
75		buf[nbytes]='\0';
76		printf("client:%s\n\n",buf);
77		
78		//6.回送响应
79		sprintf(buf,"wellcome!");
80		write(new_fd,buf,strlen(buf));
81		
82		//关闭socket
83		close(new_fd);
84	}
85	close(sockfd);
86	
87	return 0;
88}

client.c

 1#include <stdio.h>
 2#include <stdlib.h>
 3#include <string.h>
 4#include <sys/socket.h>
 5#include <netinet/in.h>
 6#include <errno.h>
 7#include <netdb.h>
 8#include <arpa/inet.h>
 9#include <unistd.h>
10
11#define MAXDATASIZE 128
12#define PORT 3000
13
14int addr_conv(char *address,struct in_addr *inaddr);
15
16int main(int argc,char **argv){
17	int sockfd,nbytes;
18	int port=PORT;
19	char buf[MAXDATASIZE];
20	struct sockaddr_in srvaddr;
21	
22	//必须输入1/2个参数,具体格式参见printf的内容
23	if(argc!=2 && argc!=3){
24		printf("usage:./client hostname|ip. Or usage:./client hostname|ip port\n");
25		exit(0);
26	}
27	//如果有指定端口号则使用,不然使用默认端口PORT=3000
28	if(argc==3) port=atoi(argv[2]);
29	
30	//1.创建网络端点
31	sockfd=socket(AF_INET,SOCK_STREAM,0);
32	if(sockfd==-1){
33		printf("can;t create socket\n");
34		exit(1);
35	}
36	//指定服务器协议簇,端口(本地socket地址采用默认值)
37	bzero(&srvaddr,sizeof(srvaddr));
38	srvaddr.sin_family=AF_INET;
39	srvaddr.sin_port=htons(port);
40	/*
41	if(inet_aton("127.0.0.1",&srvaddr.sin_addr)==-1){
42		printf("addr convert error\n");
43		exit(1);
44	}
45	*/
46	//根据传入参数指定服务器IP地址
47	if(addr_conv(argv[1],&srvaddr.sin_addr)==-1){
48		perror(strerror(errno));
49	}
50	//2.连接服务器
51	//客户端的IP地址等信息不需要手动填写,由内核自动完成
52	if(connect(sockfd,(struct sockaddr *)&srvaddr,sizeof(struct sockaddr))==-1){
53		printf("connect error\n");
54		exit(1);
55	}
56	
57	//3.发送请求
58	sprintf(buf,"hello"); write(sockfd,buf,strlen(buf));
59	sprintf(buf,"hello2"); write(sockfd,buf,strlen(buf));
60	sprintf(buf,"hello3"); write(sockfd,buf,strlen(buf));
61	
62	//4.接收响应
63	if((nbytes=read(sockfd,buf,MAXDATASIZE))==-1){
64		printf("read error\n");
65		exit(1);
66	}
67	buf[nbytes]='\0';
68	printf("srv respons:%s\n\n",buf);
69	
70	//关闭socket
71	close(sockfd);
72	return 0;
73}
74
75int addr_conv(char *address,struct in_addr *inaddr){
76	struct hostent *he;
77	//inet_aton 函数将一个“字符串类型”的IP地址转化为“整数类型”的地址
78	if(inet_aton(address,inaddr)==1){
79		printf("call inet_aton sucess.\n");
80		return 0;
81	}
82	printf("call inet_aton fail.\n");
83	//如果输入的不是IP地址而主机名/域名,那么可以通过这些来获得IP地址
84	he=gethostbyname(address);
85	if(he!=NULL){
86		printf("call gethostbyname sucess.\n");
87		*inaddr=*((struct in_addr *)(he->h_addr_list[0]));
88		return 0;
89	}
90	return -1;
91}

套接字最大创建数量

client_msock.c(主动套接字最大数量)

检查最多可创建的套接字数量 向程序传入两个参数,第一个参数为IP地址,第二个参数为想要创建的套接字数量 具体实现其实是On地尝试创建所有希望的套接字,出错则退出。

 1#include <stdio.h>
 2#include <stdlib.h>
 3#include <string.h>
 4#include <sys/socket.h>
 5#include <netinet/in.h>
 6#include <errno.h>
 7#include <netdb.h>
 8#include <arpa/inet.h>
 9#include <arpa/inet.h>
10#include <unistd.h>
11
12#define MAXDATASIZE 128
13#define PORT 3000
14
15int addr_conv(char *address,struct in_addr *inaddr);
16
17
18int main(int argc,char **argv){
19	int i,sockfd;
20	struct sockaddr_in srvaddr;
21	int n=atoi(argv[2]);
22
23	//1.创建网络端点
24	for(i=0;i<n;i++)
25	{	
26		sockfd=socket(AF_INET,SOCK_STREAM,0);
27		if(sockfd==-1){
28			printf("Failed to create the %d socket.\n",i);
29			exit(1);
30		}
31		//指定服务器地址(本地socket地址采用默认值)
32		bzero(&srvaddr,sizeof(srvaddr));
33		srvaddr.sin_family=AF_INET;
34		srvaddr.sin_port=htons(PORT);
35		
36		if(addr_conv(argv[1],&srvaddr.sin_addr)==-1){
37			perror(strerror(errno));
38		}
39	}
40	
41	printf("Finished.\n");
42	return 0;
43}

server_msock.c(被动套接字最大数量)

传入2/4个参数,表示起始端口号、创建连续套接字数量、发送信息大小、接受信息大小。

具体实现与上述类似,暴力模拟,检验是否可行。

 1#include <stdio.h>
 2#include <stdlib.h>
 3#include <string.h>
 4#include <sys/socket.h>
 5#include <netinet/in.h>
 6#include <arpa/inet.h>
 7#include <unistd.h>
 8#include <sys/types.h>
 9
10#define MAXDATASIZE 128
11#define BACKLOG 5
12
13int main(int argc,char **argv){
14	int sockfd,new_fd,nbytes,sin_size;
15	char buf[MAXDATASIZE];
16	struct sockaddr_in srvaddr;
17	int i,lastsockfd=-1;
18	int minport,n,sizeSND,sizeRCV;
19
20	if(argc!=3 && argc!=5)
21	{
22		printf("You should input 3 or 5 params: ");
23		printf("func, startPort, number, SND size, RCV size.\n");
24		exit(1);
25	}
26	
27	minport=atoi(argv[1]);
28	n=atoi(argv[2]);
29	if(argc==5)
30	{
31		sizeSND=atoi(argv[3]);
32		sizeRCV=atoi(argv[4]);
33	}
34
35	for(i=0;i<n;i++)
36	{	
37		//1.创建网络端点
38		sockfd=socket(AF_INET,SOCK_STREAM,0);
39		if(sockfd==-1){
40			printf("Failed to create %d socket.The last sockfd is %d.\n",i,lastsockfd);
41			exit(1);
42		}
43		lastsockfd=sockfd;
44
45		if(argc==5){	// get & set SO_SNDBUF,SO_RCVBUF
46			int size;
47			socklen_t size2=sizeof(size);
48			if((getsockopt(sockfd,SOL_SOCKET,SO_SNDBUF,&size,&size2))<0)
49			{
50				printf("getsockopt failed\n");
51				exit(1);
52			}
53			//printf("SND buff is %d\n",size);
54			if((getsockopt(sockfd,SOL_SOCKET,SO_RCVBUF,&size,&size2))<0)
55			{
56				printf("getsockopt failed\n");
57				exit(1);
58			}
59			//printf("RCV buff is %d\n",size);
60
61			if((setsockopt(sockfd,SOL_SOCKET,SO_SNDBUF,&sizeSND,sizeof(sizeSND)))<0 
62				|| (setsockopt(sockfd,SOL_SOCKET,SO_RCVBUF,&sizeRCV,sizeof(sizeRCV)))<0)
63			{
64				printf("setsockopt failed.\n");
65				exit(1);
66			}
67//			printf("New SND and RCV buff are %d and %d.\n",sizeSND,sizeRCV);
68//			exit(0);
69		}
70		
71		//填充地址
72		bzero(&srvaddr,sizeof(srvaddr));
73		srvaddr.sin_family=AF_INET;
74		srvaddr.sin_port=htons(minport+i);
75		srvaddr.sin_addr.s_addr=htonl(INADDR_ANY);
76	
77		//2.绑定服务器地址和端口
78		if(bind(sockfd,(struct sockaddr *)&srvaddr,sizeof(struct sockaddr))==-1){
79			printf("bind error\n");
80			exit(1);
81		}
82		//3. 监听端口
83		if(listen(sockfd,BACKLOG)==-1){
84			printf("listen error\n");
85			exit(1);
86		}
87	}
88
89	printf("Finished.The last sockfd is %d.\n",sockfd);
90	return 0;
91}

统计自由端口数量

具体实现是客户端尝试进行充分多次的套接字连接请求,服务器端每次根据发送方的IP地址更新最大最小值,这样就能知道自由端口所在的区间。.

大概自由端口保证是连续的

client_port.c

 1#include <stdio.h>
 2#include <stdlib.h>
 3#include <string.h>
 4#include <sys/socket.h>
 5#include <netinet/in.h>
 6#include <errno.h>
 7#include <netdb.h>
 8#include <arpa/inet.h>
 9#include <unistd.h>
10
11#define MAXDATASIZE 128
12#define PORT 3000
13
14int addr_conv(char *address,struct in_addr *inaddr);
15
16int main(int argc,char **argv){
17	int sockfd,nbytes,i,j;
18	char buf[MAXDATASIZE];
19	struct sockaddr_in srvaddr;
20	if(argc!=2){
21		printf("usage:./client hostname|ip\n");
22		exit(0);
23	}
24	
25	bzero(&srvaddr,sizeof(srvaddr));
26	srvaddr.sin_family=AF_INET;
27	srvaddr.sin_port=htons(PORT);
28	if(addr_conv(argv[1],&srvaddr.sin_addr)==-1)
29	{
30	   perror(strerror(errno));
31	   exit(1);
32	}
33
34	for(j=0;j<100;j++)
35	{	
36	   for(i=0;i<10000;i++)
37	   {	
38	      sockfd=socket(AF_INET,SOCK_STREAM,0);
39	      if(sockfd==-1){
40			printf("can;t create socket\n");
41			exit(1);
42	      }
43	      if(connect(sockfd,(struct sockaddr *)&srvaddr,sizeof(struct sockaddr))==-1){
44			printf("connect error\n");
45			exit(1);
46	      }
47	
48	      sprintf(buf,"hello");
49	      write(sockfd,buf,strlen(buf));
50	      if((nbytes=read(sockfd,buf,MAXDATASIZE))==-1){
51			printf("read error\n");
52			exit(1);
53	      }
54	      close(sockfd);
55	   }
56	    	
57	   printf("srv respons 10k times:%d\n",j);
58	}
59
60	return 0;
61}

server_port.c

 1#include <stdio.h>
 2#include <stdlib.h>
 3#include <string.h>
 4#include <sys/socket.h>
 5#include <netinet/in.h>
 6#include <arpa/inet.h>
 7#include <unistd.h>
 8#include <sys/types.h>
 9
10#define MAXDATASIZE 128
11#define PORT 3000
12#define BACKLOG 5
13
14int main(int argc,char **argv){
15	int sockfd, new_fd, nbytes, sin_size;
16	unsigned int port, minport, maxport;
17	char buf[MAXDATASIZE];
18	struct sockaddr_in srvaddr,clientaddr;
19	
20	//1.创建网络端点
21	sockfd=socket(AF_INET,SOCK_STREAM,0);
22	if(sockfd==-1){
23		printf("can;t create socket\n");
24		exit(1);
25	}
26
27	if(argc==2){
28		int on=1;
29		setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
30		printf("reuse addr\n");
31	}
32	//填充地址
33	bzero(&srvaddr,sizeof(srvaddr));
34	srvaddr.sin_family=AF_INET;
35	srvaddr.sin_port=htons(PORT);
36	srvaddr.sin_addr.s_addr=htonl(INADDR_ANY);
37	
38	//2.绑定服务器地址和端口
39	if(bind(sockfd,(struct sockaddr *)&srvaddr,sizeof(struct sockaddr))==-1){
40		printf("bind error\n");
41		exit(1);
42	}
43	//3. 监听端口
44	if(listen(sockfd,BACKLOG)==-1){
45		printf("listen error\n");
46		exit(1);
47	}在执行 make  
48
49	minport=65535;
50	maxport=0;
51	printf("Listening......\n");
52	for(;;){
53		//4.接受客户端连接
54		sin_size=sizeof(struct sockaddr_in);
55		if((new_fd=accept(sockfd,(struct sockaddr *)&clientaddr,&sin_size))==-1){
56			printf("accept error\n");
57			continue;
58		}
59		
60		port=(unsigned int)ntohs(clientaddr.sin_port);
61		if(port<minport || port>maxport)
62		{		
63		   if(port<minport)
64		      minport=port;
65		   else if(port>maxport)
66		      maxport=port;
67		   
68		   printf("minport is: %d\t",minport);
69		   printf("maxport is: %d\n",maxport);
70		}		
71		nbytes=read(new_fd,buf,MAXDATASIZE);
72		write(new_fd,buf,strlen(buf));
73		close(new_fd);
74	}
75	close(sockfd);
76	
77	return 0;
78}

:star: 客户端发送整数,服务器计算平均值后返回

 1#include <termio.h>
 2#include <pthread.h>
 3#include <stdarg.h>
 4
 5#define itn int
 6#define whlie while
 7#define fro for
 8#define pritnf printf
 9
10struct termios tmtemp, tm;
11char getch(void){
12    int c = 0, fd = 0;
13
14    //获取当前的终端属性设置,并保存到tm结构体中
15    if(tcgetattr(fd,&tm) != 0) return -1;
16
17    tmtemp=tm;
18    
19    //将tetemp初始化为终端原始模式的属性设置
20    cfmakeraw(&tmtemp);     
21
22    //终端设置为原始模式的设置
23    if(tcsetattr(fd,TCSANOW,&tmtemp) != 0) return -1;
24    
25    c=getchar();
26
27    if(tcsetattr(fd,TCSANOW,&tm) != 0) return -1;
28
29    return (char)c;
30}
31
32int myprintf(char *format, ...)
33{
34    struct termios now;
35    if(tcgetattr(0, &now) != 0) return -1;
36    if(tcsetattr(0,TCSANOW,&tm) != 0) return -1;
37
38    va_list args;
39    va_start(args, format);
40    int cnt = vprintf(format, args);
41    va_end(args);
42
43    if(tcsetattr(0,TCSANOW,&now) != 0) return -1;
44    return cnt;
45}

client

 1#include <string.h>
 2#include <sys/socket.h>
 3#include <netinet/in.h>
 4#include <stdio.h>
 5#include <signal.h>
 6#include <stdlib.h>
 7#include <errno.h>
 8#include <netdb.h>
 9#include <arpa/inet.h>
10#include <unistd.h>
11#include <time.h>
12
13#define  SERVER_PORT 8086
14
15int n, buf[1024], msg[1024];
16
17int fun(char *ip){
18    int sockfd;
19    struct sockaddr_in servaddr;
20    sockfd = socket(AF_INET, SOCK_STREAM, 0);
21    if(sockfd < 0){
22        fprintf(stderr, "Socket error\n");
23        return -1;
24    }
25
26    bzero(&servaddr, sizeof servaddr);
27    servaddr.sin_family = AF_INET;
28    servaddr.sin_port = htons(SERVER_PORT);
29    if(inet_aton(ip, &servaddr.sin_addr) == -1){
30        fprintf(stderr,"Inet_aton error\n");
31        return -1;
32    }
33
34    if(connect(sockfd, (struct sockaddr*)&servaddr, sizeof servaddr) < 0){
35        fprintf(stderr,"Connect error\n");
36        close(sockfd);
37        return -1;
38    }
39
40    msg[0] = htonl(n);
41    for(int i = 0; i < n; i++) msg[i + 1] = htonl(buf[i]);  msg[n + 1] = 0;
42
43    write(sockfd, msg, (n + 1) * sizeof(int)); 
44
45    n = (int)read(sockfd, msg, 1024);
46    if(n < 0){
47        fprintf(stderr, "Read error\n");
48        return -1;
49    }
50    printf("服务器返回平均值为%d\n", ntohl(msg[0]));
51    close(sockfd);
52    return 0;
53}
54
55int main(int argc, char *argv[]){
56    if(argc != 2){
57        puts("请输入一个参数表示服务器IP地址!");
58        return 0;
59    }
60
61    srand((int)time(NULL));
62
63    n = 255;
64	for(int i = 0; i < n; i++) buf[i] = rand() % 100;
65    printf("总共产生%d个数字:\n", n);
66    for(int i = 0; i < n; i++) printf("%d ", buf[i]); puts("");
67
68    fun(argv[1]);
69
70	return 0;
71}

server

  1#include <string.h>
  2#include <sys/socket.h>
  3#include <netinet/in.h>
  4#include <stdio.h>
  5#include <signal.h>
  6#include <stdlib.h>
  7#include <errno.h>
  8#include <netdb.h>
  9#include <arpa/inet.h>
 10#include <unistd.h>
 11#include <time.h>
 12
 13#include "rqdmap.h"
 14
 15
 16#define  SERVER_PORT 8086
 17#define  BACKLOG    5
 18
 19char op;
 20void * thread(void * listenfd){
 21    while(op != 'q'){
 22        fflush(stdin);
 23        op = getch();
 24        printf("receive %d\n", op);
 25    }
 26    close(*(int *)listenfd);
 27    exit(1);
 28}
 29
 30int buf[1030];
 31
 32
 33int fun(int connfd){
 34    int n = (int)read(connfd, (char*)buf, 1100);
 35    if(n < 0){
 36        fprintf(stderr,"Read error");
 37        return -1;
 38    }
 39    n = ntohl(buf[0]);
 40    int sum = 0;
 41    
 42    // for(int i = 0; i < n; i++) printf("%d ", ntohl(buf[i + 1]));
 43    for(int i = 1; i <= n; i++) sum += ntohl(buf[i]);
 44    sum /= n;
 45
 46    if(myprintf("服务器输出 %d \n", sum) < 0){
 47        fprintf(stderr, "Print error\n");
 48        return -1;
 49    }
 50
 51    buf[0] = htonl(sum); buf[1] = 0;
 52    write(connfd, buf, sizeof(int));
 53    return 0;
 54}
 55
 56
 57int main(){
 58    int listenfd, connfd;
 59    struct sockaddr_in servaddr;
 60    
 61    listenfd = socket(AF_INET, SOCK_STREAM, 0);
 62    printf("产生套接字 %d\n", listenfd);
 63    if(listenfd < 0){
 64        fprintf(stderr, "Socket error\n");
 65        exit(1);
 66    }
 67
 68    int on = 1;
 69    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
 70    
 71    bzero(&servaddr, sizeof servaddr);
 72    servaddr.sin_family = AF_INET;
 73    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
 74    servaddr.sin_port=htons(SERVER_PORT);
 75    if(bind(listenfd, (struct sockaddr*)&servaddr, sizeof (struct sockaddr)) < 0){
 76        fprintf(stderr,"Bind error\n");
 77        exit(1);    
 78    }
 79    
 80  	if(listen(listenfd, 5) < 0){
 81        fprintf(stderr,"Listen error\n");
 82        close(listenfd);
 83        exit(1);
 84    }
 85    
 86
 87    pthread_t pid;
 88    if(pthread_create(&pid, NULL, thread, &listenfd) != 0){
 89        puts("Create pthread error!");
 90        exit(1);
 91    }
 92
 93    while(1){
 94        connfd = accept(listenfd, NULL, NULL);
 95        if(connfd < 0){
 96            fprintf(stderr,"Accept error\n");
 97            exit(1);
 98        }
 99
100        if(fun(connfd) != 0){
101            fprintf(stderr, "fun error\n");
102            op = 'q'; break;
103        }
104        close(connfd);
105    }
106
107    pthread_join(pid, NULL);
108    return 0;
109}

实战记录

遇到了非常多的坑点…弄了很久

利用socket套接字进行数据通信最关键的地方其实在于 通信格式的设计,这样接收方才能够正确无误地解读发送方的比特串。

而在其中非常重要的一个细节就是 字节顺序,发送方和接收方应该使用匹配的htonlhtons,每次想要通过socket进行传输信息都应该进行编码和解码。

接收方接受数据时,如何正确地遍历不同格式的缓冲区也非常重要,需要注意到read函数返回的是接收到的字节(8位)的个数!

1int buf[];
2int n = read(connfd, (char*)buf, 1024);
3for(int i = 0; i * 4 < n; i++){
4   	//do something on buf[]
5}

为了使得服务器不仅能够接受不断地监听套接字提供服务,还能够通过键盘的键入来退出死循环、回收资源终止服务,上述程序还夹杂了非常多的私活。

  • 为了实现上述两个 不间断的服务,我们使用多线程。除去main函数中对套接字不断地监听,我们再开一个thread进行键盘的读入。

    起初不充分的设想是通过thread读取键盘,将输入的字符存储到全局变量op中,main函数在循环中不断地检查op是否为q.

    然而事实上listen会由于没有客户机的访问而一直堵塞,因而会卡在一次循环中无法得到op已经被修改的信息。

    那么考虑到这两个线程其实不存在必要的通信要求,只要thread读取到q就应该将套接字服务资源全部回收,终止进程,所以最终的实现就是在thread中回收资源并且终止进程,主函数不断地循环即可。

    不过之后由于main函数的循环监听中有可能有函数出错而终止服务,所以还是利用了op向thread传达信息

  • 听闻有函数getch可以读入键盘的输入,不需要按回车也不会在控制台回显,非常适合作为退出死循环的方法。然而好像getch不是标准库的函数,在linux下并没有实现,所以便自己实现了一个不回显、不需要空格 的键盘读入。基本原理是将控制台调整为原始模式,在原始模式下,所有的数据输入以字节为单位进行处理,当一个字节被输入后,便触发输入有效。那么将控制台设置为原始模式,完成getchar()后将终端模式恢复到原先的状态即可。具体实现参见header。

  • 上述的多线程函数带来了若干的问题。由上述分析可知,运行getch()后,当前终端会一直保持原始模式,那么如果此时main函数希望进行IO操作,输出的格式就会出现非常奇怪的问题(大概类似于阶梯状,不是很懂为什么)。所以我们还多写了一个自定义的printf函数,这个printf函数在输出前会先将终端设置为getch调用前的状态,输出完成后再将其到原始模式。因为一台机子上模拟CS不太会同时操作两个终端,所以这样基本能完成任务。但是这样实现仍然是非常朴素的,真正在设计网络服务器的时候一定要重新考虑

Unit 03

DNS

struct hostent

hostent是host entry的缩写,该结构记录主机的相关信息。

1struct hostent{
2    char * h_name;
3    char ** h_aliases;
4    short h_addrtype;
5    short h_length;
6    char ** h_addr_list;
7    #define h_addr h_addr_list[0];
8};

域名<->IP地址

1struct hostent* gethostbyname(const char *name)
2struct hostent *gethostbyaddr(const char *addr, size_t len, int family)

注意,通过IP获取主机信息时传入的参数addr不是IP字符串,而是经过inet_aton转化过的32位的网络序列地址。

1int inet_aton(const char *string, struct in_addr*addr);

函数成功返回非零,失败返回0

recv与send

1int recv(int sockfd,void* buf,int len, int flags);
2int send(int sockfd,void* buf,int len, int flags);

len为发送/接受数据的长度

flags中可以填写一些参数

  • 0:相当于write和read
  • MSG_DONTROUTE:发送数据不查找路由表
  • MSG_OOB:发送、接受带外数据
  • MSG_PEEK:接受数据时不从缓冲区移走数据,其他进程仍然可以read到数据
  • MSG_WAITALL:数据量不够时,读操作等待

shutdown

1int shutdown(int sockfd,int howto); 

参数howto指定关闭操作的类型:

  • howto = 0, 关闭读通道;丢弃尚未读取的数据,对后来接收到的数据返回确认后丢弃。
  • howto = 1, 关闭写通道;继续发送未发送完成的数据,然后发送FIN字段关闭写通道。
  • howto = 2, 关闭读写通道;任何进程都不能再操作这个socktet

shutdown与close的区别

close关闭当前进程与套接字的联系,其他进程仍然可能使用这个套接字。

shutdown直接在tcp层上操作该套接字,不管有多少个进程引用该套接字,当套接字关闭后试图读的进程会读到EOF,试图写的进程会检测到SIGPIPE信号。

readv与writev

readv为散布读,将文件中若干连续的数据块读入内存分散的缓冲区中。

writev为聚集写,即收集内存中分散的若干缓冲区中的数据写至文件的连续区域中。

1struct iovec{
2  void *  iov_base; /* 数据区的起始地址 */
3  size_t  iov_len;  /*   数据区的大小  */
4};
5
6ssize_t readv(int fildes, const struct iovec *iov, int iovcnt);
7ssize_t writev(int fildes, const struct iovec *iov, int iovcnt);

参数iovcnt指出数组iov的元素个数,元素个数至多不超过IOV_MAX。linux中定义IOV_MAX的值为1024。

给出一个示例代码

 1//发送
 2char *name = " Wang Jin-pyng";
 3char *occupation = "Head of the legislative body";
 4int len[2];
 5struct iov[3];
 6int iovcnt;
 7
 8len[0]=strlen(name);
 9len[1]=strlen(occupation);
10
11iov[1].iov_base=name;关闭后
12iov[1].iov_len=len[0];
13
14iov[2].iov_base=occupation;
15iov[2].iov_len=len[1];
16
17len[0]=htonl(len[0]);
18len[1]=htonl(len[1]);
19
20iov[0].iov_base=len;
21iov[0].iov_len=2*sizeof(int);
22
23writev(sockfd,iov,3);
24......
25
26//接收
27char name[1024];
28char occupation[1024];
29int len[2];
30struct iov[2];
31int iovlen;
32
33......//
34read(sockfd,len,2*sizeof(int));
35
36len[0]=ntohl(len[0]);
37len[1]=ntohl(len[1]);
38
39iov[0].iov_base=name;
40iov[0].iov_len=len[0];
41
42iov[1].iov_base=occupation;
43iov[1].iov_len=len[1];
44
45readv(sockfd,iov,2);
46......
47
48//Note:通常还需要调用函数
49//ioctl(fd,FIONREAD,nbyte),以确定接收缓存区内有多少个字节可读。

参考博客

readv()和writev()函数

recvmsg与sendmsg

这两个函数是最底层的函数,其余所有的IO均通过转化为该函数来进行。

具体参数不了解。

 1#include <sys/types.h>
 2#include <sys/socket.h>
 3int recvmsg(int sockfd,struct msghdr* msg,int flags);
 4int sendmsg(int sockfd,struct msghdr* msg,int flags);
 5
 6struct msghdr{
 7  void *msg_name;  
 8  int msg_namelen;
 9  struct iovec* msg_iov;
10  int msg_iovlen;
11  void* msg_control;
12  int msg_controllen;
13  int msg_flags;  //同recv、send
14}

多路复用select

检查多个文件描述符(socket描述符)是否就绪,当某一个描述符就绪(可读、可写或发生异常)时函数返回。可以实现输入输出多路复用。

1int select(int maxfd, fd_set *rdset, fd_set *wrest, fd_set *exset, struct timeval *timeout);

返回值:有描述符就绪则返回就绪的描述符个数;超时时间内没有描述符就绪返回0;执行失败返回-1。

参数:

  • maxfd-需要测试的描述符的最大值,实际测试的描述符从0-maxfd-1

  • rdset-需要测试是否可读的描述符集合(包括处于listen状态的socket接收到连接请求)

  • wrset-需要测试是否可写的描述符集合(包括以非阻塞方式调用connect是否成功)

  • exset-需要测试是否异常的描述符集合(包括接收带外数据的socket有带外数据到达)

  • timeout-指定测试超时的时间

    • timeval结构
    1struct timeval{
    2	long tv_sec;  //秒
    3	long tv_usec; //毫秒
    4} 
    
    • timeout=NULL,select将永远阻塞直到有一个描述符就绪,或者出现错误(接收到信号)。
    • timeout>0,在timeout时间内如果有描述符就绪则返回,否则在timeout时间后返回0;如果将3个描述符集合都设定为NULL则select相当于sleep函数,只是时间可以精确到毫秒
    • timeout=0,select检查完描述符集合后立即返回

操作描述符集合:

  • FD_ZERO(fd_set *fdset)-清空描述符集合
  • FD_SET(int fd, fd_set *fdset)-将一个描述符添加到描述符集合
  • FD_CLR(int fd, fd_set *fdset)-将一个描述符从描述符集合中清除
  • FD_ISSET(int fd, fd_set *fdset)-检测一个描述符是否就绪

在设置描述符集合前应该先调用FD_ZERO将集合清空,每次调用select函数前应该重新设置这3个集合

三个集合中的描述符可以交叉

不过现在的服务器一般不使用select,select本质好像是On地查询,效率不高,一般使用poll等函数方法

socket选项

获取或设置socket选项

1int getsockopt(int sockfd, int level, int optname, void *optval,sock_len *optlen);
2int setsockopt(int sockfd, int level, int optname, void *optval,sock_len optlen);

返回值: 0成功,-1失败

参数:

  • level 选项级别

    • SOL_SOCKET —通用socket选项
    • IPPROTO_IP—IP选项
    • IPPROTO_TCP—TCP选项
  • optname 选项名称

    • 一般通用socket选项

      • SO_KEEPALIVE

        设置该选项后,2小时内没有数据交换时,TCP协议将自动发送探测数据包,检查网络连接

      • SO_RCVBUF和SO_SNDBUF

        设置发送和接收数据缓冲区的大小(在连接建立以前设置)

      • SO_RCVTIMEO和SO_SNDTIMEO

        设置发送和接收超时,当指定时间内数据没有成功接收或发送,发送和接收函数将返回。

      • SO_REUSEADDR

        快速重启服务器程序

        启动服务器程序的多个实例(绑定本地IP地址的多个别名)

    • IP选项

      IP_HDRINCL:是否需要自己建立IP数据包首部,适用于原始socket

    • TCP选项

      • TCP_MAXSEG-TCP协议最大数据段长度
      • TCP_NODELAY-小数据包是否延迟发送(Nagle算法
  • optal选项值

  • opelen选项长度/存放选项长度的指针

fcntl

修改文件的属性。

1int fcntl(int fd,int cmd,)

返回值: 非负成功,-1失败

操作类型:

Cmd 参数 返回值 说明
F_GETFL 0 fd 获取描述符标志
F_SETFL O_NONBLOCK 0, -1 设置套接字为非阻塞式
F_GETOWN int* 0, -1 取得套接字的所有者
F_SETOWN int 0, -1 设置套接字的所有者

套接字的阻塞方式

设置socket为阻塞方式

1int flags;
2flags=fcntl(fd,F_GETFL,0);
3flags|=O_NONBLOCK;
4fcntl(fd,F_SETFL,flags);

设置socket为非阻塞方式

1int flags;
2flags=fcntl(fd,F_GETFL,0);
3flags&=~O_NONBLOCK;
4fcntl(fd,F_SETFL,flags); 

一般情况下,socket默认是阻塞方式的,也就是说套接字会阻塞,直到客户机发送信息为止。

在非阻塞方式下,如果没有接收到消息套接字也会返回,那么主程序为了不断监听信息就得一直轮询套接字,占用大量的CPU资源。

而在多套接字情况下,由于阻塞方式的存在,多套接字可能不能很好的同时监听。

有一些解决方案

  • 多线程。但是线程需要占用资源,linux大概能开小几百的套接字线程。
  • IO多路复用:select、poll、epoll等,同时检测多个套接字是否就绪。

参考博客

为什么网络socket编程是阻塞的?因为非阻塞轮询占CPU,用多线程和IO复用

ioctl

1int ioctl(int fd,int req,);
req 参数 返回值 说明
SIOCATMARK int* 0, -1 是否到达带外标记
FIOASYNC int* 0, -1 异步I/O标志
FIONREAD int* 0, -1 缓存区中有多少字节数据可读
嗨! 这里是 rqdmap 的个人博客, 我正关注 GNU/Linux 桌面系统, Linux 内核, 后端开发, Python, Rust 以及一切有趣的计算机技术! 希望我的内容能对你有所帮助~
如果你遇到了任何问题, 包括但不限于: 博客内容说明不清楚或错误; 样式版面混乱等问题, 请通过邮箱 rqdmap@gmail.com 联系我!
修改记录:
  • 2024-03-02 20:27:46调整部分博客的分类与标签
  • 2024-01-05 00:01:57合并小众分类的文章
  • 2023-05-29 23:05:14大幅重构了python脚本的目录结构,实现了若干操作博客内容、sqlite的助手函数;修改原本的文本数 据库(ok)为sqlite数据库,通过嵌入front-matter的page_id将源文件与网页文件相关联
  • 2023-05-08 21:44:36博客架构修改升级
  • 2022-11-16 01:27:34迁移老博客文章内容
Linux 下的 socket 编程