Netty4实战第七章:编解码器

来源:互联网 发布:907工程和t22数据对比 编辑:程序博客网 时间:2024/06/05 19:53

本章主要内容

  • 编解码
  • 解码器
  • 编码器
  后面的章节,我们会学习各种各样的方式处理逻辑链,学习怎么拦截事件或数据。虽然ChannelHandler可以实现这些功能,但它还是有一些地方需要改进。
  因此,Netty提供了编解码器框架,帮助开发者开发自定义协议的编解码器,从而开发者可以编写出很容易封装和重用的代码。
  本章主要讨论不同部分的编解码器框架以及如何使用它们。

一、编解码器

  编写网络应用的时候,一般都是需要实现自己的编解码器的。编解码器主要作用就是将原始字节数据经过某些逻辑转换成自定义的消息,也可以将自定义的消息类型转换成原始字节数据。因为实际网络中传输的数据都是原始字节数据。

  编解码器主要有两部分组成:

  • 解码器
  • 编码器
  解码器主要是将原始字节转成自定义消息或其他字节序列,而编码刚好相反,它是把自定义消息转成字节数据或将字节数据转成其他类型的字节序列。
  很显然解码器主要用于收到数据事件,而编码器主要用于发送数据事件。
  第五章和第六章都提到过,对于收到或发送的数据有时候要注意释放资源。而编解码器处理这个情况就很简单了。一旦消息被解码后就会通过ReferenceCountUtil.release()方法自动释放资源。如果你不想释放资源,例如存储消息引用后面使用,那么就可以调用ReferenceCountUtil.retain()方法。这个方法会增加消息的引用计数数量,所以不会释放资源。
  现在我们来看看Netty提供的编码器和解码器的抽象基类。

二、解码器

  Netty提供了很多抽象基类,所以我们很容易开发出自己的解码器。Netty提供的主要有下面三种类型:

  • 字节转消息
  • 消息转消息
  • 消息转字节
  这一小节会介绍这些类型的抽象基类,你可以使用它们实现自己的解码器,并了解各个解码器的优点。
  学习Netty提供的抽象基类之前,我们先来定义一下到底什么是解码器。解码器主要负责将收到的数据从一种格式转换成另一种格式。因为解码器主要是解码收到的数据,所以它实现了ChannelInboundHandler接口。
  可能大家有疑问,解码器在什么时候使用。这个很简单:当我们需要将收到的数据转换然后传给下一个ChannelInboundHandler的时候使用。
  解码器的使用也是很灵活的,你需要使用多少解码器都可以添加到ChannelPipeline中,当然也要感谢ChannelPipeline的良好的设计,可以任意组装自己可复用的组件。

2.1、字节转消息解码器

  实际开发中我们经常需要将字节转为消息或转为其他字节序列。Netty提供了一个抽象基类帮助我们完成这个常见的任务。Netty提供了ByteToMessageDecoder,它可以帮助开发者很容易实现将字节转成对象(POJO)的功能。

  下表展示了ByteToMessageDecoder常用的方法以及它们的简介。

名称

描述

decode()

这个是我们唯一需要实现的方法。当一个ByteBuf收到了数据并需要解码时调用,
解码需要多久这个方法就会耗时多久。

decodeLast()

Channel断开连接时调用此方法,而且只会执行一次,有特殊需求可以重写它。

  我们假设一个应用场景,对端发送给我们一串原始字节流,内容就是很多简单的整数。我们需要在ChannelPipeline将整数都分开,所以我们需要从收到的ByteBuf读取整数,然后将整数按列表传给下一个ChannelInboundHandler。


  从上图可以看出,解码器从输入的ByteBuf读取数据,解码为整数,然后传递给下一个ChannelInboundHandler。图中ToIntegerDecoder解码器的主要逻辑如下。

public class ToIntegerDecoder extends ByteToMessageDecoder {    @Override    public void decode(ChannelHandlerContext ctx, ByteBuf in,                       List<Object> out) throws Exception {        //达到4个字节才解码        if (in.readableBytes() >= 4) {            //读取整数,加到列表中            out.add(in.readInt());        }    }}

  使用ByteToMessageDecoder类可以很方便实现自己的解码器。不过上面的代码也有一个不让人满意的地方,就是要检查ByteBuf是否还够4个字节。读取整数之前要先判断是否够读。

  有没有更好的办法解决这个问题?是有的,Netty有一些特殊解码器可以做到,下一章会详细介绍这部分知识。

  想要查看更复杂的例子,可以参考LineBasedFrameDecoder。它是Netty自带的解码器,在包io.netty.handler.codec中。除了这个,还有其他很多常用的解码器实现。

2.2、ReplayingDecoder

  ReplayingDecoder是一个特殊的字节转消息的解码器的抽象基类,它帮我们实现了每次操作ByteBuf前检查数据是否足够的逻辑,这个是比较难实现的。它通过包装输入ByteBuf来检查缓冲区是否有足够数据,如果数据不足,它会抛出一个特殊信号,然后内部获取这个信号并进行相关处理。一旦这个特殊信号抛出,解码循环逻辑就会终止。

  因为这层封装,所以ReplayingDecoder有几个限制:

  • 它并不支持所有的ByteBuf操作,如果你使用了不支持的操作,就会抛出UnreplayableOperationException
  • 大部分情况ByteBuf.readableBytes()不会返回正确的结果
  如果上面这些限制不会影响你的代码, 那么ReplayingDecoder就更好一些。一般来说,比较简单的逻辑适合使用ByteToMessageDecoder,如果比较复杂,建议使用ReplayingDecoder。

  前面说过,ReplayingDecoder继承自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());    }}

  当从ByteBuf读取整数时,如果缓冲区字节数不够,将会抛出一个信号并缓存起来,并将ByteBuf位置调整本次读位置,再收到数据时,会再次调用decode(…)。如果有足够数据,就会将整数加到列表中。

  实际工作中的实现可能不是这么简单,但是选择使用ReplayingDecoder还是ByteToMessageDecoder也是一件麻烦的事情。不过最重要的是我们要了解Netty提供了类似ReplayingDecoder这种功能强大的基类实现。究竟使用哪个,还得看你实际遇到的情况,也就是要不要造轮子的问题。

  可以看到使用ReplayingDecoder实现代码更加简洁。

  不过,当我们想将消息解码成另一个消息时,例如一个Java对象转另一个Java对象,上面的字节解码器就不太好用了。下一节我们介绍的MessageToMessageDecoder解码器可以做到这点。

  如果想了解更复杂的ByteToMessageDecoder例子,可以参考WebSocket08FrameDecoder或者io.netty.handler.codec.http.websocketx包中其他的解码器。

2.3、MessageToMessageDecoder - 实时解码Java对象

  如果需要将一个消息类型转成另一个消息类型时使用MessageToMessageDecoder就可以很容易做到。它提供的方法和前面说的其他解码器很类似。下表列出了这些方法。

方法名

描述

decode()

decode()方法是唯一必须实现的方法。每次收到消息时会被调用,

可以将消息转成另一种格式。解码后的消息会被传入到

下一个ChannelInboundHandler。

decodeLast()

默认实现代表decode()。这个方法只会在Channel进入终结状态时调用一次。

所以如果想在这个时候做什么就可以重写decodeLast()方法。

  下面通过例子来说明它的用法。比如现在有个需求,需要将整数转成字符串。这个转换需要在ChannelPipeline中完成,所以我们应该使用解码器,为了以后的扩展和可重用性。下图展示了这个需求的主要逻辑

  这个例子操作的是非字节消息,不过,也可以操作字节消息。收到的消息直接传给decode(...)方法,然后将解码后的消息放到列表中。

  所以解码器就是干这个的,收到消息,转换解码,然后将解码后的消息存到列表中。解码完成后,就会将列表传到ChannelPipeline中的下一个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));             }        }

  主要实现很简单,就是将整数转字符串。如果想参考更复杂的例子的,可以看看包io.netty.handler.codec.http中的HttpObjectAggregator。

2.4、解码器总结

  上面的介绍已经帮助我们了解了Netty提供的抽象解码器基类以及解码器的用处。解码器其实也只完成了一部分功能,因为很多时候我们也要转换发送出去的数据。这个任务就需要编码器来完成了,也就是本章编解码器的一部分。

  下一小节我们将要学习如何编写自己的编码器。

三、编码器

  就像Netty提供的解码器基类,Netty也提供了编码器基类来帮助开发者更容易开发编码器。编码器也主要分为下面两类。

  • 一种消息编码成另一种消息
  • 消息编码成字节
  你可以注意到了,编码器的类型比解码器少一种。这是因为我们在发送数据的时候,并不需要将字节转成其他消息类型,因为我们写到Channel中的时ByteBuf,并不是字节数据流。
  深入学习编码器之前,我们先定义一下编码器的职责。编码器的主要职责就是将发送的消息从一种格式转成另一种格式。因为编码器是用来处理发送的数据,所以它也实现了ChanneOutboundHandler。
  现在我们来看看Netty提供的编码器的抽象基类。

3.1、MessageToByteEncoder

  前面我们学习了ByteToMessageDecoder解码器,将字节解码成消息。但是反过来怎么办呢?Netty提供的MessageToByteEncoder编码器就是将字节转成消息。下边展示了编码器提供的方法。

方法名

描述

encode()

这个是唯一需要实现的方法。发送数据是编码器收到的消息会转成字节并放到

ByteBuf中,然后将ByteBuf传给下一个ChannelOutboundHandler。

  举个例子,当我们需要发送一个short类型数据时,需要将它转成字节并存到ByteBuf中,然后发送出去。根据这个例子我们可以实现一个IntegerToByteEncoder编码器,主要逻辑如下图


  上图中编码器收到short类型数据,然后编码并写入到ByteBuf。然后这个ByteBuf会传入到下一个ChannelOutboundHandler中。下面是代码实现。

        public class IntegerToByteEncoder extends MessageToByteEncoder<Short> {            @Override            public void encode(ChannelHandlerContext ctx, Short msg, ByteBuf out)                    throws Exception {                out.writeShort(msg);            }        }
  Netty也提供了几个MessageToByteEncoder的实现,我们可以参考这些实现编写自己的编码器。想要了解更多,可以参考io.netty.handler.codec.http.websocketx包中的WebSocket08FrameEncoder编码器。

3.2、MessageToMessageEncoder

  还有一种编码器,将一种消息转成另一种消息,这个就是MessageToMessageEncoder编码器,它很类似收到数据时候的MessageToMessageDecoder解码器。

  还是举个例子,我们需要将整数转成字符串,使用MessageToMessageEncoder就很容易实现。主要逻辑如下图。


  同样的,编码器将收到的整数转成字符串并存入到结果列表中,然后传给下一个ChannelOutboundHandler。

        public class IntegerToStringEncoder extends MessageToMessageEncoder<Integer> {            @Override            public void encode(ChannelHandlerContext ctx, Integer msg, List<Object> out) throws Exception {                out.add(String.valueOf(msg));            }        }
  Netty也提供了其他MessageToMessageEncoder的实现,可以参考io.netty.handler.codec.protobuf包中的ProtobufEncoder。

四、编解码器

  编码器和解码器其实很类似,重要的区别就是解码器处理收到的数据,而编码器处理发出的数据,那可不可以只写一个类既能当编码器用也能当解码器用呢?当然可以,Netty提供了Codec就是做这个的,下面我们就来学习这方面知识。
  编码器和解码器都分别有自己的类型,Codec也是有的。

  • 字节转消息编解码
  • 消息转消息编解码
  当你的应用既需要编码器也需要解码器时,只有一个不能正常工作,那么这个时候可以考虑使用Codec。
  使用Codec的话,你的ChannelPipeline中不可能只有编码器或只有解码器,也就是说,这个时候要么ChannelPipeline中两个都有,要么两个都没有。
  下面我们来介绍一下Netty提供的Codec的抽象基类。

4.1、ByteToByteCodec-编解码字节

  如果需要编解码字节,那么使用ByteToByteCodec是比较正确的选择。

  其实ByteToByteCodec就类似ByteToByteDecoder和ByteToByteEncoder的组合。但是呢,Java是不支持多继承的,所以只能搞出来一个Codec基类。

  既然是ByteToByteDecoder和ByteToByteEncoder的组合,那么暴露出来的方法也就是它们暴露出来的方法,如下表。

方法名

描述

decode()

和解码器的方法一样,处理收到的数据

decodeLast()

和解码器的一样

encode()

和编码器的方法一样,处理发送的数据

  上面这些方法在学习解码器和编码器的时候都介绍过,完成的功能都是一样的。

  那么什么场景适合使用ByteToByteCodec呢?

  典型的场景就是压缩数据。解码时解压缩,编码时压缩。因为压缩和解压缩算法是成对出现的,所以很适合使用Codec。

4.2、ByteToMessageCodec-编解码消息和字节

  很多时候我们需要将收到的字节解码成消息,例如解码成Java对象;然后发送的时候再将这个Java对象编码成字节发送。这个时候就应该使用ByteToMessageCodec。

  其实呢,它也就是ByteToMessageDecoder和MessageToByteEncoder的组合。

  很多服务端-客户端类型的应用,请求和响应就很适合这种编解码器,收到字节消息解码成Java对象,然后将Java对象编码成字节发送。

