Netty指南(5)--- 编解码技术

前言

Netty提供了强大的编解码器框架,使得我们编写自定义的编解码器很容易,也容易封装重用。
在网络应用中需要实现某种编解码器,将原始字节数据与自定义的消息对象进行互相转换。
网络中都是以字节码的数据形式来传输数据的,服务器编码数据后发送到客户端,客户端需要对数据进行解码。
编码:将消息对象转成字节或其他序列形式在网络上传输
解码:负责将消息从字节或其他序列形式转成指定的消息对象

TCP粘包/拆包解码器

上层的应用协议为了对消息进行区分,往往采用如下4种方式,也就是我们上一节中讲到的TCP粘包/拆包问题的4种解决方案:

  1. 将消息长度固定,例如将消息长度len = 100,累计读到100字节后就认为读到了一个完整的消息。
  2. 将回车换行符(System.getProperty(“line.separator”))作为消息结束符,例如FTP协议,这种方式在问本协议中应用比较广泛。
  3. 将特殊的分割符作为消息的结束标志,回车换行符就是一种特殊的结束分隔符。
  4. 将消息分为消息头和消息体,消息头中包含消息总长度或消息体长度的字段。

Netty对这4种方式做了统一抽象,提供了4种解码器来解决对应的问题,下面我们分别介绍一下这4种解码器。
由于下面介绍的4种解码器都是为了解决TCP粘包/拆包,所以为了保证发生TCP粘包/拆包现象,每个示例代码中client端都会连续发送多条消息到server。

FixedLengthFrameDecoder

FixedLengthFrameDecoder可以处理长度固定的消息,它有1个参数,int frameLength,当累计读到frameLength个Byte的时候就认为读到了一个完整的消息。

TypeNameDescribe
intframeLength帧长度,当累计读到frameLength个Byte的时候就认为读到了一个完整的消息

点击查看FixedLengthFrameDecoder解码器完整示例代码
示例代码中client向server发送100条消息,由于我们演示的是定长消息解码器,但client与server消息长度不一致,所以server端并没有发送响应消息给client,所以查看server端接收消息的日志即可,你会发现并没有出现TCP粘包/拆包现象。

LineBasedFrameDecoder

LineBasedFrameDecoder可以处理以回车换行符结尾的消息,它有如下3个参数:

TypeNameDescribe
intmaxLength整帧数据的最大长度,整帧数据长度超过maxLength会抛出TooLongFrameException异常。
booleanstripDelimiterstripDelimiter = true:解码后的消息中去掉分隔符
stripDelimiter = false:解码后的消息中包含分隔符
默认:stripDelimiter = true
booleanfailFastfailFast = true:消息长度超过 maxLength 是否立即抛出异常
failFast = false:读取到换行符以后才抛出异常
默认:failFast = false。

点击查看LineBasedFrameDecoder解码器完整示例代码
示例代码中我们同样是让client向server发送100条消息,并在消息结尾增加换行符(System.getProperty(“line.separator”)),运行代码你会发现client与server消息都被正常处理了。

DelimiterBasedFrameDecoder

DelimiterBasedFrameDecoder可以处理以自定义分隔符结尾的消息,你可以定义任何符号,只要是你数据中用不到的符号都可以,它有4个参数和1个特殊的可变长参数:

TypeNameDescribe
intmaxLength整帧数据的最大长度,整帧数据长度超过maxLength会抛出TooLongFrameException异常。
booleanstripDelimiterfailFast = true:解码后的消息中去掉分隔符
failFast = false:解码后的消息中包含分隔符
默认:failFast = true
booleanfailFastfailFast = true:整帧数据长度超过 maxLength 是否立即抛出异常
failFast = false:读取到换行符以后才抛出异常
默认:failFast = false
ByteBufdelimiter消息的分隔符,将你定义的分隔符转为ByteBuf传入
例如 Unpooled.copiedBuffer("^_^".getBytes())
ByteBuf…delimiters与delimiter的区别是,这是个可变长参数,它允许你以ByteBuf[]数组形式传入多个分隔符
例如new ByteBuf[]{Unpooled.copiedBuffer("^_^".getBytes()),Unpooled.copiedBuffer("#_#".getBytes())}

点击查看DelimiterBasedFrameDecoder解码器完整示例代码

LengthFieldBasedFrameDecoder

LengthFieldBasedFrameDecoder可以处理自定义length字段的可变长消息,它需要你的业务协议中包含length字段,根据读取到的length字段来计算整帧数据的长度,它有如下7个参数:

