高性能IO框架Netty五 - Netty内置的编解码器

725 阅读5分钟

emm,最近Netty篇章比较干,没用过的人估计很晦涩难懂,想写的具体生动一些,怎奈文笔有限。如果只想知道具体作用的话,坚持,马上就到实战环节了!

 一、什么是编解码器

就像我们用的Spring里面的HttpRequest对象,难道客户端传过来就是一个HttpRequest对象吗?很显然一般网络中传输都是通过字节流进行传输的。那么由字节流转为我们需要的对象的过程就叫做解码。从对象再转为字节流或者其他对象的过程,叫做编码。

每个网络应用程序都必须定义如何解析在两个节点之间来回传输的原始字节,以及如何将其和目标应用程序的数据格式做相互转换。这种转换逻辑由编解码器处理,编解码器由编码器和解码器组成,它们每种都可以将字节流从一种格式转换为另一种格式。那么它们的区别是什么呢?

如果将消息看作是对于特定的应用程序具有具体含义的结构化的字节序列—它的数据。那么编码器是将消息转换为适合于传输的格式(最有可能的就是字节流);而对应的解码器则是将网络字节流转换回应用程序的消息格式。因此,编码器操作出站数据,而解码器处理入站数据。

我们前面所学的解决粘包半包的其实也是编解码器框架的一部分,例如下图,就是个定长的解码器。

二、解码器

Netty中,解码器一般有两种,如下

将字节解码为消息——ByteToMessageDecoder,类关系图如下

将一种消息类型解码为另一种——MessageToMessageDecoder,类关系图如下

因为解码器是负责将入站数据从一种格式转换到另一种格式的,所以Netty 的解码器实现了ChannelInboundHandler。

什么时候会用到解码器呢?

很简单:每当需要为ChannelPipeline 中的下一个Channel-InboundHandler 转换入站数据时会用到。此外,得益于ChannelPipeline 的设计,可以将多个解码器链接在一起,以实现任意复杂的转换逻辑。

2.1ByteToMessageDecoder

抽象类ByteToMessageDecoder 将字节解码为消息

将字节解码为消息(或者另一个字节序列)是一项如此常见的任务,以至于Netty 为它提供了一个抽象的基类:ByteToMessageDecoder。由于你不可能知道远程节点是否会一次性地发送一个完整的消息,所以这个类会对入站数据进行缓冲,直到它准备好处理。

它最重要方法

decode(ChannelHandlerContext ctx,ByteBuf in,List out)

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

代理示例如下图,实现一个定长处理的消息解码器

/**
 * DrakKing
 */
//扩展 ByteToMessageDecoder 以处理入站字节,并将它们解码为消息
public class FixedLengthFrameDecoder extends ByteToMessageDecoder {
    private final int frameLength;

    //指定要生成的帧的长度
    public FixedLengthFrameDecoder(int frameLength) {
        if (frameLength <= 0) {
            throw new IllegalArgumentException(
                "frameLength must be a positive integer: " + frameLength);
        }
        this.frameLength = frameLength;
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in,
                          List<Object> out) throws Exception {
        //检查是否有足够的字节可以被读取,以生成下一个帧
        while (in.readableBytes() >= frameLength) {
            //从 ByteBuf 中读取一个新帧
            ByteBuf buf = in.readBytes(frameLength);
            //将该帧添加到已被解码的消息列表中
            out.add(buf);
        }
    }
}

2.2 MessageToMessageDecoder

抽象类MessageToMessageDecoder 将一种消息类型解码为另一种,在两个消息格式之间进行转换(例如,从String->Integer)

主要方法为

decode(ChannelHandlerContext ctx,I msg,List out)

对于每个需要被解码为另一种格式的入站消息来说,该方法都将会被调用。解码消息随后会被传递给ChannelPipeline中的下一个ChannelInboundHandler

MessageToMessageDecoder,T代表源数据的类型

2.3 TooLongFrameException

