netty的基本概念-编码器和解码器

编、解码技术

通常我们也习惯将编码(Encode)称为序列化(serialization),它将对象序列化为字节数组,用于网络传输、数据持 久化或者其它用途。反之,解码(Decode)/反序列化(deserialization)把从网络、磁盘等读取的字节数组还原成原 始对象(通常是原始对象的拷贝),以方便后续的业务逻辑操作。进行远程跨进程服务调用时(例如 RPC 调用),需要使用特定的编解码技术,对需要进行网络传输的对象做编码或者解码,以便完成远程调用。

Netty 为什么要提供编解码框架?

作为一个高性能的异步、NIO 通信框架,编解码框架是 Netty 的重要组成部分。尽管站在微内核的角度看,编解码框架并不是 Netty 微内核的组成部分,但是通过 ChannelHandler 定制扩展出的编解码框架却是不可或缺的。 然而,我们已经知道在 Netty 中,从网络读取的 Inbound 消息,需要经过解码,将二进制的数据报转换成应用层协议消息或者业务消息,才能够被上层的应用逻辑识别和处理;同理,用户发送到网络的 Outbound 业务消息,需要经过编码转换成二进制字节数组(对于 Netty 就是 ByteBuf)才能够发送到网络对端。编码和解码功能是 NIO 框架的有机 组成部分,无论是由业务定制扩展实现,还是 NIO 框架内置编解码能力,该功能是必不可少的。 为了降低用户的开发难度,Netty 对常用的功能和 API 做了装饰,以屏蔽底层的实现细节。编解码功能的定制,对于熟 悉 Netty 底层实现的开发者而言,直接基于 ChannelHandler 扩展开发,难度并不是很大。但是对于大多数初学者或者不愿意去了解底层实现细节的用户,需要提供给他们更简单的类库和 API,而不是 ChannelHandler。 Netty 在这方面做得非常出色,针对编解码功能,它既提供了通用的编解码框架供用户扩展,又提供了常用的编解码类库供用户直接使用。在保证定制扩展性的基础之上,尽量降低用户的开发工作量和开发门槛,提升开发效率。

Netty 预置的编解码功能列表如下:Base64、Protobuf、JBoss Marshalling、Spdy 等。

Netty 中常用的解码器

ByteToMessageDecoder 抽象解码器

使用 NIO 进行网络编程时,往往需要将读取到的字节数组或者字节缓冲区解码为业务可以使用的 POJO 对象。为了方便业务将 ByteBuf 解码成业务 POJO 对象,Netty 提供了 ByteToMessageDecoder 抽象工具解码类。 用户自定义解码器继承 ByteToMessageDecoder,只需要实现 void decode(ChannelHandler Context ctx, ByteBuf in,List<Object> out)抽象方法即可完成 ByteBuf 到 POJO 对象的解码。

由于 ByteToMessageDecoder 并没有考虑 TCP 粘包和拆包等场景,用户自定义解码器需要自己处理“读半包”问题。正因为如此,大多数场景不会直接继承 ByteToMessageDecoder,而是继承另外一些更高级的解码器来屏蔽半包的处理。实际项目中,通常将 LengthFieldBasedFrameDecoder 和 ByteToMessageDecoder 组合使用,前者负责将网络读取的 数据报解码为整包消息,后者负责将整包消息解码为最终的业务对象。除了和其它解码器组合形成新的解码器之外,ByteToMessageDecoder 也是很多基础解码器的父类,它的继承关系如下图所示:

LineBasedFrameDecoder 行解码器

LineBasedFrameDecoder 是回车换行解码器,如果用户发送的消息以回车换行符(以\r\n 或者直接以\n 结尾)作为消 息结束的标识,则可以直接使用 Netty 的 LineBasedFrameDecoder 对消息进行解码,只需要在初始化 Netty 服务端或 者客户端时将 LineBasedFrameDecoder 正确的添加到 ChannelPipeline 中即可,不需要自己重新实现一套换行解码器。 LineBasedFrameDecoder 的工作原理是它依次遍历 ByteBuf 中的可读字节,判断看是否有“\n”或者“\r\n”,如果有,就 以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行。它是以换行符为结束标志的解码器,支持携带结束符或者不携带结束符两种解码方式,同时支持配置单行的最大长度。如果连续读取到最大长度后仍然没有发现 换行符,就会抛出异常,同时忽略掉之前读到的异常码流。防止由于数据报没有携带换行符导致接收到 ByteBuf 无限 制积压,引起系统内存溢出