4.3、MessageToMessageCodec-编解码消息

  另一个场景就是我们需要将消息编解码,这个时候用MessageToMessageCodec比较合适。

  这种编解码器一般使用在什么场景?例如应用中使用不同的API对数据进行处理,但一个API的参数是这个类型对象,那个API是另一种类型对象,这就需要对象转对象了,所以MessageToMessageCodec比较合适。下面的代码例子展示了需要在不同的WebsocketFrame之间转换的实现。

        @ChannelHandler.Sharable        public class WebSocketConvertHandler extends MessageToMessageCodec<WebSocketFrame, WebSocketConvertHandler.WebSocketFrame> {            public static final WebSocketConvertHandler INSTANCE = new WebSocketConvertHandler();            @Override            protected void encode(ChannelHandlerContext ctx, WebSocketFrame msg, List<Object> out) throws Exception {                switch (msg.getType()) {                    case BINARY:                        out.add(new BinaryWebSocketFrame(msg.getData()));                        return;                    case TEXT:                        out.add(new TextWebSocketFrame(msg.getData()));                        return;                    case CLOSE:                        out.add(new CloseWebSocketFrame(true, 0, msg.getData()));                        return;                    case CONTINUATION:                        out.add(new ContinuationWebSocketFrame(msg.getData()));                        return;                    case PONG:                        out.add(new PongWebSocketFrame(msg.getData()));                        return;                    case PING:                        out.add(new PingWebSocketFrame(msg.getData()));                        return;                    default:                        throw new IllegalStateException(                                "Unsupported websocket msg " + msg);                }            }            @Override            protected void decode(ChannelHandlerContext ctx, io.netty.handler.codec.http.websocketx.WebSocketFrame msg,                                  List<Object> out) throws Exception {                if (msg instanceof BinaryWebSocketFrame) {                    out.add(new WebSocketFrame(                            WebSocketFrame.FrameType.BINARY, msg.data().copy()));                    return;                }                if (msg instanceof CloseWebSocketFrame) {                    out.add(new WebSocketFrame(                            WebSocketFrame.FrameType.CLOSE, msg.data().copy()));                    return;                }                if (msg instanceof PingWebSocketFrame) {                    out.add(new WebSocketFrame(                            WebSocketFrame.FrameType.PING, msg.data().copy()));                    return;                }                if (msg instanceof PongWebSocketFrame) {                    out.add(new WebSocketFrame(                            WebSocketFrame.FrameType.PONG, msg.data().copy()));                    return;                }                if (msg instanceof TextWebSocketFrame) {                    out.add(new WebSocketFrame(                            WebSocketFrame.FrameType.TEXT, msg.data().copy()));                    return;                }                if (msg instanceof ContinuationWebSocketFrame) {                    out.add(new WebSocketFrame(                            WebSocketFrame.FrameType.CONTINUATION,                            msg.data().copy()));                    return;                }                throw new IllegalStateException("Unsupported websocket msg " + msg);            }            public static final class WebSocketFrame {                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 ByteBuf getData() {                    return data;                }            }        }

五、其他组合方式

  使用codecs很像组合使用编码器和解码器,但这个时候也就失去了灵活性和可扩展性,因为必须强制编码器和解码器同时出现。如果能想办法解决这个问题就好了?

  还好,Netty提供了一个CombinedChannelDuplexHandler的东西,它不属于编解码器,它是用来构建编解码器的。

  为了说明如何使用CombinedChannelDuplexHandler组合编码器和解码器,我们使用一个简单的例子。我们先实现一个简单的解码器,将字节解码成字符。

        public class ByteToCharDecoder extends ByteToMessageDecoder {            @Override            public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {                while (in.readableBytes() >= 2) {                    out.add(Character.valueOf(in.readChar()));                }            }        }
  然后再实现一个编码器,将字符转成字节。

        public class CharToByteEncoder extends MessageToByteEncoder<Character> {            @Override            public void encode(ChannelHandlerContext ctx, Character msg, ByteBuf out) throws Exception {                out.writeChar(msg);            }        }
  现在看看,怎么组合上面编写的解码器和编码器。

        public class CharCodec extends CombinedChannelDuplexHandler<ByteToCharDecoder, CharToByteEncoder> {            public CharCodec() {                super(new ByteToCharDecoder(), new CharToByteEncoder());            }        }
  可以看到,编码器和解码器分别实现,没有丢失可扩展性和灵活性;使用的时候呢,又只需要继承CombinedChannelDuplexHandler,组合使用即可。

  不过,话说回来,其他使用哪种方式实现编解码功能,都差不多,取决于你喜欢什么风格的代码。

六、总结

  这一章我们主要学习了Netty提供的编解码API,以及为什么要将编解码器拿出来单独实现,而不是直接写在ChannelHandler中。
  另外也学习了Netty提供的编码器和解码器的抽象基类。也学习了为了少写点类,可以实现自己的codec。
  又因为灵活性和可扩展性的问题,我们学习了如何组合编码器和解码器
  下一章节,我们主要学习Netty提供的ChannelHandler和编解码器的实现,在我们的应用中,这些实现可以做到开箱即用,不需要我们再去重复造轮子了。