Java并发编程学习-日记8、netty的编码和解码

Netty解码器

ByteToMessageDecoder

一个标准的解码器将输入类型为ByteBuf缓冲区的数据进行解码,输出一个一个的Java POJO对象。Netty内置了这个解码器,叫作ByteToMessageDecoder,位在Netty的io.netty.handler.codec包中。所有的Netty中的解码器,都是Inbound入站处理器类型,都直接或者间接地实现了ChannelInboundHandler接口。

ByteToMessageDecoder解码的流程,大致具体可以描述为:

  • 首先,它将上一站传过来的输入到Bytebuf中的数据进行解码,解码出一个List<Object>对象列表;
  • 然后,迭代List<Object>列表,逐个将Java POJO对象传入下一站Inbound入站处理器。

如果要实现一个自己的ByteBuf解码器,流程大致如下:

(1)首先继承ByteToMessageDecoder抽象类。

(2)然后实现其基类的decode抽象方法。将ByteBuf到POJO解码的逻辑写入此方法。将Bytebuf二进制数据,解码成一个一个的Java POJO对象。

(3)在子类的decode方法中,需要将解码后的Java POJO对象,放入decode的List<Object>实参中。这个实参是ByteTo-MessageDecoder父类传入的,也就是父类的结果收集列表。在流水线的过程中,ByteToMessageDecoder调用子类decode方法解码完成后,会将List<Object>中的结果,一个一个地分开传递到下一站的Inbound入站处理器。

自定义整数解码器:

public class Byte2IntegerDecoder extends ByteToMessageDecoder {

    @Override

    public void decode(ChannelHandlerContext ctx, ByteBuf in,List<Object> out) {

        while (in.readableBytes() >= 4) {

            int i = in.readInt();

            Logger.info("解码出一个整数: " + i);

            out.add(i);

        }

    }

}

ByteToMessageDecoder传递给下一站的是解码之后的Java POJO对象,不是ByteBuf缓冲区。

(1)ByteBuf缓冲区由谁负责进行引用计数和释放管理的呢?基类ByteToMessageDecoder负责解码器的ByteBuf缓冲区的释放工作,它会调用ReferenceCountUtil.release(in)方法,将之前的ByteBuf缓冲区的引用数减1。

(2)如果这个ByteBuf被释放了,在后面还需要用到,怎么办呢?可以在decode方法中调用一次ReferenceCountUtil .retain(in)来增加一次引用计数。

 ReplayingDecoder     

 ReplayingDecoder类是ByteToMessageDecoder的子类。其作用是:在读取ByteBuf缓冲区的数据之前,需要检查缓冲区是否有足够的字节。若ByteBuf中有足够的字节,则会正常读取;反之,如果没有足够的字节,则会停止解码。

public class Byte2IntegerDecoder extends ByteToMessageDecoder {

    @Override

     public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {

        int i = in.readInt();

        Logger.info("解码出一个整数: " + i);

        out.add(i);

    }}

 

8     ReplayingDecoder基类的关键技术就是偷梁换柱,在将外部传入的ByteBuf缓冲区传给子类之前,换成了自己装饰过的ReplayingDecoderBuffer缓冲区。

  • ReplayingDecoderBuffer类型的读取方法与ByteBuf类型的读取方法相比,做了什么样的功能增强呢?主要是进行二进制数据长度的判断,如果长度不足,则抛出异常。这个异常会反过来被ReplayingDecoder基类所捕获,将解码工作停止。
  • ReplayingDecoder的作用,远远不止于进行长度判断,它更重要的作用是用于分包传输的应用场景。
  • ReplayingDecoder类型和所有的子类都需要保存状态信息,都有状态,不适合在不同的通道之间共享。

一个案例解析两个整数,然后求和最为解码结果:

public class IntegerAddDecoder extends ReplayingDecoder<IntegerAddDecoder.Status> {

    enum Status {

        PARSE_1, PARSE_2

    }

    private int first;

    private int second;

    public IntegerAddDecoder() {

        //构造函数中,需要初始化父类的state 属性,表示当前阶段

        super(Status.PARSE_1);

    }

    @Override

    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {

        switch (state()) {

            case PARSE_1:

                //从装饰器ByteBuf 中读取数据

                first = in.readInt();

                //第一步解析成功,

                // 进入第二步,并且设置读指针断点为当前的读取位置

                checkpoint(Status.PARSE_2);

                break;

            case PARSE_2:

                second = in.readInt();

                Integer sum = first + second;

                out.add(sum);

                checkpoint(Status.PARSE_1);

                break;

            default:

                break;

        }

    }

}

checkpoint(Status)方法有两个作用:

(1)设置state属性的值,更新一下当前的状态。

(2)还有一个非常大的作用,就是设置“读断点指针”。“读断点指针”是ReplayingDecoder类的另一个重要的成员,它保存着装饰器内部ReplayingDecoderBuffer成员的起始读指针,有点儿类似于mark标记。当读数据时,一旦可读数据不够,ReplayingDecoderBuffer在抛出ReplayError异常之前,ReplayingDecoder会把读指针的值还原到之前的checkpoint(IntegerAddDecoder.Status)方法设置的“读断点指针”(checkpoint)。于是乎,在ReplayingDecoder下一次读取时,还会从之前设置的断点位置开始。

