Read book Netty in action(Chapter XI)--Encoder and decoder

序言

就像很多标准的架构模式都被各种专用框架所支持一样,常见的数据处理模式往往也是目标实现的很好的候选对象,它可以节省开发人员大量的时间和精力。当然这也适应于本章的主题:编码和解码,或者数据从一种特定协议的格式到另一种格式的转换。这些任务将由通常称为编解码器的组件来处理。Netty 提供了多种组件,简化了为了支持广泛的协议而创建自定义的编解码器的过程。例如,如果你正在构建一个基于Netty 的邮件服务器,那么你将会发现Netty 对于编解码器的支持对于实现POP3、IMAP 和SMTP 协议来说是多么的宝贵。

什么是编解码器

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

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

Tips:还记得入站和出站的方向吗?

解码器

现在,我们将学习Netty 所提供的解码器类,并提供关于何时以及如何使用它们的具体示例。这些类覆盖了两个不同的用例:
将字节解码为消息——ByteToMessageDecoder 和ReplayingDecoder
将一种消息类型解码为另一种——MessageToMessageDecoder。因为解码器是负责将入站数据从一种格式转换到另一种格式的,所以知道Netty 的解码器实现了ChannelInboundHandler 也不会让你感到意外。什么时候会用到解码器呢?很简单:每当需要为ChannelPipeline 中的下一个ChannelInboundHandler 转换入站数据时会用到。此外,得益于ChannelPipeline 的设计,可以将多个解码器链接在一起,以实现任意复杂的转换逻辑,这也是Netty 是如何支持代码的模块化以及复用的一个很好的例子。

抽象类ByteToMessageDecoder

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

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

decodeLast(ChannelHandlerContext ctx,ByteBuf in,List out)
Netty提供的这个默认实现只是简单地调用了decode()方法。当Channel的状态变为非活动时,这个方法将会被调用一次。可以重写该方法以提供特殊的处理

下面举一个如何使用这个类的示例,假设你接收了一个包含简单int 的字节流,每个int都需要被单独处理。在这种情况下,你需要从入站ByteBuf 中读取每个int,并将它传递给ChannelPipeline 中的下一个ChannelInboundHandler。为了解码这个字节流,你要扩展ByteToMessageDecoder 类。(需要注意的是,原子类型的int 在被添加到List 中时,会被自动装箱为Integer。)

每次从入站ByteBuf 中读取4 字节,将其解码为一个int,然后将它添加到一个List 中。当没有更多的元素可以被添加到该List 中时,它的内容将会被发送给下一个ChannelInboundHandler。虽然ByteToMessageDecoder 使得可以很简单地实现这种模式,但是你可能会发现,在调用readInt()方法前不得不验证所输入的ByteBuf 是否具有足够的数据有点繁琐。后面,我们将学习ReplayingDecoder,它是一个特殊的解码器,以少量的开销消除了这个步骤。

