TCP粘包/拆包问题,及Netty中的解决方案

pull/30/head
AmyliaY 5 years ago
parent 9f8b404dba
commit 973334a0b6

@ -88,17 +88,19 @@
- [IO模型](docs/Netty/IOTechnologyBase/IO模型.md)
- [四种IO编程及对比](docs/Netty/IOTechnologyBase/四种IO编程及对比.md)
### Netty NIO开发指南
- [TCP粘包/拆包问题的解决之道]()
- [分隔符解码器 和 定长解码器]()
### TCP粘包/拆包
- [TCP粘拆包问题及Netty中的解决方案](docs/Netty/TCP粘拆包/TCP粘拆包问题及Netty中的解决方案.md)
### Netty 编解码开发指南
### Netty 编解码开发
- [编解码技术]()
- [Java常用的序列化框架]()
### Netty 多协议开发和应用
- [WebSocket 协议开发]()
### Netty多协议开发
- [HTTP协议开发]()
- [WebSocket协议开发]()
- [自定义协议开发]()
### Netty 源码分析
### Netty源码分析
- [Channel和Unsafe组件]()
- [ChannelPipeline和ChannelHandler组件]()
- [EventLoop和EventLoopGroup组件]()

@ -3,7 +3,7 @@
在基于传统同步阻塞模型开发中ServerSocket 负责绑定IP 地址启动监听端口Socket负责发起连接操作。连接成功之后双方通过输入和输出流进行同步阻塞式通信。
#### BIO通信模型
### BIO通信模型
通过下面的通信模型图可以发现,采用 BIO 通信模型的服务端,通常由一个独立的 Acceptor线程 负责监听客户端的连接,它接收到客户
端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的 “一请求一应答” 通信模型。
@ -16,14 +16,14 @@
## 伪异步IO编程
为了解决 同步阻塞IO 面临的一个链路需要一个线程处理的问题,后来有人对它的线程模型进行了优化,后端通过一个线程池来处理多个客户端的请求接入,形成 客户端个数M线程池最大线程数N 的比例关系,其中 M 可以远远大于 N。通过线程池可以灵活地调配线程资源设置线程的最大值防止由于海量并发接入导致线程耗尽。
#### 伪异步IO模型图
### 伪异步IO模型图
采用线程池和任务队列可以实现一种叫做 伪异步的IO通信框架其模型图下。当有新的客户端接入时将客户端的 Socket 封装成一个 Task对象 (该类实现了java.lang.Runnable接口)投递到后端的线程池中进行处理JDK 的线程池维护一个消息队列和 N 个活跃线程,对消息队列中的任务进行处理。由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。
![avatar](/images/Netty/伪异步IO通信模型.png)
伪异步 IO通信框架 采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。但是由于它底层的通信依然采用同步阻塞模型,因此无法从根本上解决问题。
#### 伪异步IO编程弊端分析
### 伪异步IO编程弊端分析
要对 伪异步IO编程 的弊端进行深入分析,首先我们看两个 Java同步IO 的 API说明随后结合代码进行详细分析。
```java
public abstract class InputStream implements Closeable {
@ -86,7 +86,7 @@ public abstract class OutputStream implements Closeable, Flushable {
与 Socket类 和 ServerSocket类 相对应NIO 也提供了 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现。这两种新增的通道都支持阻塞和非阻塞两种模式。阻塞模式使用非常简单,但是性能和可靠性都不好,非阻塞模式则正好相反。开发人员可以根据自
己的需要来选择合适的模式。一般来说,低负载、低并发的应用程序可以选择 同步阻塞IO以降低编程复杂度对于高负载、高并发的网络应用需要使用 NIO 的非阻塞模式进行开发。
#### NIO类库简介
### NIO类库简介
NIO类库 是在 JDK 1.4 中引入的。NIO 弥补了原来 同步阻塞IO 的不足,它在 标准Java代码 中提供了高速的、面向块的IO。下面我们简单看一下 NIO类库 及其 相关概念。
**1、缓冲区Buffer**
@ -108,7 +108,7 @@ Channel 是一个通道,它就像自来水管一样,网络数据通过 Chann
一个 多路复用器Selector 可以同时轮询多个 Channel由于 JDK 使用了 epoll() 代替传统的 select 的实现,所以它并没有最大连接句柄的限制。这也就意味着,只需要一个线程负责 Selector 的轮询,就可以接入成千上万的客户端。下面,我们通过 NIO编程的序列图 和 源码分析来熟悉相关的概念。
#### NIO服务端序列图
### NIO服务端序列图
![avatar](/images/Netty/NIO服务端序列图.png)
@ -189,7 +189,7 @@ Channel 是一个通道,它就像自来水管一样,网络数据通过 Chann
```
注意:如果发送区 TCP缓冲区满会导致写半包此时需要注册监听写操作位循环写直到整包消息写入 TCP缓冲区。对于 “半包问题” 此处暂不赘述,后续会单独写一篇详细分析 Netty 的处理策略。
#### NIO 客户端序列图
### NIO 客户端序列图
![avatar](/images/Netty/NIO客户端序列图.png)
@ -299,13 +299,13 @@ NIO2.0 的异步套接字通道是真正的 异步非阻塞IO对应于 UNIX
## 选择 Netty 开发项目的理由
从可维护性角度看,由于 NIO 采用了异步非阻塞编程模型,而且是一个 IO线程 处理多条链路,它的调试和跟踪非常麻烦,特别是生产环境中的问题,我们无法进行有效的调试和跟踪,往往只能靠一些日志来辅助分析,定位难度很大。
#### 为什么不选择 Java原生NIO 进行开发
### 为什么不选择 Java原生NIO 进行开发
1. NIO 的类库和 API 使用起来非常繁杂,需要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等。
2. 需要具备其他的额外技能做铺垫,例如,熟悉 Java多线程编程。这是因为 NIO编程 涉及到 Reactor模式你必须对 多线程 和 网路编程 非常熟悉,才能编写出高质量的 NIO程序。
3. 可靠性能力补齐,工作量和难度都非常大。例如客户端面临:断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理,等问题。
4. JDK NIO 的 BUG例如臭名昭著的 epoll bug它会导致 Selector空轮询最终导致 CPU 100%。虽然官方声称修复了该问题,但是直到 JDK 1.7版本 该问题仍旧未得到彻底的解决。
#### 为什么选择 Netty 进行开发
### 为什么选择 Netty 进行开发
Netty 是业界最流行的 NIO框架 之一,它的健壮性、功能、性能、可定制性和可扩展性在同类框架中都是首屈一指的,已经得到成百上千的商用项目验证,例如 Hadoop 的 RPC框架 Avro ,阿里的 RPC框架 Dubbo 就使用了 Netty 作为底层通信框架。通过对Netty的分析我们将它的优点总结如下。
- API使用简单开发门槛低
- 功能强大,预置了多种编解码功能,支持多种主流协议;