通常情况下,LineBasedFrameDecoder 会和 StringDecoder 配合使用,组合成按行切换的文本解码器,对于文本类协议的解析,文本换行解码器非常实用,例如对 HTTP 消息头的解析、FTP 协议消息的解析等。

DelimiterBasedFrameDecoder 分隔符解码器

DelimiterBasedFrameDecoder 分隔符解码器,是按照指定分隔符进行解码的解码器, 通过分隔符, 可以将二进制流拆分成完整的数据包。回车换行解码器实际上是一种特殊的 DelimiterBasedFrameDecoder 解码器。

分隔符的指定:与大家的习惯不同,分隔符并非以 char 或者 string 作为构造参数,而是 ByteBuf

FixedLengthFrameDecoder 固定长度解码器

FixedLengthFrameDecoder 固定长度解码器,它能够按照指定的长度对消息进行自动解码,开发者不需要考虑 TCP 的粘包/拆包等问题,非常实用。

对于定长消息,如果消息实际长度小于定长,则往往会进行补位操作,它在一定程度上导致了空间和资源的浪费。但是它的优点也是非常明显的,编解码比较简单,因此在实际项目中仍然有一定的应用场景。

利用 FixedLengthFrameDecoder 解码器,无论一次接收到多少数据报,它都会按照构造函数中设置的固定长度进行解码,如果是半包消息,FixedLengthFrameDecoder 会缓存半包消息并等待下个包到达后进行拼包,直到读取到一个完整的包。

LengthFieldBasedFrameDecoder 通用解码器

了解 TCP 通信机制的该都知道 TCP 底层的粘包和拆包,当我们在接收消息的时候,显示不能认为读取到的报文就是个整包消息,特别是对于采用非阻塞 I/O 和长连接通信的程序。

如何区分一个整包消息,通常有如下 4 种做法:

1) 固定长度,例如每 120 个字节代表一个整包消息,不足的前面补位。解码器在处理这类定常消息的时候比较简单,每次读到指定长度的字节后再进行解码;

2) 通过回车换行符区分消息,例如 HTTP 协议。这类区分消息的方式多用于文本协议;

3) 通过特定的分隔符区分整包消息;

4) 通过在协议头/消息头中设置长度字段来标识整包消息。

MessageToByteEncoder 抽象编码器

同解码器一样, 编码器中也有一个抽象类叫 MessageToByteEncoder, 其中定义了编码器的骨架方法, 具体编码逻辑交给子类实现。解码器同样也是个 handler, 将写出的数据进行截取处理, 我们在学习 Pipeline 时我们知道, 写数据的时候会传递 write 事件, 传递过程中会调用 handler 的 write 方法, 所以编码器码器可以重写 write 方法, 将数据编码成二进制字节流然后再继续传递 write 事件。首先来看 MessageToByteEncoder 的类声明:MessageToByteEncoder 负责将POJO 对象编码成 ByteBuf,用户的编码器继承 Message ToByteEncoder,实现 void encode(ChannelHandlerContextctx, I msg, ByteBuf out)接口接口

它的实现原理如下:调用 write 操作时,首先判断当前编码器是否支持需要发送的消息,如果不支持则直接透传;如果支持则判断缓冲区的类型,对于直接内存分配 ioBuffer(堆外内存),对于堆内存通过 heapBuffer 方法分配.

编码使用的缓冲区分配完成之后,调用 encode 抽象方法进行编码,方法定义如下:它由子类负责具体实现。

protected abstract void encode(ChannelHandlerContext ctx, I msg, ByteBuf out) throws Exception;

编码完成之后,调用 ReferenceCountUtil 的 release 方法释放编码对象 msg。对编码后的 ByteBuf 进行以下判断: 1) 如果缓冲区包含可发送的字节,则调用 ChannelHandlerContext 的 write 方法发送 ByteBuf; 2) 如果缓冲区没有包含可写的字节,则需要释放编码后的 ByteBuf,写入一个空的 ByteBuf 到 ChannelHandlerContext中。

发送操作完成之后,在方法退出之前释放编码缓冲区 ByteBuf 对象。

MessageToMessageDecoder 抽象解码器

MessageToMessageDecoder 实际上是 Netty 的二次解码器,它的职责是将一个对象二次解码为其它对象。 为什么称它为二次解码器呢?我们知道,从 SocketChannel 读取到的 TCP 数据报是 ByteBuffer,实际就是字节数组。我们首先需要将 ByteBuffer 缓冲区中的数据报读取出来,并将其解码为 Java 对象;然后对 Java 对象根据某些规则做二次解码,将其解码为另一个 POJO 对象。因为 MessageToMessageDecoder 在 ByteToMessageDecoder 之后,所以称之为二次解码器。

