由浅入深Netty组件实战3

简介: 由浅入深Netty组件实战3

5.6 扩容

再写入一个 int 整数时,容量不够了(初始容量是 10),这时会引发扩容

buffer.writeInt(6);
log(buffer);

扩容规则是

  • 如何写入后数据大小未超过 512,则选择下一个 16 的整数倍,例如写入后大小为 12 ,则扩容后 capacity 是 16
  • 如果写入后数据大小超过 512,则选择下一个 2^n,例如写入后大小为 513,则扩容后 capacity 是 210=1024(29=512 已经不够了)
  • 扩容不能超过 max capacity 会报错

结果是

read index:0 write index:12 capacity:16
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 00 00 00 05 00 00 00 06             |............    |
+--------+-------------------------------------------------+----------------+

5.7 读取

例如读了 4 次,每次一个字节

System.out.println(buffer.readByte());
System.out.println(buffer.readByte());
System.out.println(buffer.readByte());
System.out.println(buffer.readByte());
log(buffer);

读过的内容,就属于废弃部分了,再读只能读那些尚未读取的部分

1
2
3
4
read index:4 write index:12 capacity:16
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 00 05 00 00 00 06                         |........        |
+--------+-------------------------------------------------+----------------+

如果需要重复读取 int 整数 5,怎么办?

可以在 read 前先做个标记 mark

buffer.markReaderIndex();
System.out.println(buffer.readInt());
log(buffer);

结果

5
read index:8 write index:12 capacity:16
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 00 06                                     |....            |
+--------+-------------------------------------------------+----------------+

这时要重复读取的话,重置到标记位置 reset

buffer.resetReaderIndex();
log(buffer);

这时

read index:4 write index:12 capacity:16
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 00 05 00 00 00 06                         |........        |
+--------+-------------------------------------------------+----------------+

还有种办法是采用 get 开头的一系列方法,这些方法不会改变 read index

5.8 retain & release

由于 Netty 中有堆外内存的 ByteBuf 实现,堆外内存最好是手动来释放,而不是等 GC 垃圾回收。

  • UnpooledHeapByteBuf 使用的是 JVM 内存,只需等 GC 回收内存即可
  • UnpooledDirectByteBuf 使用的就是直接内存了,需要特殊的方法来回收内存
  • PooledByteBuf 和它的子类使用了池化机制,需要更复杂的规则来回收内存

回收内存的源码实现,请关注下面方法的不同实现

protected abstract void deallocate()

Netty 这里采用了引用计数法来控制回收内存,每个 ByteBuf 都实现了 ReferenceCounted 接口

  • 每个 ByteBuf 对象的初始计数为 1
  • 调用 release 方法计数减 1,如果计数为 0,ByteBuf 内存被回收
  • 调用 retain 方法计数加 1,表示调用者没用完之前,其它 handler 即使调用了 release 也不会造成回收
  • 当计数为 0 时,底层内存会被回收,这时即使 ByteBuf 对象还在,其各个方法均无法正常使用

谁来负责 release 呢?

不是我们想象的(一般情况下)

ByteBuf buf = ...
try {
    ...
} finally {
    buf.release();
}

请思考,因为 pipeline 的存在,一般需要将 ByteBuf 传递给下一个 ChannelHandler,如果在 finally 中 release 了,就失去了传递性(当然,如果在这个 ChannelHandler 内这个 ByteBuf 已完成了它的使命,那么便无须再传递)

