关注微信公众号(瓠悠笑软件部落),大家一起学习,一起摸鱼。
Codecs
网络将数据视为一个原始字节序列。 但是,我们的应用程序以一种具有信息含义的方式构造这些字节。 将数据转换为网络字节流和从网络字节流转换数据是最常见的编程任务之一。 例如,您可能需要使用标准格式或协议(如FTP或Telnet),实施由第三方定义的专有二进制协议,或者扩展由您自己的组织创建的旧邮件格式。
处理应用程序数据到网络格式和从网络格式转换的组件分别称为编码器和解码器,具有这两种功能的单个组件称为编解码器。 Netty提供了一系列工具来创建所有这些工具,从专用于HTTP和base64等知名协议的预构建类到可根据您的特定需求自定义的通用消息转换编解码器。
第10章介绍了编码器和解码器。 通过研究一些典型的用例,您将了解Netty的基本编解码器类。 当您了解这些类如何适应整个框架时,您会发现它们是基于您已经研究过的相同API构建的,因此您将能够立即使用它们。
本章将介绍:
- decoders, encoders 和 codecs 的预览
- Netty 的 codec classes
正如专用框架支持许多标准体系结构模式一样,通用数据处理模式通常是目标实现的良好候选者,这可以为开发人员节省大量时间和精力。
这当然适用于本章的主题:编码和解码,或将数据从一种特定于协议的格式转换为另一种格式。 这些任务由通常称为编解码器的组件处理。 Netty提供的组件可简化为各种协议创建自定义编解码器的过程。 例如,如果您正在构建基于Netty的邮件服务器,您会发现Netty的编解码器支持对于实现POP3,IMAP和SMTP协议非常有用。
10.1 What is a codec?
每个网络应用程序都必须定义如何解析对等体之间传输的原始字节,并将其转换为目标程序的数据格式。 该转换逻辑由编解码器处理,该编解码器由编码器和解码器组成,每个编码器将字节流从一种格式转换为另一种格式。 他们的区别是什么?
将消息视为具有特定应用程序含义的结构化字节序列 - 其数据。 编码器将该消息转换为适合传输的格式(很可能是字节流); 相应的解码器将网络流转换回程序的消息格式。 然后,编码器对出站数据进行操作,解码器处理入站数据。
考虑到这些背景信息,让我们检查一下Netty为实现这两种组件所提供的类。
10.2 Decoders
在本节中,我们将调查Netty的解码器类,并提供何时以及如何使用它们的具体示例。 这些类涵盖两个不同的用例:
- Decoding bytes to message —— ByteToMessageDecoder 和 ReplayDecoder
- Decoding one message type to another —— MessageToMessageDecoder
由于解码器负责将入站数据从一种格式转换为另一种格式,因此了解Netty的解码器实现ChannelInboundHandler 并不会让您感到懊恼。
你什么时候使用解码器? 简单:每当您需要为ChannelPipeline中的下一 个 ChannelInboundHandler转换入站数据时。 此外,由于ChannelPipeline的设计,您可以将多个解码器链接在一起实现任意复杂的转换逻辑,这是Netty如何支持代码模块化和重用的一个主要例子。
10.2.1 Abstract class ByteToMessageDecoder
从字节到消息(或另一个字节序列)的解码是一个常见的任务,Netty为它提供了一个抽象基类:ByteToMessageDecoder。 由于您无法知道远程对等方是否会一次性发送完整的消息,因此该类会缓冲入站数据,直到它准备好进行处理。 表10.1解释了它最重要的两种方法。
方法 | 描述 |
---|---|
decode(ChannelHandlerContext ctx, ByteBuf in, List out) | 这只是一个抽象的方法,你需要实现它。使用包含传入数据的 ByteBuf 和添加了解码消息的List调用 decode() 。 重复此调用,直到确定没有新项添加到 List 或者在 ByteBuf 中没有更多字节可读。 然后,如果 List 不为空,则将其内容传递给管道中的下一个处理程序。 |
decodeLast(ChannelHandlerContext ctx, ByteBuf in, List out) | Netty提供的默认实现只是调用decode()。 当通道变为非活动状态时,此方法被调用一次。 覆盖方法以提供特殊处理。 |
有关如何使用此类的示例,假设您收到一个包含简单int的字节流,每个字节流都要单独处理。 在这种情况下,您将从入站ByteBuf中读取每个int,并将其传递给管道中的下一个ChannelInboundHandler。 要解码字节流,您将扩展ByteToMessageDecoder。 (注意,当将原始int添加到List时,它将自动装箱到整数。)
设计如图10.1所示:
从入站ByteBuf一次读取四个字节,解码为int,并添加到List。 当没有更多项可用于添加到List时,其内容将被发送到下一个ChannelInboundHandler。
让我们看看 ToIntegerDecoder 的代码。
// Extends ByteToMessageDecoder to decode bytes to a specific format
public class ToIntegerDecoder extends ByteToMessageDecoder {
@Override
public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
// Checks if there are at leaset 4 bytes readable (length of an int)
if (in.readableBytes() >= 4) {
// Reads an int from the inbound ByteBuf and addsit to the List of decoded messages
out.add(in.readInt());
}
}
}
虽然 ByteToMessageDecoder 使这个模式易于实现,但您可能会发现有必要验证输入ByteBuf 是否有足够的数据供您调用readInt()。 在下一节中,我们将讨论ReplayingDecoder,这是一种消除此步骤的特殊解码器,代价是少量的开销。
Reference counting in codecs 编解码器中的引用计数
正如我们在第5章和第6章中提到的那样,引用计数需要特别注意。 在编码器和解码器的情况下,过程非常简单:一旦消息被编码或解码,它将通过调用 ReferenceCountUtil.release(message) 自动释放。 如果您需要保留引用以供以后使用,可以调用 ReferenceCountUtil.retain(message)。 这会增加引用计数,从而阻止释放消息。
10.2.2 Abstract class ReplayingDecoder
ReplayingDecoder 继承自 ByteToMessageDecoder,让我们不必调用 readableBytes()(如清单10.1所示)。 它通过使用在内部执行调用的自定义ByteBuf实现ReplayingDecoderBuffer包装传入的ByteBuf来实现此目的。
这个类的完整声明是:
public abstract class ReplayingDecoder<S> extends ByteToMessageDecoder
参数S指定用于状态管理的类型,其中Void表示不执行任何操作。 以下清单显示了基于ReplayingDecoder 的 ToIntegerDecoder 的重新实现。
// Extends RepalyingDecoder<Void> to decode bytes to messages
public class ToIntegerDecoder2 extends ReplayingDecoder<Void> {
// The incoming ByteBuf is a ReplayingDecoderBuffer.
@Override
public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
// Reads an int from the inbound ByteBuf and adds it to the List of decoded messages
out.add(in.readInt());
}
}
和以前一样,从ByteBuf中提取的int被添加到List中。 如果没有足够的字节可用,readInt() 的这个实现会抛出一个将在基类中捕获和处理的错误。 当有更多数据准备好读取时,将再次调用 decode() 方法。 (参见表10.1中的 decode() 说明。) 请注意ReplayingDecoderBuffer 的这些方面:
- 不是所有的 ByteBuf 操作都是支持的。如果调用了一个不被支持的方法,就会抛出一个 UnsupportedOperationException 异常。
- ReplayingDecoder 方法只比 ByteToMessageDeocder 方法慢一点点。
如果比较清单10.1和10.2,很明显后者更简单。 示例本身非常基础,因此请记住,在现实生活中,更复杂的情况下,使用一个或另一个基类之间的差异可能很大。 这是一个简单的指导原则:如果没有引入过多的复杂性,请使用 ByteToMessageDecoder; 否则,请使用ReplayingDecoder。
More decoders
下面的类处理了更复杂的使用场景:
- io.netty.handler.codec.LineBasedFrameDecoder —— 这个类由Netty内部使用,使用行尾控制字符(\ n或\ r \ n)来解析消息数据。
- ip.netty.handler.codec.http.HttpObjectDecoder —— 解析 HTTP data 的 decoder
您将在 io.netty.handler.codec 的子包中找到针对特殊用例的其他编码器和解码器实现。 有关更多信息,请参考Netty Javadoc。
10.2.3 Abstract class MessageToMessageDecoder
在本节中,我们将解释如何使用抽象基类在消息格式之间进行转换(例如,从一种类型的POJO到另一种类型)
public abstract class MessageToMessageDecoder<T> extends ChannelInboundHandlerAdapter
参数 I 指定decode()的输入msg参数的类型,即
你必须实现的唯一方法。 表10.2显示了此方法的详细信息。
方法 | 描述 |
---|---|
decode(ChannelHandlerContext ctx, I msg, List out) | 为每一个入站消息所调用,以讲消息解码为其他类型的格式。解码后的消息接着传给 pipeline 中的下一个 ChannelInboundHander |
在这个例子当中,我们讲编写一个 IntegerToStringDecoder 解码器, 这个解码器继承 MessageToMessageDecoder。它的 decode() 方法将 Integer 参数转换为其 String 表示形式,这个 decode() 方法的签名形式如下:
public void decode(ChannelHandlerContext ctx, Integer msg, List<Object> out)
throws Exception
同之前一样,解码后的 String 将会添加到出站 List, 然后传递给下一个 ChannelInboundHandler。
图 10.2 描述了这种设计:
下面列出 IntegerToStringDecoder 的具体实现:
// Extends MessageToMessageDecoder<Integer>
public class IntegerToStringDecoder extends
MessageToMessageDecoder<Integer> {
// Converts the Integer message to its String representation and adds it to the output List
@Override
public void decode(ChannelHandlerContext ctx, Integer msg, List<Object> out) throws Exception {
out.add(String.valueOf(msg));
}
}
HttpObjectAggregator
对于一个更复杂的例子,请检查类io.netty.handler.codec.http.HttpObjectAggregator,它扩展了MessageToMessageDecoder 。
10.2.4 Class TooLongFrameException
由于Netty是一个异步框架,因此您需要在内存中缓冲字节,直到您能够解码它们为止。因此,您不能让您的解码器缓冲足够的数据以耗尽可用的内存。 为了解决这个常见问题,Netty提供了一个TooLongFrameException,如果一个帧超出指定的大小限制,它将被解码器抛出。
为了避免这个情况,你可以设置一个关于 bytes 的最大数量的閥值(thresthod)。如果超过了,将会导致一个 TooLongFrameException 抛出(并被ChannelHandler.exceptionCaught()捕获)。然后由解码器的用户决定如何处理异常。 某些协议(如HTTP)可能允许您返回特殊响应。 在其他情况下,唯一的选择可能是关闭连接。
代码清单10.4显示了ByteToMessageDecoder如何利用TooLongFrameException来通知ChannelPipeline中的其他ChannelHandler关于帧大小溢出的发生。 请注意,如果您正在使用具有可变帧大小的协议,则此类保护尤其重要。
// Extends ByteToMessageDecoder to decode bytes to messages
public class SafeByteToMessageDecoder extends ByteToMessageDecoder {
private static final int MAX_FRAME_SIZE = 1024;
@Override
public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
int readable = in.readableBytes();
// Checks if the buffer has more than MAX_FRAME_SIZE bytes
if(readable > MAX_FRAME_SIZE) {
in.skipBytes(readable);
throw new TooLongFrameException("Frame too big!");
}
// do something
...
}
}
到目前为止,我们已经研究了解码器的常见用例以及Netty为构建它们提供的抽象基类。 但解码器只是硬币的一面。 另一方面是编码器,它将消息转换为适合传出传输的格式。 这些编码器完成了编解码器API,它们将成为我们的下一个主题。
10.3 Encoders
回顾我们之前的定义,编码器实现了ChannelOutboundHandler,并将出站数据从一种格式转换为另一种格式,这与我们刚刚研究的解码器功能相反。 Netty提供了一组类来帮助您编写具有以下功能的编码器:
- 从消息编码到字节
- 从消息编码到消息
我们将使用抽象基类MessageToByteEncoder开始检查这些类。
10.3.1 Abstract class MessageToByteEncoder
之前我们研究过如何使用ByteToMessageDecoder将字节转换为消息。 我们现在将使用MessageToByteEncoder执行相反的操作。
表10.3显示了API。
方法 | 描述 |
---|---|
encode(ChannelHandlerContext ctx, I msg, ByteBuf out) | encode 方法只是一个抽象的方法,你需要去实现它。它使用出站消息(类型I)调用,该类将编码为ByteBuf。 然后将ByteBuf转发到 pipeline 中的下一个ChannelOutboundHandler。 |
你可能已经注意到这个类只有一个方法,而解码器只有两个。原因是解码器经常需要在Channel 关闭后产生最后一条消息(因此是 decodeLast() 方法)。 对于编码器来说,情况显然不是这样 - 在连接关闭后产生消息没有任何意义。
图10.3显示了一个ShortToByteEncoder,它接收一个Short实例作为消息,将其编码为Short primitive,并将其写入 ByteBuf,然后将其转发到管道中的下一ChannelOutboundHandler。 每个传出的 Short 将占用 ByteBuf 中的两个字节。
ShortToByteEncoder 的实现如下表所示:
// Extends MessageToByteEncoder
public class ShortToByteEncoder extends MessageToByteEncoder<Short> {
@Override
public void encode(ChannelHandlerContext ctx, Short msg, ByteBuf out) throws Exception {
// Writes Short into ByteBuf
out.writeShort(msg);
}
}
Netty提供了MessageToByteEncoder的几个特化,您可以在其上建立自己的实现。 WebSocket08FrameEncoder类提供了一个很好的实际示例。您可以在包io.netty.handler.codec.http.websocketx 中找到它。
10.3.2 Abstract class MessageToMessageEncoder
您已经了解了如何将入站数据从一种消息格式解码到另一种消息格式。 为了完成图片,我们将展示如何针对出站数据从一种消息编码为另外一种消息。MessageToMessageEncoder 的 encode() 方法提供此功能,如表10.4中所述。
名称 | 描述 |
---|---|
encode(ChannelHandlerContext ctx, I msg, List out) | 这是你需要实现的唯一方法。用 write()写的每条消息都传递给 encode(),以便编码为一个或多个出站消息。 然后将它们转发到管道中下一个ChannelOutboundHandler 。 |
为了演示,列出10.6扩展了 MessageToMessageEncoder 和 IntegerToStringEncoder。 设计如图10.4所示。
如下一个清单所示,编码器将每个出站的Integer转换为String并添加到List。
// Extends MessageToMessageEncoder
public class IntegerToStringEncoder
extends MessageToMessageEncoder<Integer> {
@Override
public void encode(ChannelHandlerContext ctx, Integer msg, List<Object> out) throws Exception {
// Converts the Integer to a String and adds it to the List
out.add(String.valueOf(msg));
}
}
有关 MessageToMessageEncoder 的一个有趣的专门用途,请查看类io.netty.handler.codec.protobuf.ProtobufEncoder,它处理由 Google’s Protocol Buffers 规范定义的数据格式。
10.4 Abstract codec classes
虽然我们一直在讨论解码器和编码器作为不同的实体,但有时您会发现在一个类中管理入站和出站数据以及消息的转换很有用。 Netty的抽象编解码器类对此非常有用,因为每个类都将解码器/编码器对捆绑在一起,以处理我们一直在研究的两种类型的操作。 您可能怀疑,这些类实现了 ChannelInboundHandler 和 ChannelOutboundHandler。
为什么我们不会一直使用这些复合类而不是单独的解码器和编码器? 因为尽可能将两个功能分开,最大限度地提高了代码的可重用性和可扩展性,这是Netty设计的基本原则。
在我们查看抽象编解码器类时,我们将比较它们并将它们与相应的单个解码器和编码器进行对比。
10.4.1 Abstract class ByteToMessageCodec
让我们来看一下我们需要将字节解码为某种消息(可能是POJO)然后再次编码的情况。 ByteToMessageCodec 将为我们处理这个问题,因为它结合了ByteToMessageDecoder 和反向的 MessageToByteEncoder。 重要的方法列于表10.5。
方法名 | 描述 |
---|---|
decode(ChannelHandlerContext ctx, ByteBuf in, List) | 只要可以使用字节,就会调用此方法。 它将入站ByteBuf转换为指定的消息格式,并转发到管道中的下一个ChannelInboundHandler。 |
decodeLast(ChannelHandlerContext ctx, ByteBuf in, List out | 此方法的默认实现委托给decode()。 当通道变为非活动状态时,它仅被调用一次。 对于特殊处理,它可以被覆盖。 |
encode(ChannelHandlerContext ctx, I msg, ByteBuf out) | 对每个要编码的消息(类型I)调用此方法并将其写入出站ByteBuf。 |
任何请求/响应协议都可以成为使用 ByteToMessageCodec 的良好候选者。 例如,在SMTP实现中,编解码器将读取传入的字节并将其解码为自定义消息类型,例如SmtpRequest。 在接收端,当创建响应时,将产生 SmtpResponse,其将被编码回字节以进行传输。
10.4.2 Abstract class MessageToMessageCodec
在9.2.2节中,您看到了MessageToMessageEncoder的示例,该示例已扩展为将一种消息格式转换为另一种消息格式。 使用MessageToMessageCodec,我们可以使用单个类进行往返。 MessageToMessageCodec是一个参数化类,定义如下:
public abstract class MessageToMessageCodec<INBOUND_IN,OUTBOUND_IN>
重要的方法如下表 10.6 所列:
方法名 | 描述 |
---|---|
protected abstract decode(ChannelHandlerContext ctx, INBOUND_IN msg, List out) | 使用INBOUND_IN类型的消息调用此方法。 它将它们解码为OUTBOUND_IN类型的消息,这些消息被转发到ChannelPipeline中的下一个ChannelInboundHandler。 |
protected abstract encode(ChannelHandlerContext ctx, OUTBOUND_IN msg, List out) | 为OUTBOUND_IN类型的每个消息调用此方法。 这些编码为INBOUND_IN类型的消息,并转发到管道中的下一个ChannelOutboundHandler。 |
decode() 方法将 INBOUND_IN 消息转换为 OUTBOUND_IN 类型,而 encode() 则反转。 将 INBOUND_IN 消息视为通过线路发送的类型,将 OUTBOUND_IN 消息视为应用程序处理的类型可能会有所帮助。
虽然这个编解码器可能看起来有些深奥,但它处理的用例相当普遍:在两个不同的消息传递API之间来回转换数据。 当我们必须与使用传统或专有消息格式的API进行互操作时,我们经常会遇到这种模式。
WebSocket protocol
以下 MessageToMessageCodec 示例引用了WebSocket,这是一种支持Web浏览器和服务器之间完全双向通信的最新协议。 我们将在第11章详细讨论Netty对WebSockets的支持。
清单10.7显示了如何进行这样的对话。 我们的WebSocketConvertHandler使用INBOUND_IN类型的WebSocketFrame和OUTBOUND_IN类型的MyWebSocketFrame参数化MessageToMessageCodec,后者是WebSocketConvertHandler本身的静态嵌套类。
public class WebSocketConvertHandler extends
MessageToMessageCodec<WebSocketFrame,
WebSocketConvertHandler.MyWebSocketFrame> {
// Encodes a MyWebSocketFrame to specified WebSocketFrame subtype
@Override
protected void encode(ChannelHandlerConvert ctx,
WebSocketConvertHandler.MyWebSocketFrame msg,
List<Object> out) throws Exception {
ByteBuf payload = msg.getData().duplicate().retain();
switch(msg.getType()) {
case BINARY:
out.add(new BinaryWebSocketFrame(payload));
break;
case TEXT:
out.add(new TextWebSocketFrame(payload));
case CLOSE:
out.add(new CloseWebSocketFrame(true, 0, payload));
break;
case CONTINUATION:
out.add(new ContinuationWebSocketFrame(payload));
break;
case PONG:
out.add(new PongWebSocketFrame(payload));
break;
case PING:
out.add(new PingWebSocketFrame(payload));
break;
default:
throw new IllegalStateException(
"Unsupported websocket msg " + msg);
}
}
// Decodes a WebSocketFrame to a MyWebSocketFrame and sets the FrameType
@Override
protected void decode(ChannelHandlerContext ctx, WebSocketFrame msg,
List<Object> out) throws Exception {
ByteBuf payload = msg.getData().duplicate().retain();
if (msg instanceof BinaryWebSocketFrame) {
out.add(new MyWebSocketFrame(
MyWebSocketFrame.FrameType.BINARY, payload));
} else
if (msg instanceof CloseWebSocketFrame) {
out.add(new MyWebSocketFrame(
MyWebSocketFrame.FrameType.CLOSE, payload));
} else
if (msg instanceof PingWebSocketFrame) {
out.add(new MyWebSocketFrame(
MyWebSocketFrame.FrameType.PING, payload));
} else
if (msg instanceof PongWebSocketFrame) {
out.add(new MyWebSocketFrame(
MyWebSocketFrame.FrameType.PONG, payload));
} else
if (msg instanceof TextWebSocketFrame) {
out.add(new MyWebSocketFrame(
MyWebSocketFrame.FrameType.TEXT, payload));
} else
if (msg instanceof ContinuationWebSocketFrame) {
out.add(new MyWebSocketFrame(
MyWebSocketFrame.FrameType.CONTINUATION, payload));
} else {
throw new IllegalStateException(
"Unsupported websocket msg " + msg);
}
}
// Declares the OUTBOUND_IN type used by WebSocketConvertFrame that owns the wrapped payload
public static final class MyWebSocketFrame {
public enum FrameType {
BINARY,
CLOSE,
PING,
PONG,
TEXT,
CONTINUATION
}
private final FrameType type;
private final ByteBuf data;
public WebSocketFrame(FrameType type, ByteBuf data) {
this.type = type;
this.data = data;
}
public FrameType getType() {
return type;
}
public FrameType getData() {
return data;
}
}
}
10.4.3 Class CombinedChannelDuplexHandler
正如我们之前提到的那样,组合解码器和编码器可能会对可重用性产生影响。 然而,有一种方法可以避免这种损失而不会牺牲将解码器和编码器部署为单个单元的便利性。 解决方案由CombinedChannelDuplexHandler提供,声明为:
public class CombinedChannelDuplexHandler
<I extends ChannelInboundHandler,
O extends ChannelOutboundHandler>
这个类充当了 ChannelInboundHandler(类参数 I) 和 ChannelOutboundHandler(类参数O) 类的容器。通过提供分别扩展解码器类和编码器类的类型,我们可以实现编解码器而无需直接扩展抽象编解码器类。 我们将在下面的例子中说明这一点。
首先,检查此列表中的 ByteToCharDecoder。 请注意,实现扩展了ByteToMessageDecoder,因为它从ByteBuf读取字符。
// Extends ByteToMessageDecoder
public class ByteToCharDecoder extends ByteToMessageDecoder {
@Override
public void decode(ChannelHandlerContext ctx, ByteBuf in,
List<Object> out) throws Exception {
while(in.readableBytes() >=2 ) {
// Adds one or more Character objects to the outgoing List
out.add(in.readChar());
}
}
}
这里 decode() 从ByteBuf一次提取2个字节,并将它们作为字符写入List,它们将作为Character对象自动装箱。
此列表包含CharToByteEncoder,它将字符转换回字节。 此类扩展了MessageToByteEncoder,因为它需要将char消息编码为ByteBuf。 这是通过直接写入ByteBuf来完成的。
// Extends MessageToByteEncoder
public class CharToByteEncoder extends
MessageToByteEncoder<Character> {
@Override
public void encode(ChannelHandlerContext ctx, Character msg,
ByteBuf out) throws Exception {
// Decodes a Character to a char and writes it into the outbound ByteBuf
out.writeChar(msg);
}
}
现在我们有一个解码器和编码器了,我们讲把他们联合起来,组装成一个编解码器。
下面显示了如何做到这一点的:
// Parameterizes CombinedByteCharCodec by the decoder and encoder implementations
public class CombinedByteCharCodec extends
CombinedChannelDuplexHandler<ByteToCharDecoder, CharToByteEncoder> {
public CombinedByCharCodec() {
// Passes the delegate instances to the parent
super(new ByteToCharDecoder(), new CharToByteEncoder());
}
}
正如您所看到的,在某些情况下,以这种方式组合实现比使用其中一个编解码器类可能更简单,更灵活。 这也可能归结为个人偏好。
10.5 Summary
在本章中,我们研究了使用Netty编解码器API编写解码器和编码器。 您了解了为什么使用此API比直接使用ChannelHandler API更为可取。
您了解了抽象编解码器类如何在一个实现中提供对处理解码和编码的支持。 如果您需要更大的灵活性或希望重用现有的实现,您还可以选择组合它们而无需扩展任何抽象的编解码器类。
在下一章中,我们将讨论ChannelHandler实现和编解码器,它们是Netty框架本身的一部分,您可以利用它来处理特定的协议和任务。