Linux下的socket编程实践(五)TCP的粘包问题和常用解决方案

TCP粘包问题的产生

由于TCP协议是基于字节流并且无边界的传输协议, 因此很有可能产生粘包问题。此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,但是接收方并不知道要一次接收多少字节的数据,这样接收方就收到了粘包数据。具体可以见下图:


假设主机A send了两条消息M1和M2 各10k 给主机B,由于主机B一次提取的字节数是不确定的,接收方提取数据的情况可能是:

• 一次性提取20k 数据
• 分两次提取,第一次5k,第二次15k
• 分两次提取,第一次15k,第二次5k
• 分两次提取,第一次10k,第二次10k(仅此正确)
• 分三次提取,第一次6k,第二次8k,第三次6k
• 其他任何可能

粘包问题产生的多种原因:

1、SQ_SNDBUF 套接字本身有缓冲区大小的限制 (发送缓冲区、接受缓冲区)

2、TCP传送的端 MSS大小限制

3、链路层也有MTU大小限制,如果数据包大于>MTU要在IP层进行分片,导致数据分割。

4、TCP的流量控制和拥塞控制,也可能导致粘包

5、文章开始提到的TCP延迟确认机制等


注: 关于MTU和MSS

MSS指的是TCP中的一个概念。MTU是一个没有固定到特定OSI层的概念,不受其他特定协议限制。也就是说第二层会有MTU,第三层会有MTU,像MPLS这样的第2.5层协议,也有自己的MTU值。并且不同层之间存在关联关系。举个例子:如果你要搬家,需要把东西打包,用车运走。这样的情况下,车的大小受路的宽度限制;箱子的大小受车限制;能够搬运的东西的大小受箱子的限制。这时可以将路的宽度理解成第二层的MTU,车的大小理解成第三层的MTU,箱子的大小理解成第四层的MTU,搬运的东西理解成MSS。


粘包问题的解决方案(本质上是要在应用层维护消息和消息之间的边界)

(1)定长包

   该方式并不实用: 如果所定义的长度过长, 则会浪费网络带宽,增加网络负担;而又如果定义的长度过短, 则一条消息又会拆分成为多条, 仅在TCP的应用一层就增加了合并的开销。

(2)包尾加\r\n(FTP使用方案)

   如果消息本身含有\r\n字符,则也分不清消息的边界;

(3)报文长度+报文内容,自定义包结构

(4)更复杂的应用层协议

注:简单的使用  setsockopt 设置开启 TCP_NODELAY 禁用  Nagle s Algorithm可以解决上述第5个问题(延迟确认机制)。

[cpp]  view plain  copy
  1. static void _set_tcp_nodelay(int fd) {  
  2.     int enable = 1;  
  3.     setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, (void*)&enable, sizeof(enable));  
  4. }  
著名的 Nginx服务器 默认是开启了这个选项的.....


因为TCP协议是面向流的,read和write调用的返回值往往小于参数指定的字节数。对于read调用(套接字标志为阻塞),如果接收缓冲区中有20字节,请求读100个字节,就会返回20;对于write调用,如果请求写100个字节,而发送缓冲区中只有20个字节的空闲位置,那么write会阻塞,直到把100个字节全部交给发送缓冲区才返回;还有信号中断之后需要处理为 继续读写;为避免这些情况干扰主程序的逻辑,确保读写我们所请求的字节数,我们实现了两个包装函数readn和writen,如下所示。

[cpp]  view plain  copy
  1. /**实现:  
  2. 这两个函数只是按需多次调用read和write系统调用直至读/写了count个数据  
  3. **/    
  4. /**返回值说明:  
  5.     == count: 说明正确返回, 已经真正读取了count个字节  
  6.     == -1   : 读取出错返回  
  7.     <  count: 读取到了末尾  
  8. **/    
  9. ssize_t readn(int fd, void *buf, size_t count)    
  10. {    
  11.     size_t nLeft = count;    
  12.     ssize_t nRead = 0;    
  13.     char *pBuf = (char *)buf;    
  14.     while (nLeft > 0)    
  15.     {    
  16.         if ((nRead = read(fd, pBuf, nLeft)) < 0)    
  17.         {    
  18.             //如果读取操作是被信号打断了, 则说明还可以继续读    
  19.             if (errno == EINTR)    
  20.                 continue;    
  21.             //否则就是其他错误    
  22.             else    
  23.                 return -1;    
  24.         }    
  25.         //读取到末尾    
  26.         else if (nRead == 0)    
  27.             return count-nLeft;    
  28.     
  29.         //正常读取    
  30.         nLeft -= nRead;    
  31.         pBuf += nRead;    
  32.     }    
  33.     return count;    
  34. }    