由于Netty 是一个异步框架,所以需要在字节可以解码之前在内存中缓冲它们。因此,不能让解码器缓冲大量的数据以至于耗尽可用的内存。为了解除这个常见的顾虑,Netty 提供了TooLongFrameException 类,其将由解码器在帧超出指定的大小限制时抛出。

为了避免这种情况,你可以设置一个最大字节数的阈值,如果超出该阈值,则会导致抛出一个TooLongFrameException(随后会被ChannelHandler.exceptionCaught()方法捕获)。然后,如何处理该异常则完全取决于该解码器的用户。某些协议(如HTTP)可能允许你返回一个特殊的响应。而在其他的情况下,唯一的选择可能就是关闭对应的连接。

三、编码器

解码器的功能正好相反。Netty 提供了一组类,用于帮助你编写具有以下功能的编码器:

将消息编码为字节;MessageToByteEncoder,

将消息编码为消息:MessageToMessageEncoder,T代表源数据的类型,类关系图如下

3.1 MessageToByteEncoder 

抽象类 MessageToByteEncoder 将消息编码为字节

encode(ChannelHandlerContext ctx,I msg,ByteBuf out)

encode()方法是你需要实现的唯一抽象方法。它被调用时将会传入要被该类编码为ByteBuf 的(类型为I 的)出站消息。该ByteBuf 随后将会被转发给ChannelPipeline中的下一个ChannelOutboundHandler

代码示例

/**
 * DarkKing
 * 类说明:序列化机制
 */
public class MsgPackEncode extends MessageToByteEncoder<Object> {
    @Override
    protected void encode(ChannelHandlerContext ctx, Object msg,
                          ByteBuf out) throws Exception {
        MessagePack messagePack = new MessagePack();
        byte[] raw = messagePack.write(msg);
        out.writeBytes(raw);
    }
}

3.2 MessageToMessageEncoder

抽象类 MessageToMessageEncoder 将消息编码为消息

encode(ChannelHandlerContext ctx,I msg,List out)

这是你需要实现的唯一方法。每个通过write()方法写入的消息都将会被传递给encode()方法,以编码为一个或者多个出站消息。随后,这些出站消息将会被转发给ChannelPipeline中的下一个ChannelOutboundHandler

代码示例

/**
 * DarkKing
 */
//扩展 MessageToMessageEncoder 以将一个消息编码为另外一种格式
public class AbsIntegerEncoder extends
        MessageToMessageEncoder<ByteBuf> {
    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext,
                          ByteBuf in, List<Object> out) throws Exception {
        //检查是否有足够的字节用来编码,int为4个字节
        while (in.readableBytes() >= 4) {
            //从输入的 ByteBuf中读取下一个整数,并且计算其绝对值
            int value = Math.abs(in.readInt());
            //将该整数写入到编码消息的 List 中
            out.add(value);
        }
    }
}

四、编解码器类

我们一直将解码器和编码器作为单独的实体讨论,但是你有时将会发现在同一个类中管理入站和出站数据和消息的转换是很有用的。Netty 的抽象编解码器类正好用于这个目的,因为它们每个都将捆绑一个解码器/编码器对,以处理我们一直在学习的这两种类型的操作。这些类同时实现了ChannelInboundHandler 和ChannelOutboundHandler 接口。

为什么我们并没有一直优先于单独的解码器和编码器使用这些复合类呢?因为通过尽可能地将这两种功能分开,最大化了代码的可重用性和可扩展性,这是Netty 设计的一个基本原则。

相关的类:

抽象类ByteToMessageCodec

抽象类MessageToMessageCodec

五、Netty内置的编解码器和ChannelHandler

Netty 为许多通用协议提供了编解码器和处理器,几乎可以开箱即用,这减少了你在那些相当繁琐的事务上本来会花费的时间与精力。

5.1 通过SSL/TLS 保护Netty 应用程序

SSL和TLS这样的安全协议,它们层叠在其他协议之上,用以实现数据安全。我们在访问安全网站时遇到过这些协议,但是它们也可用于其他不是基于HTTP的应用程序,如安全SMTP(SMTPS)邮件服务器甚至是关系型数据库系统。

