Linux下多进程服务端客户端模型二(粘包问题与一种解决方法)

时间:2022-06-05 14:57:03

一、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)最终经过其他层的包装,发送到公网上,跑来跑去,这时候,你的数据可能几段连为一条,一条可能分为几段。

        Linux下多进程服务端客户端模型二(粘包问题与一种解决方法)

二、粘包问题

     上一篇文章中,我们用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函数:

       

Linux下多进程服务端客户端模型二(粘包问题与一种解决方法)Linux下多进程服务端客户端模型二(粘包问题与一种解决方法)
 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 }
readn

   3.3  writen函数;

    

Linux下多进程服务端客户端模型二(粘包问题与一种解决方法)Linux下多进程服务端客户端模型二(粘包问题与一种解决方法)
 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 }
writen

   3.4利用这两个函数,即可完成读写。下文将介绍利用这两个函数完成的一个P2P程序,服务端与客户端互相发送用户输入的数据:程序的架构如下:

        Linux下多进程服务端客户端模型二(粘包问题与一种解决方法)

       3.4.1 服务端:

        

Linux下多进程服务端客户端模型二(粘包问题与一种解决方法)Linux下多进程服务端客户端模型二(粘包问题与一种解决方法)
  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 }
server.c

       3.4.2 客户端:

       

Linux下多进程服务端客户端模型二(粘包问题与一种解决方法)Linux下多进程服务端客户端模型二(粘包问题与一种解决方法)
  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 }
client.c

后记:

   由于上文中获得len的大小的方式是 测试buff[1024]中有效数据的长度的,所以实际上len每一个不会超过1024。

   但是,当fgets函数接受的一行长度大于1023的时候,它会将剩下的(1023以后的)字符串作为下一次的输入。然后发送端会发送两个Packet实例。

    而接收端接收到的两个pocket都有正确的长度,所以可以安全的接受,但是不幸的是,会将一条报文分成多条。。。

    该程序是单进程的,读者可以自行改成多进程的。