什么是codec
每个网络应用程序必须定义在机器之间传输的原始字节如何被解析然后转换为目标程序的数据格式。该转换逻辑由编解码器(codec,编码解码器,下文中我直接用codec表示)处理,codec由编码器和编码器组成,每个编码器或解码器负责将字节流从一种格式转换到另一种格式。那么该如何区分编码器和解码器呢?
将消息视为具有特定含义的字节的结构化序列-数据。 编码器将该消息转换成适合于传输的格式(很可能是字节流); 相应的解码器将字节流转回程序的消息格式。 编码器处理输出数据而解码器处理输入数据。
我们来看看Netty为实现这两种组件而提供的类。
解码器(decoder)
这一节中我们会研究Netty的解码器类,然后提供具体的例子来告诉你何时以及如何使用它们。这些类含有两个不同的用例:
- 将字节解码为消息—ByteToMessageDecoder和ReplayingDecoder
- 将一种格式的消息转换(解码)为另一种—MessageToMessageDecoder
什么时候使用解码器呢?当你需要为下一个ChannelInboundHandler转换输入数据时。此外,由于ChannelPipeline的设计,你可以将多个解码器链接到一起来实现任意复杂的转换逻辑
ByteToMessageDecoder抽象类
从字节到消息(或到另一个字节序列)的解码是常见的任务,Netty为其提供了一个抽象基类:ByteToMessageDecoder。 因为你不知道远程机器是否会一次性发送完整的消息,这个类缓冲输入数据,直到它可以被处理。
下表解释了它的两个最重要的方法
方法 | 描述 |
decode(ChannelHandlerContext ctx,ByteBuf in,List out) | 这是你必须实现的唯一抽象方法。参数ByteBuf包含输入数据,List用来保存解码了的数据。decode()方法会被重复调用直到确定没有任何新数据被加到List中或在ByteBuf中没有更多的可读字节。那么,如果这个List不为空,其内容传递给下一个handler |
decodeLast(ChannelHandlerContext ctx,ByteBuf in,List out) | Netty提供的默认实现,它只是简单地调用decode()。当channel变成inactive时调用这个方法。可以覆盖这个方法来提供特殊的处理 |
有关如何使用此类的示例,假设你收到包含简单int的字节流,每个int都要单独处理。 在这种情况下,你将从输入ByteBuf中读取每个int并将其传递给下一个ChannelInboundHandler。为了解码字节流,需要继承ByteToMessageDecoder。 (注意,当原始int被添加到List时,它将被自动装箱为整型)。如下图所示:
一次从输入ByteBuf中读取4字节,解码成一个int,然后添加到List中。当没有更多的数据加到List中时,List中的内容会被送到下一个ChannelInboundHandler。
首先给出ToIntegerDecoder的代码:
你发现必须验证输入的ByteBuf是否有足够的数据,这有点麻烦。在下一节中我们会讨论ReplayDecoder,它可以消除这个检查的步骤,只需少量的开销。
codec中的引用计数 一旦消息被编码或解码,它将通过调用ReferenceCountUtil.release(message)自动释放消息。如果你需要保持一个消息引用,你可以调用ReferenceCountUtil.retain(message)。 这将增加引用计数,从而防止消息被释放。
ReplayingDecoder抽象类
ReplayDecoder继承了ByteToMessageDecoder,使我们无需调用readableBytes()。它通过一个自定义的ByteBuf实现(ReplayingDecoderBuffer,它会内部调用readableBytes())来包裹传入的ByteBuf。
这个类的定义如下:
参数S指定要用于状态管理的类型,其中Void表示不执行任何操作。 以下代码显示了基于ReplayingDecoder重新实现的ToIntegerDecoder。
如果没有足够的字节,in.readInt()会抛出一个Error,这个Error会被捕获然后在基类中处理。当有更多的数据可读时,会再调用decode()方法。
请注意ReplayingDecoderBuffer的以下几点:
- 不支持所有ByteBuf操作。 如果调用了不支持的方法,将抛出UnsupportedOperationException。
- ReplayDecoder比ByteToMessageDecoder略慢。
如果不会引入过多的复杂性时使用ByteToMessageDecoder; 否则使用ReplayDecoder。
MessageToMessageDecoder抽象类
这一节会解释如何通过MessageToMessageDecoder类在两种消息格式之间转换(比如,从一种POJO类转换为另一种)
它定义如下:
参数I指定decode()方法参数msg的类型,该方法是唯一需要实现的方法。
方法 | 描述 |
decode(ChannelHandlerContext ctx,I msg,List out) | 为每个输入消息调用来解码为另一种格式。 然后解码的消息传递给下一个ChannelInboundHandler |
在这个例子中,我们将编写一个继承MessageToMessageDecoder<Integer>
的IntegerToStringDecoder
解码器。 它的decode()方法将转换Integer参数为String,并将具有以下签名:
和前面一样,解码的String会加到List然后传递到下个handler,它的设计如下所示:
下面给出IntegerToStringDecoder的实现:
TooLongFrameException类
由于Netty是一个异步框架,所以你需要将字节缓存到内存中,直到你可以对它们进行解码。 因此,你不能让你的解码器缓冲过多的数据以致消耗了太多的内存。 为了解决这个共同的问题,Netty提供了TooLongFrameException,如果一个帧(frame)的大小超过了限制则抛出这个异常。异常会被ChannelHandler.exceptionCaught()方法捕获,然后由用户决定如何处理这个异常。
下面的代码展示了ByteToMessageDecoder如何通过TooLongFrameException来通知ChannelHandler出现了帧大小溢出的问题:
介绍了解码器,下面开始介绍编码器,它将信息转换成一种适合输出的格式。
编码器(encoder)
编码器实现了ChannelOutboundHandler,将输出数据从一种格式转换为另一种。Netty提供了一些类来帮你实现以下功能:
- 将消息编码成字节
- 将消息进行格式转换
首先了解抽象基类MessageToByteEncoder
MessageToByteEncoder抽象类
下表显示了它的API:
方法 | 描述 |
encode(ChannelHandlerContext ctx,I msg,ByteBuf out) | 将类型I的输出消息编码成一个ByteBuf,然后将这个ByteBuf传递给下一个 ChannelOutboundHandler |
你可能注意到了(除非您眼神不好)这个类只有一个方法,而解码器有两个方法。原因是解码器经常需要在Channel关闭后生成最后一条消息。对于编码器则没有这种需要,在连接关闭后产生一条消息没意义。
下图显示了一个ShortToByteEncoder接收一个Short实例作为消息,将它写入ByteBuf(只会占用这个ByteBuf中的两个字节)
代码很简单:
Netty提供了几个特殊的MessageToByteEncoder,你可以基于它们实现你自己的逻辑。可以看io.netty.handler.codec.http.websocketx
包下的WebSocket08FrameEncoder
类,这是一个很好的例子。
MessageToMessageEncoder抽象类
MessageToMessageEncoder抽象类可以将一条消息从一种格式转换成另一种格式(用来输出)。
它的方法如下表所示:
名称 | 描述 |
encode(ChannelHandlerContext ctx,I msg,List out) | 通过将write()方法传过来的消息 编码成一条或多条输出消息。然后将输出消息传递给下一个ChannelOutboundHandler |
下面给一个将整数编码成String的例子,先是它的设计图:
代码如下:
codec抽象类
虽然我们一直分为不同的类来讨论解码器和编码器。有时你会发现在一个类中同时管理输入和输出数据和消息非常有用,Netty的codec类可以实现这样的目的。每个codec类绑定了decoder/encoder来处理编码和解码操作。
那为什么我们不一直使用这些具有复合功能的类而不是分离的解码器和编码器呢? 因为尽可能保持两个功能分开可以最大化代码可重用性和可扩展性,这是Netty设计的基本原则。
ByteToMessageCodec抽象类
我们考虑一下这种场景,当我们需要将字节序列解码成某种消息,可能是POJO类,然后再对它进行编码。ByteToMessageCodec就能应用于这种场景。
ByteToMessageCodec类中重要的方法如下表所示:
方法名 | 描述 |
decode(ChannelHandlerContext ctx,ByteBuf in,List) | 只要有可消耗的字节就会调用这个方法。它将输入ByteBuf转换成特定的消息格式 |
decodeLast(ChannelHandlerContext ctx,ByteBuf in,List out) | 它只会被调用一次,当Channel失效时,可以重写它来实现特殊处理 |
encode(ChannelHandlerContext ctx,I msg,ByteBuf out) | 将每条I类型的消息进行编码,然后写入一个输出ByteBuf |
任何请求/响应协议都可以考虑使用ByteToMessageCodec。 例如,在SMTP实现中,codec将读取
传入的字节并将其解码为自定义消息类型-SmtpRequest。 在接收方,当响应产生时,将产生一个SmtpResponse,它的内容会被编码成字节以便传输。
MessageToMessageCodec抽象类
MessageToMessageCodec定义如下:
它的主要方法如下:
方法名 | 描述 |
protected abstract decode(ChannelHandlerContext ctx,INBOUND_IN msg,List out) | 将INBOUND_IN类型的消息解码成OUTBOUND_IN类型,然后传递给下一个ChannelInboundHandler |
protected abstract encode(ChannelHandlerContext ctx,OUTBOUND_IN msg,List out) | 将类型OUTBOUND_IN的消息编码成INBOUND_IN类型 |
可以将INBOUND_IN类型的消息看成是通过网络发送的消息,OUTBOUND_IN消息看成是由应用产生的消息。
虽然这个codec似乎有些深奥,但它处理的用例是相当的通用:在两个不同的消息传递API之间来回转换数据。
WebSocket传输协议
下面关于MessageToMessageCodec的例子用到了WebSocket,实现浏览器和服务器之间完全双向通信
下面的代码显示了这样的对话是如何发生的。我们的WebSocketConvertHandler将MessageToMessageCodec参数化,带有一个INBOUND_IN类型的WebsocketFrame和一个OUTBOUND_IN类型的MyWebSocketFrame。
CombinedChannelDuplexHandler类
如前所述,组合解码器和编码器可能会对重用性产生影响。 然而,有一种方法来避免这种缺陷,而不牺牲将解码器和编码器部署在一起的方便。 该解决方案由CombinedChannelDuplexHandler提供,声明为
此类作为ChannelInboundHandler和ChannelOutboundHandler(作为类参数I和O)的容器。 通过提供继承一个decoder类和一个encoder类,我们可以相应地实现codec而无需直接继承抽象codec类。 我们将在下面的例子中说明这一点。
这里decode()从ByteBuf中提取2个字节,并将它们作为char写入List,字节将自动装箱为Character对象。
下面代码中的CharToByteEncoder,它将Character转换回字节。 这个类继承了MessageToByteEncoder,因为它需要将char消息编码为一个ByteBuf。 这是通过直接写入ByteBuf来实现的:
我们有一个decoder类和一个encoder类,然后将它们组合成一个codec。如下所示:
通过这种方式更简单且具有更多的灵活性。