Netty学习六:编解码之粘包和拆包

  • Post author:
  • Post category:其他




一、粘包和拆包

客户端向服务端发送数据时,可能将一个完整的报文拆解为多个小报文进行发送,也可能将多个报文合并为一个大的报文进行发送,这就是拆包和粘包。



1. 为什么要有粘包和拆包呢?


  • 拆包

    :网络通信中,每次发送的数据包大小是受多种因素影响的,如:MTU传输单元大小、MSS最大分段大小、滑动窗口等。如果一次传输请求的网络数据包大小超过传输单元大小,会被拆分成多个数据包发送出去。

  • 粘包

    :如果每次请求的网络数据包都很小,TCP会基于Nagle算法进行优化,将多个报文合并为一个大的报文进行发送。



2. MTU最大传输单元和MSS最大分段大小

  • MTU(Maximum Transimission Unit )最大传输单元:链路层一次最大传输数据的大小,一般来说是1500 byte。
  • MSS(Maximum Segement Size)最大分段大小:TCP最大报文段长度,是传输层一次最大传输数据的大小。
  • 关系:MSS = MTU – IP首部 -TCP首部。如果MSS + IP首部 + TCP首部 > MTU,数据包会被拆分为多个发送

    Drawing 1.png



3. 滑动窗口

  • 滑动窗口是TCP控制流量的一种方式。数据接收方会设置窗口大小并告知发送方,从而限制发送方每次发送的数据大小。同时发送方不需要每发送一组数据就阻塞等待接收方的确认,可以同时发送多个数据分组,每次发送的数据都在窗口大小内。
  • 如何保证数据按顺序到达?发送方发送的数据帧都是有编号的,TCP会对多个报文段回复一次ACK。假设有A、B、C三个报文段,发送方先发送B、C,接收方必须等待A到达才ACK。如果超时未等到A,则B、C也会抛弃,发送方发起重试。



4. Nagle算法

  • 可以理解为批量发送,它是在数据未得到确认之前先写入缓冲区,等待数据确认或者缓冲区积攒到一定大小再将数据包发送出去。
  • Linux默认情况下是开启Nagle算法的。但是如果业务场景要求每次发送的数据都需要获得及时响应,Nagle算法无法满足,因为Nagle算法会导致一定的数据延迟。可以通过TCP_NODELAY参数禁用Nagle算法。Netty中就默认禁用了Nagle算法。



二、拆包/粘包的解决方案

Drawing 3.png
由于拆包和粘包的存在,服务端和客户端通信的过程中,可能会出现以下五种情况 * 服务端恰巧读到两个完整的数据包A和B,没有出现拆包/粘包 * 服务端接收到A和B粘到一起的数据包,服务端需要解析出A和B * 服务端接收到完整的A和B的一部分B-1,服务端需要解析出完整的A,并等待读取完整的B数据包 * 服务端接收到A的一部分A-1,需要等待接收到完整的A数据包 * 数据包A较大,服务端需要多次接收才能接受完整的A数据包

拆包和粘包的出现使得接收方很难辨识出一个完整的数据包,因此需要提供一种机制来识别数据包的界限,即:

定义应用层的通信协议



1. 消息长度固定

  • 每个数据报文都需要一个固定长度。当接收方累计读取到固定长度的报文后,认为已经获取一个完整的消息。当发送方的数据小于固定长度,需要用空位补齐
  • 缺点在于:无法很好设置固定长度的值,太长会造成字节浪费,太短又会影响消息传输
  • 如下所示:假设固定长度为4,上述的5条数据需要发送4次报文
+----+------+------+---+----+
| AB | CDEF | GHIJ | K | LM |
+----+------+------+---+----+
+------+------+------+------+
| ABCD | EFGH | IJKL | M000 |
+------+------+------+------+



2. 特定分隔符

  • 在每次发送的报文尾部加上特定分隔符,接收方根据特殊分隔符进行消息拆分。Redis在通信过程中就是采用的换行分隔符。
  • 分隔符的选择一定要避免和消息中的字符相同,否则会出现错误的消息拆分。推荐将消息进行编码,如base64编码,然后可以选择64个编码字符之外的字符作为特定分隔符。
  • 如下所示:采用\n分隔符,可以得到AB、CDEF、GHJ、K、LM五条原始报文。
+-------------------------+
| AB\nCDEF\nGHIJ\nK\nLM\n |
+-------------------------+



3. 消息长度 + 消息内容

  • 最常用的一种协议。消息头中存放消息的长度,消息体中存放实际的二进制字节数据。接收方在解析数据时,首先读取消息头的长度字段Len,接着读取长度为Len的字节数据,判定该字节数据为一个完整的报文。
  • 消息头中不仅可以存放消息长度,还可以自定义其他必要的扩展字段,如:消息版本、算法类型等。
  • 如下所示,采用该方法进行编码后的结果如下:
消息头     消息体
+--------+----------+
| Length |  Content |
+--------+----------+
+-----+-------+-------+----+-----+
| 2AB | 4CDEF | 4GHIJ | 1K | 2LM |
+-----+-------+-------+----+-----+



版权声明:本文为weixin_41402069原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。