Netty的ByteBuf学习笔记

        网络编程中,当我们要进行数据传输时,往往需要使用缓冲区,常用的缓冲区就是Java NIO类库提供的java.io.buffer,在NIO编程时,我们主要使用的是ByteBuffer。从功能角度而言,ByteBuffer完全满足NIO编程的需要,但是由于NIO编程的复杂性,ByteBuffer也有其局限性,它的主要缺点如下:

  1. ByteBuffer长度固定,一旦分配完成,它的容量不能动态扩展和收缩,当需要编码的POJO对象大于ByteBuffer的容量时,会发生索引越界异常;
  2. ByteBuffer只有一个标识位置的指针position,读写的时候需要手工调用flip()和rewind()等方法,使用者必须小心谨慎的处理这些API,否则很容易导致程序处理失败;
  3. ByteBuffer的API功能有限,一些高级和实用的特性它并不支持,需要使用者自己编程实现。

        为了弥补这些不足,Netty提供了自己的ByteBuffer实现—ByteBuf,下边看一下ByteBuf的原理和主要功能。

一、ByteBuf的工作原理

        首先,ByteBuf依然是Byte数组的缓冲区,它的基本功能与JDK的ByteBuffer基本一致,提供以下几类基本功能:

  • 7种Java基本类型、byte数组、ByteBuffer等的读写;
  • 缓冲区自身的copy与slice等;
  • 设置网络字节序;
  • 构造缓冲区实例;
  • 操作位置指针等方法。

        ByteBuf通过两个位置指针来协助缓冲区的读写操作,读操作使用readerIndex,写操作使用writerIndex。

        readerIndex和writerIndex一开始都是0,随着数据的写入writerIndex会增加,读取数据会使readerIndex增加,但是它不会超过writerIndex。在读取之后,0~readerIndex就会视为discard的,调用discardReadBytes方法,可以释放这部分空间,它的作用类似ByteBuffer的compact方法。readerIndex和writerIndex之间的数据是可读取的,等价于ByteBuffer的position和limit之间的数据。writerIndex和capacity之间的空间是可写的,等价于ByteBuffer的limit和capacity之间的可用空间。

        由于写操作不修改readerIndex指针,读操作不修改writerIndex指针,因此读写之间不再需要调整位置指针。极大地简化了缓冲区的读写操作,避免了由于遗漏或者不熟悉flip()操作导致的功能异常。

初始分配的 ByteBuf 如下图1所示:

                                          图1:初始分配的ByteBuf 


写入N个字节之后的 ByteBuf 如图2所示:

                                     图2:写入N个字节之后的ByteBuf


读取M(<N)个字节之后的 ByteBuf 如图3所示:

                            图3:读取M(<N)个字节之后的ByteBuf


调用discardReadBytes操作之后的 ByteBuf 如图4所示:

                        图4:调用discardReadBytes操作之后的ByteBuf


调用clear操作之后的 ByteBuf 如图5所示:

                                 图5:调用clear操作之后的ByteBuf


        下面我们分析ByteBuf是如何实现动态扩展的。通常情况下,我们对ByteBuffer进行put操作时,如果缓冲区剩余可写空间不足,就会发生BufferOverflowException异常。需要重新创建一个新的ByteBuffer,并将之前的ByteBuffer复制到新的ByteBuffer中,然后释放老的ByteBuffer。

        ByteBuf对write操作进行了封装,由ByteBuf的write操作负责进行剩余可用空间的校验。如果可用缓冲区不足,ByteBuf会自动进行动态扩展。对于使用者而言,不需要关心底层的校验和扩展细节,只要不超过设置的最大缓冲区容量即可。当可用空间不足时,ByteBuf会帮助我们实现自动扩展,这极大地降低了ByteBuf的学习和使用成本,提升了开发效率。

        使用建议:为了保证安全性,可指定ByteBuf的最大容量,在容量范围内,可以先分配一个较小的初始容量,不够用时可自动扩展,这样可以达到功能和性能的最佳组合。

Mark和Rest

        当对缓冲区进行读操作时,由于某种原因,可能需要对之前的操作进行回滚。读操作并不会改变缓冲区的内容,回滚操作主要就是重新设置索引信息。

        对于JDK的ByteBuffer,调用mark操作会将当前的位置指针备份到mark变量中,当调用rest操作之后,重新将指针的当前位置恢复为备份在mark中的值。

        Netty的ByteBuf也有也有类似的mark和rest接口,因为ByteBuf有读索引和写索引,因此,它公有4个相关的方法,分别如下:

  1. markReaderIndex:将当前的readerIndex备份到markedReaderIndex中;
  2. resetReaderIndex:将当前的readerIndex设置为markedReaderIndex;
  3. markWriterIndex:将当前的writerIndex备份到markedWriterIndex中;
  4. resetWriterIndex:将当前的writerIndex设置为markedWriterIndex;

二、ByteBuf的内存分配和回收

        从内存分配的角度看,ByteBuf可以分为两类:

  1. 堆内存(HeapByteBuf)字节缓冲区:特点是内存的分配和回收速度快,可以被JVM自动回收,缺点就是如果进行Socket的I/O读写,需要额外做一次内存复制,将堆内存对应的缓冲区复制到内核Channel中,性能会有一定程度的下降。
  2. 直接内存(DirectByteBuf)字节缓冲区:非堆内存,它在堆外进行内存分配,相比于堆内存,它的分配和回收速度相对会慢一些,但是它写入或是从Socket Channel中读取时,由于少了一次内存复制,速度比堆内存块。

        正是由于各有利弊,Netty提供了多种ByteBuf供开发者使用,经验表明,ByteBuf的最佳实践是在I/O通信线程的读写缓冲区使用DirectByteBuf,后端业务消息的编解码模块使用HeapByteBuf,这样组合可以达到性能最优。

        从内存回收角度看,ByteBuf也分为两类:基于对象池的ByteBuf和普通ByteBuf。两者的主要区别就是基于对象池的ByteBuf可以复用ByteBuf对象,它自己维护了一个内存池,可以循环利用创建的ByteBuf,提升内存的使用效率,降低由于高负载导致的频繁GC。测试表明使用内存池后的Netty在高负载、大并发的冲击下内存和GC更加平稳。

        尽管推荐使用基于内存池的ByteBuf,但是内存池的管理和维护更加复杂,使用起来也需要更加谨慎,因此,Netty提供了灵活的策略供使用者来做选择。

        小提示:相比于PooledHeapByteBuf,UnpooledHeapByteBuf的实现原理更加简单,也不容易出现内存管理方面的问题,因此在满足性能的前提下,推荐使用UnpooledHeapByteBuf。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值