粘包/拆包问题与Netty解决方式

粘包/拆包问题与Netty解决方式

  • 什么是TCP粘包/半包?
  • TCP粘包/半包发生的原因
  • 解决粘包半包问题
    • 基于分隔符的解码器
      • LineBasedFrameDecoder
      • DelimiterBasedFrameDecoder
    • 基于长度的解码器
      • FixedLengthFrameDecoder
      • LengthFieldBasedFrameDecoder

什么是TCP粘包/半包?

粘包/拆包问题与Netty解决方式_第1张图片
假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4种情况。

  1. 服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包
  2. 服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包
  3. 服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容(粘包),第二次读取到了D2包的剩余内容,这被称为TCP拆包
  4. 服务端分两次读取到了两个数据包,第一次读取到了D1包的部分内容D1_1(拆包),第二次读取到了D1包的剩余内容D1_2和D2包的整包(粘包)

如果此时服务端TCP接收滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种可能,即服务端分多次才能将D1和D2包接收完全,期间发生多次拆包。

TCP粘包/半包发生的原因

由于TCP协议本身的机制(面向连接的可靠地协议-三次握手机制)客户端与服务器会维持一个连接(Channel),数据在连接不断开的情况下,可以持续不断地将多个数据包发往服务器,但是如果发送的网络数据包太小,那么他本身会启用Nagle算法(默认开启,可配置不启用)对较小的数据包进行合并(基于此,TCP的网络延迟要UDP的高些)然后再发送(超时或者包大小足够)。那么这样的话,服务器在接收到消息(数据流)的时候就无法区分哪些数据包是客户端自己分开发送的,这样产生了粘包;服务器在接收到数据库后,放到缓冲区中,如果消息没有被及时从缓存区取走,下次在取数据的时候可能就会出现一次取出多个数据包的情况,也会造成粘包现象。
UDP:本身作为无连接的不可靠的传输协议(适合频繁发送较小的数据包),他不会对数据包进行合并发送(也就没有Nagle算法之说了),他直接是一端发送什么数据,直接就发出去了,既然他不会对数据合并,每一个数据包都是完整的(数据+UDP头+IP头等等发一次数据封装一次)也就没有粘包一说了。
分包产生的原因就简单的多:可能是IP分片传输导致的,也可能是传输过程中丢失部分包导致出现的半包,还有可能就是一个包可能被分成了两次传输,在取数据的时候,先取到了一部分(还可能与接收的缓冲区大小有关系),总之就是一个数据包被分成了多次接收。

更具体的原因有三个,分别如下:

  1. 应用程序写入数据的字节大小大于套接字发送缓冲区的大小
  2. 进行MSS大小的TCP分段。MSS是最大报文段长度的缩写。MSS是TCP报文段中的数据字段的最大长度。数据字段加上TCP首部才等于整个的TCP报文段。所以MSS并不是TCP报文段的最大长度,而是:MSS=TCP报文段长度-TCP首部长度
  3. 以太网的payload大于MTU进行IP分片。MTU指:一种通信协议的某一层上面所能通过的最大数据包大小。如果IP层有一个数据包要传,而且数据的长度比链路层的MTU大,那么IP层就会进行分片,把数据包分成托干片,让每一片都不超过MTU。注意,IP分片可以发生在原始发送端主机上,也可以发生在中间路由器上。

解决粘包半包问题

由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,可以归纳如下。

基于分隔符的解码器

粘包/拆包问题与Netty解决方式_第2张图片

LineBasedFrameDecoder

粘包/拆包问题与Netty解决方式_第3张图片

  1. 在包尾增加分割符。比如回车换行符进行分割,例如FTP协议
    系统的换行符:System.getProperty(“line.separator”);
    Netty的换行分割处理器: ch.pipeline().addLast(new LineBasedFrameDecoder(1024));

DelimiterBasedFrameDecoder

  1. Netty的自定义分割处理器
    ByteBuf delimiter = Unpooled.copiedBuffer(DELIMITER_SYMBOL.getBytes());
    ch.pipeline().addLast( new DelimiterBasedFrameDecoder(1024,delimiter));

基于长度的解码器

粘包/拆包问题与Netty解决方式_第4张图片

FixedLengthFrameDecoder

粘包/拆包问题与Netty解决方式_第5张图片
3. 消息定长。例如每个报文的大小为固定长度200字节,如果不够,空位补空格;
ch.pipeline().addLast( new FixedLengthFrameDecoder(FixedLengthEchoClient.REQUEST.length()));

LengthFieldBasedFrameDecoder

粘包/拆包问题与Netty解决方式_第6张图片
4. 将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,通常设计思路为消息头的第一个字段使用2个字节来表示消息的总长度。
粘包/拆包问题与Netty解决方式_第7张图片

更多示例:
点击这里查看更多示例。

你可能感兴趣的:(网络编程与Netty)