一、Linux发送网络消息的过程
(1) 应用程序调用write()将消息发送到内核中
( 2)内核中的缓存达到了固定长度数据后,一般是SO_SNDBUF,将发送到TCP协议层
(3)IP层从TCP层收到数据,会加上自己的包头然后发送出去。一般分片的大小是MTU(含IP包头),而IPV4下IP的包头长度为40,而IPV6下为60,因此,TCP中分片后,有效的数据长度为MSS = MTU - 40 或 MSS = MTU -60
(4)最终经过其他层的包装,发送到公网上,跑来跑去,这时候,你的数据可能几段连为一条,一条可能分为几段。
二、粘包问题
上一篇文章中,我们用write()系统调用来读取数据,但是这个调用需要指定长度,例如上文中的1024,那么问题来了:
(1)报文有效数据长1025怎么办 ? 对方发“Hi,I like you!” 你期望收到“Hi, I like ”吗
(2)报文有效数据长度300怎么办? 对方发“Hi, I like you!” "You are befutiful" 你期望收到“Hi, I like you!You are”么? 你不想知道 you are 什么,还有,明明对方发送了两条消息,而你。。。收到了一条半,还当作了一条。
三、解决
3.1主要有两种解决方案,分别为
(1)认为的加边界 例如以\R\N为界限,FTP协议就是用的这种方法。
(2)建立一个数据结构,如下:
1 struct packet 2 { 3 int len; 4 char buff[1024]; 5 };
发送前,将packet.len设置好,然后将该数据结构的一个实例发送过去,读的时候先读取int长度即4个字节的数据,获得buff的有效长度,然后循环读,直到读够len字节的数据为止。
本文主要介绍第二种设定数据结构的方案。该方案的一个小缺点是,单次写不会超过buff[1024]的大小限制。。。
3.2 readn函数:
1 ssize_t readn(int sock, void *recv, size_t len) 2 { 3 size_t nleft = len; 4 ssize_t nread; 5 char *bufp = (char*)recv; // 辅助指针变量,记录位置的。 6 while(nleft > 0){ 7 if((nread = read(sock,bufp,nleft)) < 0){ //read error 读len,当然可能被中断读不够len,所以继续 8 if(errno == EINTR){ // 被信号中断到 9 continue; 10 } 11 return -1; 12 } 13 else if(nread == 0){ // 若对方已关闭,返回已读字数。 14 return len - nleft; 15 } 16 bufp += nread; // mov point 17 nleft -= nread; 18 } 19 return len; 20 }
3.3 writen函数;
1 ssize_t writen(int sock,const void *buf, size_t len) 2 { 3 size_t nleft = len; 4 ssize_t nwrite; 5 char *bufp = (char*)buf; 6 7 while(nleft > 0){ 8 if((nwrite = write(sock,bufp,nleft)) < 0){ 9 if(errno == EINTR){ // 信号中断 10 continue; 11 } 12 return -1; 13 } 14 else if(nwrite == 0){ // write返回值是0,代表对方套接字关闭,再写会失败,然后返回。 15 continue; 16 } 17 bufp += nwrite; 18 nleft -= nwrite; 19 } 20 return len; 21 }
3.4利用这两个函数,即可完成读写。下文将介绍利用这两个函数完成的一个P2P程序,服务端与客户端互相发送用户输入的数据:程序的架构如下:
3.4.1 服务端:
1 #include <unistd.h> 2 #include <sys/stat.h> 3 #include <sys/wait.h> 4 #include <sys/types.h> 5 #include <fcntl.h> 6 7 #include <stdlib.h> 8 #include <stdio.h> 9 #include <errno.h> 10 #include <string.h> 11 #include <signal.h> 12 13 #include <arpa/inet.h> 14 #include <sys/socket.h> 15 #include <netinet/in.h> 16 #include <string.h> 17 18 #define ERR_EXIT(m) \ 19 do { \ 20 perror(m);\ 21 exit(EXIT_FAILURE);\ 22 }while(0) 23 24 struct packet 25 { 26 int len; 27 char buff[1024]; 28 }; 29 30 ssize_t readn(int sock, void *recv, size_t len) 31 { 32 size_t nleft = len; 33 ssize_t nread; 34 char *bufp = (char*)recv; // 辅助指针变量,记录位置的。 35 while(nleft > 0){ 36 if((nread = read(sock,bufp,nleft)) < 0){ //read error 读len,当然可能被中断读不够len,所以继续 37 if(errno == EINTR){ // 被信号中断到 38 continue; 39 } 40 return -1; 41 } 42 else if(nread == 0){ // 若对方已关闭,返回已读字数。 43 return len - nleft; 44 } 45 bufp += nread; // mov point 46 nleft -= nread; 47 } 48 return len; 49 } 50 ssize_t writen(int sock,const void *buf, size_t len) 51 { 52 size_t nleft = len; 53 ssize_t nwrite; 54 char *bufp = (char*)buf; 55 56 while(nleft > 0){ 57 if((nwrite = write(sock,bufp,nleft)) < 0){ 58 if(errno == EINTR){ // 信号中断 59 continue; 60 } 61 return -1; 62 } 63 else if(nwrite == 0){ // write返回值是0,代表对方套接字关闭,再写会失败,然后返回。 64 continue; 65 } 66 bufp += nwrite; 67 nleft -= nwrite; 68 } 69 return len; 70 } 71 void handle(int sig) 72 { 73 printf("recv sig = %d\n", sig); 74 exit(0); 75 } 76 int main(void) 77 { 78 signal(SIGUSR1,handle); 79 80 int sockfd; 81 // 创建一个Socket 82 sockfd = socket(AF_INET,SOCK_STREAM,0); 83 if(sockfd == -1){ 84 perror("error"); 85 exit(0); 86 } 87 88 89 /////////////////////////////////////////////////////////// 90 // struct sockaddr addr; // 这是一个通用结构,一般是用具体到,然后转型 91 struct sockaddr_in sockdata; 92 sockdata.sin_family = AF_INET; 93 sockdata.sin_port = htons(8001); 94 sockdata.sin_addr.s_addr = inet_addr("192.168.59.128"); 95 96 int optval = 1; 97 if(setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&optval,sizeof(optval)) == -1) 98 { 99 perror("error"); 100 exit(0); 101 } 102 if(bind(sockfd,(struct sockaddr *)&sockdata,sizeof(sockdata)) < 0){ 103 perror("error"); 104 exit(0); 105 } 106 107 //////////////////////////////////////////////////////////// 108 if(listen(sockfd,SOMAXCONN) == -1){ //变成被动侦听套接字。 109 perror("error"); 110 exit(0); 111 } 112 113 ////////////////////////////////////////////////////////// 114 struct sockaddr_in peeradr; 115 socklen_t peerlen = sizeof(peeradr); // 得有初始值 116 117 118 ///////////////////////////////////////////////////////// 119 int conn = 0; 120 conn = accept(sockfd,(struct sockaddr *)&peeradr,&peerlen); 121 if(conn == -1){ 122 perror("error"); 123 exit(0); 124 } 125 126 printf("收到的IP %s\n 客户端端口是:%d\n,conn == %d\n",inet_ntoa(peeradr.sin_addr),ntohs(peeradr.sin_port),conn); 127 128 pid_t twopid; 129 twopid = fork(); 130 131 if(twopid == -1){ 132 perror("error"); 133 exit(0); 134 } 135 if(twopid > 0){ // father , 接受数据 136 struct packet recvBuff; 137 memset(&recvBuff,0,sizeof(recvBuff)); 138 int ret = 0; 139 int rn; 140 while(1){ 141 ret = readn(conn,&recvBuff,4); // 先获得长度 142 if(ret == -1){ 143 ERR_EXIT("READ"); 144 } 145 if(ret < 4){ 146 printf("client close\n"); 147 break; 148 } 149 rn = ntohl(recvBuff.len); 150 ret = readn(conn,recvBuff.buff,rn); 151 if(ret == -1){ 152 ERR_EXIT("READ"); 153 } 154 if(ret < rn){ 155 printf("client close\n"); 156 break; 157 } 158 fputs(recvBuff.buff,stdout); 159 memset(&recvBuff,0,sizeof(recvBuff)); 160 } 161 printf("client closed"); // may this create a guer process 162 // send signal to child 163 kill(twopid, SIGUSR1); 164 close(conn); 165 close(sockfd); 166 sleep(2); 167 exit(0); 168 } 169 if(twopid == 0){ // child send data 170 close(sockfd); 171 int n; 172 struct packet sendBuff; 173 memset(&sendBuff,0,sizeof(sendBuff)); 174 while(fgets(sendBuff.buff,sizeof(sendBuff.buff),stdin) != NULL){ 175 n = strlen(sendBuff.buff); 176 sendBuff.len = htonl(n); 177 writen(conn,&sendBuff,4+n); 178 memset(&sendBuff,0,sizeof(sendBuff)); 179 } 180 exit(0); 181 } 182 return 0; 183 }
3.4.2 客户端:
1 #include <unistd.h> 2 #include <sys/stat.h> 3 #include <sys/wait.h> 4 #include <sys/types.h> 5 #include <fcntl.h> 6 7 #include <stdlib.h> 8 #include <stdio.h> 9 #include <errno.h> 10 #include <string.h> 11 #include <signal.h> 12 13 #include <arpa/inet.h> 14 #include <sys/socket.h> 15 #include <netinet/in.h> 16 #include <string.h> 17 #define ERR_EXIT(m) \ 18 do { \ 19 perror(m);\ 20 exit(EXIT_FAILURE);\ 21 }while(0) 22 23 struct packet 24 { 25 int len; 26 char buff[1024]; 27 }; 28 29 ssize_t readn(int sock, void *recv, size_t len) 30 { 31 size_t nleft = len; 32 ssize_t nread; 33 char *bufp = (char*)recv; // 辅助指针变量,记录位置的。 34 while(nleft > 0){ 35 if((nread = read(sock,bufp,nleft)) < 0){ //read error 读len,当然可能被中断读不够len,所以继续 36 if(errno == EINTR){ // 被信号中断到 37 continue; 38 } 39 return -1; 40 } 41 else if(nread == 0){ // 若对方已关闭,返回已读字数。 42 return len - nleft; 43 } 44 bufp += nread; // mov point 45 nleft -= nread; 46 } 47 return len; 48 } 49 ssize_t writen(int sock,const void *buf, size_t len) 50 { 51 size_t nleft = len; 52 ssize_t nwrite; 53 char *bufp = (char*)buf; 54 55 while(nleft > 0){ 56 if((nwrite = write(sock,bufp,nleft)) < 0){ 57 if(errno == EINTR){ // 信号中断 58 continue; 59 } 60 return -1; 61 } 62 else if(nwrite == 0){ // write返回值是0,代表对方套接字关闭,再写会失败,然后返回。 63 continue; 64 } 65 bufp += nwrite; 66 nleft -= nwrite; 67 } 68 return len; 69 } 70 int main(void) 71 { 72 int sockfd; 73 // 创建一个Socket 74 sockfd = socket(AF_INET,SOCK_STREAM,0); 75 if(sockfd == -1){ 76 perror("error"); 77 exit(0); 78 } 79 80 81 /////////////////////////////////////////////////////////// 82 // struct sockaddr addr; // 这是一个通用结构,一般是用具体到,然后转型 83 struct sockaddr_in sockdata; 84 sockdata.sin_family = AF_INET; 85 sockdata.sin_port = htons(8001); 86 sockdata.sin_addr.s_addr = inet_addr("192.168.59.128"); 87 if(connect(sockfd,(struct sockaddr *)&sockdata,sizeof(sockdata)) == -1){ 88 perror("error"); 89 exit(0); 90 } 91 pid_t pid = 0; 92 pid = fork(); 93 if(pid == -1){ perror("error"); 94 exit(0); 95 } 96 if(pid > 0){ // father // ccept data from keyboad 97 struct packet sendBuff; 98 memset(&sendBuff,0,sizeof(sendBuff)); 99 int n; 100 while(fgets(sendBuff.buff,sizeof(sendBuff.buff),stdin) != NULL){ 101 102 n = strlen(sendBuff.buff); 103 // 设置发送消息到长度。 104 sendBuff.len = htonl(n); 105 // 将结构体实例写入。 106 writen(sockfd,&sendBuff,4+n); 107 108 // 清零 109 memset(&sendBuff,0,sizeof(sendBuff)); 110 } 111 112 } 113 if(pid == 0){ // child recv data 114 struct packet recvBuff; 115 memset(&recvBuff,0,sizeof(recvBuff)); 116 // 从服 117 int ret; 118 int rn; 119 while(1){ 120 // 首先获得要读取到长度,前4个字节 121 ret = readn(sockfd,&recvBuff.len,4); 122 if(ret == -1){ 123 ERR_EXIT("READ"); 124 } 125 if(ret < 4){ 126 printf("server close\n"); 127 break; 128 } 129 130 // 读取4个字节开始到数据。 131 rn = ntohl(recvBuff.len); 132 ret = readn(sockfd,recvBuff.buff,rn); 133 if(ret == -1){ 134 ERR_EXIT("read error"); 135 } 136 if(ret < rn ){ 137 printf("server close\n"); 138 break; 139 } 140 // put it to screen 141 fputs(recvBuff.buff,stdout); 142 // 清零 143 memset(&recvBuff,0,sizeof(recvBuff)); 144 } 145 146 } 147 148 close(sockfd); 149 return 0; 150 }
后记:
由于上文中获得len的大小的方式是 测试buff[1024]中有效数据的长度的,所以实际上len每一个不会超过1024。
但是,当fgets函数接受的一行长度大于1023的时候,它会将剩下的(1023以后的)字符串作为下一次的输入。然后发送端会发送两个Packet实例。
而接收端接收到的两个pocket都有正确的长度,所以可以安全的接受,但是不幸的是,会将一条报文分成多条。。。
该程序是单进程的,读者可以自行改成多进程的。