李林锋 IT哈哈 java
1.1. 编解码技术程序员
一般咱们也习惯将编码(Encode)称为序列化(serialization),它将对象序列化为字节数组,用于网络传输、数据持久化或者其它用途。算法
反之,解码(Decode)/反序列化(deserialization)把从网络、磁盘等读取的字节数组还原成原始对象(一般是原始对象的拷贝),以方便后续的业务逻辑操做。编程
进行远程跨进程服务调用时(例如RPC调用),须要使用特定的编解码技术,对须要进行网络传输的对象作编码或者解码,以便完成远程调用。后端
1.2. 经常使用的编解码框架数组
1.2.1. Java序列化缓存
相信大多数Java程序员接触到的第一种序列化或者编解码技术就是Java默认提供的序列化机制,须要序列化的Java对象只须要实现 java.io.Serializable接口并生成序列化ID,这个类就可以经过java.io.ObjectInput和 java.io.ObjectOutput序列化和反序列化。网络
因为使用简单,开发门槛低,Java序列化获得了普遍的应用,可是因为它自身存在不少缺点,所以大多数的RPC框架并无选择它。Java序列化的主要缺点以下:数据结构
1) 没法跨语言:是Java序列化最致命的问题。对于跨进程的服务调用,服务提供者可能会使用C++或者其它语言开发,当咱们须要和异构语言进程交互 时,Java序列化就难以胜任。因为Java序列化技术是Java语言内部的私有协议,其它语言并不支持,对于用户来讲它彻底是黑盒。Java序列化后的 字节数组,别的语言没法进行反序列化,这就严重阻碍了它的应用范围;架构
2) 序列化后的码流太大: 例如使用二进制编解码技术对同一个复杂的POJO对象进行编码,它的码流仅仅为Java序列化以后的20%左右;目前主流的编解码框架,序列化以后的码流都远远小于原生的Java序列化;
3) 序列化效率差:在相同的硬件条件下、对同一个POJO对象作100W次序列化,二进制编码和Java原生序列化的性能对比测试以下图所示:Java原生序列化的耗时是二进制编码的16.2倍,效率很是差。
图1-1 二进制编码和Java原生序列化性能对比
1.2.2. Google的Protobuf
Protobuf全称Google Protocol Buffers,它由谷歌开源而来,在谷歌内部久经考验。它将数据结构以.proto文件进行描述,经过代码生成工具能够生成对应数据结构的POJO对象和Protobuf相关的方法和属性。
它的特色以下:
1) 结构化数据存储格式(XML,JSON等);
2) 高效的编解码性能;
3) 语言无关、平台无关、扩展性好;
4) 官方支持Java、C++和Python三种语言。
首先咱们来看下为何不使用XML,尽管XML的可读性和可扩展性很是好,也很是适合描述数据结构,可是XML解析的时间开销和XML为了可读性而牺牲的空间开销都很是大,所以不适合作高性能的通讯协议。Protobuf使用二进制编码,在空间和性能上具备更大的优点。
Protobuf另外一个比较吸引人的地方就是它的数据描述文件和代码生成机制,利用数据描述文件对数据结构进行说明的优势以下:
1) 文本化的数据结构描述语言,能够实现语言和平台无关,特别适合异构系统间的集成;
2) 经过标识字段的顺序,能够实现协议的前向兼容;
3) 自动代码生成,不须要手工编写一样数据结构的C++和Java版本;
4) 方便后续的管理和维护。相比于代码,结构化的文档更容易管理和维护。
1.2.3. Apache的Thrift
Thrift源于Facebook,在2007年Facebook将Thrift做为一个开源项目提交给Apache基金会。对于当时的 Facebook来讲,创造Thrift是为了解决Facebook各系统间大数据量的传输通讯以及系统之间语言环境不一样须要跨平台的特性,所以 Thrift能够支持多种程序语言,如C++、C#、Cocoa、Erlang、Haskell、Java、Ocami、Perl、PHP、 Python、Ruby和Smalltalk。
在多种不一样的语言之间通讯,Thrift能够做为高性能的通讯中间件使用,它支持数据(对象)序列化和多种类型的RPC服务。Thrift适用于静 态的数据交换,须要先肯定好它的数据结构,当数据结构发生变化时,必须从新编辑IDL文件,生成代码和编译,这一点跟其余IDL工具相比能够视为是 Thrift的弱项。Thrift适用于搭建大型数据交换及存储的通用工具,对于大型系统中的内部数据传输,相对于JSON和XML在性能和传输大小上都 有明显的优点。
Thrift主要由5部分组成:
1) 语言系统以及IDL编译器:负责由用户给定的IDL文件生成相应语言的接口代码;
2) TProtocol:RPC的协议层,能够选择多种不一样的对象序列化方式,如JSON和Binary;
3) TTransport:RPC的传输层,一样能够选择不一样的传输层实现,如socket、NIO、MemoryBuffer等;
4) TProcessor:做为协议层和用户提供的服务实现之间的纽带,负责调用服务实现的接口;
5) TServer:聚合TProtocol、TTransport和TProcessor等对象。
咱们重点关注的是编解码框架,与之对应的就是TProtocol。因为Thrift的RPC服务调用和编解码框架绑定在一块儿,因此,一般咱们使用Thrift的时候会采起RPC框架的方式。可是,它的TProtocol编解码框架仍是能够以类库的方式独立使用的。
与Protobuf比较相似的是,Thrift经过IDL描述接口和数据结构定义,它支持8种Java基本类型、Map、Set和List,支持可选和必选定义,功能很是强大。由于能够定义数据结构中字段的顺序,因此它也能够支持协议的前向兼容。
Thrift支持三种比较典型的编解码方式:
1) 通用的二进制编解码;
2) 压缩二进制编解码;
3) 优化的可选字段压缩编解码。
因为支持二进制压缩编解码,Thrift的编解码性能表现也至关优异,远远超过Java序列化和RMI等。
1.2.4. JBoss Marshalling
JBoss Marshalling是一个Java对象的序列化API包,修正了JDK自带的序列化包的不少问题,但又保持跟java.io.Serializable接口的兼容;同时增长了一些可调的参数和附加的特性,而且这些参数和特性可经过工厂类进行配置。
相比于传统的Java序列化机制,它的优势以下:
1) 可插拔的类解析器,提供更加便捷的类加载定制策略,经过一个接口便可实现定制;
2) 可插拔的对象替换技术,不须要经过继承的方式;
3) 可插拔的预约义类缓存表,能够减少序列化的字节数组长度,提高经常使用类型的对象序列化性能;
4) 无须实现java.io.Serializable接口,便可实现Java序列化;
5) 经过缓存技术提高对象的序列化性能。
相比于前面介绍的两种编解码框架,JBoss Marshalling更可能是在JBoss内部使用,应用范围有限。
1.2.5. 其它编解码框架
除了上述介绍的编解码框架和技术以外,比较经常使用的还有MessagePack、kryo、hession和Json等。限于篇幅所限,再也不一一枚举,感兴趣的朋友能够自行查阅相关资料学习。
2.1. Netty为何要提供编解码框架
做为一个高性能的异步、NIO通讯框架,编解码框架是Netty的重要组成部分。尽管站在微内核的角度看,编解码框架并非Netty微内核的组成部分,可是经过ChannelHandler定制扩展出的编解码框架倒是不可或缺的。
下面咱们从几个角度详细谈下这个话题,首先一块儿看下Netty的逻辑架构图:
图2-1 Netty逻辑架构图
从网络读取的inbound消息,须要通过解码,将二进制的数据报转换成应用层协议消息或者业务消息,才可以被上层的应用逻辑识别和处理;同理,用 户发送到网络的outbound业务消息,须要通过编码转换成二进制字节数组(对于Netty就是ByteBuf)才可以发送到网络对端。编码和解码功能 是NIO框架的有机组成部分,不管是由业务定制扩展实现,仍是NIO框架内置编解码能力,该功能是必不可少的。
为了下降用户的开发难度,Netty对经常使用的功能和API作了装饰,以屏蔽底层的实现细节。编解码功能的定制,对于熟悉Netty底层实现的开发者 而言,直接基于ChannelHandler扩展开发,难度并非很大。可是对于大多数初学者或者不肯意去了解底层实现细节的用户,须要提供给他们更简单 的类库和API,而不是ChannelHandler。
Netty在这方面作得很是出色,针对编解码功能,它既提供了通用的编解码框架供用户扩展,又提供了经常使用的编解码类库供用户直接使用。在保证定制扩展性的基础之上,尽可能下降用户的开发工做量和开发门槛,提高开发效率。
Netty预置的编解码功能列表以下:base6四、Protobuf、JBoss Marshalling、spdy等。
图2-2 Netty预置的编解码功能列表
2.2. 经常使用的解码器
2.2.1. LineBasedFrameDecoder解码器
LineBasedFrameDecoder是回车换行解码器,若是用户发送的消息以回车换行符做为消息结束的标识,则能够直接使用Netty的 LineBasedFrameDecoder对消息进行解码,只须要在初始化Netty服务端或者客户端时将LineBasedFrameDecoder 正确的添加到ChannelPipeline中便可,不须要本身从新实现一套换行解码器。
LineBasedFrameDecoder的工做原理是它依次遍历ByteBuf中的可读字节,判断看是否有“\n”或者“\r\n”,若是有, 就以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行。它是以换行符为结束标志的解码器,支持携带结束符或者不携带结束符两种解码方式,同 时支持配置单行的最大长度。若是连续读取到最大长度后仍然没有发现换行符,就会抛出异常,同时忽略掉以前读到的异常码流。防止因为数据报没有携带换行符导 致接收到ByteBuf无限制积压,引发系统内存溢出。
它的使用效果以下:
解码以前:
+-----------------------------------------------------+ 接收到的数据报 “This is a netty example for using the nio framework.\r\n When you“ +-----------------------------------------------------+ 解码以后的ChannelHandler接收到的Object以下: +-----------------------------------------------------+ 解码以后的文本消息 “This is a netty example for using the nio framework.“ +------------------------------------------------- ---+
一般状况下,LineBasedFrameDecoder会和StringDecoder配合使用,组合成按行切换的文本解码器,对于文本类协议的解析,文本换行解码器很是实用,例如对HTTP消息头的解析、FTP协议消息的解析等。
下面咱们简单给出文本换行解码器的使用示例:
@Override protected void initChannel(SocketChannel arg0) throws Exception { arg0.pipeline().addLast(new LineBasedFrameDecoder(1024)); arg0.pipeline().addLast(new StringDecoder()); arg0.pipeline().addLast(new UserServerHandler()); }
初始化Channel的时候,首先将LineBasedFrameDecoder添加到ChannelPipeline中,而后再依次添加字符串解码器StringDecoder,业务Handler。
2.2.2. DelimiterBasedFrameDecoder解码器
DelimiterBasedFrameDecoder是分隔符解码器,用户能够指定消息结束的分隔符,它能够自动完成以分隔符做为码流结束标识的消息的解码。回车换行解码器其实是一种特殊的DelimiterBasedFrameDecoder解码器。
分隔符解码器在实际工做中也有很普遍的应用,笔者所从事的电信行业,不少简单的文本私有协议,都是以特殊的分隔符做为消息结束的标识,特别是对于那些使用长链接的基于文本的私有协议。
分隔符的指定:与你们的习惯不一样,分隔符并不是以char或者string做为构造参数,而是ByteBuf,下面咱们就结合实际例子给出它的用法。
假如消息以“$_”做为分隔符,服务端或者客户端初始化ChannelPipeline的代码实例以下:
@Override public void initChannel(SocketChannel ch) throws Exception { ByteBuf delimiter = Unpooled.copiedBuffer("$_" .getBytes()); ch.pipeline().addLast( new DelimiterBasedFrameDecoder(1024, delimiter)); ch.pipeline().addLast(new StringDecoder()); ch.pipeline().addLast(new UserServerHandler()); }
首先将“$_”转换成ByteBuf对象,做为参数构造DelimiterBasedFrameDecoder,将其添加到 ChannelPipeline中,而后依次添加字符串解码器(一般用于文本解码)和用户Handler,请注意解码器和Handler的添加顺序,若是 顺序颠倒,会致使消息解码失败。
DelimiterBasedFrameDecoder原理分析:解码时,判断当前已经读取的ByteBuf中是否包含分隔符ByteBuf,若是包含,则截取对应的ByteBuf返回,源码以下:
详细分析下indexOf(buffer, delim)方法的实现,代码以下:
该算法与Java String中的搜索算法相似,对于原字符串使用两个指针来进行搜索,若是搜索成功,则返回索引位置,不然返回-1。
2.2.3. FixedLengthFrameDecoder解码器
FixedLengthFrameDecoder是固定长度解码器,它可以按照指定的长度对消息进行自动解码,开发者不须要考虑TCP的粘包/拆包等问题,很是实用。
对于定长消息,若是消息实际长度小于定长,则每每会进行补位操做,它在必定程度上致使了空间和资源的浪费。可是它的优势也是很是明显的,编解码比较简单,所以在实际项目中仍然有必定的应用场景。
利用FixedLengthFrameDecoder解码器,不管一次接收到多少数据报,它都会按照构造函数中设置的固定长度进行解码,若是是半包消息,FixedLengthFrameDecoder会缓存半包消息并等待下个包到达后进行拼包,直到读取到一个完整的包。
假如单条消息的长度是20字节,使用FixedLengthFrameDecoder解码器的效果以下:
解码前:
+----------------------------------------------------------+
接收到的数据报
“HELLO NETTY FOR USER DEVELOPER“
+----------------------------------------------------------+
解码后:
+----------------------------------------------------------+
解码后的数据报
“HELLO NETTY FOR USER“
+----------------------------------------------------------+
2.2.4. LengthFieldBasedFrameDecoder解码器
了解TCP通讯机制的读者应该都知道TCP底层的粘包和拆包,当咱们在接收消息的时候,显示不能认为读取到的报文就是个整包消息,特别是对于采用非阻塞I/O和长链接通讯的程序。
如何区分一个整包消息,一般有以下4种作法:
1) 固定长度,例如每120个字节表明一个整包消息,不足的前面补位。解码器在处理这类定常消息的时候比较简单,每次读到指定长度的字节后再进行解码;
2) 经过回车换行符区分消息,例如HTTP协议。这类区分消息的方式多用于文本协议;
3) 经过特定的分隔符区分整包消息;
4) 经过在协议头/消息头中设置长度字段来标识整包消息。
前三种解码器以前的章节已经作了详细介绍,下面让咱们来一块儿学习最后一种通用解码器-LengthFieldBasedFrameDecoder。
大多数的协议(私有或者公有),协议头中会携带长度字段,用于标识消息体或者整包消息的长度,例如SMPP、HTTP协议等。因为基于长度解码需求 的通用性,以及为了下降用户的协议开发难度,Netty提供了LengthFieldBasedFrameDecoder,自动屏蔽TCP底层的拆包和粘 包问题,只须要传入正确的参数,便可轻松解决“读半包“问题。
下面咱们看看如何经过参数组合的不一样来实现不一样的“半包”读取策略。第一种经常使用的方式是消息的第一个字段是长度字段,后面是消息体,消息头中只包含一个长度字段。它的消息结构定义如图所示:
图2-3 解码前的字节缓冲区(14字节)
使用如下参数组合进行解码:
1) lengthFieldOffset = 0;
2) lengthFieldLength = 2;
3) lengthAdjustment = 0;
4) initialBytesToStrip = 0。
解码后的字节缓冲区内容如图所示:
图2-4 解码后的字节缓冲区(14字节)
经过ByteBuf.readableBytes()方法咱们能够获取当前消息的长度,因此解码后的字节缓冲区能够不携带长度字段,因为长度字段在起始位置而且长度为2,因此将initialBytesToStrip设置为2,参数组合修改成:
1) lengthFieldOffset = 0;
2) lengthFieldLength = 2;
3) lengthAdjustment = 0;
4) initialBytesToStrip = 2。
解码后的字节缓冲区内容如图所示:
图2-5 跳过长度字段解码后的字节缓冲区(12字节)
解码后的字节缓冲区丢弃了长度字段,仅仅包含消息体,对于大多数的协议,解码以后消息长度没有用处,所以能够丢弃。
在大多数的应用场景中,长度字段仅用来标识消息体的长度,这类协议一般由消息长度字段+消息体组成,如上图所示的几个例子。可是,对于某些协议,长 度字段还包含了消息头的长度。在这种应用场景中,每每须要使用lengthAdjustment进行修正。因为整个消息(包含消息头)的长度每每大于消息 体的长度,因此,lengthAdjustment为负数。图2-6展现了经过指定lengthAdjustment字段来包含消息头的长度:
1) lengthFieldOffset = 0;
2) lengthFieldLength = 2;
3) lengthAdjustment = -2;
4) initialBytesToStrip = 0。
解码以前的码流:
图2-6 包含长度字段自身的码流
解码以后的码流:
图2-7 解码后的码流
因为协议种类繁多,并非全部的协议都将长度字段放在消息头的首位,当标识消息长度的字段位于消息头的中间或者尾部时,须要使用lengthFieldOffset字段进行标识,下面的参数组合给出了如何解决消息长度字段不在首位的问题:
1) lengthFieldOffset = 2;
2) lengthFieldLength = 3;
3) lengthAdjustment = 0;
4) initialBytesToStrip = 0。
其中lengthFieldOffset表示长度字段在消息头中偏移的字节数,lengthFieldLength 表示长度字段自身的长度,解码效果以下:
解码以前:
图2-8 长度字段偏移的原始码流
解码以后:
图2-9长度字段偏移解码后的码流
因为消息头1的长度为2,因此长度字段的偏移量为2;消息长度字段Length为3,因此lengthFieldLength值为3。因为长度字段仅仅标识消息体的长度,因此lengthAdjustment和initialBytesToStrip都为0。
最后一种场景是长度字段夹在两个消息头之间或者长度字段位于消息头的中间,先后都有其它消息头字段,在这种场景下若是想忽略长度字段以及其前面的其它消息头字段,则能够经过initialBytesToStrip参数来跳过要忽略的字节长度,它的组合配置示意以下:
1) lengthFieldOffset = 1;
2) lengthFieldLength = 2;
3) lengthAdjustment = 1;
4) initialBytesToStrip = 3。
解码以前的码流(16字节):
图2-10长度字段夹在消息头中间的原始码流(16字节)
解码以后的码流(13字节):
图2-11长度字段夹在消息头中间解码后的码流(13字节)
因为HDR1的长度为1,因此长度字段的偏移量lengthFieldOffset为1;长度字段为2个字节,因此 lengthFieldLength为2。因为长度字段是消息体的长度,解码后若是携带消息头中的字段,则须要使用lengthAdjustment进行 调整,此处它的值为1,表明的是HDR2的长度,最后因为解码后的缓冲区要忽略长度字段和HDR1部分,因此lengthAdjustment为3。解码 后的结果为13个字节,HDR1和Length字段被忽略。
事实上,经过4个参数的不一样组合,能够达到不一样的解码效果,用户在使用过程当中能够根据业务的实际状况进行灵活调整。
因为TCP存在粘包和组包问题,因此一般状况下用户须要本身处理半包消息。利用LengthFieldBasedFrameDecoder解码器能够自动解决半包问题,它的习惯用法以下:
pipeline.addLast("frameDecoder", new LengthFieldBasedFrameDecoder(65536,0,2));
pipeline.addLast("UserDecoder", new UserDecoder());
在pipeline中增长LengthFieldBasedFrameDecoder解码器,指定正确的参数组合,它能够将Netty的 ByteBuf解码成整包消息,后面的用户解码器拿到的就是个完整的数据报,按照逻辑正常进行解码便可,再也不须要额外考虑“读半包”问题,下降了用户的开 发难度。
2.3. 经常使用的编码器
Netty并无提供与2.2章节匹配的编码器,缘由以下:
1) 2.2章节介绍的4种经常使用的解码器本质都是解析一个完整的数据报给后端,主要用于解决TCP底层粘包和拆包;对于编码,就是将POJO对象序列化为 ByteBuf,不须要与TCP层面打交道,也就不存在半包编码问题。从应用场景和须要解决的实际问题角度看,双方是非对等的;
2) 很难抽象出合适的编码器,对于不一样的用户和应用场景,序列化技术不尽相同,在Netty底层统一抽象封装也并不合适。
Netty默认提供了丰富的编解码框架供用户集成使用,本文对较经常使用的Java序列化编码器进行讲解。其它的编码器,实现方式大同小异。
2.3.1. ObjectEncoder编码器
ObjectEncoder是Java序列化编码器,它负责将实现Serializable接口的对象序列化为byte [],而后写入到ByteBuf中用于消息的跨网络传输。
下面咱们一块儿分析下它的实现:
首先,咱们发现它继承自MessageToByteEncoder,它的做用就是将对象编码成ByteBuf:
若是要使用Java序列化,对象必须实现Serializable接口,所以,它的泛型类型为Serializable。
MessageToByteEncoder的子类只须要实现encode(ChannelHandlerContext ctx, I msg, ByteBuf out)方法便可,下面咱们重点关注encode方法的实现:
首先建立ByteBufOutputStream和ObjectOutputStream,用于将Object对象序列化到ByteBuf中,值得注意的是在writeObject以前须要先将长度字段(4个字节)预留,用于后续长度字段的更新。
依次写入长度占位符(4字节)、序列化以后的Object对象,以后根据ByteBuf的writeIndex计算序列化以后的码流长度,最后调用ByteBuf的setInt(int index, int value)更新长度占位符为实际的码流长度。
有个细节须要注意,更新码流长度字段使用了setInt方法而不是writeInt,缘由就是setInt方法只更新内容,并不修改readerIndex和writerIndex。
尽管Netty预置了丰富的编解码类库功能,可是在实际的业务开发过程当中,老是须要对编解码功能作一些定制。使用Netty的编解码框架,能够很是方便的进行协议定制。本章节将对经常使用的支持定制的编解码类库进行讲解,以期让读者可以尽快熟悉和掌握编解码框架。
3.1. 解码器
3.1.1. 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也是不少基础解码器的父类,它的继承关系以下图所示:
图3-1 ByteToMessageDecoder继承关系图
3.1.2. MessageToMessageDecoder抽象解码器
MessageToMessageDecoder其实是Netty的二次解码器,它的职责是将一个对象二次解码为其它对象。
为何称它为二次解码器呢?咱们知道,从SocketChannel读取到的TCP数据报是ByteBuffer,实际就是字节数组。咱们首先须要 将ByteBuffer缓冲区中的数据报读取出来,并将其解码为Java对象;而后对Java对象根据某些规则作二次解码,将其解码为另外一个POJO对 象。由于MessageToMessageDecoder在ByteToMessageDecoder以后,因此称之为二次解码器。
二次解码器在实际的商业项目中很是有用,以HTTP+XML协议栈为例,第一次解码每每是将字节数组解码成HttpRequest对象,而后对 HttpRequest消息中的消息体字符串进行二次解码,将XML格式的字符串解码为POJO对象,这就用到了二次解码器。相似这样的场景还有不少,不 再一一枚举。
事实上,作一个超级复杂的解码器将多个解码器组合成一个大而全的MessageToMessageDecoder解码器彷佛也能解决屡次解码的问 题,可是采用这种方式的代码可维护性会很是差。例如,若是咱们打算在HTTP+XML协议栈中增长一个打印码流的功能,即首次解码获取 HttpRequest对象以后打印XML格式的码流。若是采用多个解码器组合,在中间插入一个打印消息体的Handler便可,不须要修改原有的代码; 若是作一个大而全的解码器,就须要在解码的方法中增长打印码流的代码,可扩展性和可维护性都会变差。
用户的解码器只须要实现void decode(ChannelHandlerContext ctx, I msg, List<Object> out)抽象方法便可,因为它是将一个POJO解码为另外一个POJO,因此通常不会涉及到半包的处理,相对于ByteToMessageDecoder更 加简单些。它的继承关系图以下所示:
图3-2 MessageToMessageDecoder 解码器继承关系图
3.2. 编码器
3.2.1. MessageToByteEncoder抽象编码器
MessageToByteEncoder负责将POJO对象编码成ByteBuf,用户的编码器继承Message ToByteEncoder,实现void encode(ChannelHandlerContext ctx, I msg, ByteBuf out)接口接口,示例代码以下:
public class IntegerEncoder extends MessageToByteEncoder<Integer> { @Override public void encode(ChannelHandlerContext ctx, Integer msg,ByteBuf out) throws Exception { out.writeInt(msg); } }
它的实现原理以下:调用write操做时,首先判断当前编码器是否支持须要发送的消息,若是不支持则直接透传;若是支持则判断缓冲区的类型,对于直接内存分配ioBuffer(堆外内存),对于堆内存经过heapBuffer方法分配,源码以下:
编码使用的缓冲区分配完成以后,调用encode抽象方法进行编码,方法定义以下:它由子类负责具体实现。
编码完成以后,调用ReferenceCountUtil的release方法释放编码对象msg。对编码后的ByteBuf进行如下判断:
1) 若是缓冲区包含可发送的字节,则调用ChannelHandlerContext的write方法发送ByteBuf;
2) 若是缓冲区没有包含可写的字节,则须要释放编码后的ByteBuf,写入一个空的ByteBuf到ChannelHandlerContext中。
发送操做完成以后,在方法退出以前释放编码缓冲区ByteBuf对象。
3.2.2. MessageToMessageEncoder抽象编码器
将一个POJO对象编码成另外一个对象,以HTTP+XML协议为例,它的一种实现方式是:先将POJO对象编码成XML字符串,再将字符串编码为HTTP请求或者应答消息。对于复杂协议,每每须要经历屡次编码,为了便于功能扩展,能够经过多个编码器组合来实现相关功能。
用户的解码器继承MessageToMessageEncoder解码器,实现void encode(Channel HandlerContext ctx, I msg, List<Object> out)方法便可。注意,它与MessageToByteEncoder的区别是输出是对象列表而不是ByteBuf,示例代码以下:
public class IntegerToStringEncoder extends MessageToMessageEncoder <Integer> { @Override public void encode(ChannelHandlerContext ctx, Integer message, List<Object> out) throws Exception { out.add(message.toString()); } }
MessageToMessageEncoder编码器的实现原理与以前分析的MessageToByteEncoder类似,惟一的差异是它编码后的输出是个中间对象,并不是最终可传输的ByteBuf。
简单看下它的源码实现:建立RecyclableArrayList对象,判断当前须要编码的对象是不是编码器可处理的类型,若是不是,则忽略,执行下一个ChannelHandler的write方法。
具体的编码方法实现由用户子类编码器负责完成,若是编码后的RecyclableArrayList为空,说明编码没有成功,释放RecyclableArrayList引用。
若是编码成功,则经过遍历RecyclableArrayList,循环发送编码后的POJO对象,代码以下所示:
3.2.3. LengthFieldPrepender编码器
若是协议中的第一个字段为长度字段,Netty提供了LengthFieldPrepender编码器,它能够计算当前待发送消息的二进制字节长度,将该长度添加到ByteBuf的缓冲区头中,如图所示:
图3-3 LengthFieldPrepender编码器
经过LengthFieldPrepender能够将待发送消息的长度写入到ByteBuf的前2个字节,编码后的消息组成为长度字段+原消息的方式。
经过设置LengthFieldPrepender为true,消息长度将包含长度自己占用的字节数,打开LengthFieldPrepender后,图3-3示例中的编码结果以下图所示:
图3-4 打开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。
相关代码以下:
最后将原须要发送的ByteBuf复制到List<Object> out中,完成编码: