zoukankan      html  css  js  c++  java
  • Netty——编解码

    Netty编解码

    Netty 涉及到编解码的组件有 Channel 、 ChannelHandler 、 ChannelPipe 等,我们先大概了解下这几个组件的作用。

    ChannelHandler

    ChannelHandler 充当来处理入站和出战数据的应用程序逻辑容器。

    例如,实现 ChannelInboundHandler 接口(或 ChannelInboundHandlerAdapter),你就可以接收入站事件和数据,这些数据随后会被你的应用程序的业务逻辑处理。
    当你要给连接的客户端发送响应时,也可以从 ChannelInboundHandler 刷数据。你的业务逻辑通常存在一个或者多个 ChannelInboundHandler 中。
     
    ChannelOutboundHandler 原理一样,只不过它是用来处理出站数据的。
     

    ChannelPipeline

    ChannelPipeline 提供了 ChannelHandler 链的容器。

    以客户端应用程序为例,如果有事件的运动方向是从客户端到服务端,那么我们称这些事件为出站的,即客户端发送给服务端的数据会通过 pipeline 中的一系列 ChannelOutboundHandler (

    ChannelOutboundHandler 调用是从 tail 到 head 方向逐个调用每个 handler 的逻辑),并被这些 Hadnler 处理。反之称为入站的,入站只调用 pipeline 里的 ChannelInboundHandler 逻辑(

    ChannelInboundHandler 调用是从 head tail 方向 逐个调用每个 handler 的逻辑。)

    编解码器

    当你通过Netty发送或者接受一个消息的时候,就将会发生一次数据转换。入站消息会被解码从字节转换为另一种格式(比如java对象);如果是出站消息,它会被编码成字节。

    Netty提供了一系列实用的编码解码器,他们都实现了ChannelInboundHadnler或者ChannelOutboundHandler接口。在这些类中, channelRead方法已经被重写了。

    以入站为例,对于每个从入站Channel读取的消息,这个方法会被调用。随后,它将调用由已知解码器所提供的decode()方法进行解码,并将已经解码的字节转发给ChannelPipeline中的下一个ChannelInboundHandler

    Netty提供了很多编解码器,比如编解码字符串的StringEncoderStringDecoder,编解码对象的ObjectEncoderObjectDecoder 等。

    当然也可以通过集成ByteToMessageDecoder自定义编解码器。
     

    Netty 中编解码器分类

    编码解码分类:

    分层解码分类:

    一次解码:一次解码用于解决 TCP 拆包/粘包问题,按协议解析得到的字节数据。常用一次编解码器:MessageToByteEncoder / ByteToMessageDecoder。
    二次解码:对一次解析后的字节数据做对象模型的转换,这时候需要二次解码器,同理编码器的过程是反过来的。常用二次编解码器:MessageToMessageEncoder / MessageToMessageDecoder。

     

    抽像编码类

    通过抽象编码类的继承图可以看出,编码类是 ChanneOutboundHandler 的抽象类实现,具体操作的是 Outbound 出站数据。

    MessageToByteEncoder

    MessageToByteEncoder 用于将对象编码成 字节流,只需要实现其 encode 方法即可完成自定义编码。

    MessageToByteEncoder 的核心源码片段,如下所示:

        @Override
        public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
            ByteBuf buf = null;
            try {
                if (acceptOutboundMessage(msg)) { // 1. 消息类型是否匹配
                    @SuppressWarnings("unchecked")
                    I cast = (I) msg;
        
                    buf = allocateBuffer(ctx, cast, preferDirect); // 2. 分配 ByteBuf 资源
        
                    try {
                        encode(ctx, cast, buf); // 3. 执行 encode 方法完成数据编码
                    } finally {
                        ReferenceCountUtil.release(cast);
                    }
        
                    if (buf.isReadable()) {
                        ctx.write(buf, promise); // 4. 向后传递写事件
                    } else {
                        buf.release();
                        ctx.write(Unpooled.EMPTY_BUFFER, promise);
                    }
                    buf = null;
                } else {
                    ctx.write(msg, promise);
                }
            } catch (EncoderException e) {
                throw e;
            } catch (Throwable e) {
                throw new EncoderException(e);
            } finally {
                if (buf != null) {
                    buf.release();
                }
            }
        }

    MessageToByteEncoder 重写了 ChanneOutboundHandlerwrite() 方法,其主要逻辑分为以下几个步骤:

    • acceptOutboundMessage 判断是否有匹配的消息类型,如果匹配需要执行编码流程,如果不匹配直接继续传递给下一个 ChannelOutboundHandler
    • 分配 ByteBuf 资源,默认使用堆外内存;
    • 调用子类实现的 encode 方法完成数据编码,一旦消息被成功编码,会通过调用 ReferenceCountUtil.release(cast) 自动释放;
    • 如果 ByteBuf 可读,说明已经成功编码得到数据,然后写入 ChannelHandlerContext 交到下一个节点;如果 ByteBuf 不可读,则释放 ByteBuf 资源,向下传递空的 ByteBuf 对象。

    编码器实现非常简单,不需要关注拆包/粘包问题。如下例子,展示了如何将字符串类型的数据写入到 ByteBuf 实例,ByteBuf 实例将传递给 ChannelPipeline 链表中的下一个 ChannelOutboundHandler

        public class StringToByteEncoder extends MessageToByteEncoder<String> {
                @Override
                protected void encode(ChannelHandlerContext channelHandlerContext, String data, ByteBuf byteBuf) throws Exception {
                    byteBuf.writeBytes(data.getBytes());
                }
        }

    MessageToMessageEncoder

    MessageToMessageEncoder 是将一种格式的消息转换为另一种格式的消息,它的子类同样只需要实现 encode 方法。

    MessageToMessageEncoder 常用的实现子类有 StringEncoder、LineEncoder、Base64Encoder 等。

    StringEncoder 可以直接实现 String 类型数据的编码。源码示例如下:

    @Override
        protected void encode(ChannelHandlerContext ctx, CharSequence msg, List<Object> out) throws Exception {
            if (msg.length() == 0) {
                return;
            }
            out.add(ByteBufUtil.encodeString(ctx.alloc(), CharBuffer.wrap(msg), charset));
        }

    抽像解码类

    解码类是 ChanneInboundHandler 的抽象类实现,操作的是 Inbound 入站数据。解码器的主要难度在于拆包和粘包问题

    由于接收方可能没有接受到完整的消息,所以编码框架还要对入站数据做缓冲处理,直到获取到完整的消息。

    ByteToMessageDecoder

    • 累加字节流
    • 调用子类的decode方法进行解析
    • 将解析到的ByteBuf向下传播

    ByteToMessageDecoder,所有的解码器包括Netty自定义的解码器都是这个类的子类。在Pipeline注册的解码器,最终就会执行到以下的这个方法。

    ByteToMessageDecoder相当于定义的解码的模板,具体的即系则由子类去实现。

    以下是ByteToMessageDecoder的相关源码:

    public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter {
     
    ByteBuf cumulation;//cumulation用户缓存解析后的二进制数据
    private boolean singleDecode;//在解析数据时,是否每次只回调一次decode方法,后面介绍
    private boolean decodeWasNull;
    private boolean first;//是否是第一次接受到输入的数据
    //当接受到数据时,channelRead方法会被回调
    //关于参数Object msg的说明:由于ByteToMessageDecoder只处理二进制数据 ,因此Object类型应该是ByteBuf。
    //如果不是,将会直接交给pipeline中下一个handler处理。
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            if (msg instanceof ByteBuf) {//如果msg类型是ByteBuf,进行解析
            //out用于存储l解析二进制流得到的结果,一个二进制流可能会解析出多个消息,所以out是一个list
                RecyclableArrayList out = RecyclableArrayList.newInstance();
                try {
                    ByteBuf data = (ByteBuf) msg;//将msg强转为ByteBuf类型
                    //判断cumulation == null;并将结果赋值给first。因此如果first为true,则表示第一次接受到数据                
                    first = cumulation == null;
                    if (first) {//如果是第一次接受到数据,直接将接受到的数据赋值给缓存对象cumulation
                        cumulation = data;
                    } else {//如果不是第一次接受到数据
                        if (cumulation.writerIndex() > cumulation.maxCapacity() - data.readableBytes()
                                || cumulation.refCnt() > 1) {
                                //如果cumulation中的剩余空间,不足以存储接收到的data
                                  expandCumulation(ctx, data.readableBytes());//将cumulation扩容
                                }
                              cumulation.writeBytes(data);//将data拷贝到cumulation中
                              data.release();
                        }
                       //调用callDecode,开始解析cumulation中的数据,解析结果放到out中,这是一个list
                       //因为我们可能根据cumulation中的数据,解析出多个有效数据
                    callDecode(ctx, cumulation, out);
                } catch (DecoderException e) {
                    throw e;
                } catch (Throwable t) {
                    throw new DecoderException(t);
                } finally {
                   //如果cumulation没有数据可读了,说明所有的二进制数据都被解析过了
                   //此时对cumulation进行释放,以节省内存空间。
                         //反之cumulation还有数据可读,那么if中的语句不会运行,因为不对cumulation进行释放
                         //因此也就缓存了用户尚未解析的二进制数据。
                    if (cumulation != null && !cumulation.isReadable()) {
                           cumulation.release();
                                  cumulation = null;
                    }
                    int size = out.size();//获得解析二进制流得到的消息的个数
                    decodeWasNull = size == 0;
                                    //迭代每一个解析出来的消息,调用下一个ChannelHandler进行处理
                    for (int i = 0; i < size; i ++) {
                        ctx.fireChannelRead(out.get(i));
                    }
                    out.recycle();
                }
            } else {//如果msg类型是不是ByteBuf,直接调用下一个handler进行处理
                ctx.fireChannelRead(msg);
               }
      }
     
         //callDecode方法主要用于解析cumulation 中的数据,并将解析的结果放入List<Object> out中。
         //由于cumulation中缓存的二进制数据,可能包含了出多条有效信息,因此在callDecode方法中,默认会调用多次decode方法
         //我们在覆写decode方法时,每次只解析一个消息,添加到out中,callDecode通过多次回调decode
         //每次传递进来都是相同的List<Object> out实例,因此每一次解析出来的消息,都存储在同一个out实例中。
         //当cumulation没有数据可以继续读,或者某次调用decode方法后,List<Object> out中元素个数没有变化,则停止回调decode方法。
       protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
            try {
              while (in.isReadable()) {//如果in,即cumulation中有数据可读的话,一直循环调用decode
                   int outSize = out.size();//获取上一次decode方法调用后,out中元素数量,如果是第一次调用,则为0。
                  int oldInputLength = in.readableBytes();//上次decode方法调用后,in的剩余可读字节数
                  //回调decode方法,由开发者覆写,用于解析in中包含的二进制数据,并将解析结果放到out中。
                  decode(ctx, in, out);
                    // See https://github.com/netty/netty/issues/1664
                        if (ctx.isRemoved()) {
                           break;
                       }
                    //outSize是上一次decode方法调用时out的大小,out.size()是当前out大小
                    //如果二者相等,则说明当前decode方法调用没有解析出有效信息。
                    if (outSize == out.size()) {
                    //此时,如果发现上次decode方法和本次decode方法调用候,in中的剩余可读字节数相同
                    //则说明本次decode方法没有读取任何数据解析
                    //(可能是遇到半包等问题,即剩余的二进制数据不足以构成一条消息),跳出while循环。
                     if (oldInputLength == in.readableBytes()) {
                       break;
                     } else {
                       continue;
                      }
                    }
                    //处理人为失误 。如果走到这段代码,则说明outSize != out.size()。
                    //也就是本次decode方法实际上是解析出来了有效信息放到out中。
                    //但是oldInputLength == in.readableBytes(),说明本次decode方法调用并没有读取任何数据
                    //但是out中元素却添加了。
                    //这可能是因为开发者错误的编写了代码,例如mock了一个消息放到List中。
                    if (oldInputLength == in.readableBytes()) {
                        throw new DecoderException(
                                StringUtil.simpleClassName(getClass()) +
                                ".decode() did not read anything but decoded a message.");
                    }
     
                    if (isSingleDecode()) {
                        break;
                    }
                }
            } catch (DecoderException e) {
                throw e;
            } catch (Throwable cause) {
                throw new DecoderException(cause);
            }
        }
     
        /**抽象方法,由子类覆盖,建议在decode方法中,一次只解析一条信息,不足以构成一条信息的数据,不要读取*/
        protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception;
        
    }

    MessageToMessageDecoder

    MessageToMessageDecoder 是将一种消息类型的编码成另外一种消息类型。MessageToMessageDecoder 不对数据报文继续缓存,其主要用作转换消息模型。

    • 使用 ByteToMessageDecoder 解析 TCP 协议,解决拆包/粘包问题。解析得到有效 ByteBuf 数据
    • 使用 MessageToMessageDecoder 做数据对象的转换。
     
     

    引用:
  • 相关阅读:
    A.3.1. 与MySQL客户端库的链接问题
    c++ mysqlclient library linkage problem Stack Overflow
    找房 爱合住, ihezhu.com
    21.4.5.1. MySQL Connector/C++ Connecting to MySQL
    如何对链接了mysqlclient的程序静态编译?
    分享:[组图] 科技圈最具权势 25 大女工程师
    linux 静态链接 mysql glibc 库的悲催过程 mango的日志 网易博客
    /usr/bin/ld: cannot find lgcc_s 问题解决小记
    « 静态编译的MySQL易挂起 »
    SQL C++代码自动生成器(sql2class)介绍 Newzai的专栏 博客频道 CSDN.NET
  • 原文地址:https://www.cnblogs.com/caoweixiong/p/14666076.html
Copyright © 2011-2022 走看看