[cpp]  view plain  copy
  1. /**返回值说明:  
  2.     == count: 说明正确返回, 已经真正写入了count个字节  
  3.     == -1   : 写入出错返回  
  4. **/    
  5. ssize_t writen(int fd, const void *buf, size_t count)    
  6. {    
  7.     size_t nLeft = count;    
  8.     ssize_t nWritten = 0;    
  9.     char *pBuf = (char *)buf;    
  10.     while (nLeft > 0)    
  11.     {    
  12.         if ((nWritten = write(fd, pBuf, nLeft)) < 0)    
  13.         {    
  14.             //如果写入操作是被信号打断了, 则说明还可以继续写入    
  15.             if (errno == EINTR)    
  16.                 continue;    
  17.             //否则就是其他错误    
  18.             else    
  19.                 return -1;    
  20.         }    
  21.         //如果 ==0则说明是什么也没写入, 可以继续写    
  22.         else if (nWritten == 0)    
  23.             continue;    
  24.     
  25.         //正常写入    
  26.         nLeft -= nWritten;    
  27.         pBuf += nWritten;    
  28.     }    
  29.     return count;    
  30. }    

报文长度+报文内容(自定义包结构)

 发报文时:前四个字节长度+报文内容一次性发送;

 收报文时:先读前四个字节,求出报文内容长度;根据长度读数据

 自定义包结构:
[cpp]  view plain  copy
  1. struct Packet    
  2. {    
  3.     unsigned int      msgLen;     //数据部分的长度(注:这是网络字节序)    
  4.     char            text[1024]; //报文的数据部分    
  5. };  
[cpp]  view plain  copy
  1. //echo 回射client端发送与接收代码  
  2. ...  
  3.     struct Packet buf;  
  4.     memset(&buf, 0, sizeof(buf));  
  5.     while (fgets(buf.text, sizeof(buf.text), stdin) != NULL)  
  6.     {  
  7.         /**写入部分**/  
  8.         unsigned int lenHost = strlen(buf.text);  
  9.         buf.msgLen = htonl(lenHost);  
  10.         if (writen(sockfd, &buf, sizeof(buf.msgLen)+lenHost) == -1)  
  11.             err_exit("writen socket error");  
  12.   
  13.         /**读取部分**/  
  14.         memset(&buf, 0, sizeof(buf));  
  15.         //首先读取首部  
  16.         ssize_t readBytes = readn(sockfd, &buf.msgLen, sizeof(buf.msgLen));  
  17.         if (readBytes == -1)  
  18.             err_exit("read socket error");  
  19.         else if (readBytes != sizeof(buf.msgLen))  
  20.         {  
  21.             cerr << "server connect closed... \nexiting..." << endl;  
  22.             break;  
  23.         }  
  24.   
  25.         //然后读取数据部分  
  26.         lenHost = ntohl(buf.msgLen);  
  27.         readBytes = readn(sockfd, buf.text, lenHost);  
  28.         if (readBytes == -1)  
  29.             err_exit("read socket error");  
  30.         else if (readBytes != lenHost)  
  31.         {  
  32.             cerr << "server connect closed... \nexiting..." << endl;  
  33.             break;  
  34.         }  
  35.         //将数据部分打印输出  
  36.         cout << buf.text;  
  37.         memset(&buf, 0, sizeof(buf));  
  38.     }  
  39. ...  

[cpp]  view plain  copy
  1. //server端echo部分的改进代码    
  2. void echo(int clientfd)    
  3. {    
  4.     struct Packet buf;    
  5.     int readBytes;    
  6.     //首先读取首部    
  7.     while ((readBytes = readn(clientfd, &buf.msgLen, sizeof(buf.msgLen))) > 0)    
  8.     {    
  9.         //网络字节序 -> 主机字节序    
  10.         int lenHost = ntohl(buf.msgLen);    
  11.         //然后读取数据部分    
  12.         readBytes = readn(clientfd, buf.text, lenHost);    
  13.         if (readBytes == -1)    
  14.             err_exit("readn socket error");    
  15.         else if (readBytes != lenHost)    
  16.         {    
  17.             cerr << "client connect closed..." << endl;    
  18.             return ;    
  19.         }    
  20.         cout << buf.text;    
  21.     
  22.         //然后将其回写回socket    
  23.         if (writen(clientfd, &buf, sizeof(buf.msgLen)+lenHost) == -1)    
  24.             err_exit("write socket error");    
  25.         memset(&buf, 0, sizeof(buf));    
  26.     }    
  27.     if (readBytes == -1)    
  28.         err_exit("read socket error");    
  29.     else if (readBytes != sizeof(buf.msgLen))    
  30.         cerr << "client connect closed..." << endl;    
  31. }    
注:网络字节序和本机字节序之间是必要的转 换。
按行读取(由\r\n判断)
[cpp]  view plain  copy
  1. ssize_t recv(int sockfd, void *buf, size_t len, int flags);    
  2. ssize_t send(int sockfd, const void *buf, size_t len, int flags);    
与read相比,recv只能用于套接字文件描述符,但是多了一个flags,这个flags能够帮助我们实现解决粘包问题的操作。

MSG_PEEK(可以读数据,但不从缓存区中读走[仅仅是一瞥],利用此特点可以方便的实现按行读取数据;一个一个字符的读,多次调用系统调用read方法,效率不高,但是可以判断'\n')。

   This  flag  causes the receive operation to return data from the beginning of 