@ -0,0 +1,53 @@
## TCP粘包/拆包
熟悉 TCP编程 的都知道,无论是服务端还是客户端,当我们读取或者发送消息的时候,都需要考虑 TCP底层 的 粘包/拆包机制。TCP粘包/拆包问题,在功能测试时往往不会怎么出现,而一旦并发压力上来,或者发送大报文之后,就很容易出现 粘包 / 拆包问题。如果代码没有考虑,往往就会出现解码错位或者错误,导致程序不能正常工作。本篇博文,我们先简单了解 TCP粘包/拆包 的基础知识,然后来看看 Netty 是如何解决这个问题的。
### TCP粘包/拆包问题说明
TCP 是个 “流” 协议所谓流就是没有界限的一串数据。TCP底层 并不了解上层(如 HTTP协议业务数据的具体含义它会根据 TCP缓冲区 的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被 TCP 拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的 TCP粘包和拆包问题。我们可以通过下面的示例图对 TCP粘包和拆包问题 进行说明。
![avatar](/images/Netty/TCP粘包拆包问题.png)
假设客户端依次发送了两个数据包 DI 和 D2 给服务端由于服务端一次读取到的字节数是不确定的故可能存在以下4种情况。
1. 服务端分两次读取到了两个独立的数据包,分别是 D1 和 D2没有粘包和拆包
2. 服务端一次接收到了两个数据包DI 和 D2 粘合在一起,被称为 TCP粘包
3. 服务端分两次读取到了两个数据包,第一次读取到了完整的 DI包 和 D2包的部分内容第二次读取到了 D2包 的剩余内容,这被称为 TCP拆包
4. 服务端分两次读取到了两个数据包,第一次读取到了 D1包的部分内容第二次读取到了 D1包的剩余内容 和 D2包的整包。
如果此时服务端 TCP 接收滑窗非常小,而 数据包DI 和 D2 比较大很有可能会发生第5种可能即服务端分多次才能将 D1 和 D2包 接收完全,期间发生多次拆包。
### TCP粘包/拆包发生的原因
问题产生的原因有三个,分别如下。
1. **应用程序 write写入的字节大小 超出了 套接口发送缓冲区大小;**
2. 进行 MSS 大小的 TCP分段
3. 以太网帧的 payload 大于 MTU 进行 IP分片。
### 粘包问题的解决策略
由于底层的 TCP 无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,可以归纳如下。
1. 消息定长例如每个报文的大小为固定长度200字节如果不够空位补空格
2. 在包尾增加回车换行符进行分割例如FTP协议
3. 将消息分为消息头和消息体,消息头中包含表示消息总长度 (或者消息体长度) 的字段,通常设计思路为消息头的第一个字段使用 int32 来表示消息的总长度;
4. 更复杂的应用层协议。
介绍完了 TCP粘包/拆包 的基础,下面我们来看看 Netty 是如何使用一系列 “半包解码器” 来解决 TCP粘包/拆包问题的。
## 利用 Netty的解码器 解决 TCP粘拆包问题
TCP 以流的方式进行数据传输上层应用协议为了对消息进行区分往往采用如下4种方式。
1. 固定消息长度,累计读取到长度总和为 定长Len 的报文后,就认为读取到了一个完整的消息,将计数器置位,重新开始读取下一个数据报;
2. 将 “回车换行符” 作为消息结束符,例如 FTP协议这种方式在文本协议中应用比较广泛
3. 将特殊的分隔符作为消息的结束标志,回车换行符是特殊的结束分隔符之一;
4. 在消息头中定义一个 长度字段Len 来标识消息的总长度。
Netty 对上面4种应用做了统一的抽象提供了4种解码器来解决对应的问题。有了这些解码器用户不需要自己对读取的报文进行人工解码也不需要考虑TCP的粘包和拆包。
### LineBasedFrameDecoder 和 StringDecoder 的原理分析
为了解决 TCP粘包 / 拆包 导致的 半包读写问题Netty 默认提供了多种编解码器用于处理半包只要能熟练掌握这些类库的使用TCP粘拆包问题 从此会变得非常容易,你甚至不需要关心它们,这也是其他 NIO框架 和 JDK原生的 NIO API 所无法匹敌的。对于使用者来说,只要将支持半包解码的 Handler 添加到 ChannelPipeline对象 中即可,不需要写额外的代码,使用起来非常简单。
```java
// 示例代码,其中 socketChannel 是一个 SocketChannel对象
socketChannel.pipeline().addLast( new LineBasedFrameDecoder(1024) );
socketChannel.pipeline().addLast( new StringDecoder() );
```
LineBasedFrameDecoder 的工作原理是它依次遍历 ByteBuf 中的可读字节,判断看是否有 “\n” 或者 “\r\n”如果有就以此位置为结束位置从可读索引到结束位置区间的字节就组成了一行。它是以换行符为结束标志的解码器支持携带结束符或者不携带结束符两种解码方式同时支持配置单行的最大长度。如果连续读取到最大长度后仍然没有发现换行符就会抛出异常同时忽略掉之前读到的异常码流。
StringDecoder 的功能非常简单,就是将接收到的对象转换成字符串,然后继续调用后面的 Handler。LineBasedFrameDecoder + StringDecoder组合 就是按行切换的文本解码器,它被设计用来支持 TCP 的粘包和拆包。
### 其它解码器
除了 LineBasedFrameDecoder 以外,还有两个常用的解码器 DelimiterBasedFrameDecoder 和 FixedLengthFrameDecoder前者能自动对 “以分隔符做结束标志的消息” 进行解码,后者可以自动完成对定长消息的解码。使用方法也和前面的示例代码相同,结合 字符串解码器StringDecoder轻松完成对很多消息的自动解码。

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Loading…
Cancel
Save