0%

TCP的拆包和粘包

粘包、半包

定义

在网络传输中,粘包和半包应该是最常出现的问题。

TCP 传输中,客户端发送数据,实际是把数据写入到了 TCP 的缓存中,粘包和半包也就会在此时产生。

客户端给服务端发送了两条消息ABCDEF,服务端这边的接收会有多少种情况呢?有可能是一次性收到了所有的消息ABCDEF,有可能是收到了三条消息ABCDEF

上面所说的一次性收到了所有的消息ABCDEF,类似于粘包。如果客户端发送的包的大小比 TCP 的缓存容量小,并且 TCP 缓存可以存放多个包,那么客户端和服务端的一次通信就可能传递了多个包,这时候服务端从 TCP 缓存就可能一下读取了多个包,这种现象就叫粘包

上面说的后面那种收到了三条消息ABCDEF,类似于半包。如果客户端发送的包的大小比 TCP 的缓存容量大,那么这个数据包就会被分成多个包,通过 Socket 多次发送到服务端,服务端第一次从接受缓存里面获取的数据,实际是整个包的一部分,这时候就产生了半包(半包不是说只收到了全包的一半,是说收到了全包的一部分)。

产生原因

因为TCP是面向连接的传输协议,TCP传输的数据是以流的形式,而流数据是没有明确的开始结尾边界,所以TCP也没办法判断哪一段流属于一个消息。

TCP协议传输过程.jpg

因为TCP会根据缓冲区的实际情况进行包的划分,在业务上认为,有的包被拆分成多个包进行发送,也可能多个晓小的包封装成一个大的包发送,这就是TCP的粘包或者拆包。

粘包拆包图解.png

假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到字节数是不确定的,故可能存在以下几种情况:

  1. 服务端分两次读取到两个独立的数据包,分别是D1和D2,没有粘包和拆包。
  2. 服务端一次接收到了两个数据包,D1和D2粘在一起,发生粘包。
  3. 服务端分两次读取到数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,发生拆包。
  4. 服务端分两次读取到数据包,第一次读取到部分D1包,第二次读取到剩余的D1包和全部的D2包。

当TCP缓存再小一点的话,会把D1和D2分别拆成多个包发送。

解决方法

解决半包粘包的问题其实就是定义消息边界的问题。因为TCP只负责数据发送,并不处理业务上的数据,所以只能在上层应用协议栈解决,目前的解决方案归纳:

  1. 消息定长,每个报文的大小固定,如果数据不够,空位补空格。
  2. 在包的尾部加回车换行符标识。
  3. 将消息分为消息头与消息体,消息头中包含消息总长度。
  4. 设计更复杂的协议。

Netty解决方案

LineBasedFrameDecoder

基于回车换行符的解码器,当遇到”n”或者 “rn”结束符时,分为一组。支持携带结束符或者不带结束符两种编码方式,也支持配置单行的最大长度。

DelimiterBasedFrameDecoder

分隔符解码器,可以指定消息结束的分隔符,它可以自动完成以分隔符作为码流结束标识的消息的解码。回车换行解码器实际上是一种特殊的DelimiterBasedFrameDecoder解码器。

FixedLengthFrameDecoder

固定长度解码器,它能够按照指定的长度对消息进行自动解码,当制定的长度过大,消息过短时会有资源浪费,但是使用起来简单。

LengthFieldBasedFrameDecoder

通用解码器,一般协议头中带有长度字段,通过使用LengthFieldBasedFrameDecoder传入特定的参数,来解决拆包粘包。