基本规则是,谁是最后使用者,谁负责 release,详细分析如下


    起点,对于 NIO 实现来讲,在 io.netty.channel.nio.AbstractNioByteChannel.NioByteUnsafe#read 方法中首次创建

    • ByteBuf 放入 pipeline(line 163 pipeline.fireChannelRead(byteBuf))
    • 入站 ByteBuf 处理原则
    • 对原始 ByteBuf 不做处理,调用 ctx.fireChannelRead(msg) 向后传递,这时无须 release
    • 将原始 ByteBuf 转换为其它类型的 Java 对象,这时 ByteBuf 就没用了,必须 release
    • 如果不调用 ctx.fireChannelRead(msg) 向后传递,那么也必须 release
    • 注意各种异常,如果 ByteBuf 没有成功传递到下一个 ChannelHandler,必须 release
    • 假设消息一直向后传,那么 TailContext 会负责释放未处理消息(原始的 ByteBuf)
    • 出站 ByteBuf 处理原则

    • 出站消息最终都会转为 ByteBuf 输出,一直向前传,由 HeadContext flush 后 release
    • 异常处理原则
    • 有时候不清楚 ByteBuf 被引用了多少次,但又必须彻底释放,可以循环调用 release 直到返回 true

    TailContext 释放未处理消息逻辑

    // io.netty.channel.DefaultChannelPipeline#onUnhandledInboundMessage(java.lang.Object)
    protected void onUnhandledInboundMessage(Object msg) {
        try {
            logger.debug(
                "Discarded inbound message {} that reached at the tail of the pipeline. " +
                "Please check your pipeline configuration.", msg);
        } finally {
            ReferenceCountUtil.release(msg);
        }
    }
    

    具体代码

    // io.netty.util.ReferenceCountUtil#release(java.lang.Object)
    public static boolean release(Object msg) {
        if (msg instanceof ReferenceCounted) {
            return ((ReferenceCounted) msg).release();
        }
        return false;
    }
    

    5.9 slice

    【零拷贝】的体现之一,对原始 ByteBuf 进行切片成多个 ByteBuf,切片后的 ByteBuf 并没有发生内存复制,还是使用原始 ByteBuf 的内存,切片后的 ByteBuf 维护独立的 read,write 指针

    例,原始 ByteBuf 进行一些初始操作

    ByteBuf origin = ByteBufAllocator.DEFAULT.buffer(10);
    origin.writeBytes(new byte[]{1, 2, 3, 4});
    origin.readByte();
    System.out.println(ByteBufUtil.prettyHexDump(origin));
    

    输出

             +-------------------------------------------------+
             |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
    +--------+-------------------------------------------------+----------------+
    |00000000| 02 03 04                                        |...             |
    +--------+-------------------------------------------------+----------------+
    

    这时调用 slice 进行切片,无参 slice 是从原始 ByteBuf 的 read index 到 write index 之间的内容进行切片,切片后的 max capacity 被固定为这个区间的大小,因此不能追加 write

    ByteBuf slice = origin.slice();
    System.out.println(ByteBufUtil.prettyHexDump(slice));
    // slice.writeByte(5); 如果执行,会报 IndexOutOfBoundsException 异常
    

    输出

             +-------------------------------------------------+
             |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
    +--------+-------------------------------------------------+----------------+
    |00000000| 02 03 04                                        |...             |
    +--------+-------------------------------------------------+----------------+
    

    如果原始 ByteBuf 再次读操作(又读了一个字节)

    origin.readByte();
    System.out.println(ByteBufUtil.prettyHexDump(origin));

    输出

             +-------------------------------------------------+
             |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
    +--------+-------------------------------------------------+----------------+
    |00000000| 03 04                                           |..              |
    +--------+-------------------------------------------------+----------------+
    

    这时的 slice 不受影响,因为它有独立的读写指针

    System.out.println(ByteBufUtil.prettyHexDump(slice));

    输出

             +-------------------------------------------------+
             |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
    +--------+-------------------------------------------------+----------------+
    |00000000| 02 03 04                                        |...             |
    +--------+-------------------------------------------------+----------------+
    

    如果 slice 的内容发生了更改

    slice.setByte(2, 5);
    System.out.println(ByteBufUtil.prettyHexDump(slice));

    输出

             +-------------------------------------------------+
             |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
    +--------+-------------------------------------------------+----------------+
    |00000000| 02 03 05                                        |...             |
    +--------+-------------------------------------------------+----------------+
    

    这时,原始 ByteBuf 也会受影响,因为底层都是同一块内存

    System.out.println(ByteBufUtil.prettyHexDump(origin));

    输出

             +-------------------------------------------------+
             |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
    +--------+-------------------------------------------------+----------------+
    |00000000| 03 05                                           |..              |
    +--------+-------------------------------------------------+----------------+
    

    5.10 duplicate

    【零拷贝】的体现之一,就好比截取了原始 ByteBuf 所有内容,并且没有 max capacity 的限制,也是与原始 ByteBuf 使用同一块底层内存,只是读写指针是独立的

    5.11 copy

    会将底层内存数据进行深拷贝,因此无论读写,都与原始 ByteBuf 无关

    5.12 CompositeByteBuf

    【零拷贝】的体现之一,可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf,避免拷贝

    有两个 ByteBuf 如下

    ByteBuf buf1 = ByteBufAllocator.DEFAULT.buffer(5);
    buf1.writeBytes(new byte[]{1, 2, 3, 4, 5});
    ByteBuf buf2 = ByteBufAllocator.DEFAULT.buffer(5);
    buf2.writeBytes(new byte[]{6, 7, 8, 9, 10});
    System.out.println(ByteBufUtil.prettyHexDump(buf1));
    System.out.println(ByteBufUtil.prettyHexDump(buf2));
    

    输出

             +-------------------------------------------------+
             |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
    +--------+-------------------------------------------------+----------------+
    |00000000| 01 02 03 04 05                                  |.....           |
    +--------+-------------------------------------------------+----------------+
             +-------------------------------------------------+
             |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
    +--------+-------------------------------------------------+----------------+
    |00000000| 06 07 08 09 0a                                  |.....           |
    +--------+-------------------------------------------------+----------------+
    

    现在需要一个新的 ByteBuf,内容来自于刚才的 buf1 和 buf2,如何实现?

    方法1:

    ByteBuf buf3 = ByteBufAllocator.DEFAULT
        .buffer(buf1.readableBytes()+buf2.readableBytes());
    buf3.writeBytes(buf1);
    buf3.writeBytes(buf2);
    System.out.println(ByteBufUtil.prettyHexDump(buf3));
    

    结果

             +-------------------------------------------------+
             |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
    +--------+-------------------------------------------------+----------------+
    |00000000| 01 02 03 04 05 06 07 08 09 0a                   |..........      |
    +--------+-------------------------------------------------+----------------+
    

    这种方法好不好?回答是不太好,因为进行了数据的内存复制操作

    方法2:

    CompositeByteBuf buf3 = ByteBufAllocator.DEFAULT.compositeBuffer();
    // true 表示增加新的 ByteBuf 自动递增 write index, 否则 write index 会始终为 0
    buf3.addComponents(true, buf1, buf2);
    

    结果是一样的

             +-------------------------------------------------+
             |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
    +--------+-------------------------------------------------+----------------+
    |00000000| 01 02 03 04 05 06 07 08 09 0a                   |..........      |
    +--------+-------------------------------------------------+----------------+
    

    CompositeByteBuf 是一个组合的 ByteBuf,它内部维护了一个 Component 数组,每个 Component 管理一个 ByteBuf,记录了这个 ByteBuf 相对于整体偏移量等信息,代表着整体中某一段的数据。

    • 优点,对外是一个虚拟视图,组合这些 ByteBuf 不会产生内存复制
    • 缺点,复杂了很多,多次操作会带来性能的损耗

    5.13 Unpooled

    Unpooled 是一个工具类,类如其名,提供了非池化的 ByteBuf 创建、组合、复制等操作

    这里仅介绍其跟【零拷贝】相关的 wrappedBuffer 方法,可以用来包装 ByteBuf

    ByteBuf buf1 = ByteBufAllocator.DEFAULT.buffer(5);
    buf1.writeBytes(new byte[]{1, 2, 3, 4, 5});
    ByteBuf buf2 = ByteBufAllocator.DEFAULT.buffer(5);
    buf2.writeBytes(new byte[]{6, 7, 8, 9, 10});
    // 当包装 ByteBuf 个数超过一个时, 底层使用了 CompositeByteBuf
    ByteBuf buf3 = Unpooled.wrappedBuffer(buf1, buf2);
    System.out.println(ByteBufUtil.prettyHexDump(buf3));
    

    输出

             +-------------------------------------------------+
             |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
    +--------+-------------------------------------------------+----------------+
    |00000000| 01 02 03 04 05 06 07 08 09 0a                   |..........      |
    +--------+-------------------------------------------------+----------------+
    

    也可以用来包装普通字节数组,底层也不会有拷贝操作

    ByteBuf buf4 = Unpooled.wrappedBuffer(new byte[]{1, 2, 3}, new byte[]{4, 5, 6});
    System.out.println(buf4.getClass());
    System.out.println(ByteBufUtil.prettyHexDump(buf4));
    

    输出

    class io.netty.buffer.CompositeByteBuf
             +-------------------------------------------------+
             |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
    +--------+-------------------------------------------------+----------------+
    |00000000| 01 02 03 04 05 06                               |......          |
    +--------+-------------------------------------------------+----------------+
    

    ByteBuf 优势

    • 池化 - 可以重用池中 ByteBuf 实例,更节约内存,减少内存溢出的可能
    • 读写指针分离,不需要像 ByteBuffer 一样切换读写模式
    • 可以自动扩容
    • 支持链式调用,使用更流畅
    • 很多地方体现零拷贝,例如 slice、duplicate、CompositeByteBuf
    目录
    相关文章
    |
    8月前
    Netty实战: HTTP文件列表服务器
    Netty实战: HTTP文件列表服务器
    87 0
    |
    8月前
    |
    Java Unix Linux
    【Netty技术专题】「原理分析系列」Netty强大特性之Native transports扩展开发实战
    当涉及到网络通信和高性能的Java应用程序时,Netty是一个强大的框架。它提供了许多功能和组件,其中之一是JNI传输。JNI传输是Netty的一个特性,它为特定平台提供了高效的网络传输。 在本文中,我们将深入探讨Netty提供的特定平台的JNI传输功能,分析其优势和适用场景。我们将介绍每个特定平台的JNI传输,并讨论其性能、可靠性和可扩展性。通过了解这些特定平台的JNI传输,您将能够更好地选择和配置适合您应用程序需求的网络传输方式,以实现最佳的性能和可靠性。
    153 7
    【Netty技术专题】「原理分析系列」Netty强大特性之Native transports扩展开发实战
    |
    8月前
    |
    网络协议 Java 测试技术
    阿里内部Netty实战小册,值得拥有
    Netty 是一个高性能的 Java 网络通信框架,简化了网络编程并涵盖了最新的Web技术。它提供了一种抽象,降低了底层复杂性,使得更多开发者能接触网络编程。Netty 因其易用性、高效性和广泛的应用场景受到推崇,适合互联网行业从业者学习,有助于理解和开发基于Netty的系统。免费的《Netty实战小册》详细介绍了Netty的各个方面,包括概念、架构、编解码器、网络协议和实际案例,帮助读者深入理解和应用Netty。如需完整版小册,可点击链接获取。
    阿里内部Netty实战小册,值得拥有
    |
    5月前
    |
    编解码 NoSQL Redis
    (十一)Netty实战篇:基于Netty框架打造一款高性能的IM即时通讯程序
    关于Netty网络框架的内容,前面已经讲了两个章节,但总归来说难以真正掌握,毕竟只是对其中一个个组件进行讲解,很难让诸位将其串起来形成一条线,所以本章中则会结合实战案例,对Netty进行更深层次的学习与掌握,实战案例也并不难,一个非常朴素的IM聊天程序。
    116 3
    |
    5月前
    |
    前端开发 网络协议
    Netty实战巅峰:从零构建高性能IM即时通讯系统,解锁并发通信新境界
    【8月更文挑战第3天】Netty是一款高性能、异步事件驱动的网络框架,适用于开发高并发网络应用,如即时通讯(IM)系统。本文将指导你利用Netty从零构建高性能IM程序,介绍Netty基础及服务器/客户端设计。服务器端使用`ServerBootstrap`启动,客户端通过`Bootstrap`连接服务器。示例展示了简单的服务器启动过程。通过深入学习,可进一步实现用户认证等功能,打造出更完善的IM系统。
    213 1
    |
    5月前
    |
    移动开发 网络协议 算法
    (十)Netty进阶篇:漫谈网络粘包、半包问题、解码器与长连接、心跳机制实战
    在前面关于《Netty入门篇》的文章中,咱们已经初步对Netty这个著名的网络框架有了认知,本章的目的则是承接上文,再对Netty中的一些进阶知识进行阐述,毕竟前面的内容中,仅阐述了一些Netty的核心组件,想要真正掌握Netty框架,对于它我们应该具备更为全面的认知。
    266 2
    |
    8月前
    |
    设计模式 前端开发 网络协议
    面试官:说说Netty的核心组件?
    Netty 核心组件是指 Netty 在执行过程中所涉及到的重要概念,这些核心组件共同组成了 Netty 框架,使 Netty 框架能够正常的运行。 Netty 核心组件包含以下内容: 1. 启动器 Bootstrap/ServerBootstrap 2. 事件循环器 EventLoopGroup/EventLoop 3. 通道 Channel 4. 通道处理器 ChannelHandler 5. 通道管道 ChannelPipeline 这些组件的交互流程如下: ![image.png](https://cdn.nlark.com/yuque/0/2024/png/92791/1716
    57 0
    面试官:说说Netty的核心组件?
    |
    8月前
    |
    NoSQL Redis
    Netty实战:模拟Redis的客户端
    Netty实战:模拟Redis的客户端
    106 0
    |
    8月前
    |
    前端开发 Java 网络安全
    【Netty 网络通信】Netty 核心组件
    【1月更文挑战第9天】【Netty 网络通信】Netty 核心组件
    |
    8月前
    |
    前端开发 网络协议 Java
    Netty | 工作流程图分析 & 核心组件说明 & 代码案例实践
    Netty | 工作流程图分析 & 核心组件说明 & 代码案例实践
    446 0