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

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

(1)定长包

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

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

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

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

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

static void _set_tcp_nodelay(int fd) {int enable = 1;setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, (void*)&enable, sizeof(enable));}

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

/**实现: 这两个函数只是按需多次调用read和write系统调用直至读/写了count个数据 **/ /**返回值说明:== count: 说明正确返回, 已经真正读取了count个字节== -1 : 读取出错返回< count: 读取到了末尾 **/ ssize_t readn(int fd, void *buf, size_t count) {size_t nLeft = count;ssize_t nRead = 0;char *pBuf = (char *)buf;while (nLeft > 0){if ((nRead = read(fd, pBuf, nLeft)) < 0){//如果读取操作是被信号打断了, 则说明还可以继续读if (errno == EINTR)continue;//否则就是其他错误elsereturn -1;}//读取到末尾else if (nRead == 0)return count-nLeft;//正常读取nLeft -= nRead;pBuf += nRead;}return count; } /**返回值说明:== count: 说明正确返回, 已经真正写入了count个字节== -1 : 写入出错返回 **/ ssize_t writen(int fd, const void *buf, size_t count) {size_t nLeft = count;ssize_t nWritten = 0;char *pBuf = (char *)buf;while (nLeft > 0){if ((nWritten = write(fd, pBuf, nLeft)) < 0){//如果写入操作是被信号打断了, 则说明还可以继续写入if (errno == EINTR)continue;//否则就是其他错误elsereturn -1;}//如果 ==0则说明是什么也没写入, 可以继续写else if (nWritten == 0)continue;//正常写入nLeft -= nWritten;pBuf += nWritten;}return count; }

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

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

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

自定义包结构:

struct Packet {unsigned intmsgLen;//数据部分的长度(注:这是网络字节序)chartext[1024]; //报文的数据部分 };//echo 回射client端发送与接收代码…struct Packet buf;memset(&buf, 0, sizeof(buf));while (fgets(buf.text, sizeof(buf.text), stdin) != NULL){/**写入部分**/unsigned int lenHost = strlen(buf.text);buf.msgLen = htonl(lenHost);if (writen(sockfd, &buf, sizeof(buf.msgLen)+lenHost) == -1)err_exit("writen socket error");/**读取部分**/memset(&buf, 0, sizeof(buf));//首先读取首部ssize_t readBytes = readn(sockfd, &buf.msgLen, sizeof(buf.msgLen));if (readBytes == -1)err_exit("read socket error");else if (readBytes != sizeof(buf.msgLen)){cerr << "server connect closed… \nexiting…" << endl;break;}//然后读取数据部分lenHost = ntohl(buf.msgLen);readBytes = readn(sockfd, buf.text, lenHost);if (readBytes == -1)err_exit("read socket error");else if (readBytes != lenHost){cerr << "server connect closed… \nexiting…" << endl;break;}//将数据部分打印输出cout << buf.text;memset(&buf, 0, sizeof(buf));}…//server端echo部分的改进代码 void echo(int clientfd) {struct Packet buf;int readBytes;//首先读取首部while ((readBytes = readn(clientfd, &buf.msgLen, sizeof(buf.msgLen))) > 0){//网络字节序 -> 主机字节序int lenHost = ntohl(buf.msgLen);//然后读取数据部分readBytes = readn(clientfd, buf.text, lenHost);if (readBytes == -1)err_exit("readn socket error");else if (readBytes != lenHost){cerr << "client connect closed…" << endl;return ;}cout << buf.text;//然后将其回写回socketif (writen(clientfd, &buf, sizeof(buf.msgLen)+lenHost) == -1)err_exit("write socket error");memset(&buf, 0, sizeof(buf));}if (readBytes == -1)err_exit("read socket error");else if (readBytes != sizeof(buf.msgLen))cerr << "client connect closed…" << endl; } 注:网络字节序和本机字节序之间是必要的转换。

按行读取(由\r\n判断)

世俗的纷扰,生活的琐碎使人精疲力尽,

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

相关文章:

你感兴趣的文章:

标签云: