Netty系列7-编解码器

编码器是将消息转换为适合于传输的格式,一般是二进制;而对应的解码器则是将网络字节流转换回应用程序的消息格式。因此,编码器操作出站数据,而解码器处理入站数据。我们上一篇文章解决粘包拆包用到的也是编解码器。

1. 解码器

解码器是负责将入站数据从一种格式转换到另一种格式的,所以解码器实现ChannelInboundHandler。Netty提供了丰富的解码器抽象基类,主要分两类:

1.1 将字节解码为消息

(1)ByteToMessageDecoder
由于你不可能知道远程节点是否会一次性地发送一个完整的消息,所以这个类会对入站数据进行缓冲,直到它准备好处理。
方法

  • decode(ChannelHandlerContext ctx,ByteBuf in,List out):被调用时将会传入一个包含了传入数据的ByteBuf,以及一个用来添加解码消息的List。对这个方法的调用将会重复进行,直到确定没有新的元素被添加到该List,或者该ByteBuf中没有更多可读取的字节时为止。如果该List 不为空,那么它的内容将会被传递给ChannelPipeline中的下一个ChannelInboundHandler。
  • decodeLast:channel状态变为inactive时调用,可以做一些特殊消息传递
public class ToIntegerDecoder extends ByteToMessageDecoder {  

    @Override
    public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
            throws Exception {
        if (in.readableBytes() >= 4) { 
            out.add(in.readInt()); 
        }
    }
}

(2)ReplayingDecoder
ByteToMessageDecoder读取缓冲区的数据之前需要检查缓冲区是否有足够的字节,使用ReplayingDecoder就无需检查,若ByteBuf中有足够的字节,则会正常读取,若没有足够的字节则会停止解码。
ReplayingDecoder继承自ByteToMessageDecoder,但是不是所有的标准 ByteBuf 操作都被支持,如果调用一个不支持的操作会抛出 UnreplayableOperationException,而且ReplayingDecoder 略慢于 ByteToMessageDecode
ByteToMessageDecoder 和 ReplayingDecoder

public class ToIntegerDecoder2 extends ReplayingDecoder<Void> {  

    @Override
    public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
            throws Exception {
        out.add(in.readInt());  
    }
}

1.2 将消息解码为消息

(1)MessageToMessageDecoder
在两个消息格式之间进行转换时使用,T代表源数据的类型。
方法
decode(ChannelHandlerContext ctx,I msg,List out)
对于每个需要被解码为另一种格式的入站消息,该方法都将会被调用。解码消息随后会被传递给ChannelPipeline中的下一个ChannelInboundHandler

1.3 TooLongFrameException

Netty 是一个异步框架,所以需要在字节可以解码之前在内存中缓冲它们。因此,不能让解码器缓冲大量的数据以至于耗尽可用的内存。为了避免这种情况,你可以设置一个最大字节数的阈值,如果超出该阈值,则会导致抛出一个TooLongFrameException(随后会被ChannelHandler.exceptionCaught()方法捕获)。

2. 编码器

编码器是负责将出站数据从一种格式转换到另一种格式的,所以编码器实现ChannelOutboundHandler。Netty提供了丰富的编码器抽象基类,主要分两类:

1.1 将消息编码为字节

(1)MessageToByteEncoder
方法

  • encode(ChannelHandlerContext ctx,I msg,ByteBuf out):它被调用时将会传入要被该类编码为ByteBuf 的出站消息。该ByteBuf随后将会被转发给ChannelPipeline中的下一个ChannelOutboundHandler。
public class ShortToByteEncoder extends
        MessageToByteEncoder<Short> {  
    @Override
    public void encode(ChannelHandlerContext ctx, Short msg, ByteBuf out)
            throws Exception {
        out.writeShort(msg); 
    }
}

1.2 将消息编码为消息

(1)MessageToMessageEncoder
方法
decode(ChannelHandlerContext ctx,I msg,List out)
每个通过write()方法写入的消息都将会被传递给encode()方法,以编码为一个或者多个出站消息。随后,这些出站消息将会被转发给ChannelPipeline中的下一个ChannelOutboundHandler。

