linux网络编程之socket(五):tcp流协议产生的粘包问题和解决方案

来源:互联网 发布:秃鹰配件名称及数据 编辑:程序博客网 时间:2024/05/29 15:22

首先说明的是发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,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、定长包
2、包尾加\r\n(ftp)
3、包头加上包体长度

4、更复杂的应用层协议


对于条目2,缺点是如果消息本身含有\r\n字符,则也分不清消息的边界。

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

[cpp] view plain copy
  1. //readn函数包装了read函数,用于读取定长包  
  2. ssize_t readn(int fd, void *buf, size_t count)  
  3. {  
  4.     size_t nleft = count;//所要读取的字节数  
  5.     ssize_t nread;  
  6.     char *bufp = (char *)buf;//指向buf缓冲区  
  7.   
  8.     while (nleft > 0)//读取数据,若读取到buf的字节与请求的字节相同,则直接返回请求字节数  
  9.     {  
  10.   
  11.         if ((nread = read(fd, bufp, nleft)) < 0)  
  12.         {  
  13.   
  14.             if (errno == EINTR)  
  15.                 continue;  
  16.             return -1;  
  17.         }  
  18.   
  19.         else if (nread == 0) //对方关闭或者已经读到eof  
  20.             return count - nleft;//返回实际读取的字节数,在此应该小于请求读取的字节数  
  21.   
  22.         bufp += nread;  
  23.         nleft -= nread;  
  24.     }  
  25.   
  26.     return count;  
  27. }  
  28.   
  29. //writen函数包装了write函数,用于写入定长包  
  30. ssize_t writen(int fd, const void *buf, size_t count)  
  31. {  
  32.     size_t nleft = count;  
  33.     ssize_t nwritten;  
  34.     char *bufp = (char *)buf;  
  35.   
  36.     while (nleft > 0)  
  37.     {  
  38.   
  39.         if ((nwritten = write(fd, bufp, nleft)) < 0)  
  40.         {  
  41.   
  42.             if (errno == EINTR)  
  43.                 continue;  
  44.             return -1;  
  45.         }  
  46.   
  47.         else if (nwritten == 0)//没有写满buf缓冲区,继续写入数据  
  48.             continue;  
  49.   
  50.         bufp += nwritten;  
  51.         nleft -= nwritten;  
  52.     }  
  53.     return count;  
  54. }  
需要注意的是一旦在我们的客户端/服务器程序中使用了这两个函数,则每次读取和写入的大小应该是一致的,比如设置为1024个字节,但定长包的问题在于不能根据实际情况读取数据,可能会造成网络阻塞,比如现在我们只是敲入了几个字符,却还是得发送1024个字节,造成极大的空间浪费。


此时条目3是比较好的解决办法,其实也可以算是自定义的一种简单应用层协议。比如我们可以自定义一个包体结构

[cpp] view plain copy
  1. struct packet {  
  2.     int len;  
  3.     char buf[1024];  
  4. };  
先接收固定的4个字节,从中得知实际数据的长度n,再调用readn 读取n个字符,这样数据包之间有了界定,且不用发送定长包浪费网络资源,是比较好的解决方案。服务器端在前面的fork程序的基础上把do_service函数更改如下:

[cpp] view plain copy
  1. void do_service(int conn)  
  2. {  
  3.     struct packet recvbuf;  
  4.     int n;  
  5.     while (1)  
  6.     {  
  7.         memset(&recvbuf, 0, sizeof(recvbuf));  
  8.         int ret = readn(conn, &recvbuf.len, 4);  
  9.         if (ret == -1)  
  10.             ERR_EXIT("read error");  
  11.         else if (ret < 4)   //客户端关闭  
  12.         {  
  13.             printf("client close\n");  
  14.             break;  
  15.         }  
  16.   
  17.         n = ntohl(recvbuf.len);  
  18.         ret = readn(conn, recvbuf.buf, n);  
  19.         if (ret == -1)  
  20.             ERR_EXIT("read error");  
  21.         if (ret < n)   //客户端关闭  
  22.         {  
  23.             printf("client close\n");  
  24.             break;  
  25.         }  
  26.   
  27.         fputs(recvbuf.buf, stdout);  
  28.         writen(conn, &recvbuf, 4 + n);  
  29.     }  
  30. }  
对于条目4,举例如 如TLV 编解码格式

[cpp] view plain copy
  1. struct TLV  
  2. {  
  3.     uint8_t tag;  
  4.     uint16_t len;  
  5.     char value[0];  
  6. }__attribute__((packed));  

注意value分配的是0大小,最后一个成员为可变长的数组(c99中的柔性数组),对于TLV(Type-Length-Value)形式的结构,或者其他需要变长

度的结构体,用这种方式定义最好。使用起来非常方便,创建时,malloc一段结构体大小加上可变长数据长度的空间给它,可变长部分可按数组的方式

访问,释放时,直接把整个结构体free掉就可以了。__attribute__(packed)用来强制不对struct TLV进行4字节对齐,目的是为了获取真实的TLV的

空间使用情况。

[cpp] view plain copy
  1. int main(void)  
  2. {  
  3.     char *szMsg = "aaaaaaaaa";  
  4.     cout << sizeof(TLV) << endl; //the size of TLV  
  5.     uint16_t len = strlen(szMsg) + 1;  
  6.     struct TLV *pTLV;  
  7.     pTLV = (struct TLV *)malloc(sizeof(struct TLV) + sizeof(char) * len);  
  8.     pTLV->tag = 0x2;  
  9.     pTLV->len = len;  
  10.     memcpy(pTLV->value, szMsg, len);  
  11.     cout << pTLV->value << endl;  
  12.     free(pTLV);  
  13.     pTLV = NULL;  
  14.     return 0;  

0 0
原创粉丝点击