一、前言

我们知道,TCP是一个面向字节流的传输层协议。“流” 意味着 TCP 所传输的数据是没有边界的。这不同于 UDP 协议提供的是面向消息的传输服务,其传输的数据是有边界的。TCP 的发送方无法保证对方每次收到的都是一个完整的数据包。于是就有了粘包、拆包问题的出现。粘包、拆包问题只发生在TCP协议中。

二、什么是粘包、拆包?

假设客户端向服务器连续发送了两个数据包,用 packet1 和 packet2 来表示,那么服务端收到的数据可以分为下面三种情况:

  • 第一种情况,接收端正常收到两个数据包,即没有发生拆包和粘包的现象,此种情况不在本文的讨论范围内。

  • 第二种情况,接收端只收到一个TCP报文段,去掉首部后,这一个报文段中包含了发送端发送来的两个数据包的信息,这种现象即为粘包。这种情况由于接收端不知道这两个数据包的界限,所以对于接收端来说很难处理。

  • 第三种情况,这种情况有两种表现形式,如下图。接收端收到了两个TCP报文段,但是去掉首部后,两个数据包要么是不完整的,要么就是多出来一部分,这种现象即为粘包、拆包问题。同第二种情况一样,由于不知道两个数据包的界限,对于接收端来说同样不好处理。

综上所述,我们可以得出结论:

TCP粘包是指发送方发送的若干数据包到达接收方时粘成一个数据包,从接收缓存看,后一个数据包的头紧接着前一个数据包的尾。

TCP拆包是指应用程序的数据包被拆分成若干部分发送出去,从接收缓存看,接收方收到的只是数据包的一部分内容。

由此可见,粘包情况有两种,一种是粘在一起的包都是完整的数据包,另一种情况是粘在一起的包有不完整的数据包。

三、粘包产生原因及解决办法

3.1 TCP粘包产生原因

TCP 发生粘包、拆包问题,主要是由于以下几个原因:

(1)应用进程写入的数据量大于TCP发送缓冲区的大小,这将会发生拆包。应用进程调用write()系统调用,将应用层的缓冲区中的数据拷贝到TCP socket 的发送缓冲区。而TCP的发送缓冲区有一个 SO_SNDBUF 的大小限制,如果应用层的缓冲区数据大小大于TCP发送缓冲区的大小,则数据包需要进行拆包处理,分多次进行发送。

(2)应用进程写入的数据量小于TCP发送缓冲区的大小,这将会发生粘包。应用进程调用write()系统调用,将应用层的缓冲区中的数据拷贝到TCP socket 的发送缓冲区。由于TCP发送缓存空间足够,它会等到有多个数据包时,再组装成一个TCP报文段,然后通过网卡发送到网络中去。

TCP协议允许发送端将几次发送的数据包先缓存起来合成一个数据包发送到网络上去,因为这样可以获得更高的效率,这一行为通常是在操作系统提供的socket中实现,所以在应用层对此毫无所觉。所以我们在程序中调用socketsend()函数,发送了数据后操作系统有可能是先缓存了起来,等待后续的数据。当socket的发送缓存的数据足够多时,再一起发送出去,而不是立即发送出去。这就是缓存发送,它可以提高网络信道的利用率。

(3)当应用进程发送的数据包大于 MSS(最大报文段长度)时,将会发生拆包。TCP在传送数据时,是以报文段为单位发送数据的,而待发送的数据是以 MSS 为单位进行分割的。如果应用进程发送的数据包packet的长度大于 MSS,必然需要对该数据包进行拆包处理。

<注1> MSS(Maximum Segment Size,最大报文段长度)  这个名词的含义是:每一个 TCP 报文段中的数据部分的最大长度。它不是指整个TCP报文段的最大长度,而是 “TCP报文段的长度 - TCP首部长度”。TCP 在组装报文段时,是以 MSS 为单位对需要传送的数据进行分割的,然后加上TCP首部后,就组装成一个完整的TCP报文段了。MSS的默认值一般是 536 字节长。

补充说明】TCP 之所以有一个 MSS 的限定,是因为在数据链路层的数据传输单位—数据帧,它有一个最大传送长度的限制,即 MTU。要求发送的帧的数据部分长度不超过 MTU,如果超过了就需要进行分包发送处理。

MTU(Maximum Transfer Unit,最大传送单元) 这个名词的含义是:数据链路层协议中规定的所能传送的帧的数据部分长度上限

<注2> TCP报文段 = TCP首部 + TCP数据部分

