本篇内容主要梳理一下 Netty 中编解码器的逻辑和编解码器在 Netty 整个链路中的位置。

前面我们在分析 ChannelPipeline 的时候说到入站和出站事件的处理都在 pipeline 中维护着,通过list的形式将处理事件的 handler 按照先后关系保存为一个列表,有对应的事件过来就按照列表顺序取出 handler 来处理事件。

如果是入站事件按照 list 自然顺序调用 handler 来处理,如果是出站事件则反序调用 handler 来处理。所有的入站事件处理器都继承自 ChannelInboundHandler,出站事件处理器都继承自 ChannelOutboundHandler。channelPipeline 上的注释有说明 inbound 事件的传播顺序是:

  1. * 入栈事件传播方法
  2. * <li>{@link ChannelHandlerContext#fireChannelRegistered()}</li>
  3. * <li>{@link ChannelHandlerContext#fireChannelActive()}</li>
  4. * <li>{@link ChannelHandlerContext#fireChannelRead(Object)}</li>
  5. * <li>{@link ChannelHandlerContext#fireChannelReadComplete()}</li>
  6. * <li>{@link ChannelHandlerContext#fireExceptionCaught(Throwable)}</li>
  7. * <li>{@link ChannelHandlerContext#fireUserEventTriggered(Object)}</li>
  8. * <li>{@link ChannelHandlerContext#fireChannelWritabilityChanged()}</li>
  9. * <li>{@link ChannelHandlerContext#fireChannelInactive()}</li>
  10. * <li>{@link ChannelHandlerContext#fireChannelUnregistered()}</li>
  11. * </ul>
  12. * </li>

即 handler 中的方法调用顺序是如上所示,我们主要关注的点在 channelRead() 方法上。下面就由 channelRead() 出发,去看看编解码器的使用。

1. channelRead 解析

inbound 事件的入口在 NioEventLoop #run() 方法#processSelectedKeys()#processSelectedKeysPlain()#processSelectedKey()#unsafe.read()。

这里的 UnSafe 是定义在 Channel 接口中的子接口,并不是 JDK 的 UnSafe 类。UnSafe作为 channel 的内部类承担着 channel 网络读写相关的功能,这里可以抽出一节讨论,不是本篇的重点。我们继续看 UnSafe 的子类 NioByteUnsafe 重写的 read() 方法:

  1. @Override
  2. public final void read() {
  3. final ChannelConfig config = config();
  4. final ChannelPipeline pipeline = pipeline();
  5. //allocator负责建立缓冲区
  6. final ByteBufAllocator allocator = config.getAllocator();
  7. final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
  8. allocHandle.reset(config);
  9. ByteBuf byteBuf = null;
  10. boolean close = false;
  11. try {
  12. do {
  13. //分配内存
  14. byteBuf = allocHandle.allocate(allocator);
  15. //读取socketChannel数据到分配的byteBuf,对写入的大小进行一个累计叠加
  16. allocHandle.lastBytesRead(doReadBytes(byteBuf));
  17. if (allocHandle.lastBytesRead() <= 0) {
  18. // nothing was read. release the buffer.
  19. byteBuf.release();
  20. byteBuf = null;
  21. close = allocHandle.lastBytesRead() < 0;
  22. break;
  23. }
  24. allocHandle.incMessagesRead(1);
  25. readPending = false;
  26. //触发pipeline的ChannelRead事件来对byteBuf进行后续处理
  27. pipeline.fireChannelRead(byteBuf);
  28. byteBuf = null;
  29. } while (allocHandle.continueReading());
  30. // 记录总共读取的大小
  31. allocHandle.readComplete();
  32. pipeline.fireChannelReadComplete();
  33. if (close) {
  34. closeOnRead(pipeline);
  35. }
  36. } catch (Throwable t) {
  37. handleReadException(pipeline, byteBuf, t, close, allocHandle);
  38. } finally {
  39. // Check if there is a readPending which was not processed yet.
  40. // This could be for two reasons:
  41. // * The user called Channel.read() or ChannelHandlerContext.read() in channelRead(...) method
  42. // * The user called Channel.read() or ChannelHandlerContext.read() in channelReadComplete(...) method
  43. //
  44. // See https://github.com/netty/netty/issues/2254
  45. if (!readPending && !config.isAutoRead()) {
  46. removeReadOp();
  47. }
  48. }
  49. }
  50. }

read()方法从内存读取数据给到 ByteBuf,上一节我们提到了ByteBuf,Netty 自己实现的 byte 字节累加器。下面有一个while循环,每次读取的 bytebuf 会给到 pipeline.fireChannelRead(byteBuf)方法去处理。继续看 ChannelPipeline 的默认实现类 DefaultChannelPipeline 中的实现:

  1. @Override
  2. public final ChannelPipeline fireChannelRead(Object msg) {
  3. AbstractChannelHandlerContext.invokeChannelRead(head, msg);
  4. return this;
  5. }

调用了 AbstractChannelHandlerContext#invokeChannelRead()方法:

  1. static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
  2. final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
  3. EventExecutor executor = next.executor();
  4. if (executor.inEventLoop()) {
  5. next.invokeChannelRead(m);
  6. } else {
  7. executor.execute(new Runnable() {
  8. @Override
  9. public void run() {
  10. next.invokeChannelRead(m);
  11. }
  12. });
  13. }
  14. }
  15. private void invokeChannelRead(Object msg) {
  16. if (invokeHandler()) {
  17. try {
  18. ((ChannelInboundHandler) handler()).channelRead(this, msg);
  19. } catch (Throwable t) {
  20. notifyHandlerException(t);
  21. }
  22. } else {
  23. fireChannelRead(msg);
  24. }
  25. }

重点就在 invokeChannelRead() 的这一句:

  1. ((ChannelInboundHandler) handler()).channelRead(this, msg);

最终触发了 ChannelInboundHandler#channelRead(ChannelHandlerContext ctx, Object msg) 方法。

所有的入站事件都实现了 ChannelInboundHandler 接口,不难理解我们的 handler 就是这样接收到 bytebuf 然后进行下一步处理的。

2. Read 事件一次可以读多少字节

说编解码器之前我们先解决一个问题,如果不使用任何的编解码器,默认的传输对象应该是 byteBuf,那么 Netty 默认一次是读取多少字节呢?前面在讲粘包的文章里我在 packageEvent1工程示例中演示了不使用任何编解码工具读取数据,默认一次会话会读取1024字节,大家有兴趣可以回到上一篇看看 Netty 中的粘包和拆包,在 handler 中打上断点就知道当前一次读取包的长度。既然知道是1024,就好奇到底是在哪里设置的,出发点肯定还是上面提到的 read() 方法:

  1. byteBuf = allocHandle.allocate(allocator);

这一句就是从内存中拿出字节分配到 bytebuf,allocate() 是 RecvByteBufAllocator 接口中的方法,这个接口有很多实现类,那到底默认是哪个实现类生效呢?

我们再回到 NioSocetChannel ,看他的构造方法:

  1. public NioSocketChannel(Channel parent, SocketChannel socket) {
  2. super(parent, socket);
  3. config = new NioSocketChannelConfig(this, socket.socket());
  4. }
  5. private final class NioSocketChannelConfig extends DefaultSocketChannelConfig {
  6. private NioSocketChannelConfig(NioSocketChannel channel, Socket javaSocket) {
  7. super(channel, javaSocket);
  8. }
  9. @Override
  10. protected void autoReadCleared() {
  11. clearReadPending();
  12. }
  13. }

这里会生成一些配置信息,主要是一些 socket 默认参数以供初始化连接使用。NioSocketChannelConfig 构造方法里面调用了父类 DefaultSocketChannelConfig 的构造方法:

  1. public DefaultSocketChannelConfig(SocketChannel channel, Socket javaSocket) {
  2. super(channel);
  3. if (javaSocket == null) {
  4. throw new NullPointerException("javaSocket");
  5. }
  6. this.javaSocket = javaSocket;
  7. // Enable TCP_NODELAY by default if possible.
  8. if (PlatformDependent.canEnableTcpNoDelayByDefault()) {
  9. try {
  10. setTcpNoDelay(true);
  11. } catch (Exception e) {
  12. // Ignore.
  13. }
  14. }
  15. }

同样这里又往上调用了父类 DefaultChannelConfig :

  1. public DefaultChannelConfig(Channel channel) {
  2. this(channel, new AdaptiveRecvByteBufAllocator());
  3. }
  4. protected DefaultChannelConfig(Channel channel, RecvByteBufAllocator allocator) {
  5. setRecvByteBufAllocator(allocator, channel.metadata());
  6. this.channel = channel;
  7. }

怎样,是不是看到了 AdaptiveRecvByteBufAllocator, 他就是 RecvByteBufAllocator 的实现类之一。所以我们只要看它是怎样设置默认值即可。

AdaptiveRecvByteBufAllocator 的默认构造方法:

  1. public AdaptiveRecvByteBufAllocator() {
  2. this(DEFAULT_MINIMUM, DEFAULT_INITIAL, DEFAULT_MAXIMUM);
  3. }

这3个参数的默认值为:

  1. static final int DEFAULT_MINIMUM = 64;
  2. static final int DEFAULT_INITIAL = 1024;
  3. static final int DEFAULT_MAXIMUM = 65536;

DEFAULT_MINIMUM 是缓冲区最小值,DEFAULT_INITIAL 是缓冲区默认值,DEFAULT_MAXIMUM是缓冲区最大值,到这里我们就找到了默认值是从哪里来的了。

默认大小是1024,但是并不是固定不变,它会有一个动态调整的动作。除了这三个字段外,还定义了两个动态调整容量的步长索引参数:

  1. private static final int INDEX_INCREMENT = 4;
  2. private static final int INDEX_DECREMENT = 1;

扩张的步进索引为4,收缩的步进索引为1。

  1. private static final int[] SIZE_TABLE;
  2. static {
  3. List<Integer> sizeTable = new ArrayList<Integer>();
  4. for (int i = 16; i < 512; i += 16) {
  5. sizeTable.add(i);
  6. }
  7. for (int i = 512; i > 0; i <<= 1) {
  8. sizeTable.add(i);
  9. }
  10. SIZE_TABLE = new int[sizeTable.size()];
  11. for (int i = 0; i < SIZE_TABLE.length; i ++) {
  12. SIZE_TABLE[i] = sizeTable.get(i);
  13. }
  14. }

SIZE_TABLE 为长度向量表,作用就是保存步长。上面的 static 修饰的代码块作用就是初始化长度向量表。从16开始,每次递增16,直到512,这里数组的下标为30。下标31的初始值为512, i递增的值为左移一位,左移一位相当于乘以2,所以每次递增是以当前值的倍数增加的,最终增加到的值直到 Integer 能达到的最大值。

长度向量表的值可以得出:

  1. 0-->16
  2. 1-->32
  3. 2-->48
  4. 3-->64
  5. 4-->80
  6. 5-->96
  7. 6-->112
  8. 7-->128
  9. 8-->144
  10. 9-->160
  11. 10-->176
  12. 11-->192
  13. 12-->208
  14. 13-->224
  15. 14-->240
  16. 15-->256
  17. 16-->272
  18. 17-->288
  19. 18-->304
  20. 19-->320
  21. 20-->336
  22. 21-->352
  23. 22-->368
  24. 23-->384
  25. 24-->400
  26. 25-->416
  27. 26-->432
  28. 27-->448
  29. 28-->464
  30. 29-->480
  31. 30-->496
  32. 31-->512
  33. 32-->1024
  34. 33-->2048
  35. 34-->4096
  36. 35-->8192
  37. 36-->16384
  38. 37-->32768
  39. 38-->65536
  40. 39-->131072
  41. 40-->262144
  42. 41-->524288
  43. 42-->1048576
  44. 43-->2097152
  45. 44-->4194304
  46. 45-->8388608
  47. 46-->16777216
  48. 47-->33554432
  49. 48-->67108864
  50. 49-->134217728
  51. 50-->268435456
  52. 51-->536870912
  53. 52-->1073741824

SIZE_TABLE 里面的值是干啥用的呢,刚才提到会将 byte 数据先预读到缓冲区,初始默认大小为1024,当目前没有这么多字节需要读的时候,会动态缩小缓冲区,而预判待读取的字节有很多的时候会扩大缓冲区。

动态预估下一次可能会有多少数据待读取的操作在哪里呢?还是回到 read()方法,while 循环完一轮之后,会执行一句:

  1. allocHandle.readComplete();

对应到 AdaptiveRecvByteBufAllocator 中:

  1. @Override
  2. public void readComplete() {
  3. record(totalBytesRead());
  4. }
  5. //根据当前的actualReadBytes大小,对nextReceiveBufferSize进行更新
  6. private void record(int actualReadBytes) {
  7. //如果actualReadBytes 小于 当前索引-INDEX_DECREMENT-1 的值,说明容量需要缩减
  8. if (actualReadBytes <= SIZE_TABLE[Math.max(0, index - INDEX_DECREMENT - 1)]) {
  9. if (decreaseNow) {
  10. //则取 当前索引-INDEX_DECREMENT 与 minIndex的最大值
  11. index = Math.max(index - INDEX_DECREMENT, minIndex);
  12. nextReceiveBufferSize = SIZE_TABLE[index];
  13. decreaseNow = false;
  14. } else {
  15. decreaseNow = true;
  16. }
  17. //读到的值大于缓冲大小
  18. } else if (actualReadBytes >= nextReceiveBufferSize) {
  19. // INDEX_INCREMENT=4 index前进4
  20. index = Math.min(index + INDEX_INCREMENT, maxIndex);
  21. nextReceiveBufferSize = SIZE_TABLE[index];
  22. decreaseNow = false;
  23. }
  24. }

通过上一次的流大小来预测下一次的流大小,可针对不同的应用场景来进行缓冲区的分配。像IM消息可能是几K ,文件传输可能是几百M,不同的场景用到的内存缓冲大小不一样对性能的影响也不同。如果所有的场景都是同一种内存空间分配,客户端连接多的情况下,线程数过多可能导致内存溢出。

3. Netty 中的编解码器

上面两小节聊到消息从哪里来,默认消息格式为 ByteBuf,缓冲区大小默认为1024,会动态预估下次缓冲区大小。下面我们就正式来说一下编解码相关的内容,编解码相关的源码都在 codec 包中:

因为编码器要实现的是对输出的内容编码,都是实现 ChannelOutboundHandler 接口,解码器对接收的内容解码,都是实现 ChannelInboundHandler 接口,所以可以完全适配 ChannelPipeline 将编解码器作为一种插件的形式做一些灵活的搭配。

3.1 decoder

解码器负责将输入的消息解析为指定的格式。消息输入都来自inbound,即继承 ChannelInboundHandler 接口,顶级的解码器有两种类型:

  • 将字节解码为消息:ByteToMessageDecoder
  • 将一种消息类型解码为另一种 类型:MessageToMessageDecoder

字节码解析为消息这应该是最普通,最基本的使用方式,这里所谓的字节码就是上面我们讲到的 ByteBuf 序列,默认包含1024字节的字节数组。关于 ByteToMessageDecoder 的分析上一节在讲粘包的时候顺带提及,大家有兴趣可以回去看看:ByteToMessageDecoder 分析

MessageToMessageDecoder 更好理解,比如消息的类型为Integer,需要将 Integer 转为 String。那么就可以继承 MessageToMessageDecoder 实现自己的转换方法。我们先简单看一下它的实现:

  1. @Override
  2. public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
  3. CodecOutputList out = CodecOutputList.newInstance();
  4. try {
  5. if (acceptInboundMessage(msg)) {
  6. @SuppressWarnings("unchecked")
  7. I cast = (I) msg;
  8. try {
  9. decode(ctx, cast, out);
  10. } finally {
  11. ReferenceCountUtil.release(cast);
  12. }
  13. } else {
  14. out.add(msg);
  15. }
  16. } catch (DecoderException e) {
  17. throw e;
  18. } catch (Exception e) {
  19. throw new DecoderException(e);
  20. } finally {
  21. int size = out.size();
  22. for (int i = 0; i < size; i ++) {
  23. ctx.fireChannelRead(out.getUnsafe(i));
  24. }
  25. out.recycle();
  26. }
  27. }
  28. protected abstract void decode(ChannelHandlerContext ctx, I msg, List<Object> out) throws Exception;

上面的 channelRead()方法中将 msg 转为消息原本的类型,然后进入 decode()方法。 decode() 是一个抽象方法,言意之下你想转为啥类型,你就实现该方法去转便是。

3.2 encoder

编码器主要的作用是将出站事件的消息按照指定格式编码输出。那么编码器应该是继承 outBound 事件,看一下主要的类图:

编码器的基本类型与解码器相反:将对象拆解为字节,将对象编码为另一种对象。

关于基本编解码器的使用和自定义编解码器上一节我们已经讲过,这里就不再复述。下一篇单独看看在 Netty 中使用protobuf编码格式进行数据传输。

Netty 中的消息解析和编解码器的更多相关文章

  1. Netty中的基本组件及关系

    原文:https://blog.csdn.net/summerZBH123/article/details/79344226---------------------  概述    这篇文章主要是用来 ...

  2. 深度解析VC中的消息(转发)

    http://blog.csdn.net/chenlycly/article/details/7586067 这篇转发的文章总结的比较好,但是没有告诉我为什么ON_MESSAGE的返回值必须是LRES ...

  3. 深度解析VC中的消息

    消息是指什么? 消息系统对于一个win32程序来说十分重要,它是一个程序运行的动力源泉.一个消息,是系统定义的一个32位的值,他唯一的定义了一个事件,向Windows发出一个通知,告诉应用程序某个事情 ...

  4. 基于大量图片与实例深度解析Netty中的核心组件

    本篇文章主要详细分析Netty中的核心组件. 启动器Bootstrap和ServerBootstrap作为Netty构建客户端和服务端的路口,是编写Netty网络程序的第一步.它可以让我们把Netty ...

  5. Netty中解码基于分隔符的协议和基于长度的协议

    在使用Netty的过程中,你将会遇到需要解码器的基于分隔符和帧长度的协议.本节将解释Netty所提供的用于处理这些场景的实现. 基于分隔符的协议 基于分隔符的(delimited)消息协议使用定义的字 ...

  6. Netty 中的粘包和拆包

    Netty 底层是基于 TCP 协议来处理网络数据传输.我们知道 TCP 协议是面向字节流的协议,数据像流水一样在网络中传输那何来 "包" 的概念呢? TCP是四层协议不负责数据逻 ...

  7. Netty源码分析之自定义编解码器

    在日常的网络开发当中,协议解析都是必须的工作内容,Netty中虽然内置了基于长度.分隔符的编解码器,但在大部分场景中我们使用的都是自定义协议,所以Netty提供了  MessageToByteEnco ...

  8. 通过大量实战案例分解Netty中是如何解决拆包黏包问题的?

    TCP传输协议是基于数据流传输的,而基于流化的数据是没有界限的,当客户端向服务端发送数据时,可能会把一个完整的数据报文拆分成多个小报文进行发送,也可能将多个报文合并成一个大报文进行发送. 在这样的情况 ...

  9. Netty那点事: 概述, Netty中的buffer, Channel与Pipeline

    Netty那点事(一)概述 Netty和Mina是Java世界非常知名的通讯框架.它们都出自同一个作者,Mina诞生略早,属于Apache基金会,而Netty开始在Jboss名下,后来出来自立门户ne ...

随机推荐

  1. 鸟哥Linux私房菜(基础篇)——第五章:首次登入与在线求助 man page笔记

    1.X Winsows与文本模式的切换 ●[Ctrl] + [Alt] + [F1] ~ [F6] :文字接口登入 tty1 ~ tty6 终端机.        ●[Ctrl] + [Alt] + ...

  2. 二进制安装MySQL及破解密码

    二进制安装MySQL及破解密码 1.确保系统中有依赖的libaio 软件,如果没有: yum -y install libaio 2.解压二进制MySQL软件包 tar xf mysql-5.7.24 ...

  3. ip-端口-协议等基本概念

    互联网上的计算机,都会有一个唯一的32位的地址——ip地址.我们访问服务器,就必须通过这个ip地址.   局域网里也有预留的ip地址:192/10/172开头.局域网里的ip地址也是唯一的.   NA ...

  4. CS Requirements and Resources

    有感于国内令人发指的CS教育,决定以自学为主. 路线会按照计算机科学与技术的技能树,主要学习四大的比较完整的课程,video没时间看,但reading会仔细看.lab会认真做,对于一些比较有意义.代码 ...

  5. ACM思维题训练 Section A

    题目地址: 选题为入门的Codeforce div2/div1的C题和D题. 题解: A:CF思维联系–CodeForces -214C (拓扑排序+思维+贪心) B:CF–思维练习-- CodeFo ...

  6. HDU - 1253 胜利大逃亡 (搜索)

    Ignatius被魔王抓走了,有一天魔王出差去了,这可是Ignatius逃亡的好机会. 魔王住在一个城堡里,城堡是一个A*B*C的立方体,可以被表示成A个B*C的矩阵,刚开始Ignatius被关在(0 ...

  7. 超轻量级网络SqueezeNet网络解读

    SqueezeNet网络模型非常小,但分类精度接近AlexNet. 这里复习一下卷积层参数的计算 输入通道ci,核尺寸k,输出通道co,参数个数为: 以AlexNet第一个卷积为例,参数量达到:3*1 ...

  8. 聚合类型与POD类型

    Lippman在<深度探索C++对象模型>的前言中写道: I have heard a number of people over the years voice opinions sim ...

  9. http协议跟tcp协议的简单理解

    在说明这两个协议之前,我们先简单说一下网络的分层. 1)应用层 支持网络应用,应用协议仅仅是网络应用的一个组成部分,运行在不同主机上的进程则使用应用层协议进行通信.主要的协议有:http.ftp.te ...

  10. LeetCode最长回文子串

    题目: 给定一个字符串 s,找到 s 中最长的回文子串.你可以假设 s 的最大长度为 1000. 示例 1: 输入: "babad"输出: "bab"注意: & ...