the receive queue without removing that  data  from the queue.  Thus, a subsequent 

receive call will return the same data.

readline实现思想:

   在readline函数中,我们先用recv_peek”偷窥” 一下现在缓冲区有多少个字符并读取到pBuf,然后查看是否存在换行符'\n'。如果存在,则使用readn连同换行符一起读取(作用相当于清空socket缓冲区); 如果不存在,也清空一下缓冲区, 且移动pBuf的位置,回到while循环开头,再次窥看。注意,当我们调用readn读取数据时,那部分缓冲区是会被清空的,因为readn调用了read函数。还需注意一点是,如果第二次才读取到了'\n',则先用returnCount保存了第一次读取的字符个数,然后返回的ret需加上原先的数据大小。


[cpp]  view plain  copy
  1. /**示例: 通过MSG_PEEK封装一个recv_peek函数(仅查看数据, 但不取走)**/    
  2. ssize_t recv_peek(int sockfd, void *buf, size_t len)    
  3. {    
  4.     while (true)    
  5.     {    
  6.         int ret = recv(sockfd, buf, len, MSG_PEEK);    
  7.         //如果recv是由于被信号打断, 则需要继续(continue)查看    
  8.         if (ret == -1 && errno == EINTR)    
  9.             continue;    
  10.         return ret;    
  11.     }    
  12. }    
  13.     
  14. /**使用recv_peek实现按行读取readline(只能用于socket)**/    
  15. /** 返回值说明:  
  16.     == 0:   对端关闭  
  17.     == -1:  读取出错  
  18.     其他:    一行的字节数(包含'\n')  
  19. **/    
  20. ssize_t readline(int sockfd, void *buf, size_t maxline)    
  21. {    
  22.     int ret;    
  23.     int nRead = 0;    
  24.     int returnCount = 0;    
  25.     char *pBuf = (char *)buf;    
  26.     int nLeft = maxline;    
  27.     while (true)    
  28.     {    
  29.         ret = recv_peek(sockfd, pBuf, nLeft);    
  30.         //如果查看失败或者对端关闭, 则直接返回    
  31.         if (ret <= 0)    
  32.             return ret;    
  33.         nRead = ret;    
  34.         for (int i = 0; i < nRead; ++i)    
  35.             //在当前查看的这段缓冲区中含有'\n', 则说明已经可以读取一行了    
  36.             if (pBuf[i] == '\n')    
  37.             {    
  38.                 //则将缓冲区内容读出    
  39.                 //注意是i+1: 将'\n'也读出    
  40.                 ret = readn(sockfd, pBuf, i+1);    
  41.                 if (ret != i+1)    
  42.                     exit(EXIT_FAILURE);    
  43.                 return ret + returnCount;    
  44.             }    
  45.     
  46.         // 如果在查看的这段消息中没有发现'\n', 则说明还不满足一条消息,    
  47.         // 在将这段消息从缓冲中读出之后, 还需要继续查看    
  48.         ret = readn(sockfd, pBuf, nRead);;    
  49.         if (ret != nRead)    
  50.             exit(EXIT_FAILURE);    
  51.         pBuf += nRead;    
  52.         nLeft -= nRead;    
  53.         returnCount += nRead;    
  54.     }    
  55.     //如果程序能够走到这里, 则说明是出错了    
  56.     return -1;    
  57. }    

client端:
[cpp]  view plain  copy
  1. ...    
  2.     char buf[512] = {0};    
  3.     memset(buf, 0, sizeof(buf));    
  4.     while (fgets(buf, sizeof(buf), stdin) != NULL)    
  5.     {    
  6.         if (writen(sockfd, buf, strlen(buf)) == -1)    
  7.             err_exit("writen error");    
  8.         memset(buf, 0, sizeof(buf));    
  9.         int readBytes = readline(sockfd, buf, sizeof(buf));    
  10.         if (readBytes == -1)    
  11.             err_exit("readline error");    
  12.         else if (readBytes == 0)    
  13.         {    
  14.             cerr << "server connect closed..." << endl;    
  15.             break;    
  16.         }    
  17.         cout << buf;    
  18.         memset(buf, 0, sizeof(buf));    
  19.     }    
  20. ...    

server端:
[cpp]  view plain  copy
  1. void echo(int clientfd)    
  2. {    
  3.     char buf[512] = {0};    
  4.     int readBytes;    
  5.     while ((readBytes = readline(clientfd, buf, sizeof(buf))) > 0)    
  6.     {    
  7.         cout << buf;    
  8.         if (writen(clientfd, buf, readBytes) == -1)    
  9.             err_exit("writen error");    
  10.         memset(buf, 0, sizeof(buf));    
  11.     }    
  12.     if (readBytes == -1)    
  13.         err_exit("readline error");    
  14.     else if (readBytes == 0)    
  15.         cerr << "client connect closed..." << endl;    
  16. }    

最后附上 TLV格式及其编解码的示例   http://blog.csdn.net/chexlong/article/details/6974201








 



猜你喜欢

转载自blog.csdn.net/zjy900507/article/details/80046822
今日推荐