TypeNameDescribe
ByteOrderbyteOrder字节序
大端序:byteOrder = ByteOrder.BIG_ENDIAN
小端序:byteOrder = ByteOrder.LITTLE_ENDIAN
Java采用大端字节序保存数据,如果你的业务协议本身就是大端字节序,则就无需设置。
intmaxFrameLength完整数据包的最大长度,整帧长度超过maxFrameLength会抛出TooLongFrameException异常。
intlengthFieldOffset整帧数据中length字段的起始索引
intlengthFieldLength整帧数据中length字段所占字节长度
intlengthAdjustmentlength字段的补偿值,这个值的设置取决于你业务协议中length字段的值,如果length字段为整包长度,那么lengthAdjustment的值一般为负值,如果length字段为数据体长度,数据体后面还有数据的话,就要通过lengthAdjustment告诉解码器后面还有多少个字节的数据。
intinitialBytesToStrip从解码帧中取出的第一个字节数,可以理解为需要调过的字节数,比如完整数据包长度10字节,length字段占用4字节,剩下6字节为数据体,如果你想只保留数据体,你可将lengthAdjustment=4,length字段所占用的4字节就会被丢弃。
booleanfailFastfailFast = true:整帧数据长度超过 maxLength 是否立即抛出异常
failFast = false:读取到换行符以后才抛出异常
默认:failFast = false

点击查看LengthFieldBasedFrameDecoder解码器完整示例代码

自定义编解码器

Netty为我们抽象了编解码操作,在Netty程序中我们只需要继承下面对应的抽象类,重写其对应的编解码方法,并把编解码器追加到ChannelPipeline中,Netty就会执行我们自定义的编解码器。

  • MessageToByteEncoder 通过重写encode方法,将用户定义的类型转化为byte类型
  • MessageToMessageEncoder 通过重写encode方法,将用户定义的类型转化为另外一种用户定义的类型
  • ByteToMessageDecoder 通过重写decode方法,将byte类型转化为用户定义的类型
  • MessageToMessageDecoder 通过重写decode方法,将用户定义的类型转化为另外一种用户定义的类型

下面链接的代码中,我们分别实现了2个编码器和2个解码器,为了让示例代码变得更加简单,我没有创建自定义的Java对象,而是直接使用字符串类型来代替我们自定义的对象。
自定义编解码器示例代码及client与server的编解码器调用流程图如下:
点击查看Netty自定义编解码器完整示例代码

Java原生序列化存在的问题

Java原生序列化起源于JDK1.1,他不需要添加额外的类库,只需要 implements java.io.Serializable 并生成 serialVersionUID 即可,应用广泛。
但是在远程服务调用(RPC)时很少使用Java原生序列化进行消息的编解码和传输,这是为什么呢,下面来分析一下Java序列化的缺点。

无法跨语言

这是Java原生序列化的致命问题,对于跨进程调用,对端很可能使用其他语言,如C++、Python等,当我们需要和异构语言的程序交互时,Java原生序列化就难以胜任了。
由于Java序列化是Java语言内部的私有协议,其他语言并不支持,对于用户来说它完全是黑盒,使用Java原生序列化后的字节数组,其他语言是没有办法反序列化的,所以目前几乎所有流行的Java RPC通信框架都没有使用Java原生序列化作为编解码框架,主要原因就在于它无法跨语言。

序列化后的码流太大

点击查看Java原生序列化码流大小测试完整示例代码
测试代码的运行结果表明,Java原生序列化的码流大小 是 二进制编码 6倍左右,受数据影响可能差距更大。
评价一个编解码框架往往会考虑以下因素:

  • 是否支持跨语言,支持的语言种类是否丰富
  • 编码后的码流大小
  • 编解码性能
  • 类库是否轻量,API是否简单易用
    在同等情况下,编码后的码流越大,存储的时候占用的空间就越大,硬件成本就越高,在网络传输中占用带宽越高,进而导致系统吞吐量降低。
    所以Java原生序列化,除了无法跨语言,码流太大也是一个很大的缺点。

序列化性能低

点击查看Java原生序列化性能测试完整示例代码
测试代码的运行结果表明,在100w次的序列化测试中,Java原生序列化耗时 是 二进制序列化 7倍左右,受数据影响可能差距更大。
但也足以表明Java原生序列化 无论是 码流大小 还是 性能,都表现的很差,因此我们通常不会选择Java序列化作为RPC调用的编解码框架。

主流第三方编解码框架

下面介绍一些业界主流的编解码框架,并提供示例代码。

MessagePack

MessagePack是一个高效的二进制序列化框架,与JSON类似,支持不同语言间的数据交换,但MessagePack的性能更高,编码后的码流更小。
MessagePack官网
点击查看MessagePack编解码完整示例代码

Protobuf

Protobuf — Google Protocol Buffer,由Google开源的一款结构化数据序列化框架,相比于传统的MXL/JSON等,它更小,更快。
它将数据结构以 .proto 文件进行描述,通过代码生成工具可以生成数据结构对应的对象和Protobuf相关的属性和方法。
Protobuf官网
点击查看Protobuf编解码完整示例代码

Marshalling

Marshalling 是由 JBoss开源的一个Java序列化框架,它修正了JDK自带序列化包的很多问题,但又保持和 java.io.Serializable 接口的兼容,同时增加了可调参数和特性可通过工厂类进行配置。
Marshalling官网
点击查看Marshalling编解码完整示例代码

Choice wechat
关注公众号,获取文章更新通知。
-------------本文结束感谢您的阅读-------------
坚持原创技术分享,您的支持将鼓励我继续创作!