19.Netty源码之粘包/拆包

本节课开始我们将学习 Netty 通信过程中的编解码技术。

编解码技术这是实现网络通信的基础,让我们可以定义任何满足业务需求的应用层协议。

在网络编程中,我们经常会使用各种网络传输协议,其中 TCP 是最常用的协议。

我们首先需要了解的是 TCP 最基本的拆包/粘包问题以及常用的解决方案,才能更好地理解 Netty 的编解码框架。

为什么会出现拆包/粘包现象呢?

UDP没有拆包半包

提醒:UDP 像邮寄的包裹,虽然一次运输多个,但每个包裹都有“界限”,一个一个签收, 所以无粘包、半包问题。

☆为什么TCP有拆包/粘包

根本原因:TCP传输协议是面向流的,是流式协议,没有数据包界限。客户端向服务端发送数据时,可能将一个完整的报文拆分成多个小报文进行发送,也可能将多个报文合并成一个大的报文进行发送。因此就有了拆包和粘包。

在网络通信的过程中,每次可以发送的数据包大小受多种因素限制,如 MTU 传输单元大小、MSS 最大分段大小、滑动窗口等。

如果一次传输的网络包数据大小超过传输单元大小,那么我们的数据可能会拆分为多个数据包发送出去。

如果每次请求的网络包数据都很小,一共请求了 10000 次,TCP 并不会分别发送 10000 次。

因为 TCP 采用的 Nagle 算法对此作出了优化。

tcp为什么会粘包

粘包的根本原因: TCP 传输协议是面向流的,是流式协议,没有数据包界限。 粘包的主要原因: • 发送方每次写入数据小于套接字缓冲区大小,需要多次发送 • 接收方读取套接字缓冲区数据不够及时 ​ 半包的主要原因: • 发送方写入数据大于套接字缓冲区大小 • 发送的数据大于协议的 MTU(Maximum Transmission Unit,最大传输单元),必须拆包 ​ 换个角度看: • 收发 一个发送可能被多次接收,多个发送可能被一次接收 • 传输 一个发送可能占用多个传输包,多个发送可能公用一个传输包 ​

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

MTU(Maxitum Transmission Unit) 是链路层一次最大传输数据的大小。MTU 一般来说大小为 1500 byte。

MSS(Maximum Segement Size)是指 TCP 最大报文段长度,它是传输层一次发送最大数据的大小。MSS一般大小是1460

如下图所示,MTU 和 MSS 一般的计算关系为:MSS = MTU - IP 首部 - TCP首部,如果 MSS + TCP 首部 + IP 首部 > MTU,那么数据包将会被拆分为多个发送。这就是拆包现象。

image.png

滑动窗口:限制发送方单次发送数据大小

滑动窗口是 TCP 传输层用于流量控制的一种有效措施,也被称为通告窗口

滑动窗口是数据接收方设置的窗口大小,数据接收方会把窗口大小告诉发送方,以此限制发送方每次发送数据的大小,从而达到流量控制的目的。

这样数据发送方不需要每发送一组数据就阻塞等待接收方确认,允许发送方同时发送多个数据分组,每次发送的数据都会被限制在窗口大小内。

由此可见,滑动窗口可以大幅度提升网络吞吐量。

那么 TCP 报文是怎么确保数据包按次序到达且不丢数据呢?

首先,所有的数据帧都是有编号的,TCP 并不会为每个报文段都回复 ACK 响应,它会对多个报文段回复一次 ACK。假设有三个报文段 A、B、C,发送方先发送了B、C,接收方则必须等待 A 报文段到达,如果一定时间内仍未等到 A 报文段,那么 B、C 也会被丢弃,发送方会发起重试。如果已接收到 A 报文段,那么将会回复发送方一次 ACK 确认。

Nagle 算法:批量发送

Nagle 算法于 1984 年被福特航空和通信公司定义为 TCP/IP 拥塞控制方法。它主要用于解决频繁发送小数据包而带来的网络拥塞问题。试想如果每次需要发送的数据只有 1 字节,加上 20 个字节 IP Header 和 20 个字节 TCP Header,每次发送的数据包大小为 41 字节,但是只有 1 字节是有效信息,这就造成了非常大的浪费。Nagle 算法可以理解为批量发送,也是我们平时编程中经常用到的优化思路,它是在数据未得到确认之前先写入缓冲区,等待数据确认或者缓冲区积攒到一定大小再把数据包发送出去。

Linux 在默认情况下是开启 Nagle 算法的,在大量小数据包的场景下可以有效地降低网络开销。 但如果你的业务场景每次发送的数据都需要获得及时响应,那么 Nagle 算法就不能满足你的需求了,因为 Nagle 算法会有一定的数据延迟。

你可以通过 Linux 提供的 TCP_NODELAY 参数禁用 Nagle 算法。

Netty 中为了使数据传输延迟最小化,就默认禁用了 Nagle 算法,这一点与 Linux 操作系统的默认行为是相反的。

