第十章,第十一章序
对于网络而言,数据只是原始字节序列,但是我们的程序将这些字节按照某种方式去组织成我们能够看懂的语言,我们一般称这些信息叫“信息”,将信息转换成字节或者从网络中将字节装换成我们能够看懂的信息这些都是我们网络传输中最最常见的任务之一,你可能需要在标准的格式或者协议下工作,例如FTP协议或者Telnet协议,或者是从第三方自定义的专有协议,亦或者是根据字自已的应用去继承一种已有的信息格式
处理将网络中的数据转化成应用数据的组件叫做解码器或者编码器,相对应而言,一个单独的组件这两个功能都有的情况下,我们称这个组件叫做译码器,Netty提供了大量的工具来创造这些译码器,我们可以通过重构一些类来实现现在比较著名的一些通用的传输译码器例如HTTP,Base64等,通过这样的方式来满足你自定义的需求
第十章内容是对编码和解码进行了基本的介绍,你将会通过学些一些经典的用户案例了解到Netty最基本的译码器,随后你将会学会如何将这些类合适地运用到整个Netty框架里,你会发现这些译码器可以构建与我们以前学过的组件的API之上,所以你能够很快的上手运用这些类
在第十一章节中,我们将探索Netty提供的解码器和编码器在一些更加特殊具体的场景下如何工作,Websocket是比较令人感兴趣的一个小节,所以我们准备在第三个模块详细讨论这个先进的网络传输协议
第十章本章内容包括:
1)对编码器,解码器,译码器的一个基本讲解
2)Netty的编码工具类
由于很多专业的框架提供了很多标准的架构设计模式,一些比较通用的数据处理模式经常作为实战应用的最热候选方案,这些比较常用的解决方案可以为开发者节省很多时间和经历
这个章节的当仁不让的主题肯定是编码或者解码,或者是将数据从一个格式转化成另一种格式,处理这种问题的组件我们经常称之为译码器,Netty为很多协议提供了一些组件来简化自定义译码器的实现,举例来说,你会发现Netty的译码器可以无缝地支持POP3,IMAP,SMTP协议的实现
10.1 What is a codec?
每一个应用框架都需要定义如何在远程传输的两端去将原始字节转化或者解析原始字节,需要考虑如何将目标信息转成原始字节或者做相反的过程将原始字节转化成目标编程信息,这种转化的逻辑是由译码器做处理的,它包含了编码和解码的功能,每一个译码器都可以将一串字节从一个格式转化成另一个格式,那么我们怎么区分编码和解码的区别
想象一个场景,一个信息以一种有结构化的字节序列形式用于我们的应用中,那么编码器就是将这个信息转化成一种适合在网络中传输的格式,例如字节流,相对应的就是译码器,将从网络传输中返回的字节流转化成程序能够识别的信息格式,简而言之,编码器操作一个输出数据,译码器操作一个输入数据
请将这个知识背景牢记心中,然后我们一起看Netty是如何去实现这两种组件的
10.2 Decoders
在这个小节中,我们将会调查Netty的解码器,通过几个具体的案例来说明在什么时候合适的去使用这些类,这些类主要分成两种不同的用户案例:
1)将字节解码成信息-----ByteToMessageDecoder和ReplayingDecoder
2)将一种信息格式转化成另一种信息格式----------MessageToMessageDecoder
因为解码器负责将传输的输入数据转化成另一种格式,所以如果你知道Netty的解码器实现了ChannelInboundHandler的话,你一定不会惊讶
那么什么时候你可以使用一个解码器呢?很简单:当你需要将输入数据传输到ChannelPipeline中的下一个ChannelInboundHandler的时候,还有就是我们需要感谢ChannelPipeline的设计,你可以将多个解码器以链式的格式去将所有的格式连接起来去处理任意复杂的传输逻辑,这样做的主要目的就是Netty支持模块化和可重复利用性
10.2.1 Abstract class ByteToMessageDecoder
将字节转化成信息对于使用Netty提供的抽象类ByteToMessageDecoder来说就是一个很普通的任务,由于你无法知道远程数据输入端是否能够一次性传输整个信息,这个类将会缓冲所有的输入数据直到输入的字节满足被处理的要求,表10.1向你展示了这个类最为重要的两个方法
我们举例说明如何使用该类,假设你需要接收一个字节流,这个字节流中只包含简单的int变量,每一个int变量可以被单独处理,在这个案例中,你在输入的ByteBuf中每读到一个int变量就将其传输到管道中的下一个ChannelInboundHandler中去,为了对ByteBuf中的字节流进行解码,我们需要继承ByteToMessageDecoder
我们的设计如图10.1展示一样:
每次从输入的ByteBuf中读取四个字节,解码成一个int型变量,然后将解码后的int变量加入到List中,当没有更多的对象加入到List的时候,那么List中的内容将会发送到下一个ChannelInboundHandler中去
下面的代码清单展示了ToIntegerDecoder的代码
尽管ByteToMessageDecoder可以让这种转化模式变得很简单,但是你可能会发现每次你都需要去验证输入的ByteBuf中是否有足够的字节数去转化成int型的时候都要进行数量的验证,下一个小节我们将会讨论ReplayingDecoder,一个特殊的Decoder,这个解码器可以消除这个步骤以很小的代价
TIPS:在译码器中的技术引用:
我们在第五和第六章节中提及过,计数应用有一些需要值得主要的事情,在解码器和译码器中例子中,其实过程很简单,一旦信息被解码或者译码之后,它将会自动调用ReferenceCountUtil.release(message)方法释放自己,如果你需要继续获取这些对象的引用用来以后使用的话,你可以调用ReferenceCountUtil.retain(message)方法来获取这个对象的持续引用,因为对计数引用值的增加,这样可以防止该信息被释放
10.2.2 Abstract
class ReplayingDecoder
ReplayingDecoder继承了ByteToMessageDecoder使我们可以从调用readableBytes的方法中解放出来,它通过用自定义的ByteBuf实现ReplayingDecoderBuffer来包装输入的ByteBuf来完成这种需求,这个类完整的声明如下:
参数S指定了用来进行状态管理时的类型,在这里的Void指定了没有值将被指定,下面的代码清单用ReplayingDecoder重新实现了ToIntegerDecoder2的功能
之前,int型的变量是从ByteBuf中演化过来的然后添加到List中,如果不足的字节输出,那么readInt的方法就会抛出一个错误,这个错误会被基础的类捕获且处理,然后decode方法也会当更多的字节输出的时候再次被调用执行
以下关于ReplayingDecoder的几个重要的方面需要注意:
1)它并不支持所有的ByteBuf的所有操作方式,如果一个不支持的方式被调用了,那么一个UnsupportedOperationException异常将会被抛出
2)ReplayingDecoder的执行效率是稍稍比ByteToMessageDecoder略差
如果你通过将10.1和10.2的代码清单作比较的话,你会发现后者更加简单,当然例子本身就是很简单的,但是请你牢记于心,在真实的生产环境中使用ByteToMessageDecoder还是使用ReplayingDecoder的选择其实是很重要的,会影响你程序的性能,我们这里有个用户使用准则你可以参考一下:在很简单的业务场景下不想引入额外的复杂性的情况下使用ByteToMessageDecoder,否则就使用ReplayingDecoder
TIPS:更多的decoders
下面是一些类用来处理更加复杂的用户需求
1)io.netty.handler.codec.LineBasedFrameDecoder这个类被Netty内部使用,使用行结束控制符\n或者\r\n来解析信息数据
2)io.netty.handler.codec.http.HttpObjectDecoder这个解码器用来处理HTTP类型的数据
你可以在io.netty.handler.codec的子包下找到一些额外的译码器和解码器实现用来处理一些比较特殊的用户案例,详细地信息请查看Netty的Java文档
10.2.3 Abstract class MessageToMessageDecoder
在这个小节中,我们将会说明如何在两种信息格式之间做转换,举例来说将一种合适的POJO转化成另一种格式的POJO,我们使用基类的抽象类
参数I指定了decode方法中的msg的参数的类型,decode是唯一一个需要实现的方法,表10.2展示了这个方法的细节
在这个例子中,我们写了一个继承与MessageToMessageDecoder<Integer>的IntegerToStringDecoder的解码器,它的decode方法将会将Integer的参数转化成String型,下面是这个方法的完整声明:
与之前一样,解码器的输出的String类型的变量将会加入到List中,然后传输到管道中的下一个Handler中去
这个设计图如10.2图所示:
下面的代码清单是IntegerToStringDecoder的具体实现:
一些更加复杂的例子,你可以去查看io.netty.handler.codec.http.HttpObjectAggregator类,它继承了MessageToMessageDecoder<HttpObject>
10.2.4 Class
TooLongFrameException
由于Netty是一个异步框架,所以有时候你需要把缓存字节数放入到内存中直到你可以读取解码这些字节,所以你不能将解码的缓存字节大于可利用的内存空间,为了处理这种比较常见的担忧,Netty提供了TooLongFrameException的类型的异常,这个异常将会在解码的时候,如果一个帧数据超过了指定大小限制的时候抛出
为了阻止这种异常,你可以设置帧数据的最大字节的阀门,如果超过了,就会导致抛出TooLongFrameException的异常,关于这个异常的处理完全取决于解码端的用户,有些场景下,例如HTTP的协议的时候,可能会允许你返回一个特殊的返回,在一些其他的场景下,可能会只有一个选择就是关闭连接
代码清单10.4中向你展示了一个ByteToMessageDecoder如何充分使用TooLongFrameException的异常来通知管道中的其他ChannelHandler发生了帧数据超过阀门的异常,请注意这种保护是很重要的,尤其是你工作的协议是有可变大小的帧数据的情况下
截止到目前为止,我们学习了解码器的一些常用的使用方式,也学习了Netty提供的一些构建这些类的一些抽象类,但是解码只是译码硬币的一面,在解码器的另一面就是编码器了,编码器可以将信息转化成适合传输的类型,关于编码器的详细API将会在下一个章节讨论
10.3 Encoders
回顾一下我们之前的定义,一个编码器实现了ChannelOutboundhandler,将一种格式的数据转化成另一种格式用于传输,这与我们学习过的译码器是有相反的功能,Netty提供了很多类来帮助你写入如下功能的编码器
1)从message到字节的编码
2)从message到messag的编码
我们将通过学习一个抽象基础类MessageToByteEncoder作为我们学习这些编码器的开始
10.3.1 Abstract class MessageToByteEncoder
之前我们使用ByteToMessageDecoder来将字节转化成我们能够识别的message,现在我们使用MessageToByteEncoder来做相反的事情,表10.3展示了MessageToByteEncoder的API
可能你已经注意到这个类只有一个方法,但是对应的解码器却由两个方法,关于为什么解码器有两个方法的原因是因为解码器通常需要在Channel关闭的时候,生成最后一个信息,这明显不会再编码器中出现,在连接关闭之后是没有意义再去生产一个信息的
图10.3展示了一个ShortToByteEncoder接收一个Short类型的实例作为信息,将其编码成原始的short变量,然后写入到ByteBuf中,最后传输到管道中的写一个ChannelOutboundHandler中,每一个输出的Short将会占据ByteBuf中的2个字节
下面的代码清单展示了ShortToByteEncoder的具体实现:
Netty提供了几个专业的MessageToByteEncoder依赖于这些Encoder你可以构建你自己的实现,类WebSocket08FrameEncoder提供了一个很好的实战示例,你将会在包io.netty.handler.codec.http.websocketx下
10.3.2 Abstract class MessageToMessageEncoder
你已经了解了如何将一个输入数据从一个格式转化成另一种格式的解码过程了,为了完成译码器的整个蓝图,我们将向你展示对于一个输出数据来说怎么从一个格式转化成另一种格式,MessageToMessageEncoder的encode方法就提供了这种实现,如表10.4中描述的一样
为了说明这点,代码清单10.6写了一个IntegerToStringEncoder,该类继承于MessageToMessageEncoder,这个设计如图10.4展示:
在下面的代码清单中,一个编码器将Integer转化成String然后存储到List中
关于更多的MessageToMessageEncoder的专业使用,你可以查看io.netty.handler.codec.protobuf.ProtobufEncoder,这个类可以处理Google定义的Protocol Buffer定义的数据格式
10.4 Abstract codec classes
虽然我们将解码器和编码器作为两种不同的组件学习过了,但是有时候你会发现将编码和译码放在同一个类中来管理数据传输会更加的高效,Netty的抽象译码器类可以帮助我们完成这个功能,因为译码器成对地绑定了译码器和编码器用来处理我们刚刚学过的两种类型的操作,你可能会质疑,是不是这些译码器的类是不是继承了两个类,一个是ChannelInboundHandler一个是ChannelOutboundHandler
在真实的生产环境下,我们为什么总是不用这个符合的译码器而是优先选用编码器或者译码器呢,因为单独使用这两个组件可以尽可能地最大化代码的可重复利用性和扩展性,这是符合Netty的设计原则的
当我们在研究译码器的时候,我们会将译码器与相对应的译码器或者编码器做比较和权衡的
10.4.1 Abstract class ByteToMessageCodec
让我们从一个案例开始入手学习译码器,在这个案例中我们需要将字节解码成某种类型的信息,可能是一个POJO,然后对这个POJO再次编码,ByteToMessageCodec这个类可以帮助我们完成这个功能,因为它内部整合了ByteToMessageDecoder和与ByteToMessageDecoder功能相反的类MessageToByteEncoder,关于ByteToMessageCodec这个类的一些核心方法如表10.5展示
任何一种请求响应类的协议使用ByteToMessageCodec这个类应该是一个不错的候选方案,例如,在一个SMTP实现中,此译码器可以输入的字节中读取然后将其解码成一个自定义的数据类型,一般称之为“SmtpRequest”,在接收端,当一个响应到达的时候,一个对应的SmtpResponse也会被创建,这个会将刚才解码的对象重新编码成一个新的对象用于传输
10.4.2 Abstract class MessageToMessageCodec
在章节10.2.2中我们学习一个继承于MessageToMessageEncoder的类用于将一个message转化成另一种格式,我们可以使用一个单独的类MessageToMessageCodec来完成这一个轮询的过程,MessageToMessageCodec是一个指定参数的类,它的具体定义如下:
这个方法一些比较重要的方法如表10.6展示:
decode方法将一个INBOUND_IN类型的信息转化成OUTBOUND_IN类型,然后encode方法做这相反的事情,这可以帮助你想象INBOUND_IN类型的数据可以直接传输,然后OUT_BOUND信息可以被应用直接处理应用
尽管这个译码器看起来的确有点深奥,但是使用它的时候还是相对简单的:当你使用这两个不同的message的API的时候来回将信息转换四次,我们会经常遇到这种场景,例如当我们不得不与一个类型的数据API交互的时候使用这个类还是很合适的
TIPS:webSocket protocol
接下来的例子我们是参考WebSocket的写的MessageToMessageCodec,它是一个比较现代的协议,它可以做到完全的浏览器端和服务器端的双向通信,我们会在第11章节深度地讨论Netty对Websocket的支持
代码清单10.7展示了在一次通信中发生了多少次的转化,我们自定义的WebSocketConvertHandler继承了MessageToMessageCodec用WebSocketFrame指定了INBOUND_IN参数的类型,用MyWebsocketFrame类指定了OUTBOUND_IN变量,后者是WebSocketConvertHandler类本身的静态内部类
10.4.3 Class CombinedChannelDuplexHandler
我们之前提及过,如果联合一个编码器或者一个解码器会对我们的可重复利用性造成一定的影响,但是我们有办法可以消除这种问题并且我们不需要将一个编码器和一个译码器放在同一个单元类里面,这个解决方法是由CombinedChannelDuplexHandler提供的,它的完整声明如下:
这个类你可以将其看成一种ChannelInboundHandler和ChannelOutboundHandler的容器,通过可以继承一对编码器和译码器,通过这种方式,我们就不需要直接继承一个抽象译码器来实现我们自己的功能了,我们用下面的一个完整的例子来说明这个问题:
首先,我们可以看下下面代码清单中的ByteToCharDecoder,注意到这种实现继承与ByteToMessageDecoder,因为这个类需要从ByteBuf中读取char类型的数据
在这里的decode方法每次从ByteBuf中提取2个字节然后将其当做一个char写入到List中,注意这里可以自动装箱成一个Character的对象
下面的代码清单中是CharToByteEncoder类,这个再将char重新转化成字节,这个类继承与MessageToByteEncoder,因为它需要将char重新编码成字节,这个可以直接由ByteBuf的方法直接写入:
现在我们已经有了编码器和解码器了,让我们将其联合起来构建成一个译码器,下面的代码清单展示了如何实现这个功能
你可以看到,这种方式在某些使用场景下相比于直接使用译码器而言可能更加简单更加灵活,当然这归根结底而言是与个人喜好有关系的
10.5 Summary
在这个章节中,我们学习了Netty译码器的API的使用,学习了如何编写编码器和译码器,你也了解到为什么使用这些API比直接使用ChannelHandler的API好
你了解到一个抽象的译码器是如何在一个类中提供编码和译码的功能的,当然,如果你需要更多的灵活性或者可重复利用性的话,你也可以将两种功能联合起来,且是在不需要继承任何抽象译码器的基础上
在下一个章节中,我们将一起讨论ChannelHandler的实现,讲解作为Netty框架本身组成的一部分的原始译码器的更多实现细节,并且使用原始译码器来完成一些特殊的协议或者任务。