public class ToIntegerDecoder extends ByteToMessageDecoder {

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

对于编码器和解码器来说,其过程也是相当的简单:一旦消息被编码或者解码,它就会被ReferenceCountUtil.release(message)调用自动释放。如果你需要保留引用以便稍后使用,那么你可以调用ReferenceCountUtil.retain(message)方法。这将会增加该引用计数,从而防止该消息被释放。

抽象类ReplayingDecoder

ReplayingDecoder扩展了ByteToMessageDecoder类(如代码清单10-1 所示),使得我们不必调用readableBytes()方法。它通过使用一个自定义的ByteBuf实现,ReplayingDecoderByteBuf,包装传入的ByteBuf实现了这一点,其将在内部执行该调用。

public abstract class ReplayingDecoder<S> extends ByteToMessageDecoder

我们尝试用ReplayingDecoder来实现ToIntegerDecoder。

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

是不是简单了很多?
并不是所有的ByteBuf 操作都被支持,如果调用了一个不被支持的方法,将会抛出一个UnsupportedOperationException;
ReplayingDecoder 稍慢于ByteToMessageDecoder。
ReplayingDecoder 和 ByteToMessageDecoder,你会发现后者明显更简单。示例本身是很基本的,所以请记住,在真实的、更加复杂的情况下,使用一种或者另一种作为基类所带来的差异可能是很显著的。这里有一个简单的准则:如果使用ByteToMessageDecoder 不会引入太多的复杂性,那么请使用它;否则,请使用ReplayingDecoder。

编码器

编码器实现了ChannelOutboundHandler,并将出站数据从一种格式转换为另一种格式,和解码器的功能正好相反。
Netty中解码器的功能:
将消息编码为字节;
将消息编码为消息

抽象类MessageToByteEncoder

encode(ChannelHandlerContext ctx,I msg,ByteBuf out)
encode()方法是你需要实现的唯一抽象方法。它被调用时将会传入要被该类编码为ByteBuf 的(类型为I 的)出站消息。该ByteBuf 随后将会被转发给ChannelPipeline中的下一个ChannelOutboundHandler。
你可能已经注意到了,这个类只有一个方法,而解码器有两个。原因是解码器通常需要在Channel 关闭之后产生最后一个消息(因此也就有了decodeLast()方法)。这显然不适用于编码器的场景——在连接被关闭之后仍然产生一个消息是毫无意义的。

ShortToByteEncoder

public class ShortToByteEncoder extends MessageToByteEncoder<Short> {
    @Override
    protected void encode(ChannelHandlerContext ctx, Short msg, ByteBuf out) throws Exception {
        out.writeShort(msg);
        // do others
    }
}

其接受一个Short 类型的实例作为消息,将它编码为Short 的原子类型值,并将它写入ByteBuf 中,其将随后被转发给ChannelPipeline 中的下一个ChannelOutboundHandler。每个传出的Short 值都将会占用ByteBuf 中的2 字节。

抽象类MessageToMessageEncoder

你已经看到了如何将入站数据从一种消息格式解码为另一种。为了完善这幅图,我们将展示对于出站数据将如何从一种消息编码为另一种。MessageToMessageEncoder 类的encode()方法提供了这种能力

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

IntegerToStringEncoder

public class IntegerToStringEncoder extends MessageToMessageEncoder<Integer> {
    @Override
    protected void encode(ChannelHandlerContext ctx, Integer msg, List<Object> out) throws Exception {
        out.add(String.valueOf(msg));
    }
}

抽象的编解码器类

虽然我们一直将解码器和编码器作为单独的实体讨论,但是你有时将会发现在同一个类中管理入站和出站数据和消息的转换是很有用的。Netty 的抽象编解码器类正好用于这个目的,因为它们每个都将捆绑一个解码器/编码器对,以处理我们一直在学习的这两种类型的操作。正如同你可能已经猜想到的,这些类同时实现ChannelInboundHandler 和ChannelOutboundHandler 接口。为什么我们并没有一直优先于单独的解码器和编码器使用这些复合类呢?因为通过尽可能地将这两种功能分开,最大化了代码的可重用性和可扩展性,这是Netty 设计的一个基本原则。在我们查看这些抽象的编解码器类时,我们将会把它们与相应的单独的解码器和编码器进行比较和参照。

抽象类ByteToMessageCodec

让我们来研究这样的一个场景:我们需要将字节解码为某种形式的消息,可能是POJO,随后再次对它进行编码。ByteToMessageCodec 将为我们处理好这一切,因为它结合了ByteToMessageDecoder 以及它的逆向—MessageToByteEncoder。
任何的请求/响应协议都可以作为使用ByteToMessageCodec的理想选择。例如,在某个SMTP的实现中,编解码器将读取传入字节,并将它们解码为一个自定义的消息类型,如SmtpRequest。而在接收端,当一个响应被创建时,将会产生一个SmtpResponse,其将被编码回字节以便进行传输。

decode(ChannelHandlerContext ctx,ByteBuf in,List)
只要有字节可以被消费,这个方法就将会被调用。它将入站ByteBuf 转换为指定的消息格式, 并将其转发给ChannelPipeline 中的下一个ChannelInboundHandler

decodeLast(ChannelHandlerContext ctx,ByteBuf in,List out)
这个方法的默认实现委托给了decode()方法。它只会在Channel 的状态变为非活动时被调用一次。它可以被重写以实现特殊的处理。

encode(ChannelHandlerContext ctx,I msg,ByteBuf out)
对于每个将被编码并写入出站ByteBuf 的(类型为I 的)消息来说,这个方法都将会被调用

抽象类MessageToMessageCodec

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 类型的消息,然后被转发给ChannelPipeline 中的下一个ChannelOutboundHandler

decode() 方法是将INBOUND_IN 类型的消息转换为OUTBOUND_IN 类型的消息, 而encode()方法则进行它的逆向操作。将INBOUND_IN类型的消息看作是通过网络发送的类型,而将OUTBOUND_IN类型的消息看作是应用程序所处理的类型,

package com.anxin.netty.chapter10;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageCodec;
import io.netty.handler.codec.http.websocketx.*;

import java.util.List;

public class WebSocketConvertHandler extends MessageToMessageCodec<WebSocketFrame,
        WebSocketConvertHandler.MyWebSocketFrame> {

    @Override
    protected void encode(ChannelHandlerContext ctx, 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));
                break;
            case CLOSE:
                out.add(new CloseWebSocketFrame(true, 0, payload));
                break;
            case CONTINUATION:
                out.add(new ContinuationWebSocketFrame(payload));
                break;
            case PING:
                out.add(new PingWebSocketFrame(payload));
                break;
            case PONG:
                out.add(new PongWebSocketFrame(payload));
                break;
            default:
                throw new IllegalStateException(
                        "Unsupported websocket msg " + msg);
        }
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, WebSocketFrame msg, List<Object> out) throws Exception {
        ByteBuf payload = msg.content().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);
        }
    }

    public static final class MyWebSocketFrame {
        public enum FrameType {
            BINARY,
            CLOSE,
            PING,
            PONG,
            TEXT,
            CONTINUATION
        }
        private final FrameType type;
        private final ByteBuf data;
        public MyWebSocketFrame(FrameType type, ByteBuf data) {
            this.type = type;
            this.data = data;
        }
        public FrameType getType() {
            return type;
        }
        public ByteBuf getData() {
            return data;
        }
    }

}