如何获取字符串的长度信息呢?

这个问题和程序所使用的具体传输协议是强相关的。一般来说,在Netty中进行字符串的传输,可以采用普通的Header-Content内容传输协议: (1)在协议的Head部分放置字符串的字节长度。Head部分可以用一个整型int来描述即可。(2)在协议的Content部分,放置的则是字符串的字节数组。

通过ReplayingDecoder解码器,可以正确地解码分包后的ByteBuf数据包。但是,在实际的开发中,不太建议继承这个类,原因是:

(1)不是所有的ByteBuf操作都被ReplayingDecoderBuffer装饰类所支持,可能有些ByteBuf操作在ReplayingDecoder子类的decode实现方法中被使用时就会抛出ReplayError异常。

(2)在数据解析逻辑复杂的应用场景,ReplayingDecoder在解析速度上相对较差。

MessageToMessageDecoder<I>

将POJO解解码为另一个POJO的解码器基类MessageToMessageDecoder<I>。

NETTY中开箱即用的Decoder:

(1)固定长度数据包解码器——FixedLengthFrameDecoder 适用场景:每个接收到的数据包的长度,都是固定的,例如100个字节。

(2)行分割数据包解码器——LineBasedFrameDecoder 适用场景:每个ByteBuf数据包,使用换行符(或者回车换行符)作为数据包的边界分割符。

(3)自定义分隔符数据包解码器——DelimiterBasedFrameDecoder。DelimiterBasedFrameDecoder是LineBasedFrameDecoder按照行分割的通用版本。

(4)自定义长度数据包解码器——LengthFieldBasedFrameDecoder 这是一种基于灵活长度的解码器。在ByteBuf数据包中,加了一个长度字段,保存了原始数据包的长度。

Netty编译器

首先,编码器是一个Outbound出站处理器,负责处理“出站”数据;其次,编码器将上一站Outbound出站处理器传过来的输入(Input)数据进行编码或者格式转换,然后传递到下一站ChannelOutboundHandler出站处理器。编码器是ChannelOutboundHandler出站处理器的实现类。

public class Integer2ByteEncoder extends MessageToByteEncoder<Integer> {

    @Override

    public void encode(ChannelHandlerContext ctx, Integer msg, ByteBuf out)

            throws Exception {

        out.writeInt(msg);

        Logger.info("encoder Integer = " + msg);

    }

}

具有相互配套逻辑的编码器和解码器能否放在同一个类中呢?

答案是肯定的:这就要用到Netty的新类型——Codec类型。ByteToMessageCodec同时包含了编码encode和解码decode两个抽象方法。都需要自己实现。编码器和解码器如果要结合起来,除了继承的方法之外,还可以通过组合的方式实现。与继承相比,组合会带来更大的灵活性:编码器和解码器可以捆绑使用,也可以单独使用。Netty提供了一个新的组合器——CombinedChannelDuplexHandler基类。

public class Byte2IntegerCodec extends ByteToMessageCodec<Integer> {

    @Override

    public void encode(ChannelHandlerContext ctx, Integer msg, ByteBuf out) throws Exception {

        out.writeInt(msg);

        System.out.println("write Integer = " + msg);

    }

    @Override

    public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {

        if (in.readableBytes() >= 4) {

            int i = in.readInt();

            System.out.println("Decoder i= " + i);

            out.add(i);

        }

    }

}

public class IntegerDuplexHandler extends CombinedChannelDuplexHandler<Byte2IntegerDecoder,

 Integer2ByteEncoder>{

    public IntegerDuplexHandler() {

        super(new Byte2IntegerDecoder(), new Integer2ByteEncoder());

    }

}

对于对性能要求不是太高的服务器程序,可以选择JSON系列的序列化框架;对于性能要求比较高的服务器程序,则应该选择传输效率更高的二进制序列化框架,目前的建议是Protobuf。Netty也提供了相应的编解码器,为Protobuf解决了有关Socket通信中“半包、粘包”等问题。

底层网络是以二进制字节报文的形式来传输数据的。读数据的过程大致为:当IO可读时,Netty会从底层网络将二进制数据读到ByteBuf缓冲区中,再交给Netty程序转成Java POJO对象。写数据的过程大致为:这中间编码器起作用,是将一个Java类型的数据转换成底层能够传输的二进制ByteBuf缓冲数据。解码器的作用与之相反,是将底层传递过来的二进制ByteBuf缓冲数据转换成Java能够处理的Java POJO对象。

在Netty中,分包的方法,主要有两种方法:

(1)可以自定义解码器分包器:基于ByteToMessageDecoder或者ReplayingDecoder,定义自己的进程缓冲区分包器。

(2)使用Netty内置的解码器。如使用Netty内置的LengthFieldBasedFrameDecoder自定义分隔符数据包解码器,对进程缓冲区ByteBuf进行正确的分包。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章