MessageToMessageEncoder 抽象编码器

将一个 POJO 对象编码成另一个对象,以 HTTP+XML 协议为例,它的一种实现方式是:先将 POJO 对象编码成 XML字符串,再将字符串编码为 HTTP 请求或者应答消息。对于复杂协议,往往需要经历多次编码,为了便于功能扩展,可以通过多个编码器组合来实现相关功能。

用户的解码器继承 MessageToMessageEncoder 解码器,实现 void encode(Channel HandlerContext ctx, I msg, List out)方法即可。注意,它与 MessageToByteEncoder 的区别是输出是对象列表而不是 ByteBuf,

MessageToMessageEncoder 编码器的实现原理与之前分析的 MessageToByteEncoder 相似,唯一的差别是它编码后的输出是个中间对象,并非最终可传输的 ByteBuf。

ObjectEncoder 序列化编码器

ObjectEncoder 是 Java 序列化编码器,它负责将实现 Serializable 接口的对象序列化为 byte [],然后写入到 ByteBuf中用于消息的跨网络传输。下面我们一起分析下它的实现,首先,我们发现它继承自 MessageToByteEncoder,它的作用就是将对象编码成 ByteBuf

如果要使用 Java 序列化,对象必须实现 Serializable 接口,因此,它的泛型类型为 Serializable。 MessageToByteEncoder 的子类只需要实现 encode(ChannelHandlerContext ctx, I msg, ByteBuf out)方法即可

首先创建 ByteBufOutputStream 和 ObjectOutputStream,用于将 Object 对象序列化到 ByteBuf 中,值得注意的是在writeObject 之前需要先将长度字段(4 个字节)预留,用于后续长度字段的更新。 依次写入长度占位符(4 字节)、序列化之后的 Object 对象,之后根据 ByteBuf 的 writeIndex 计算序列化之后的码流长度,最后调用 ByteBuf 的 setInt(int index, int value)更新长度占位符为实际的码流长度。 有个细节需要注意,更新码流长度字段使用了 setInt 方法而不是 writeInt,原因就是 setInt 方法只更新内容,并不修改readerIndex 和 writerIndex

LengthFieldPrepender 通用编码器

如果协议中的第一个字段为长度字段,Netty 提供了 LengthFieldPrepender 编码器,它可以计算当前待发送消息的二进制字节长度,将该长度添加到 ByteBuf 的缓冲区头中

通过 LengthFieldPrepender 可以将待发送消息的长度写入到 ByteBuf 的前 2 个字节,编码后的消息组成为长度字段+原消息的方式.

通过设置 LengthFieldPrepender 为 true,消息长度将包含长度本身占用的字节数,打开 LengthFieldPrepender 后

LengthFieldPrepender 工作原理分析如下:首先对长度字段进行设置,如果需要包含消息长度自身,则在原来长度的基础之上再加上 lengthFieldLength 的长度。 如果调整后的消息长度小于 0,则抛出参数非法异常。对消息长度自身所占的字节数进行判断,以便采用正确的方法将长度字段写入到 ByteBuf 中,共有以下 6 种可能: 1) 长度字段所占字节为 1:如果使用 1 个 Byte 字节代表消息长度,则最大长度需要小于 256 个字节。对长度进行校验, 如果校验失败,则抛出参数非法异常;若校验通过,则创建新的 ByteBuf 并通过 writeByte 将长度值写入到 ByteBuf 中; 2) 长度字段所占字节为 2:如果使用 2 个 Byte 字节代表消息长度,则最大长度需要小于 65536 个字节,对长度进行 校验,如果校验失败,则抛出参数非法异常;若校验通过,则创建新的 ByteBuf 并通过 writeShort 将长度值写入到 ByteBuf 中; 3) 长度字段所占字节为 3:如果使用 3 个 Byte 字节代表消息长度,则最大长度需要小于 16777216 个字节,对长度进 行校验,如果校验失败,则抛出参数非法异常;若校验通过,则创建新的 ByteBuf 并通过 writeMedium 将长度值写入 到 ByteBuf 中; 4) 长度字段所占字节为 4:创建新的 ByteBuf,并通过 writeInt 将长度值写入到 ByteBuf 中; 5) 长度字段所占字节为 8:创建新的 ByteBuf,并通过 writeLong 将长度值写入到 ByteBuf 中; 6) 其它长度值:直接抛出 Error。

猜你喜欢

转载自blog.csdn.net/madongyu1259892936/article/details/109669058