TCP粘包问题的解决方案01——自定义包体

 
粘包问题:应用层要发送数据,需要调用write函数将数据发送到套接口发送缓冲区。如果应用层数据大小大于SO_SNDBUF,
那么,可能产生这样一种情况,应用层的数据一部分已经被发送了,还有一部分还在套接口缓冲区待发送。此时,对方延迟接收,就容易产生粘包。
另一方面,TCP传输有MSS限制,也会对数据进行分割。第三个原因,由于MTU存在,也可能分割数据。都会产生粘包问题



   粘包问题解决方案:本质上是要在应用层维护消息与消息的边界。

1、定长包

2、包尾加 (FTP协议)

3、包头加上包体长度

4、更加复杂的应用层协议

  利用发送定常包解决粘包问题时,对于定长包的接收,是一个主要问题,在程序中,封装了readn(接收确切数目的读操作)与writen(发送。。。)函数来解决这个问题。

   

定长包发送程序:
  1 /*
  2 客户端程序中发送定长包解决粘包问题:
  3 */
  4 #include<unistd.h>
  5 #include<sys/types.h>
  6 #include<sys/socket.h>
  7 #include<string.h>
  8 #include<stdlib.h>
  9 #include<stdio.h>
 10 #include<errno.h>
 11 #include<netinet/in.h>
 12 #include<arpa/inet.h>
 13 #include<signal.h>
 14 #define ERR_EXIT(m)
 15     do
 16     {
 17         perror(m);
 18         exit(EXIT_FAILURE);
 19     }while(0)
 20 struct packet
 21 {
 22     int len;//包头
 23     char buf[1024];//包体
 24 };
 25 //接收确切数目的读操作
 26 ssize_t readn(int fd,void *buf,size_t count)
 27 {
 28     size_t nleft=count;
 29     ssize_t nread;
 30     char *bufp=(char*)buf;
 31 //剩余字节数大于0就循环
 32     while(nleft>0)
 33     {
 34         if((nread=read(fd,bufp,nleft))<0)
 35         {
 36             if(errno==EINTR)
 37                 continue; //被信号中断
 38             else
 39                 return -1;//失败
 40         }
 41         //对等方关闭了
 42         else if(nread==0)
 43             return (count-nleft);//已经读取的字节数
 44         bufp+=nread;
 45         nleft-=nread;
 46     }
 47     return count;
 48 }
 49 //发送确切数目的写操作
 50 ssize_t writen(int fd, const void *buf, size_t count)
 51 {
 52     size_t nleft=count;
 53     ssize_t nwritten;
 54     char *bufp=(char*)buf;
 55     while(nleft>0)
 56     {
 57         if((nwritten=write(fd,bufp,nleft))<=0)
 58         {
 59             if(errno==EINTR)
 60                 continue;//信号中断
 61             return -1;
 62         }else if(nwritten==0)//write返回0,此时write()什么也不做,好像什么都没发生
 63             continue;
 64         bufp+=nwritten;
 65         nleft-=nwritten;
 66     }
 67     return count;
 68 
 69 }
 70 int main(void)
 71 {
 72     int sock;//客户端创建套接字
 73     if((sock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0)
 74         ERR_EXIT("socket error");
 75     
 76     struct sockaddr_in servaddr;//本地协议地址赋给一个套接字
 77     memset(&servaddr,0,sizeof(servaddr));
 78     servaddr.sin_family=AF_INET;
 79     servaddr.sin_port=htons(5188);
 80     
 81     servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");//服务器段地址
 82     //inet_aton("127.0.0.1",&servaddr.sin_addr);
 83     
 84     if(connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
 85         ERR_EXIT("connect");
 86     struct packet sendbuf;//从标准输入接收发送包
 87     struct packet recvbuf;//获得服务器端的回射包
 88     memset(&sendbuf,0,sizeof(sendbuf));
 89     memset(&recvbuf,0,sizeof(recvbuf));
 90     int n;//fgets() 函数中的 size 如果小于字符串的长度,那么字符串将会被截取;如果 size 大于字符串的长度则多余的部分系统会自动用 '' 填充。所以假如你定义的字符数组长度为 n,那么 fgets() 中的 size 就指定为 n–1,留一个给 '' 就行了。
 91     while(fgets(sendbuf.buf,sizeof(sendbuf.buf),stdin)!=NULL)//默认有换行符
 92     {
 93         n=strlen(sendbuf.buf);
 94         //包体长度要转成网络字节序。
 95         sendbuf.len=htonl(n);
 96         writen(sock,&sendbuf,4+n);
 97         //客户端接收回射数据包头内容。
 98         int ret=readn(sock,&recvbuf.len,4);
 99         if(ret==-1)
100             ERR_EXIT("readn");
101         else if(ret<4)    //可能中途中断了
102         {
103             printf("clinet close
");
104             break;
105         }
106         n=ntohl(recvbuf.len);
107         //接收包体内容    
108         ret=readn(sock,recvbuf.buf,n);
109         if(ret==-1)
110             ERR_EXIT("readn");
111         else if(ret<n)
112         {
113             printf("clinet close
");
114             break;
115         }        
116         fputs(recvbuf.buf,stdout);
117         memset(&sendbuf,0,sizeof(sendbuf));
118         memset(&recvbuf,0,sizeof(recvbuf));
119     }
120     close(sock);
121     
122     return 0;
123 }

  定长包的接收服务器程序:

  1 /*
  2 流协议与粘包
  3 粘包产生的原因:若SO_SNDBUF的大小没有应用层一条消息大,可能产生粘包问题,因为因为应用层消息被分割,一部分发送了,一部分还在应用层缓冲区。详见UNP48页
  4 粘包处理方案:本质上是要在应用层维护消息与消息的边界
  5 1、定长包 2、包尾加 
 (ftp) 3、包头加上包体长度(例如包头定长4字节,收取时先读取包头算出包体长度) 4、更复杂的应用层协议
  6 
  7 封装readn  writen程序
  8 ssize_t read(int fd, void *buf, size_t count);
  9 ssize_t write(int fd, const void *buf, size_t count);
 10 */
 11 #include<unistd.h>
 12 #include<sys/types.h>
 13 #include<sys/socket.h>
 14 #include<string.h>
 15 #include<stdlib.h>
 16 #include<stdio.h>
 17 #include<errno.h>
 18 #include<netinet/in.h>
 19 #include<arpa/inet.h>
 20 #define ERR_EXIT(m)
 21     do
 22     {
 23         perror(m);
 24         exit(EXIT_FAILURE);
 25     }while(0)
 26 struct packet
 27 {
 28     int len;//包头
 29     char buf[1024];//包体
 30 };
 31 //接收确切数目的读操作
 32 ssize_t readn(int fd,void *buf,size_t count)
 33 {
 34     size_t nleft=count;
 35     ssize_t nread;
 36     char *bufp=(char*)buf;
 37 //剩余字节数大于0就循环
 38     while(nleft>0)
 39     {
 40         if((nread=read(fd,bufp,nleft))<0)
 41         {
 42             if(errno==EINTR)
 43                 continue;
 44             else
 45                 return -1;
 46         }
 47         //对等方关闭了
 48         else if(nread==0)
 49             return (count-nleft);//已经读取的字节数
 50         bufp+=nread;
 51         nleft-=nread;
 52     }
 53     return count;
 54 }
 55 //发送确切数目的写操作
 56 ssize_t writen(int fd, const void *buf, size_t count)
 57 {
 58     size_t nleft=count;
 59     ssize_t nwritten;
 60     char *bufp=(char*)buf;
 61     while(nleft>0)
 62     {
 63         if((nwritten=write(fd,bufp,nleft))<=0)
 64         {
 65             if(errno==EINTR)
 66                 continue;
 67             return -1;
 68         }else if(nwritten==0)//好像什么都没发生
 69             continue;
 70         bufp+=nwritten;
 71         nleft-=nwritten;
 72     }
 73     return count;
 74 
 75 }
 76 //服务器回射。
 77 void do_service(int conn)
 78 {
 79         struct packet recvbuf;
 80         int n;
 81         while(1)
 82         {
 83             memset(&recvbuf,0,sizeof(recvbuf));
 84             //使用readn之后客户端发送的数据不足n会阻塞
 85             //在客户端程序中确定消息的边界,发送定长包
 86             int ret=readn(conn,&recvbuf.len,4);                                                               
 87             //客户端关闭
 88             if(ret==-1)
 89                 ERR_EXIT("read error");            
 90             else if(ret<4)//中途中断了。
 91             {
 92                 printf("client close
");
 93                 break;//不用继续循环等待客户端数据
 94             }
 95             //接收包体
 96             n=ntohl(recvbuf.len);//包体长度
 97             ret=readn(conn,recvbuf.buf,n);
 98             if(ret==-1)
 99                 ERR_EXIT("read error");            
100             else if(ret<n)//接收到的字节数不足,对端中途关闭
101             {
102                 printf("client close
");
103                 break;//不用继续循环等待客户端数据
104             }
105             fputs(recvbuf.buf,stdout);
106             writen(conn,&recvbuf,4+n);
107         }
108 }
109 int main(void)
110 {
111     int listenfd;
112     if((listenfd=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0)
113         ERR_EXIT("socket error");
114     //if((listenfd=socket(PF_INET,SOCK_STREAM,0))<0)
115     
116 
117     //本地协议地址赋给一个套接字
118     struct sockaddr_in servaddr;
119     memset(&servaddr,0,sizeof(servaddr));
120     servaddr.sin_family=AF_INET;
121     servaddr.sin_port=htons(5188);
122     servaddr.sin_addr.s_addr=htonl(INADDR_ANY);//表示本机地址
123     //servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");
124     //inet_aton("127.0.0.1",&servaddr.sin_addr);
125     
126     //开启地址重复使用,关闭服务器再打开不用等待TIME_WAIT
127     int on=1;
128     if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0)
129         ERR_EXIT("setsockopt error");
130     //绑定本地套接字
131     if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
132         ERR_EXIT("bind error");
133     if(listen(listenfd,SOMAXCONN)<0)//设置监听套接字(被动套接字)
134         ERR_EXIT("listen error");
135     
136     struct sockaddr_in peeraddr;//对方套接字地址
137     socklen_t peerlen=sizeof(peeraddr);
138     int conn;//已连接套接字(主动套接字)
139     pid_t pid;
140     while(1){
141         if((conn=accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen))<0)
142             ERR_EXIT("accept error");
143         //连接好之后就构成连接,端口是客户端的。peeraddr是对端
144         printf("ip=%s port=%d
",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
145         pid=fork();
146         if(pid==-1)
147             ERR_EXIT("fork");
148         if(pid==0){    
149                 close(listenfd);
150                 do_service(conn);
151                 //某个客户端关闭,结束该子进程,否则子进程也去接受连接
152                 exit(EXIT_SUCCESS);
153         }else     close(conn);
154     }
155     return 0;
156 }
原文地址:https://www.cnblogs.com/wsw-seu/p/8214422.html