拆包/粘包解决方案:消息边界

image.png

在客户端和服务端通信的过程中,服务端一次读到的数据大小是不确定的。如上图所示,拆包/粘包可能会出现以下五种情况:

  • 服务端恰巧读到了两个完整的数据包 A 和 B,没有出现拆包/粘包问题;
  • 服务端接收到 A 和 B 粘在一起的数据包,服务端需要解析出 A 和 B;
  • 服务端收到完整的 A 和 B 的一部分数据包 B-1,服务端需要解析出完整的 A,并等待读取完整的 B 数据包;
  • 服务端接收到 A 的一部分数据包 A-1,此时需要等待接收到完整的 A 数据包;
  • 数据包 A 较大,服务端需要多次才可以接收完数据包 A。

由于拆包/粘包问题的存在,数据接收方很难界定数据包的边界在哪里,很难识别出一个完整的数据包。所以需要提供一种机制来识别数据包的界限,这也是解决拆包/粘包的唯一方法:定义应用层的通信协议

下面我们一起看下主流协议的解决方案。

消息长度固定

每个数据报文都需要一个固定的长度。当接收方累计读取到固定长度的报文后,就认为已经获得一个完整的消息。

当发送方发送的数据小于固定长度时,则需要将空位补齐再发送。

``` +----+------+------+---+----+

| AB | CDEF | GHIJ | K | LM |

+----+------+------+---+----+ ```

假设我们的固定长度为 4 字节,那么如上所示的 5 条数据一共需要发送 4 个报文:

+------+------+------+------+ | ABCD | EFGH | IJKL | M000 | +------+------+------+------+

长度如何设置是问题

消息定长法使用非常简单,但是缺点也非常明显,无法很好设定固定长度的值,如果长度太大会造成字节浪费,长度太小又会影响消息传输,所以在一般情况下消息定长法不会被采用。

特定分隔符:redis

既然接收方无法区分消息的边界,那么我们可以在每次发送报文的尾部加上特定分隔符,接收方就可以根据特殊分隔符进行消息拆分。以下报文根据特定分隔符 \n 按行解析,即可得到 AB、CDEF、GHIJ、K、LM 五条原始报文。

``` +-------------------------+

| AB\nCDEF\nGHIJ\nK\nLM\n |

+-------------------------+ ```

由于在发送报文时尾部需要添加特定分隔符,所以对于分隔符的选择一定要避免和消息体中字符相同,以免冲突。否则可能出现错误的消息拆分。

比较推荐的做法是将消息进行编码,例如 base64 编码,然后可以选择 64 个编码字符之外的字符作为特定分隔符。

base64编码中的64 指代的是64个字符

A-Z a-z 0-9 + / 26 + 26 + 10 + 2 = 64

特定分隔符法在消息协议足够简单的场景下比较高效,例如大名鼎鼎的 Redis 在通信过程中采用的就是换行分隔符。

消息长度+变长消息:dubbo&rocketMQ

消息头 消息体 +--------+----------+ | Length | Content | +--------+----------+

消息长度 + 消息内容是项目开发中最常用的一种协议,如上展示了该协议的基本格式。

消息头中存放消息的总长度,例如使用 4 字节的 int 值记录消息的长度,消息体实际的二进制的字节数据。

接收方在解析数据时,首先读取消息头的长度字段 Len,然后紧接着读取长度为 Len 的字节数据,该数据即判定为一个完整的数据报文。

依然以上述提到的原始字节数据为例,使用该协议进行编码后的结果如下所示:

+-----+-------+-------+----+-----+ | 2AB | 4CDEF | 4GHIJ | 1K | 2LM | +-----+-------+-------+----+-----+

消息长度 + 消息内容的使用方式非常灵活,且不会存在消息定长法和特定分隔符法的明显缺陷。当然在消息头中不仅只限于存放消息的长度,而且可以自定义其他必要的扩展字段,例如消息版本、算法类型等。

使用JSON

每种都不同,例如JSON 可以看{}是否应已经成对.

Netty 对三种常用封帧方式的支持

| 方式 | 解码 | 编码 | | --------------- | ---------------------------- | -------------------- | | 固定长度 | FixedLengthFrameDecoder | 简单 | | 分割符 | DelimiterBasedFrameDecoder | 简单 | | 固定长度字段存个内容的长度信息 | LengthFieldBasedFrameDecoder | LengthFieldPrepender | | JSON | 可以看{}是否应已经成对. | 简单 |

总结

本节课我们详细讨论了 TCP 中的拆包/粘包问题,以及如何通过应用层的通信协议来解决拆包/粘包问题。

其中基于消息长度 + 消息内容的变长协议是项目开发中最常用的一种方法,需要我们重点掌握,例如开源中间件 Dubbo、RocketMQ 等都基于该方法自定义了自己的通信协议。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值