public class IntegerToStringEncoder extends
        MessageToMessageEncoder<Integer> { //1

    @Override
    public void encode(ChannelHandlerContext ctx, Integer msg, List<Object> out)
            throws Exception {
        out.add(String.valueOf(msg));  //2
    }
}

3. 编解码器

netty的编解码器类可以在同一个类中管理入站和出站数据和消息的转换,这些类同时实现ChannelInboundHandler和ChannelOutboundHandler接口。
ByteToMessageCodec集成了ByteToMessageDecoder和MessageToByteEncoder。
MessageToMessageCodec集成了MessageToMessageDecoder和MessageToMessageEncoder。

4. Netty内置的编解码器

4.1 使用SSL/TLS加密

为了支持SSL/TLS,Java提供了javax.net.ssl包,使用SSLContext和SSLEngine类实现解密和加密。Netty通过名为SslHandler的ChannelHandler实现利用了这个API,SslHandler在内部使用SSLEngine来完成实际的工作。
Netty还提供了使用OpenSSL的SSLEngine实现相同效果。OpenSsl-Engine类提供了比JDK的SSLEngine更好的性能。
在大多数情况下,SslHandler将是ChannelPipeline中的第一个ChannelHandler,SslHandler同时实现了入站和出站。

public class SslChannelInitializer extends ChannelInitializer<Channel> {

    private final SslContext context;
    private final boolean startTls;
    public SslChannelInitializer(SslContext context,
    boolean client, boolean startTls) {  
        this.context = context;
        this.startTls = startTls;
    }
    @Override
    protected void initChannel(Channel ch) throws Exception {
        SSLEngine engine = context.newEngine(ch.alloc()); 
        engine.setUseClientMode(client); 
        ch.pipeline().addFirst("ssl", new SslHandler(engine, startTls));  
    }
}

4.2 构建HTTP/HTTPS

HTTP是基于请求/响应模式的,客户端发送一个HTTP请求,服务器返回一个HTTP响应。Netty提供了多种编码器和解码器以简化对这个协议的使用。
一个HTTP请求/响应可能包含一个请求/响应头和由多个数据HttpContent部分组成,并且它总是以一个LastHttpContent作为结束。FullHttpRequest和FullHttpResponse是HttpObject的特殊子类型,分别代表了完整的请求和响应。

  • HttpRequestEncoder:将HttpRequest、HttpContent 和LastHttpContent 消息编码为字节
  • HttpResponseEncoder:将HttpResponse、HttpContent 和LastHttpContent 消息编码为字节
  • HttpRequestDecoder:将字节解码为HttpRequest、HttpContent 和LastHttpContent消息
  • HttpResponseDecoder:将字节解码为HttpResponse、HttpContent 和LastHttpContent消息

由于HTTP 的请求和响应可能由许多部分组成,因此你需要聚合它们以形成完整的消息。Netty 提供了一个聚合器HttpObjectAggregator,它可以将多个消息部分合并为FullHttpRequest 或者FullHttpResponse 消息。

public class HttpAggregatorInitializer extends ChannelInitializer<Channel> {

    private final boolean client;

    public HttpAggregatorInitializer(boolean client) {
        this.client = client;
    }

    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        if (client) {
            pipeline.addLast("codec", new HttpClientCodec());  
        } else {
            pipeline.addLast("codec", new HttpServerCodec());  
        }
        pipeline.addLast("aggegator", new HttpObjectAggregator(512 * 1024));  
    }
}

4.3 HTTP 压缩

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

public class HttpAggregatorInitializer extends ChannelInitializer<Channel> {

    private final boolean isClient;
    public HttpAggregatorInitializer(boolean isClient) {
        this.isClient = isClient;
    }
    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        if (isClient) {
            pipeline.addLast("codec", new HttpClientCodec()); 
            pipeline.addLast("decompressor",new HttpContentDecompressor());
        } else {
            pipeline.addLast("codec", new HttpServerCodec()); 
            pipeline.addLast("compressor",new HttpContentCompressor()); 
        }
    }
}

