一、什么是编解码器框架
网络只将数据看作是原始的字节序列。但我们的应用程序则会把这些字节组织成有意义的信息。在数据和网络字节流之间做相互转换是最常见的编程任务之一。例如,我们可能需要处理标准的格式或者协议(如 FTP 或 Telnet)、实现一种由第三方定义的专有二进制协议,或者扩展一种由自己的组织创建的遗留的消息格式。将应用程序的数据转换为网络格式,以及将网络格式转换为应用程序的数据的组件分别叫作编码器和解码器,同时具有这两种功能的单一组件叫作编解码器。
Netty 提供了一系列用来创建所有这些编码器、解码器以及编解码器的工具,从专门为知名协议(如 HTTP以及 Base64)预构建的类,到你可以按需定制的通用的消息转换编解码器,应有尽有。
1.1 解码器
上面我们说了将网络格式转换为应用程序的数据的组件分别叫作编码器和解码器,现在我们来说说解处理入站数据的-解码器。
Netty提供的解码器类有两个类型:
- 将字节解码为消息——ByteToMessageDecoder 和 ReplayingDecoder;
- 将一种消息类型解码为另一种——MessageToMessageDecoder。
因为解码器是负责将入站数据从一种格式转换到另一种格式的,所以哪怕 Netty 的解码器实现了 ChannelInboundHandler 也很正常。
什么时候会用到解码器呢?每当需要为 ChannelPipeline 中的下一个 ChannelInboundHandler 转换入站数据时会用到。此外,得益于 ChannelPipeline 的设计,可以将多个解码器链接在一起,以实现任意复杂的转换逻辑。
1.1.1 抽象类 ByteToMessageDecoder
将字节解码为消息(或者另一个字节序列)是最常见的,Netty 为它提供了一个抽象的基类:ByteToMessageDecoder。
我们不可能知道远程节点是否会一次性地发送一个完整的消息,所以这个类会对入站数据进行缓冲,直到准备好处理。
ByteToMessageDecoder这个类提供了两个方法:
1、decode(ChannelHandlerContext ctx,ByteBuf in,List
这个是必须实现的唯一抽象方法。decode()方法被调用时将会传入一个包含了传入数据的 ByteBuf,以及一个用来添加解码消息的 List。对这个方法的调用将会重复进行,直到确定没有新的元素被添加到该 List,或者该 ByteBuf 中没有更多可读取的字节时为止。然后,如果该 List 不为空,那么它的内容将会被传递给ChannelPipeline 中的下一个 ChannelInboundHandler。
2、decodeLast(ChannelHandlerContext ctx,ByteBuf in,List
这个默认只是简单地调用了decode()方法。当Channel的状态变为非活动时,这个方法将会被调用一次。可以重写该方法以提供特殊的处理。
举个例子:
假设你接收了一个包含简单 int 的字节流,每个 int都需要被单独处理。在这种情况下,你需要从入站 ByteBuf 中读取每个 int,并将它传递给ChannelPipeline 中的下一个 ChannelInboundHandler。为了解码这个字节流,你要扩展ByteToMessageDecoder 类。(需要注意的是,原子类型的 int 在被添加到 List 中时,会被自动装箱为 Integer。)
//扩展 ByteToMessageDecoder 类,以将字节解码为特定的格式
public class ToIntegerDecoder extends ByteToMessageDecoder {
@Override
public void decode(ChannelHandlerContext ctx, ByteBuf in,List<Object> out) throws Exception{
//检查是否至少有 4字节可读(一个 int的字节长度)
if (in.readableBytes() >= 4) {
//从入站 ByteBuf 中读取一个 int,并将其添加到解码消息的 List 中
out.add(in.readInt());
}
}
}
编解码器中的引用计数
之前提过引用计数需要特别的注意。对于编码器和解码器来说,其过程也是相当的简单:一旦消息被编码或者解码,它就会被 ReferenceCountUtil.release(message)调用自动释放。如果你需要保留引用以便稍后使用,那么你可以调用 ReferenceCountUtil.retain(message)方法。这将会增加该引用计数,从而防止该消息被释放。
1.1.2 抽象类 ReplayingDecoder
ReplayingDecoder扩展了ByteToMessageDecoder类,使得我们不必调用 readableBytes()方法。它通过使用一个自定义的ByteBuf实现 ,ReplayingDecoderByteBuf,包装传入的ByteBuf实现了这一点,其将在内部执行该调用 。
这个类的完整声明是:
public abstract class ReplayingDecoder<S> extends ByteToMessageDecoder
类型参数 S 指定了用于状态管理的类型,其中 Void 代表不需要状态管理。
同样是上面那个例子,用这个类来写就变得简单一些:
//扩展 ReplayingDecoder<Void>以将字节解码为消息
public class ToIntegerDecoder2 extends ReplayingDecoder<Void> {
@Override
//传入的 ByteBuf 是 ReplayingDecoderByteBuf
public void decode(ChannelHandlerContext ctx, ByteBuf in,List<Object> out) throws Exception{
//从入站 ByteBuf 中读取一个 int,并将其添加到解码消息的 List 中
out.add(in.readInt());
}
}
那么这些类什么时侯使用?该使用哪个?
这里有一个简单的准则:如果使用 ByteToMessageDecoder不会引入太多的复杂性,那么请使用它;否则,请使用 ReplayingDecoder。
1.1.3 抽象类 MessageToMessageDecoder
MessageToMessageDecoder主要用于在两个消息格式之间进行转换,例如:一种 POJO 类型转换为另一种。
它的完整声明是这样:
public abstract class MessageToMessageDecoder<I> extends ChannelInboundHandlerAdapter
类型参数 I 指定了 decode()方法的输入参数 msg 的类型,是必须实现的唯一方法。
这个类包括一个 decode(ChannelHandlerContext ctx,I msg,List
举个例子:
我们编写一个 IntegerToStringDecoder 解码器来扩展 MessageToMessageDecoder。使用decode()方法会把 Integer 参数转换为String后再添加到传出的List中,并转发给下一个ChannelInboundHandler。
public class IntegerToStringDecoder extends
MessageToMessageDecoder<Integer> {
@Override
public void decode(ChannelHandlerContext ctx, Integer msg,List<Object> out) throws Exception {
out.add(String.valueOf(msg));
}
}
1.1.4 TooLongFrameException 类
Netty是一个异步框架,所以需要在字节可以解码之前在内存中缓冲它们。因此,不能让解码器缓冲大量的数据以至于耗尽可用的内存。为了解除这个常见的顾虑,Netty提供了TooLongFrameException 类,其将由解码器在帧超出指定的大小限制时抛出。
为了避免帧超出指定的大小限制,我们可以设置一个最大字节数的阈值,如果超出该阈值,则会导致抛出一个 TooLongFrameException(随后会被 ChannelHandler.exceptionCaught()方法捕获)。然后,如何处理该异常则完全取决于该解码器的用户。某些协议(如 HTTP)允许返回一个特殊的响应。而在其他的情况下,唯一的选择可能就是关闭对应的连接。
1.2 编码器
编码器实现了 ChannelOutboundHandler,并将出站数据从一种格式转换为另一种格式。和解码器正好相反。
编码器也有两种类型:
- 将消息编码为字节;
- 将消息编码为消息
1.2.1 抽象类 MessageToByteEncoder
MessageToByteEncoder是ByteToMessageDecoder的逆向,它提供了一个方法:
encode(ChannelHandlerContext ctx,I msg,ByteBuf out)
这个encode()方法是需要实现的唯一抽象方法。它被调用时将会传入要被该类编码为 ByteBuf 的(类型为 I 的)出站消息。该 ByteBuf 随后将会被转发给 ChannelPipeline中的下一个 ChannelOutboundHandler。
那为什么解码器由两个方法,而作为它逆向的编码器却只有一个方法?
原因是解码器通常需要在Channel 关闭之后产生最后一个消息(因此也就有了 decodeLast()方法)。这显然不适用于编码器的场景——在连接被关闭之后仍然产生一个消息是毫无意义的。
1.2.2 抽象类 MessageToMessageEncoder
和解码器类似, MessageToMessageEncode类是将出站数据将从一种消息编码为另一种。它同样有一个:`encode(
ChannelHandlerContext ctx,
I msg,
List
每个通过 write()方法写入的消息都将会被传递给 encode()方法,以编码为一个或者多个出站消息。随后,这些出站消息将会被转发给 ChannelPipeline中的下一个ChannelOutboundHandler。
二、抽象的编解码器类
虽然我们一直将解码器和编码器作为单独的实体讨论,但大部分情况大家都是在同一个类中管理入站和出站数据和消息的转换。Netty的抽象编解码器类每个都捆绑一个解码器/编码器对,以处理我们一直在学习的这两种类型的操作。这些类同时实现了ChannelInboundHandler 和 ChannelOutboundHandler 接口。
2.1 抽象类 ByteToMessageCodec
假设我们需要将字节解码为某种形式的消息,可能是 POJO,随后再次对它进行编码。那么就要用到ByteToMessageCodec,它结合了ByteToMessageDecoder 以及它的逆向——MessageToByteEncoder。
任何的请求/响应协议都可以作为使用ByteToMessageCodec的理想选择。例如,在某个SMTP的实现中,编解码器将读取传入字节,并将它们解码为一个自定义的消息类型,如SmtpRequest。而在接收端,当一个响应被创建时,将会产生一个SmtpResponse,其将被编码回字节以便进行传输。
ByteToMessageCodec提供了3个方法:
1、decode
decode(ChannelHandlerContext ctx,ByteBuf in,List<Object>)
只要有字节可以被消费,这个方法就将会被调用。它将入站ByteBuf 转换为指定的消息格式,并将其转发给ChannelPipeline 中的下一个 ChannelInboundHandler.
2、decodeLast
decodeLast(ChannelHandlerContext ctx,ByteBuf in,List<Object> out
这个方法的默认实现委托给了 decode()方法。它只会在Channel 的状态变为非活动时被调用一次。它可以被重写以实现特殊的处理
3、encode
encode(ChannelHandlerContext ctx,I msg,ByteBuf out)
对于每个将被编码并写入出站 ByteBuf 的(类型为 I 的)消息来说,这个方法都将会被调用。
2.2 抽象类 MessageToMessageCodec
MessageToMessageCodec可以帮助我们在一个类中实现以将一种消息格式转换为另外一种消息格式这种转换的往返过程,它是一个参数化的类。
MessageToMessageCodec定义如下:
public abstract class MessageToMessageCodec<INBOUND_IN,OUTBOUND_IN>
它提供了两种方法:
第一种:
protected abstract decode(ChannelHandlerContext ctx,INBOUND_IN msg,List<Object> out)
这个方法被调用时会被传入 INBOUND_IN 类型的消息。它将把它们解码为 OUTBOUND_IN 类型的消息,这些消息将被转发给 ChannelPipeline 中的下一个 ChannelInboundHandler
第二种:
protected abstract encode(ChannelHandlerContext ctx,OUTBOUND_IN msg,List<Object> out)
对于每个 OUTBOUND_IN 类型的消息,这个方法都将会被调用。这些消息将会被编码为 INBOUND_IN 类型的消息,然后被转发给 ChannelPipeline 中的下一个ChannelOutboundHandler。
decode()方法是将INBOUND_IN类型的消息转换为OUTBOUND_IN类型的消息,而encode()方法则进行它的逆向操作。将INBOUND_IN类型的消息看作是通过网络发送的类型,而将OUTBOUND_IN类型的消息看作是应用程序所处理的类型。
2.3 CombinedChannelDuplexHandler 类
前面提到大家大多数情况下会将编码器和解码器放在同一个类中进行管理,这样便于管理的同时也会带来一个问题。那就是这样的类重用性不高,那么有没有一种既有高可重用性,又不会牺牲将一个解码器和一个编码器作为一个单独的单元部署所带来的便利性的方案呢?
答案就是:CombinedChannelDuplexHandler 。
public class CombinedChannelDuplexHandler<I extends ChannelInboundHandler,O extends ChannelOutboundHandler>
这个类充当了 ChannelInboundHandler 和 ChannelOutboundHandler(该类的类型参数 I 和 O)的容器。通过提供分别继承了解码器类和编码器类的类型,我们可以实现一个编解码器,而又不必直接扩展抽象的编解码器类。