Netty学习系列|Netty学习六(编解码之粘包和拆包)

一、粘包和拆包
客户端向服务端发送数据时,可能将一个完整的报文拆解为多个小报文进行发送,也可能将多个报文合并为一个大的报文进行发送,这就是拆包和粘包。
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,数据包会被拆分为多个发送
    Netty学习系列|Netty学习六(编解码之粘包和拆包)
    文章图片
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算法。
二、拆包/粘包的解决方案
Netty学习系列|Netty学习六(编解码之粘包和拆包)
文章图片
由于拆包和粘包的存在,服务端和客户端通信的过程中,可能会出现以下五种情况 * 服务端恰巧读到两个完整的数据包A和B,没有出现拆包/粘包 * 服务端接收到A和B粘到一起的数据包,服务端需要解析出A和B * 服务端接收到完整的A和B的一部分B-1,服务端需要解析出完整的A,并等待读取完整的B数据包 * 服务端接收到A的一部分A-1,需要等待接收到完整的A数据包 * 数据包A较大,服务端需要多次接收才能接受完整的A数据包 【Netty学习系列|Netty学习六(编解码之粘包和拆包)】拆包和粘包的出现使得接收方很难辨识出一个完整的数据包,因此需要提供一种机制来识别数据包的界限,即:定义应用层的通信协议。
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 | +-----+-------+-------+----+-----+

    推荐阅读