4.4 空闲的连接和超时

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

  • IdleStateHandler:当连接空闲时间太长时,将会触发IdleStateEvent事件。你可以通过在你的ChannelInboundHandler 中重写userEventTriggered()方法来处理该IdleStateEvent事件。
  • ReadTimeoutHandler:在指定的时间间隔内没有收到任何的入站数据,则抛出一个ReadTimeoutException并关闭对应的Channel。可以重写ChannelHandler 中的exceptionCaught()方法来检测该ReadTimeoutException。
  • WriteTimeoutHandler:在指定的时间间隔内没有任何出站数据写入,则抛出一个WriteTimeoutException,并关闭对应的Channel 。可以重写ChannelHandler的exceptionCaught()方法检测该WriteTimeoutException。
public class IdleStateHandlerInitializer extends ChannelInitializer<Channel> {

    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast(new IdleStateHandler(0, 0, 60, TimeUnit.SECONDS)); 
        pipeline.addLast(new HeartbeatHandler());
    }

    public static final class HeartbeatHandler extends ChannelInboundHandlerAdapter {
        private static final ByteBuf HEARTBEAT_SEQUENCE = Unpooled.unreleasableBuffer(
                Unpooled.copiedBuffer("HEARTBEAT", CharsetUtil.ISO_8859_1));  

        @Override
        public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
            if (evt instanceof IdleStateEvent) {
                 ctx.writeAndFlush(HEARTBEAT_SEQUENCE.duplicate())
                         .addListener(ChannelFutureListener.CLOSE_ON_FAILURE);  
            } else {
                super.userEventTriggered(ctx, evt);  
            }
        }
    }
}

4.5 编写大型数据

因为写操作是非阻塞的,在进行写操作的时候,会产生的大量的数据,在这种情况下我们要准备好处理因为连接远端缓慢而导致的延迟释放内存的问题。在前面我们提到过NIO的零拷贝功能,消除移动一个文件的内容从文件系统到网络堆栈的复制步骤。netty中可以使用FileRegion来实现零拷贝进行文件传输。

FileInputStream in = new FileInputStream(file); 
FileRegion region = new DefaultFileRegion(in.getChannel(), 0, file.length()); 

channel.writeAndFlush(region).addListener(new ChannelFutureListener() { 
    @Override
    public void operationComplete(ChannelFuture future) throws Exception {
        if (!future.isSuccess()) {
            Throwable cause = future.cause(); 
            // Do something
        }
    }
});

这种只适用直接传输一个文件的内容,没有数据处理的应用程序。否则需要将数据从文件系统复制到用户内存,这时候可以使用 ChunkedWriteHandler。这个类提供了支持异步写大数据流不引起高内存消耗。
这里还需依赖另外一个接口ChunkedInput,重要的4个实现。

  • ChunkedFile:从文件中一块一块的获取数据
  • ChunkedNioFile:与 ChunkedFile类似,处理使用了NIOFileChannel
  • ChunkedStream:从InputStream中一块一块的转移内容
  • ChunkedNioStream:从ReadableByteChannel中一块一块的转移内容
public class ChunkedWriteHandlerInitializer extends ChannelInitializer<Channel> {
    private final File file;
    private final SslContext sslCtx;

    public ChunkedWriteHandlerInitializer(File file, SslContext sslCtx) {
        this.file = file;
        this.sslCtx = sslCtx;
    }

    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast(new SslHandler(sslCtx.createEngine());
        pipeline.addLast(new ChunkedWriteHandler());
        pipeline.addLast(new WriteStreamHandler());
    }

    public final class WriteStreamHandler extends ChannelInboundHandlerAdapter {  

        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            super.channelActive(ctx);
            ctx.writeAndFlush(new ChunkedStream(new FileInputStream(file)));
        }
    }
}

4.6 序列化

Java序列化不需要添加额外的类库,只需实现java.io.Serializable并生成序列ID即可。但是Java序列化无法跨语言、序列化后的码流太大、并且序列化性能太低。所以在netty中会集成第三方序列化包。
JBoss Marshalling
Netty支持JBoss Marshalling的编解码器。CompatibleMarshallingDecoder和CompatibleMarshallingEncoder。
ProtoBuf
Netty支持ProtoBuf的编解码器。ProtobufDecoder和ProtobufEncoder。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值