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 做数据对象的转换。
 
 

引用:

posted on 2021-05-22 18:47  曹伟雄  阅读(566)  评论(0编辑  收藏  举报

导航