netty源码解解析(4.0)-18 ChannelHandler: codec--编解码框架
编解码框架和一些常用的实现位于io.netty.handler.codec包中。
编解码框架包含两部分:Byte流和特定类型数据之间的编解码,也叫序列化和反序列化。不类型数据之间的转换。
下图是编解码框架的类继承体系:
其中MessageToByteEncoder和ByteToMessageDecoder是实现了序列化和反序列化框架。 MessageToMessage是不同类型数据之间转换的框架。
序列化抽象实现: MessageToByteEncoder<I>
序列化是把 I 类型的数据转换成Byte流。这个抽象类通过实现ChannelOutboundHandler的write方法在写数据时把 I 类型的数据转换成Byte流,下面是write方法的实现:
- @Override
- public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
- ByteBuf buf = null;
- try {
- if (acceptOutboundMessage(msg)) {
- @SuppressWarnings("unchecked")
- I cast = (I) msg;
- buf = allocateBuffer(ctx, cast, preferDirect);
- try {
- encode(ctx, cast, buf);
- } finally {
- ReferenceCountUtil.release(cast);
- }
- if (buf.isReadable()) {
- ctx.write(buf, promise);
- } 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();
- }
- }
- }
5行, 检查msg的类型,如果是 I 类型返回true, 否则返回false。
7-10行, 分配一块buffer, 并调用encode方法把msg编码成Byte流放进这个buffer中。
15-19行,对含有Byte流程数据的buffer继续执行写操作。(不清楚写操作流程的可以参考<<netty源码解解析(4.0)-15 Channel NIO实现:写数据>>)
23行,如果msg不是 I 类型,跳过这个Handler, 继续执行写操作。
这里调用的encode方法是一个抽象方法,留给子类实现定制的序列化操作。
反序列化抽象实现: ByteToMessageDecoder
这个抽象类型解决的主要问题是从Byte流中提取数据包。数据包是指刚好可以反序列化成一个特定类型Message的Byte数组。但是在数据包长度不确定的情况下,没办法每次刚好从Byte流中刚好分离一个数据包。每次从Byte流中读取数据有多种可能:
- 刚好是一个或多个完整的数据包。
- 不足一个完整的数据包,或错误的数据。
- 包含一个或多个完整的数据包,但有多余的数据不足一个完整的数据包或错误的数据。
这个问题本质上和"TCP粘包"问题相同。解决这个问题有两个关键点:
- 能够确定数据包在Byte流中的开始位置和长度。
- 需要暂时缓存不完整的数据包,等待后续数据拼接完整。
关于第(1)点,在这个抽象类中没有处理,只是定义了一个抽象方法decode,留给子类处理。关于第(2)点,这个类定义了一个Cumulator(堆积器)来处理,把不完整的数据包暂时堆积到Cumulator中。Cumulator有两个实现: MERGE_CUMULATOR(合并堆积器),COMPOSITE_CUMULATOR(组合堆积器)。默认使用的是MERGE_CUMULATOR。下面详细分析一下这两种Cumulator的实现。
MERGE_CUMULATOR的实现
这是一个合并堆积器,使用ByteBuf作为堆积缓冲区,把通过把数据写到堆积缓冲实现新旧数据合并堆积。
- @Override
- public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
- final ByteBuf buffer;
- if (cumulation.writerIndex() > cumulation.maxCapacity() - in.readableBytes()
- || cumulation.refCnt() > 1 || cumulation instanceof ReadOnlyByteBuf) {
- // Expand cumulation (by replace it) when either there is not more room in the buffer
- // or if the refCnt is greater then 1 which may happen when the user use slice().retain() or
- // duplicate().retain() or if its read-only.
- //
- // See:
- // - https://github.com/netty/netty/issues/2327
- // - https://github.com/netty/netty/issues/1764
- buffer = expandCumulation(alloc, cumulation, in.readableBytes());
- } else {
- buffer = cumulation;
- }
- buffer.writeBytes(in);
- in.release();
- return buffer;
- }
4-13行,如果当前的堆积缓冲区不能用了,分配一块新的,把旧缓冲区中的数据转移到新缓冲区中,并用新的替换旧的。当前堆积缓冲区不能用的条件是:
cumulation.writerIndex() > cumulation.maxCapacity() - in.readableBytes(): 容量不够
或者 cumulation.refCnt() > 1 : 在其他地方本引用
或者 cumulation instanceof ReadOnlyByteBuf 是只读的
17行,把数据追加到堆积缓冲区中。
COMPOSITE_CUMULATOR的实现
这是一个合并堆积器,和MERGE_CUMULATOR不同的是他使用的是CompositeByteBuf作为堆积缓冲区。
- @Override
- public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
- ByteBuf buffer;
- if (cumulation.refCnt() > 1) {
- // Expand cumulation (by replace it) when the refCnt is greater then 1 which may happen when the user
- // use slice().retain() or duplicate().retain().
- //
- // See:
- // - https://github.com/netty/netty/issues/2327
- // - https://github.com/netty/netty/issues/1764
- buffer = expandCumulation(alloc, cumulation, in.readableBytes());
- buffer.writeBytes(in);
- in.release();
- } else {
- CompositeByteBuf composite;
- if (cumulation instanceof CompositeByteBuf) {
- composite = (CompositeByteBuf) cumulation;
- } else {
- composite = alloc.compositeBuffer(Integer.MAX_VALUE);
- composite.addComponent(true, cumulation);
- }
- composite.addComponent(true, in);
- buffer = composite;
- }
- return buffer;
- }
4-13行,和MERGE_CUMULATOR一样。
15-23行,如果当前的堆积缓冲区不是CompositeByteBuf类型,使用一个新的CompositeByteBuf类型的堆积缓冲区代替,并把数据转移的新缓冲区中。
分离数据包的主流程
ByteToMessageDecoder是ChannelInboundHandlerAdapter的派生类,它通过覆盖channelRead实现了反序列化的主流程。这个主流程主要是对堆积缓冲区cumulation的管理,主要步骤是:
- 把Byte流数据追加到cumulation中。
- 调用decode方法从cumulation中分离出完整的数据包,并把数据包反序列化成特定类型的数据,直到不能分离数据包为止。
- 检查cumulation,如果没有剩余数据,就销毁掉这个cumulation。否则,增加读计数。如果读计数超过丢弃阈值,丢掉部分数据,这一步是为了防止cumulation中堆积的数据过多。
- 把反序列化得到的Message List传递到pipeline中的下一个ChannelInboundHandler处理。
由于使用了cumulation,ByteToMessageDecoder就变成了一个有状态的ChannelHandler, 它必须是独占的,不能使用ChannelHandler.@Sharable注解。
在channelRead中,并没有直接调用decode方法,而是通过callDecode间接调用。而callDecdoe也不是直接调用,而是调用了decodeRemovalReentryProtection方法,这个方法只是对decode调用的简单封装。参数in是堆积缓冲区cumulation。 这个方法主要实现上面描述的第2个步骤。
- //在channelRead中调用方式:callDecode(ctx, cumulation, out);
- protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
- try {
- while (in.isReadable()) {
- int outSize = out.size();
- if (outSize > 0) {
- fireChannelRead(ctx, out, outSize);
- out.clear();
- // Check if this handler was removed before continuing with decoding.
- // If it was removed, it is not safe to continue to operate on the buffer.
- //
- // See:
- // - https://github.com/netty/netty/issues/4635
- if (ctx.isRemoved()) {
- break;
- }
- outSize = 0;
- }
- int oldInputLength = in.readableBytes();
- decodeRemovalReentryProtection(ctx, in, out);
- // Check if this handler was removed before continuing the loop.
- // If it was removed, it is not safe to continue to operate on the buffer.
- //
- // See https://github.com/netty/netty/issues/1664
- if (ctx.isRemoved()) {
- break;
- }
- if (outSize == out.size()) {
- if (oldInputLength == in.readableBytes()) {
- break;
- } else {
- continue;
- }
- }
- 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 (Exception cause) {
- throw new DecoderException(cause);
- }
- }
5-19行,如果已经成功分离出了至少一个数据包并成功反序列化,就调用fireChannelRead把得到的Message传递给pipeline中的下一个Handler处理。fireChannelRead会对out中的每一个Message调用一次ctx.fireChannelRead。
22,23行,先记下in中的数据长度,再执行反序列化操作。
33,39行,如果outSize == out.size()(没有反序列化到新的Message), 且oldInputLength == in.readableBytes()(in中的数据长度没有变化)表示in中的数据不足以完成一次反序列化操作,跳出循环。否则,继续。
41行,出现了异常,完成了一次反序列化操作,但in中的数据没变化,凭空多了(或少了)一些反序列化的后Message。
同时可以进行序列化和反序列化的抽象类: ByteToMessageCodec<I>
这个类是ChannelDuplexHandler的派生类,可以同时序列化和反序列化操作。和前面两个类相比,它没什么特别是实现,内部使用MessageToByteEncoder<I>
序列化,使用ByteToMessageDecoder反序列化。
类型转换编码的抽象实现: MessageToMessageEncoder<I>
这个类是ChannelOutboundHandlerAdapter的派生类,它在功能是在write过程中,把 I 类型的数据转换成另一种类型的数据。它定义了抽象方法encode,有子类负责实现具体的转换操作。
- @Override
- public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
- CodecOutputList out = null;
- try {
- if (acceptOutboundMessage(msg)) {
- out = CodecOutputList.newInstance();
- @SuppressWarnings("unchecked")
- I cast = (I) msg;
- try {
- encode(ctx, cast, out);
- } finally {
- ReferenceCountUtil.release(cast);
- }
- if (out.isEmpty()) {
- out.recycle();
- out = null;
- throw new EncoderException(
- StringUtil.simpleClassName(this) + " must produce at least one message.");
- }
- } else {
- ctx.write(msg, promise);
- }
- } catch (EncoderException e) {
- throw e;
- } catch (Throwable t) {
- throw new EncoderException(t);
- } finally {
- if (out != null) {
- final int sizeMinusOne = out.size() - 1;
- if (sizeMinusOne == 0) {
- ctx.write(out.get(0), promise);
- } else if (sizeMinusOne > 0) {
- // Check if we can use a voidPromise for our extra writes to reduce GC-Pressure
- // See https://github.com/netty/netty/issues/2525
- ChannelPromise voidPromise = ctx.voidPromise();
- boolean isVoidPromise = promise == voidPromise;
- for (int i = 0; i < sizeMinusOne; i ++) {
- ChannelPromise p;
- if (isVoidPromise) {
- p = voidPromise;
- } else {
- p = ctx.newPromise();
- }
- ctx.write(out.getUnsafe(i), p);
- }
- ctx.write(out.getUnsafe(sizeMinusOne), promise);
- }
- out.recycle();
- }
- }
- }
6-12行,如果msg是 I 类型的数据,调用encode把它转换成另一种类型。
16-20行,如果没有转换成功,抛出异常。
23行, 如果msg不是 I 类型,跳过当前的Handler。
31-50, 如果转换成功,把转换后的数据传到到下一个Handler处理。33行处理只有一个转换结果的情况。37-48行处理有多个转换结果的情况。
类型转换解码的抽象实现: MessageToMessageDecoder<I>
这个类是ChannelInboundHandlerAdapter的派生类,它的功能是在read的过程中,把 I 类型的数据转换成另一种类型的数据。它定义了抽象方法decode,有子类负责实现具体的转换操作。它的channelRead和上面的类实现相似,但更简单,这里就不再分析源码了。
类型转换编解码的抽象实现: MessageToMessageCodec<INBOUND_IN, OUTBOUND_IN>
这个类是ChannelDuplexHandler的派生类,它的功能是在write过程中把OUTBOUND_IN类型的数据转换成INBOUND_IN类型的数据,在read过程中进程相反的操作。它没有特别的实现,内部使用前面的两个类实现编解码。
netty源码解解析(4.0)-18 ChannelHandler: codec--编解码框架的更多相关文章
- netty源码解解析(4.0)-17 ChannelHandler: IdleStateHandler实现
io.netty.handler.timeout.IdleStateHandler功能是监测Channel上read, write或者这两者的空闲状态.当Channel超过了指定的空闲时间时,这个Ha ...
- netty源码解解析(4.0)-20 ChannelHandler: 自己实现一个自定义协议的服务器和客户端
本章不会直接分析Netty源码,而是通过使用Netty的能力实现一个自定义协议的服务器和客户端.通过这样的实践,可以更深刻地理解Netty的相关代码,同时可以了解,在设计实现自定义协议的过程中需要解决 ...
- netty源码解解析(4.0)-19 ChannelHandler: codec--常用编解码实现
数据包编解码过程中主要的工作就是:在编码过程中进行序列化,在解码过程中从Byte流中分离出数据包然后反序列化.在MessageToByteEncoder中,已经解决了序列化之后的问题,ByteToMe ...
- netty源码解解析(4.0)-16 ChannelHandler概览
本章开始分析ChannelHandler实现代码.ChannelHandler是netty为开发者提供的实现定制业务的主要接口,开发者在使用netty时,最主要的工作就是实现自己的ChannelHan ...
- netty源码解解析(4.0)-11 Channel NIO实现-概览
结构设计 Channel的NIO实现位于io.netty.channel.nio包和io.netty.channel.socket.nio包中,其中io.netty.channel.nio是抽象实 ...
- netty源码解解析(4.0)-10 ChannelPipleline的默认实现--事件传递及处理
事件触发.传递.处理是DefaultChannelPipleline实现的另一个核心能力.在前面在章节中粗略地讲过了事件的处理流程,本章将会详细地分析其中的所有关键细节.这些关键点包括: 事件触发接口 ...
- netty源码解解析(4.0)-15 Channel NIO实现:写数据
写数据是NIO Channel实现的另一个比较复杂的功能.每一个channel都有一个outboundBuffer,这是一个输出缓冲区.当调用channel的write方法写数据时,这个数据被一系列C ...
- netty源码解解析(4.0)-14 Channel NIO实现:读取数据
本章分析Nio Channel的数据读取功能的实现. Channel读取数据需要Channel和ChannelHandler配合使用,netty设计数据读取功能包括三个要素:Channel, Eve ...
- netty源码解解析(4.0)-12 Channel NIO实现:channel初始化
创建一个channel实例,并把它register到eventLoopGroup中之后,这个channel然后处于inactive状态,仍然是不可用的.只有在bind或connect方法调用成功之后才 ...
随机推荐
- Asp.net之实现自定义跨域
跨域是指在浏览器的同源策略下导致前端和接口部署在不同域下导致无法直接访问的问题. 针对跨域有多种解决方案常见的有: JSNOP: 可参考Jquery实现,缺点是需要后端支持: Access-Con ...
- Spring Boot微服务电商项目开发实战 --- 基础配置及搭建
根据SpringBoot实现分布式微服务项目近两年的开发经验,今天决定开始做SpringBoot实现分布式微服务项目的系列文章,帮助其他正在使用或计划使用SringBoot开发的小伙伴们.本次系列文章 ...
- bulk更新mongodb的脚本
bulk批处理mongodb,比普通的js脚本来的更快一些. 官方网址:https://docs.mongodb.com/manual/reference/method/Bulk/ bulk支持的方法 ...
- 小米OJ 12. 找出可能的合的组合
利用dfs解决,从给出的数组左边或右边开始遍历,对每一个数字进行判断,有三种情况: 1. 加上当前数字的值,遍历下一个数字 2. 加上当前数字的值,继续遍历该数字 3. 不加上当前的数字的值,遍历下一 ...
- Oracle RAC运维所遇问题记录一
Oracle11gR2,版本11.2.0.4集群测试环境运行正常 主机名:rac1,rac2 hosts文件: # Public172.17.188.12 rac1172.17.188.13 rac2 ...
- Oracle导入dump文件
imp USER/PASSWORD@host/DB_name file=xxx.dmp(path) full=y ignore=y
- 准备开始,选好第一个C#的封装库
如今C#做工业图像处理和开发,最多资料和例子的就是Emgu.不过现在人家开始商业收费了,对于我们这些小企业就不是很好了.这里,我要介绍和推荐的是另外一个也同样牛逼的C#封装Opnecv的库,叫做Ope ...
- Python基础总结之第三天开始重新认识‘字符串’(新手可相互督促)
年薪20万的梦想,又进了一步... 戏好多 ’字符串‘开始啦~ 字符串的定义:字符串可以用英文单引号或双引号又或者三引号包围起来. 为毛有单引号,还要有双引号和三引号??? 看案例吧: 字符串的其他使 ...
- 必懂的wenpack优化
webpack优化 1.production 模式打包自带优化 tree shaking tree shaking是一个术语.通常用于打包时移除js中未引用的代码(dead-code),它依赖于ES6 ...
- tp3 的前端内置标签
Volist 标签 volist标签通常用于查询数据集(select 方法),对于查询出来的结果数组进行遍历输出. 首先赋值: $User = M("User"); $list = ...