Netty核心指南——TCP通信与数据处理
Netty系列文章
目录
1. TCP粘包、拆包
1.1 TCP粘包/拆包
TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分。所以业务上,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送。
-
问题说明

还有第5种可能:如果此时服务端TCP接收滑窗非常小,而数据包大,可能会发生多次拆包。 -
发生的原因
- 应用程序write写入的字节大小大于套接口发送缓冲区大小
- 进行MSS大小的TCP分段
- 以太网帧的payload大于MTU进行IP分片
- 解决策略 - 由于底层的TCP无法理解上层的业务数据,所以底层是无法保证数据包不被拆分和重组的,只能通过上层的应用协议栈设计来解决:
- 消息定长,如果不够,空位补空格;
- 在包尾增加回车换行符进行分割,比如FTP协议;
- 将消息分为消息头和消息体,消息头中包含表示消息总长度的字段;
- 更复杂的应用层协议。
1.2 利用LineBasedFrameDecoder解决TCP粘包问题
- 为了解决TCP粘包/拆包导致的半包读写问题,Netty默认提供了多种编解码器用于处理半包。
- 在TimeServer和TimeClient的例子中:通过使用
LineBasedFrameDecoder和StringDecoder成功解决了TCP粘包导致的读半包问题。只要将支持半包解码的Handler添加到ChannelPipeline中即可。 LineBasedFrameDecoder- 是以换行符为结束标志的解码器。
- 工作原理:它依次遍历
ByteBuf中的可读字节,判断看是否有"\n"或者"\r\n",如果有,就以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行。 - 支持携带结束符或不携带结束符两种解码方式;支持配置单行最大长度。
StringDecoder- 将接收到的对象转换成字符串,然后继续调用后面的Handler。
LineBasedFrameDecoder+StringDecoder组合就是按行切换的文本解码器。它被设计用来支持TCP的粘包和拆包。
2. 分隔符和定长解码器的应用
TCP以流的方式进行数据传输,上层的应用协议为了对消息区分,往往采用以下四种方式(第4章有提到):
- 消息定长
- 将回车换行符作为消息结束符
- 将特殊的分隔符作为消息的结束标志
- 消息头中定义消息总长度的字段
Netty对这4种应用做了统一的抽象,提供了4种解码器来解决对应问题。
2.1 DelimiterBasedFrameDecoder应用开发
使用DelimiterBasedFrameDecoder,可以自动完成以分隔符做结束标志的消息的解码。以Echo服务为例:
EchoServer服务端EchoServer
- 首先创建分隔符缓冲对象
ByteBuf,本例使用"$_ "作为分隔符。 - 创建
DelimiterBasedFrameDecoder对象并将其加入到ChannelPipeline中。DelimiterBasedFrameDecoder有多个构造方法,这里传递两个参数:- 1024表示单条消息的最大长度
- 分隔符缓冲对象
- 首先创建分隔符缓冲对象
EchoServerHandler- 由于
DelimiterBasedFrameDecoder自动对请求消息进行了解码,后续的ChannelHandler接收到的msg对象就是个完整的信息包; - 第二个
ChannelHandler是StringDecoder,它将ByteBuf解码成字符串对象; - 第三个
EchoServerHandler接收到的msg消息就是解码后的字符串对象。
- 由于
EchoClient客户端
与服务端类似,分别将DelimiterBasedFrameDecoder和StringDecoder添加到客户ChannelPipeline中,最后添加客户端I/O事件处理类EchoClientHandler。
2.2 FixedLengthFrameDecoder应用开发
- 是固定长度解码器,它能够按照指定长度对消息进行自动解码,开发者不需要考虑TCP粘包/拆包问题。
- 在Echo服务端ChannelPipeline中新增FixedLengthFrameDecoder,长度设置为20,然后再依次增加字符串解码器和EchoServerHandler。

- 无论一次接收到多少数据报,它都会按照构造函数中设置的固定长度进行解码,如果是半包消息,它会缓存半包消息并等待下个包到达后进行拼包,直到读取到一个完整的包。
2.3 总结
应用DelimiterBasedFrameDecoder和FixedLengthFrameDecoder进行开发非常简单,绝大多数情况下,只要将DelimiterBasedFrameDecoder或FixedLengthFrameDecoder添加到对应ChannelPipeline的起始位即可。
3. 编解码技术
- 基于Java提供的对象输入/输出流
ObjectInputStream和ObjectOutputStream可以直接把Java对象作为可存储的字节数组写入文件,也可以传输到网络上。基于JDK默认的序列化机制可以避免操作底层的字节数组,提高开发效率。 - Java序列化的目的主要有两个:
- 网络传输
- 对象持久化
- Java对象编解码技术:当进行远程跨进程服务调用时,需要把被传输的Java对象编码为字节数组或ByteBuffer对象。而当远程服务读取到ByteBuffer对象或者字节数组时,需要将其解码为发送时的Java对象。
- Java序列化:仅仅是Java编解码技术的一种,由于它的种种缺陷,衍生出了多种编解码技术和框架。
3.1 Java序列化的缺点
Java序列化不需要添加额外的类库,只需实现java.io.Serializable并生称序列ID即可,但在远程服务调用(RPC)时,很少直接使用Java序列化进行消息的编解码和传输。它的缺点如下:
- 无法跨语言
- 对于跨进程的服务调用,服务提供者可能会使用C++或者其他语言开发,需要交互时Java序列化会难以胜任。对于Java序列化后的字节数组,别的语言无法进行反序列化。
- 序列化后的码流太大
- 经过测试,采用JDK序列化机制编码后的二进制数组大小是二进制编码的5.29倍。编码后的字节数组越大,存储时越占空间,存储的硬件成本越高,网络传输时更占带宽,系统吞吐量降低。
- 序列化性能太低
- 只有二进制编码的6.17%左右。
- 不使用JDK提供的默认序列化框架,业界有其他很多优秀的编解码框架,它们在克服了这些缺点的基础上还增加了很多亮点。
3.2 业界主流的编解码框架
3.2.1 Google的Protobuf
- 全称Google Protocol Buffers,它由谷歌开源而来。它用.proto文件描述数据结构,通过代码生成工具可以生成对应数据结构的POJO对象和Protobuf相关的方法和属性。
- 特点:
- 结构化数据存储格式(XML,JSON等);
- 高效的编解码性能;
- 语言无关、平台无关、扩展性好;
- 官方支持Java、C++和Python三种语言。
- 为什么不用XML?——XML解析时间开销和为可读性而牺牲的空间开销都很大,不适合做高性能的通信协议。
- Protobuf编解码性能远远高于其他几种序列化框架的序列化和反序列化。
3.2.2 Facebook的Thrift
- Facebook当时创造它是为了解决其各系统间大数据量的传输通信、以及系统之间语言环境不同需要跨平台的特性。因此Thrift可以支持多种程序语言(如C++, C#, Cocoa, Erlang, Haskell, Java, Ocami, Perl, PHP, Python, Ruby和Smalltalk)。
- Thrift可以作为高性能的通信中间件使用,适用于静态的数据交换,适用于搭建大型数据交换及存储的通用工具。
- Thrift支持三种比较典型的编解码方式:
- 通用的二进制编解码
- 压缩二进制编解码
- 优化的可选字段压缩编解码
3.2.3 JBoss Marshalling
是一个Java对象的序列化API包,修正了JDK自带的序列化包的很多问题,但又保持跟java.io.Serializable接口的兼容。相比于前面两种,它更多是在JBoss内部使用,应用范围有限。
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)