CombinedChannelDuplexHandler

正如我们前面所提到的,结合一个解码器和编码器可能会对可重用性造成影响。但是,有一种方法既能够避免这种惩罚,又不会牺牲将一个解码器和一个编码器作为一个单独的单元部署所带来的便利性。CombinedChannelDuplexHandler 提供了这个解决方案,其声明为:

public class CombinedChannelDuplexHandler<I extends ChannelInboundHandler,O e xtends ChannelOutboundHandler>

这个类充当了ChannelInboundHandler 和ChannelOutboundHandler(该类的类型
参数I 和O)的容器。通过提供分别继承了解码器类和编码器类的类型,我们可以实现一个编解码器,而又不必直接扩展抽象的编解码器类。

注意看,这是一个解码器,它的作用是一次将从ByteBuf 中提取2 字节,并将它们作为char 写入到List中,其将会被自动装箱为Character 对象。

public class ByteToCharDecoder extends ByteToMessageDecoder {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        if (in.readableBytes() >= 2){
            out.add(in.readChar());
        }
    }
}

这是一个编码器,它能将Character 转换回字节。这个类扩展了MessageToByteEncoder,因为它需要将char 消息编码到ByteBuf 中。这是通过直接写入ByteBuf 做到的。


public class CharToByteEncoder extends MessageToByteEncoder<Character> {
    @Override
    protected void encode(ChannelHandlerContext ctx, Character msg, ByteBuf out) throws Exception {
        out.writeChar(msg);
    }
}

CombinedChannelDuplexHandler<I,O>的做法

package com.anxin.netty.chapter10;

import io.netty.channel.CombinedChannelDuplexHandler;

public class CombinedByteCharCodec extends CombinedChannelDuplexHandler<ByteToCharDecoder, CharToByteEncoder> {
    public CombinedByteCharCodec() {
        super(new ByteToCharDecoder(), new CharToByteEncoder());
    }
}



正如你所能看到的,在某些情况下,通过这种方式结合实现相对于使用编解码器类的方式来说可能更加的简单也更加的灵活。当然,这可能也归结于个人的偏好问题。

结束语

你看到了抽象的编解码器类是如何为在一个实现中处理解码和编码提供支持的。如果你需要更大的灵活性,或者希望重用现有的实现,那么你还可以选择结合他们,而无需扩展任何抽象的编解码器类。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值