Netty实战(十)编解码器框架

简介: 网络只将数据看作是原始的字节序列。但我们的应用程序则会把这些字节组织成有意义的信息。在数据和网络字节流之间做相互转换是最常见的编程任务之一。例如,我们可能需要处理标准的格式或者协议(如 FTP 或 Telnet)、实现一种由第三方定义的专有二进制协议,或者扩展一种由自己的组织创建的遗留的消息格式。将应用程序的数据转换为网络格式,以及将网络格式转换为应用程序的数据的组件分别叫作编码器和解码器,同时具有这两种功能的单一组件叫作编解码器。

@TOC

一、什么是编解码器框架

网络只将数据看作是原始的字节序列。但我们的应用程序则会把这些字节组织成有意义的信息。在数据和网络字节流之间做相互转换是最常见的编程任务之一。例如,我们可能需要处理标准的格式或者协议(如 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 out)

这个是必须实现的唯一抽象方法。decode()方法被调用时将会传入一个包含了传入数据的 ByteBuf,以及一个用来添加解码消息的 List。对这个方法的调用将会重复进行,直到确定没有新的元素被添加到该 List,或者该 ByteBuf 中没有更多可读取的字节时为止。然后,如果该 List 不为空,那么它的内容将会被传递给ChannelPipeline 中的下一个 ChannelInboundHandler。

2、decodeLast(ChannelHandlerContext ctx,ByteBuf in,List out)

这个默认只是简单地调用了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 out) 方法,对于每个需要被解码为另一种格式的入站消息来说,这个方法都将会被调用。解码消息随后会被传递给 ChannelPipeline中的下一个ChannelInboundHandler。

举个例子:

我们编写一个 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<Object> out) 方法。

每个通过 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)的容器。通过提供分别继承了解码器类和编码器类的类型,我们可以实现一个编解码器,而又不必直接扩展抽象的编解码器类。

目录
相关文章
|
2月前
Netty实战: HTTP文件列表服务器
Netty实战: HTTP文件列表服务器
21 0
|
3月前
|
Java Unix Linux
【Netty技术专题】「原理分析系列」Netty强大特性之Native transports扩展开发实战
当涉及到网络通信和高性能的Java应用程序时,Netty是一个强大的框架。它提供了许多功能和组件,其中之一是JNI传输。JNI传输是Netty的一个特性,它为特定平台提供了高效的网络传输。 在本文中,我们将深入探讨Netty提供的特定平台的JNI传输功能,分析其优势和适用场景。我们将介绍每个特定平台的JNI传输,并讨论其性能、可靠性和可扩展性。通过了解这些特定平台的JNI传输,您将能够更好地选择和配置适合您应用程序需求的网络传输方式,以实现最佳的性能和可靠性。
57 7
【Netty技术专题】「原理分析系列」Netty强大特性之Native transports扩展开发实战
|
21天前
|
网络协议 Java 测试技术
阿里内部Netty实战小册,值得拥有
Netty 是一个高性能的 Java 网络通信框架,简化了网络编程并涵盖了最新的Web技术。它提供了一种抽象,降低了底层复杂性,使得更多开发者能接触网络编程。Netty 因其易用性、高效性和广泛的应用场景受到推崇,适合互联网行业从业者学习,有助于理解和开发基于Netty的系统。免费的《Netty实战小册》详细介绍了Netty的各个方面,包括概念、架构、编解码器、网络协议和实际案例,帮助读者深入理解和应用Netty。如需完整版小册,可点击链接获取。
阿里内部Netty实战小册,值得拥有
|
存储 设计模式 网络协议
Netty网络框架(一)
Netty网络框架
36 1
|
2月前
|
NoSQL Redis
Netty实战:模拟Redis的客户端
Netty实战:模拟Redis的客户端
14 0
|
2月前
|
前端开发 Java 数据库连接
探索Java中最常用的框架:Spring、Spring MVC、Spring Boot、MyBatis和Netty
探索Java中最常用的框架:Spring、Spring MVC、Spring Boot、MyBatis和Netty
|
3月前
|
前端开发 Java 数据库连接
认识Java中最常用的框架:Spring、Spring MVC、Spring Boot、MyBatis和Netty
Spring框架 Spring是一个轻量级的开源框架,用于构建企业级应用。它提供了广泛的功能,包括依赖注入、面向切面编程、事务管理、消息传递等。Spring的核心思想是控制反转(IoC)和面向切面编程(AOP)。
81 3
|
4月前
|
负载均衡 Java 调度
【分布式技术专题】「探索高性能远程通信」基于Netty的分布式通信框架实现(Dispatcher和EventListener)(下)
经过阅读《【分布式技术专题】「探索高性能远程通信」基于Netty的分布式通信框架实现(附通信协议和代码)(上)》,相信您已经对网络通信框架的网络通信层的实现原理和协议模型有了一定的认识和理解。
42 0
【分布式技术专题】「探索高性能远程通信」基于Netty的分布式通信框架实现(Dispatcher和EventListener)(下)
|
4月前
|
Dubbo Java 应用服务中间件
【分布式技术专题】「探索高性能远程通信」基于Netty的分布式通信框架实现(附通信协议和代码)(上)
今天,我要向大家实现一个基于Netty实现的高性能远程通信框架!这个框架利用了 Netty 的强大功能,提供了快速、可靠的远程通信能力。 无论是构建大规模微服务架构还是实现分布式计算,这个分布式通信框架都是一个不可或缺的利器。
65 2
【分布式技术专题】「探索高性能远程通信」基于Netty的分布式通信框架实现(附通信协议和代码)(上)
|
4月前
|
分布式计算 前端开发 网络协议
13W字!腾讯高工手写“Netty速成手册”,3天能走向实战
在java界,netty无疑是开发网络应用的拿手菜。你不需要太多关注复杂的nio模型和底层网络的细节,使用其丰富的接口,可以很容易的实现复杂的通讯功能。