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提供了很多编解码器,比如编解码字符串的StringEncoder和StringDecoder,编解码对象的ObjectEncoder和ObjectDecoder 等。
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 重写了 ChanneOutboundHandler 的 write() 方法,其主要逻辑分为以下几个步骤:
- 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 做数据对象的转换。
引用:
- https://juejin.cn/post/6883746965629173767
- https://ezlippi.com/blog/2019/05/netty-byte2message-decoder.html