(4) 接收方不及时读取接收缓冲区中的数据,将会发生粘包。这是因为接收方先把接收到的数据存放在内核接收缓冲区中,用户进程从接收缓冲区读取数据,若下一个数据包到达时前一个数据包尚未被用户进程取走,则下一个数据包放到内核接收缓冲区时就和前一数据包粘在一起,而用户进程根据预先设定的缓冲区大小从内核接收缓冲区读取数据,这样就一次性读取到了多个数据包。

以上这几种情况,都会导致一个完整的应用层数据包被分割成多片或者是多个应用层数据包拼接成一个更大的传输层数据包,再发送出去,从而导致接收方不是按完整的应用层数据包方式来接收数据的。

3.2 什么时候需要考虑粘包问题?

TCP是长连接,并且传输的是结构化数据时,如:传送的是一个结构体类型的数据,由于不知道结构化数据的边界,容易导致粘包问题的出现。这时需要考虑粘包问题的影响。

什么时候不需要考虑粘包问题?

(1)如果 TCP 是短连接,即只进行一次数据通信过程,通信完成就关闭连接,这样就不会出现粘包问题。

(2)如果传输的是字符串、文件等无结构化数据时,也不会出现粘包问题。因为发送方只管发送,接收方只管接收存储就行了。

3.3 粘包问题的解决办法

粘包问题的最本质原因在于接收方无法分辨出消息与消息之间的边界在哪?我们通过使用某种方法给出边界,常用的方法有以下几种:

