
2.3 编码和解码
在第1章介绍Netty客户端与服务端的通信原理时,使用过编码器和解码器,但并未对其底层原理进行详细的介绍。如果使用Java NIO来实现TCP网络通信,则需要对TCP连接中的问题进行全面的考虑,如拆包和粘包导致的半包问题和数据序列化等。对于这些问题,Netty都做了很好的处理。本节通过Netty的编码和解码架构及其源码对上述问题进行详细剖析。下面先看一幅简单的TCP通信图,如图2-4所示。

图2-4 TCP通信图
在图2-4中,客户端给服务端发送消息并收到服务端返回的结果,共经历了以下6步。
①TCP是面向字节流传输的协议,它把客户端提交的请求数据看作一连串的无结构的字节流,并不知道所传送的字节流的含义,也并不关心有多少数据流入TCP输出缓冲区中。
②每次发多少数据到网络中与当前的网络拥塞情况和服务端返回的TCP窗口的大小有关,涉及TCP的流量控制和阻塞控制,且与Netty的反压有关。如果客户端发送到TCP输出缓冲区的数据块太多,那么TCP会分割成多次将其传送出去;如果太少,则会等待积累足够多的字节后发送出去。很明显,TCP这种传输机制会产生粘包问题。
③当服务端读取TCP输入缓冲区中的数据时,需要进行拆包处理,并解决粘包和拆包问题,比较常用的方案有以下3种。
• 将换行符号或特殊标识符号加入数据包中,如HTTP和FTP等。
• 将消息分为head和body,head中包含body长度的字段,一般前面4个字节是body的长度值,用int类型表示,但也有像Dubbo协议那种,head中除body长度外,还有版本号、请求类型、请求id等。
• 固定数据包的长度,如固定100个字节,不足补空格。
步骤④~⑥与步骤①~③类似。TCP的这些机制与Netty的编码和解码有很大的关系。Netty采用模板设计模式实现了一套编码和解码架构,高度抽象,底层解决TCP的粘包和拆包问题,对前面介绍的3种方案都做了具体实现。
第1种方案,Netty有解码器LineBasedFrameDecoder,可以判断字节中是否出现了“\n”或“\r\n”。
第2种方案,Netty有编解码器LengthFieldPrepender和LengthFieldBasedFrameDecoder,可以在消息中加上消息体长度值,这两个编解码器在之前的实战中用到过。
第3种方案,Netty有固定数据包长度的解码器FixedLengthFrameDecoder。此方案一般用得较少,比较常用的是前两种方案。
Netty对编码和解码进行了抽象处理。编码器和解码器大部分都有共同的编码和解码父类,即MessageToMessageEncoder与ByteToMessageDecoder。ByteToMessageDecoder父类在读取TCP缓冲区的数据并解码后,将剩余的数据放入了读半包字节容器中,具体解码方案由子类负责。在解码的过程中会遇到读半包,无法解码的数据会保存在读半包字节容器中,等待下次读取数据后继续解码。编码逻辑比较简单,MessageToMessageEncoder父类定义了整个编码的流程,并实现了对已读内存的释放,具体编码格式由子类负责。
Netty的编码和解码除了解决TCP协议的粘包和拆包问题,还有一些编解码器做了很多额外的事情,如StringEncode(把字符串转换成字节流)、ProtobufDecoder(对Protobuf序列化数据进行解码);还有各种常用的协议编解码器,如HTTP2、Websocket等。本节只是介绍了Netty为什么要编/解码、Netty编/解码的实现思想,以及一些常用编/解码的使用,后续章节会对常用的编码器和解码器进行实战应用,并进行详细的源码剖析。