Netty
1、概述
2、线程模型
3、核心组件
3.1 Channel
3.2 ChannelHandler和ChannelPipeline
3.3 EventLoop和EventLoopGroup
3.4 Future和Promise
4、创建过程
4.1 服务端创建过程
4.2 客户端创建过程
5、TCP粘包和拆包
6、序列化与反序列化
-
+
游客
注册
登录
TCP粘包和拆包
## 1 为什么有粘包和拆包 1. **TCP 传输协议是面向流的**,**没有数据包界限**,**客户端向服务端发送数据时**,**可能将一个完整的报文拆分成多个小报文进行发送**,**也可能将多个报文合并成一个大的报文进行发送**,因此就有了粘包和拆包。 2. **数据流在 TCP 协议下进行传播**,因为**协议本身对于流有一些规则的限制**,**这些规则导致当前对端接收到的数据包不完整**,归结原因有下面三种情况: 1. **Socket 缓冲区与滑动窗口**: 1. 对于 TCP 协议而言,他**传输数据是基于字节流传输的**,**应用层在传输数据时**,**实际上会先将数据写入到 TCP 套接字的缓冲区**,**当缓冲区被写满后**,**数据才被写出去**。 2. **每个 TCP Socket 在内核中都有一个发送缓冲区**(`SO_SNDBUF`)**和一个接收缓冲区**(`SO_RCVBUF`),**TCP 的全双工模式以及 TCP 的滑动窗口便是依赖于这两个独立的 `buffer` 以及此 `buffer` 的填充状态**: 1. `SO_SNDBUF`: 1. **进程发送数据的时候假设调用了一个 `send()` 方法**,**将数据拷贝进入 Socket 的内核发送缓冲区中**,**然后 `send()` 便在上层返回**。 2. `send()`**在返回的时候**,**数据不一定会发送到对端去**,**他仅仅是把应用层 `buffer` 的数据拷贝到了 Socket 的内核发送 `buffer` 中**。 2. `SO_RCVBUF`: 1. **对端发来的数据都会经由内核接收并且缓存到 Socket 的内核接收缓冲区中**,`read()`**所做的工作就是把内核缓冲区中的数据拷贝到应用层用户的 `buffer` 里面**。 2. **如果应用进程一直没有调用 `read()` 读取的话**,**此数据会一直缓存在相应 Socket 的接收缓存区内**,**当 Socket 的接收缓冲区满了之后**,**就会通知对端 TCP 协议的窗口关闭**,**保证 TCP 套接口接收缓冲区不会溢出**,**从而保证了 TCP 是可靠传输**。 3. **TCP 连接在三次握手的时候**,**会将自己的窗口发送给对方**,其实**就是 `SO_RCVBUF` 指定的值**,**之后在发送数据时**,**发送方必须要先确认接收方的窗口有没有被填满**,**如果没有填满**,**则可以发送**。 4. **每次发送数据后**,**发送方将自己维护的对方的窗口大小减小**,**表示对方的 `SO_RCVBUF` 可用空间变小**。 5. **当接收方开始处理 `SO_RCVBUF` 中的数据时**,**会将数据从 Socket 在内核中的接收缓冲区读出**,**此时接收方的 `SO_RCVBUF` 可用空间变大**,即**窗口大小变大**,**接收方会以 `ack` 消息的方式将自己最新的窗口大小返回给发送方**,**此时发送方将自己维护的接收方的窗口大小设置为 `ack` 消息返回的窗口大小**。 6. 此外,**发送方可以连续的给接收方发送消息**,**只要保证对方的 `SO_RCVBUF` 空间可以缓存数据即可**,**当接收方的 `SO_RCVBUF` 被填充满时**,**发送方不能再继续发送数据**,**要等待接收方 `ack` 消息**,**以获得最新可用的窗口大小**。 2. ****MSS/MTU 限制**。** 1. MTU,全称是 Maximum Transmission Unit,即**最大传输单元**,**是链路层对一次可以发送的最大数据的限制**。 2. MSS,全称是 Maximum Segment SIze,即**最大分段大小**,**是 TCP 报文中 `data` 部分的最大长度**,**是传输层对一次可以发送的最大数据的限制**。 3. 数据在传输的过程中,每经过一层,都会加上一些额外的信息:![](/media/202108/2021-08-27_1124590.0025852146647976637.png) 1. **应用层**:**只关心发送的数据 `data`**,**将数据写入 Socket 在内核的缓冲区 `SO_SNDBUF` 即返回**,**操作系统会将 `SO_SNDBUF` 中的数据取出来进行发送**。 2. **传输层**:**会在 `data` 前面加上 TCP header**(20 字节)。 3. **网络层**:**会在 TCP 报文的基础上再添加一个 IP header**,也就是**将自己的网络地址加入到报文中**,**IPv4 中 IP header 长度是 20 字节**,**IPv6 中 IP header 长度是 40 字节**。 4. **链路层**:**加上 Datalink header 和 CRC**,**会将 SMAC**(Source MAC,数据发送方的 MAC 地址)、**DMAC**(Destination MAC,数据接收方的 MAC 地址)**和 Type 域加入**,**SMAC + DMAC + Type + CRC 总长度为 18 字节**。 5. **物理层**:**进行传输**。 4. MTU 和 MSS 的关系如下: $$ MSS = MTU(1500) - IP Header(20 or 40) - TCP Header(20) $$ 5. **发送方发送数据时**,**当 `SO_SNDBUF` 中的数据量大于 MSS 时**,**操作系统会将数据进行拆分**,**使得每一部分都小于 MSS**,即**形成了拆包**,然后**每一部分都加上 TCP Header**,**构成多个完整的 TCP 报文进行发送**,**当经过网络层和数据链路层的时候**,**还会分别加上相应的内容**。 6. 需要注意的是,对于**本地回环地址**(Lookback)**不需要走以太网**,所以**不受到以太网 $MTU = 1500$ 的限制**: ![](/media/202108/2021-08-27_114423_716891.png) 上图展示了 2 个网卡信息: 1. `eth0`**需要走以太网**,所以**MTU 是 1500**. 2. `lo` 是**本地回环**,**不需要走以太网**,所以**不受 1500 的限制**。 3. **Nagle 算法**: 1. TCP/IP协议中,无论发送多少数据,**总是要在数据**(`data`)**前面加上协议头**(TCP Header + IP Header),同时,**对方接收到数据**,**也需要发送ACK表示确认**。 2. 为了**尽可能的利用网络带宽**,TCP**总是希望尽可能的发送足够大的数据**,Nagle算法就是为了**尽可能发送大块数据**,**避免网络中充斥着许多小数据块**。 3. Nagle算法的基本定义是**最多只能有一个未被确认的小段**,所谓小段,指的是**小于MSS尺寸的数据块**,所谓未被确认,是指**一个数据块发送出去后**,**没有收到对方发送的ACK确认该数据已收到**。 4. Nagle算法的规则如下: 1. 如果`SO_SNDBUF`**中的数据长度达到MSS**,则**允许发送**。 2. 如果该`SO_SNDBFU`中**含有 `FIN`**,表示**请求关闭连接**,则**先将 `SO_SNDBUF`中的剩余数据发送**,**再关闭**。 3. **设置了 `TCP_NODELAY = true`**,则**允许发送**,`TCP_NODELAY`是**取消了TCP的确认延迟机制**,**相当于禁用了Nagle算法**,正常情况下,**当Server端收到数据之后**,他并**不会马上向客户端发送ACK**,而是**会将ACK的发送延迟一段时间**(一般是40ms),他**希望在$t$时间内服务端向客户端发送应答数据**,**这样ACK就能和应答数据一起发送**,**就像是应答数据捎带着ACK过去**,当然,**TCP确认延迟40ms不是一成不变的**,TCP连接的延迟确认时间**一般初始化为最小值40ms**,随后**根据连接的重传超时时间**(RTO)、**上次收到数据包与本次接收数据包的时间间隔等参数进行不断调整**,另外,**可以通过设置 `TCP_QUICKACK`选项来取消确认延迟**。 4. **未设置 `TCP_CORK`选项时**,**若所有发出去的小数据包**(包长度小于MSS)**均被确认**,**则允许发送**。 5. **上述条件都未满足**,但**发生了超时**(一般为200ms),**则立即发送**。 5. 基于以上问题,TCP层肯定是会出现当次接收到的数据是不完整数据的情况,**出现粘包**的可能原因有: 1. **发送方每次写入数据小于套接字缓冲区大小**。 2. **接收方读取套接字缓冲区数据不够及时**。 6. **出现半包**的可能原因有: 1. **发送方每次写入数据大于套接字缓冲区大小**。 2. **发送的数据大于协议MTU**,所以**必须要拆包**。 3. 粘包和拆包可能会出现下面 5 中情况:![Drawing 3.png](/media/202108/2021-08-26_1540320.5632445375530375.png) 1. **服务端恰巧读到了两个完整的数据包 A 和 B**,**没有出现粘包和拆包问题**。 2. **服务端接收到 A 和 B 黏在一起的数据包**,**服务端需要解析出 A 和 B**。 3. **服务端收到完整的 A 和 B 的一部分数据包 B-1**,**服务端需要解析出完整的 A**,**并等待读取完整的 B 数据包**。 4. **服务端接收到 A 的一部分数据包 A-1**,此时**需要等待接收完整的 A 数据包**。 5. **数据包 A 较大**,**服务端需要多次才可以接收完数据包 A**。 ## 2 常用解决方案 > 由于粘包和拆包问题的存在,**数据接收方很难界定数据包的边界在哪里**,**很难识别出一个完整的数据包**,所以**需要提供一种机制来识别数据包的界限**,这也**是解决粘包和拆包问题的唯一方法**,即**定义应用层的通信协议**。 主流协议的解决方案主要由以下几种: 1. **消息长度固定**。 2. **特定分隔符**。 3. **消息长度 + 消息内容**。 ### 2.1 消息长度固定 1. **每个数据报文都需要一个固定的长度**,**当接收方累计读取到固定长度的报文后**,**就认为已经获得一个完整的消息**,**当发送方的数据小于固定长度时**,则**需要空位补齐**: 1. 例如有以下需要发送的数据: ```txt +----+------+------+---+----+ | AB | CDEF | GHIJ | K | LM | +----+------+------+---+----+ ``` 2. 假设我们的固定长度为 4 字节,那么如上所示的 5 条数据一共需要发送 4 个报文: ```txt +------+------+------+------+ | ABCD | EFGH | IJKL | M000 | +------+------+------+------+ ``` 2. 消息定长法**使用非常简单**,但是缺点也非常明显,**无法很好设定固定长度的值**,如果**长度太大会造成字节浪费**,**长度太小又会影响消息传输**,所以在**一般情况下消息定长法不会被采用**。 3. Netty 中**提供了 `FixedLengthFrameDecoder` 解码器**,**支持把固定长度的字节数当做一个完整的消息进行解码**。 ### 2.2 特定分隔符 1. 既然接收方无法区分消息的边界,那么我们可以**在每次发送报文的尾部加上特定分隔符**,**接收方就可以根据特殊分隔符进行消息拆分**,以下报文根据特定分隔符 `\n` 按行解析,即可得到 `AB`、`CDEF`、`GHIJ`、`K`、`LM` 五条原始报文: ```txt +-------------------------+ | AB\nCDEF\nGHIJ\nK\nLM\n | +-------------------------+ ``` 2. 由于在发送报文时尾部需要添加特定分隔符,所以**对于分隔符的选择一定要避免和消息体中字符相同**,**以免冲突**,**否则可能出现错误的消息拆分**,比较推荐的做法是**将消息进行编码**,例如 `base64` 编码,然后可以选择 64 个编码字符之外的字符作为特定分隔符,**特定分隔符法在消息协议足够简单的场景下比较高效**,例如 Redis 在通信过程中采用的就是按行分隔符。 3. Netty 中**提供了 `DelimiterBasedFrameDecoder` 解码器**,**根据特殊字符进行解码**,**提供了 `LineBasedFrameDecoder` 解码器**,**默认以换行符作为分隔符进行解码**。 ### 2.3 消息长度 + 消息内容 1. **消息长度 + 消息内容是项目开发中最常用的一种协议**,**消息头中存放消息的总长度**,**消息体中存放实际的二进制的字节数据**,**接收方在解析数据时**,**首先读取消息头的长度字段 $length$**,**然后紧接着读取长度为 $length$ 的字节数据**,**该数据即为一个完整的数据报文**,依然以上述提到的原始字节为例,使用该协议进行编码后的结果如下所示: ```txt +-----+-------+-------+----+-----+ | 2AB | 4CDEF | 4GHIJ | 1K | 2LM | +-----+-------+-------+----+-----+ ``` 2. 消息长度 + 消息内容的**使用方式非常灵活**,且**不会存在消息定长法和特定分隔符的明显缺陷**,当然**在消息头中不仅只限于存放消息长度**,而且**可以自定义其他必要的扩展字段**,例如消息版本、算法类型等。 3. Netty 中**提供了 `LengthFieldBasedFrameDecoder` 解码器**,**用于给实际的消息体添加 `length` 字段**。 ## 参考文献 1. [06 粘包拆包问题:如何获取一个完整的网络包?](http://learn.lianglianglee.com/%E4%B8%93%E6%A0%8F/Netty%20%E6%A0%B8%E5%BF%83%E5%8E%9F%E7%90%86%E5%89%96%E6%9E%90%E4%B8%8E%20RPC%20%E5%AE%9E%E8%B7%B5-%E5%AE%8C/06%20%20%E7%B2%98%E5%8C%85%E6%8B%86%E5%8C%85%E9%97%AE%E9%A2%98%EF%BC%9A%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96%E4%B8%80%E4%B8%AA%E5%AE%8C%E6%95%B4%E7%9A%84%E7%BD%91%E7%BB%9C%E5%8C%85%EF%BC%9F.md) 2. 《Netty 权威指南 第 2 版》 3. [Netty 中的粘包和拆包](https://www.cnblogs.com/rickiyang/p/12904552.html)。
ricear
2021年8月27日 15:05
©
BY-NC-ND(4.0)
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
分享
链接
类型
密码
更新密码