为了支持SSL/TLS,Java 提供了javax.net.ssl 包,它的SSLContext 和SSLEngine类使得实现解密和加密相当简单直接。Netty 通过一个名为SslHandler 的ChannelHandler实现利用了这个API,其中SslHandler 在内部使用SSLEngine 来完成实际的工作。

Netty 还提供了使用OpenSSL 工具包(www.openssl.org)的SSLEngine 实现。这个OpenSsl-Engine 类提供了比JDK 提供的SSLEngine 实现更好的性能。

如果OpenSSL库可用,可以将Netty 应用程序(客户端和服务器)配置为默认使用OpenSslEngine。如果不可用,Netty 将会回退到JDK 实现。

在大多数情况下,SslHandler 将是ChannelPipeline 中的第一个ChannelHandler。

例如,给自己的应用添加SSL功能,只需添加SSL相关的handler即可。

5.2 HTTP 系列

HTTP 是基于请求/响应模式的:客户端向服务器发送一个HTTP 请求,然后服务器将会返回一个HTTP 响应。Netty 提供了多种编码器和解码器以简化对这个协议的使用。

5.2.1 Neety内置Http相关解码器

一个HTTP 请求/响应可能由多个数据部分组成,并且它总是以一个LastHttpContent 部分作为结束。FullHttpRequest 和FullHttpResponse 消息是特殊的子类型,分别代表了完整的请求和响应。所有类型的HTTP 消息(FullHttpRequest、LastHttpContent等等)都实现了HttpObject 接口。

HttpRequestEncoder 将HttpRequest、HttpContent 和LastHttpContent 消息编码为字节

HttpResponseEncoder 将HttpResponse、HttpContent 和LastHttpContent 消息编码为字节

HttpRequestDecoder 将字节解码为HttpRequest、HttpContent 和LastHttpContent 消息

HttpResponseDecoder 将字节解码为HttpResponse、HttpContent 和LastHttpContent 消息

5.2.2 聚合 HTTP 消息

由于HTTP 的请求和响应可能由许多部分组成,因此你需要聚合它们以形成完整的消息。为了消除这项繁琐的任务,Netty 提供了一个聚合器,它可以将多个消息部分合并为FullHttpRequest 或者FullHttpResponse 消息。通过这样的方式,你将总是看到完整的消息内容。

5.2.3 HTTP 压缩

当使用HTTP 时,建议开启压缩功能以尽可能多地减小传输数据的大小。虽然压缩会带来一些CPU 时钟周期上的开销,但是通常来说它都是一个好主意,特别是对于文本数据来说。Netty 为压缩和解压缩提供了ChannelHandler 实现,它们同时支持gzip 和deflate 编码。

5.2.4 使用 HTTPS

启用HTTPS 只需要将SslHandler 添加到ChannelPipeline 的ChannelHandler 组合中。

SSL和HTTP的代码参见模块netty-http

5.2.5 WebSocket

后面单独当做应用来讲

5.2.6 空闲的连接和超时

检测空闲连接以及超时对于及时释放资源来说是至关重要的。由于这是一项常见的任务,Netty 特地为它提供了几个ChannelHandler 实现。

IdleStateHandler 当连接空闲时间太长时,将会触发一个IdleStateEvent 事件。然后,你可以通过在你的ChannelInboundHandler 中重写userEventTriggered()方法来处理该IdleStateEvent 事件。

ReadTimeoutHandler 如果在指定的时间间隔内没有收到任何的入站数据,则抛出一个Read-TimeoutException 并关闭对应的Channel。可以通过重写你的ChannelHandler 中的exceptionCaught()方法来检测该Read-TimeoutException。

WriteTimeoutHandler 如果在指定的时间间隔内没有任何出站数据写入,则抛出一个Write-TimeoutException 并关闭对应的Channel 。可以通过重写你的ChannelHandler 的exceptionCaught()方法检测该WriteTimeout-Exception。

到这里,Netty的一些基础理论知识差不多就讲完了。后面开始准备进入Netty的实战环节。针对Netty实现相关的一些应用。加深对Netty组建的一些了解。