TCP粘包和拆包的处理方案

问题定义

TCP是一个“流”协议,所谓流,就是没有界限的一长串二进制数据。TCP作为传输层协议并不不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行数据包的划分,所以在业务上认为是一个完整的包,可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。

理解什么是粘包、拆包问题,先举两个简单的应用场景:

假设应用层协议是http

我从浏览器中访问了一个网站,网站服务器给我发了200k的数据。建立连接的时候,通告的MSS是50k,所以为了防止ip层分片,tcp每次只会发送50k的数据,一共发了4个tcp数据包。如果我又访问了另一个网站,这个网站给我发了100k的数据,这次tcp会发出2个包,问题是,客户端收到6个包,怎么知道前4个包是一个页面,后两个是一个页面。既然是tcp将这些包分开了,那tcp会将这些包重组吗,它送给应用层的是什么?

这是我自己想的一个场景,正式一点讲的话,这个现象叫拆包。

我们再考虑一个问题。

tcp中有一个negal算法,用途是这样的:通信两端有很多小的数据包要发送,虽然传送的数据很少,但是流程一点没少,也需要tcp的各种确认,校验。这样小的数据包如果很多,会造成网络资源很大的浪费,negal算法做了这样一件事,当来了一个很小的数据包,我不急于发送这个包,而是等来了更多的包,将这些小包组合成大包之后一并发送,不就提高了网络传输的效率的嘛。这个想法收到了很好的效果,但是我们想一下,如果是分属于两个不同页面的包,被合并在了一起,那客户那边如何区分它们呢?

这就是粘包问题。

从粘包问题我们更可以看出为什么tcp被称为流协议,因为它就跟水流一样,是没有边界的,没有消息的边界保护机制,所以tcp只有流的概念,没有包的概念。

我们还需要有两个概念

长连接: Client方与Server方先建立通讯连接,连接建立后不断开, 然后再进行报文发送和接收。
短连接:Client方与Server每进行一次报文收发交易时才进行通讯连接,交易完毕后立即断开连接。此种方式常用于一点对多点 通讯,比如多个Client连接一个Server.
下面我们揭晓答案:

我想象的关于粘包的场景是不对的,http连接是短连接,请求之后,收到回答,立马断开连接,不会出现粘包。
拆包现象是有可能存在的

处理拆包

既然拆包现象可能存在,如果遇到了,那么该如何处理呢?这里提供两种方法

通过包头+包长+包体的协议形式,当服务器端获取到指定的包长时才说明获取完整。
指定包的结束标识,这样当我们获取到指定的标识时,说明包获取完整。

处理粘包

我们从上面的分析看到,虽然像http这样的短连接协议不会出现粘包的现象,但是一旦建立了长连接,粘包还是有可能会发生的。

网上的处理方法有很多,这里不列举了,但大家看这些处理方法,都会发现,这些方法并不好,都会做一些牺牲。比如禁用negal算法,就是以网络性能作为牺牲。


  1. 客户端和服务器建立一个连接,客户端发送一条消息,客户端关闭与服务器的连接。
  2. 客户端和服务器建立一个连接,客户端连续发送两条消息,客户端关闭与服务器的连接。

对于第一种情况,服务器的处理流程可以是这样的:当客户端与服务器的连接建立成功以后,服务器不断读取客户端发送过来的数据,当客户端与服务器连接断开以后,服务器知道已经读完了一条消息,然后进行解码和后续处理。对于第二种情况,如果按照上面相同的处理逻辑来处理,那就有问题了,我们来看看第二种情况下客户端发送的两条消息递交到服务端有可能出现的情况:

第一种情况:

服务器一共读到两个数据包,第一个包包含客户端发出的第一条消息的完整信息,第二个包包含客户端发出的第二条消息,那这种情况比较好处理,服务器只需要简单的从网络缓冲区去读就好了,第一次读到第一条消息的完整信息,消费完再从网络缓冲区将第二条完整消息读出来消费。

第二种情况:

服务器一共就读到一个数据包,这个数据包包含客户端发出的两条消息的完整性,这个时候基于之前逻辑实现的服务器就懵了。因为服务器不知道第一条消息从哪结束以及第二条消息从哪开始,这是发生了TCP粘包

第三种情况:

服务器一共收到了两个数据包,
第一个数据包只包含了第一条消息的一部分,第一条消息的后半部分和第二条消息都在第二个数据包中;
或者第一个数据包包含了第一条消息的完整信息和第二条消息的一部分信息,第二个数据包包含了第二条消息的剩下部分,这种情况其实是发送了TCP拆包。
因为发生了一条消息被拆分在了两个包里面发送了,同样上面的服务器逻辑对于这种情况是不好处理的。

产生TCP粘包和拆包的原因

我们知道TCP是以流动的方式传输数据的,传输的最小单位为一个报文段(Segment)。TCP Header中有个Options标识位。常见的标识位为MSS(Maximum Segment Size)指的是,连接层每次传输的数据有个最大限制MTU(Maximum Transmission Unit),一般是1500bit,超过这个量要分成多个报文段,MSS则是这个最大限制减去TCP的header,光是要传输的数据的大小,一般为1460bit。换算成字节,也就是180多字节。
TCP为提高性能,发送端会将需要发送的数据发送到缓冲区,等待缓冲区满了以后,再将缓冲中的数据发送到接收方。同理,接收方也有缓冲区这样的机制来接受数据。
发生TCP粘包、拆包主要是以下原因:
1、应用程序写入数据大于套接字缓冲区大小,会发生拆包。
2、应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发送粘包。
3、进行MSS(最大报文长度)大小的TCP分段,当TCP报文长度-TCP header长度>MSS 的时候会发生拆包。
4、接收方法不及时读取套接字缓冲区数据,这将发生粘包。
......

如何解决拆包、粘包

既然知道TCP是无界的数据流,且协议本身无法避免粘(拆)包的发生。那我们只能再应用层数据协议上加以控制。通常再制定传输数据时,可以使用如下方法:
1、使用带消息头的协议。消息头存储消息开始标识消息长度信息,服务器获取消息头的时候解析出消息长度,然后向后读取该长度的内容。
2、设置定长消息。服务器每次读取既定长度的内容作为一条完整消息。
3、设置消息边界。服务器从网络流中按消息编辑分离出消息内容。

a)先基于第3种方法,假设区分数据边界的标识为换行符" "(【注意】:请求数据本身内部不能包含换行符),数据格式为JSON。如下是一个符合该规则的请求包。

{"type":"message","content":"hello"}

( 代表一个请求的结束)
b)基于第1种方法,可以制定,首部固定10的字节长度用来保存整个数据包长度,位数不够补0的数据协议。

0000000036{"type":"message","content":"hello"}

c)基于第1种方法。可以制定,首部4字节网络字节序unsigned int,标记整个包的长度

****{"type":"message","content":"hello all"}

其中首部4字节*代表一个网阔字节序的unsigned int数据,为不可见字符,紧接着是JSON的数据格式的包体数据。

参考:
tcp是流的一些思考--拆包和粘包
tcp粘包和拆包的处理方案

原文地址:https://www.cnblogs.com/Roni-i/p/11218535.html