(1)发送定长包。即发送端将每个数据包封装为固定长度(长度不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。(适合定长结构的数据)

(2)包头加上包体长度。发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便可以知道每一个数据包的实际长度了。(适合不定长结构的数据)

(3)在包尾部设置边界标记。发送端在每个数据包尾部添加边界标记,可以使用特殊符号作为边界标记。如此,接收端通过这个边界标记就可以将不同的数据包拆分开来。但这可能会存在一个问题:如果数据包内容中也包含有边界标记,则会被误判为消息的边界,导致出错。这样方法要视具体情况而定。例如,FTP协议就是采用 "\r\n" 来识别一个消息的边界的。

四、粘包解决方案—代码实现

4.1 粘包解决方案1:使用定长包

这里需要实现对 read()、write() 系统调用的封装,函数声明如下:

ssize_t readn(int fd, void *buf, size_t count);ssize_t writen(int fd, void *buf, size_t count);

这两个封装函数的参数列表和返回值与 read、write 一致。它们的作用是读取 / 写入 count 个字节后再返回。代码实现如下:

/*
readn 函数
读取count字节的数据
*/
ssize_t readn(int fd, void *buf, size_t count)
{int left = count;           //剩余的字节数char * ptr = (char*)buf ;while(left>0){int readBytes = read(fd, ptr, left);if(readBytes < 0)//read函数小于0有两种情况:1中断;2出错{if(errno == EINTR) //读被中断{continue;}elsereturn -1;}if(readBytes == 0) //读到了EOF{//对方关闭了呀printf("peer close\n");break;}left -= readBytes;ptr += readBytes;}return count - left;
}/*
writen 函数
写入count字节的数据
*/
ssize_t writen(int fd, void *buf, size_t count)
{int left = count ;char * ptr = (char *)buf;while(left > 0){int writeBytes = write(fd, ptr, left);if(writeBytes<0){if(errno == EINTR)continue;return -1;}else if(writeBytes == 0)continue;left -= writeBytes;ptr += writeBytes;}return count;
}

有了这两个封装函数,我们就可以使用定长包来发送/接收数据了。示例代码如下:

char readbuf[512] = {0};
readn(conn,readbuf,sizeof(readbuf));  //每次读取512个字节//同理,写入的时候也写入512个字节
char writebuf[512] = {0};
fgets(writebuf, sizeof(writebuf), stdin);  //从终端输入数据
writen(conn, writebuf, sizeof(writebuf));

程序分析:每个消息体都是以固定的 512 字节长度来发送,以此来区分每一个消息,这就是用定长包的方法解决粘包的问题。

缺点:定长包解决方案的缺点在于会增加网络的负担,无论每次发送的有效数据是多大,都得按照固定的数据长度进行发送。

4.2 粘包解决方案2:使用结构体,显式说明数据部分的长度

在这个方案中,我们需要定义一个消息结构体,结构体中指明包体中数据部分的长度,用4个字节的无符号整数来表示。当接收端收到消息后,先读取前4个字节,获取消息的长度,然后根据消息长度进行数据的读取。定义的结构体如下:

typedef unsigned int uint32;typedef struct packet
{uint32 msgLen;   //4个字节大小,原来描述数据部分的长度char data[512];  //数据部分
}PACKET_T;

读写过程如下所示,这里只给出关键代码进行说明:

//发送数据过程
PACKET_T writebuf;
memset(&writebuf,0,sizeof(writebuf));
while(fgets(writebuf.data, sizeof(writebuf.data), stdin) != NULL)
{int n = strlen(writebuf.data);     //计算要发送的数据的字节数writebuf.msgLen =htonl(n);         //将数据部分长度保存在msgLen字段中,注意字节序的转换writen(conn, &writebuf, 4+n);      //发送数据,数据长度为4个字节的msgLen 加上data长度memset(&writebuf,0,sizeof(writebuf));
}

下面是读取数据的过程,先读取4个字节长度,获取msgLen字段的值,该字段指示了有效数据的长度,然后依据该字段给出长度再读取data部分。

#define err_exit(m)\
do{\perror(m);\exit(EXIT_FAILURE);\
}while(0)//读取数据过程
PACKET_T readbuf;
memset(&readbuf,0,sizeof(readbuf));
int ret = readn(conn, &readbuf.msgLen, 4); //先读取四个字节,确定后续数据部分的长度
if(ret == -1)
{err_exit("readn");
}
else if(ret == 0)
{printf("peer close\n");break;
}
int dataBytes = ntohl(readbuf.msgLen); //字节序的转换
int readBytes = readn(conn, readbuf.data, dataBytes); //读取出后续的数据
if(readBytes == 0)
{printf("peer close\n");break;
}
if(readBytes<0)
{err_exit("read");
}

4.3 粘包解决方案3:按行读取

FTP 协议就是采用 "\r\n" 标记来识别一个消息的边界的。我们这里实现一个按行读取的函数,该函数能够按 '\n' 来识别消息的边界。这里先介绍一个函数:

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

与 read 函数相比,recv 函数的区别在于两点:

1. recv 函数只能用于用于socket IO。

2. recv 函数含义 flags 参数,可以指定一些选项。

recv 函数的 flags 参数常用的选项有:

  • MSG_OOB:接收带外数据,即通过紧急指针发送的数据。
  • MSG_PEEK:从缓冲区中读取数据,但并不从缓冲区中清除所读数据。

为了实现按行读取,我们需要使用 recv 函数的 MSG_PEEK 选项。PEEK 本意是“偷看、窥视”,我们可以理解为“窥视数据”,看看 socket 的缓冲区是否有某种数据,但不清除缓冲区中的内容。

/**
封装了recv函数
函数功能:从缓冲区中读取指定长度的数据,但不清除缓冲区内容。
返回值说明:-1 读取出错
*/
ssize_t read_peek(int sockfd, void *buf, size_t len )
{while(1){int ret = recv (sockfd , buf ,len ,MSG_PEEK);if(ret  == -1){if(errno ==EINTR)  //出现中断continue ;}return ret ;}
}

下面的函数实现了按行读取的功能,代码如下:

/**
按行读取数据
参数说明:sockfd:  套接字描述符buf:    应用层缓冲区,保存读取到的数据maxline: 所规定的一行的长度
返回值说明:== 0 : 对端关闭== -1 :读取错误其他:  一行的字节数,包含\n
*/
ssize_t read_line (int sockfd , void *buf ,size_t maxline)
{int ret;int nRead = 0;int left = maxline ;         //剩下的字节数char *pbuf = (char *) buf ;int count = 0;while(1){ret = read_peek ( sockfd, pbuf, left);  //从socket缓冲区中读取指定长度的内容,但并不删除if(ret <0){return ret;}nRead = ret;for(int i = 0; i<nRead; ++i)//看看读取出来的数据中是否有换行符\n{if(pbuf[i]=='\n')//如果有换行符{ret = readn(sockfd , pbuf , i+1);//读取一行if(ret != i+1)  //一定会读到i+1个字符,否则是读取出错{exit(EXIT_FAILURE);}return ret + count ;}}//如果窥探的数据中并没有换行符//把这段没有换行符\n的内容读取出来ret = readn(sockfd , pbuf, nRead);if(ret != nRead ){exit(EXIT_FAILURE);}pbuf += nRead;left -= nRead;count += nRead;}return -1;
}

参考

TCP粘包,拆包及解决方法

Socket编程(4)TCP粘包问题及解决方案

TCP新手误区--粘包的处理

TCP粘包问题分析和解决(全)

TCP协议-TCP粘包问题相关推荐

  1. tcp协议与粘包现象【转http://www.cnblogs.com/wzd24/archive/2007/12/24/1011932.html】

    Socket开发之通讯协议及处理 在Socket应用开发中,还有一个话题是讨论的比较多的,那就是数据接收后如何处理的问题.这也是一个令刚接触Socket开发的人很头疼的问题. 因为Socket的TCP ...

  2. 为什么tcp不采用停等协议_为什么 TCP 协议有粘包问题

    来自公众号:真没什么逻辑 链接:https://draveness.me/whys-the-design-tcp-message-frame/ 为什么这么设计(Why's THE Design)是一系 ...

  3. TCP协议的粘包问题

    TCP粘包 首先TCP作为面向字节流的传输方式,创建一个tcp的socket,同时在内核中创建一个发送缓冲区和一个接受缓冲区. 当调用write时,向会将数据写入到发送缓冲区中 如果发送的字节数太长, ...

  4. Socket TCP协议解决粘包、半包问题的三种解决方案

    什么是粘包.半包问题:         粘包:例如服务端依次将两条消息发送给客户端,我们暂且简单的将这两条消息举例为"Hello"."Unity",而客户端一次 ...

  5. TCP协议的粘包问题(数据无边界性)及解决方法

    其他相关文章:http://c.biancheng.net/view/2350.html https://blog.csdn.net/seamanj/article/details/40063093 ...

  6. TCP协议的粘包问题(数据的无边界性)

    上节我们讲到了socket缓冲区和数据的传递过程,可以看到数据的接收和发送是无关的,read()/recv() 函数不管数据发送了多少次,都会尽可能多的接收数据.也就是说,read()/recv() ...

  7. TCP和UDP 粘包 消息保护边界

    在socket网络程序中,TCP和UDP分别是面向连接和非面向连接的.因此TCP的socket编程,收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有 ...

  8. 计网 - TCP 的封包格式:TCP 为什么要粘包和拆包?

    文章目录 Pre TCP 的拆包和粘包 TCP数据发送 TCP Segment Sequence Number 和 Acknowledgement Number MSS(Maximun Segment ...

  9. Python3之socket编程(TCP/UDP,粘包问题,数据传输、文件上传)

    一.socket的定义 Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口.在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后 ...

  10. Netty(三) 什么是 TCP 拆、粘包?如何解决?

    前言 记得前段时间我们生产上的一个网关出现了故障. 这个网关逻辑非常简单,就是接收客户端的请求然后解析报文最后发送短信. 但这个请求并不是常见的 HTTP ,而是利用 Netty 自定义的协议. 有个 ...

最新文章

  1. 用一个创业故事串起操作系统原理(二)
  2. linux与python客户端,《使用python进行unix和linux管理》§5网络 §5.1 网络客户端
  3. boost::hana::detail::create用法的测试程序
  4. 开源项目使用经验原则
  5. JVM——三个ClassLoader详解
  6. 我在SharePoint行业的从业经历(一)
  7. [css] 说下background-color:transparent和opacity:0的区别是什么?
  8. ubuntu navicat删除目录破解如何保留配置信息
  9. 输出有样式的php,PHP导出带样式的Excel
  10. 崩溃!还未修复的 Bug,凌晨三点遭到黑客 DDoS 攻击 | 技术头条
  11. Want VS Needs,产品经理基于场景的需求挖掘
  12. mrp手机qq2008下载-MRP格式的手机QQ2008软件介绍及如何正确安装MRPQQ2008挂Q版
  13. 哈勃分析系统解密:中招敲诈木马不用交赎金
  14. php 微信机器人_php7 版本的微信机器人来了!(这应该是最灵活的版本了)
  15. 使用ensembl的API下载数据
  16. python求斜边上的高是多少厘米_已知一个直角三角形的两条直角边,如何求斜边上的高的长度...
  17. python str和repr的区别_python str与repr的区别
  18. 微积分中几个重要的不等式:Jensen不等式、平均值不等式、Holder不等式、Schwarz不等式、Minkovski不等式 及其证明
  19. SZ19网安密码学期末考试(回忆版)
  20. unity获取麦克风音量_深入探究Valve Index的耳机、麦克风设计过程

热门文章

  1. 八款最佳的远程桌面工具
  2. 由于应用程序配置不正确,应用程序未能启动。重新安装应用程序可能会纠正这个问题
  3. javascript用DOM解释XML
  4. tomato(番茄)固件的简单设置截图
  5. JS中的List转Map
  6. python 会议室预约系统 开源_最新PHP会议室预定管理系统mrbs-1.8.0开源会议室预订系统安装教程...
  7. msql--基础使用
  8. 统计学权威盘点过去50年最重要的统计学思想
  9. 机器学习根据文字生成图片教程(附python代码)
  10. 微信好友只有昵称没有微信号_没微信号能找到人吗 只有微信昵称怎么找人