zoukankan      html  css  js  c++  java
  • (一) ByteBuf和ByteBuffer区别比较

    当我们进行数据传输的时候,往往需要使用到缓冲区,常用的缓冲区就是JDK NIO类库提供的java.nio.Buffer。

    7种基础类型(Boolean除外)都有自己的缓冲区实现,对于NIO编程而言,我们主要使用的是ByteBuffer。从功能角度而言,ByteBuffer完全可以满足NIO编程的需要,但是由于NIO编程的复杂性,ByteBuffer也有其局限性,它的主要缺点如下。

    (1)ByteBuffer长度固定,一旦分配完成,它的容量不能动态扩展和收缩,当需要编码的对象大于ByteBuffer的容量时,会发生索引越界异常;

    (2)ByteBuffer只有一个标识位置的指针position,读写的时候需要手工调用flip()和rewind()等,使用者必须小心谨慎地处理这些API,否则很容易导致程序处理失败;

    (3)ByteBuffer的API功能有限,一些高级和实用的特性它不支持,需要使用者自己编程实现。

    为了弥补这些不足,Netty提供了自己的ByteBuffer实现——ByteBuf。

    ByteBuf的工作原理

    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如图:

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

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

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

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

    ByteBuf是如何实现动态扩展

    通常情况下,当我们对ByteBuffer进行put操作的时候,如果缓冲区剩余可写空间不够,就会发生BufferOverflowException异常。为了避免发生这个问题,通常在进行put操作的时候会对剩余可用空间进行校验,如果剩余空间不足,需要重新创建一个新的ByteBuffer,并将之前的ByteBuffer复制到新创建的ByteBuffer中,最后释放老的ByteBuffer,代码示例如下。

    1.  
      if(this.buffer.remaining() < needSize) {
    2.  
      int toBeExtSize = needSize < 128 ? needSize : 128;
    3.  
      ByteBuffer tmpBuffer = ByteBuffer.allocate(this.buffer.capacity() + toBeExtSize);
    4.  
      this.buffer.flip();
    5.  
      tmpBuffer.put(this.buffer);
    6.  
      this.buffer = tmpBuffer;
    7.  
      }

    从示例代码可以看出,为了防止ByteBuffer溢出,每进行一次put操作,都需要对可用空间进行校验,这导致了代码冗余,稍有不慎,就可能引入其他问题。为了解决这个问题,ByteBuf对write操作进行了封装,由ByteBuf的write操作负责进行剩余可用空间的校验,如果可用缓冲区不足,ByteBuf会自动进行动态扩展,对于使用者而言,不需要关心底层的校验和扩展细节,只要不超过设置的最大缓冲区容量即可。当可用空间不足时,ByteBuf会帮助我们实现自动扩展。

    当进行write操作时会对需要write的字节进行校验,如果可写的字节数小于需要写入的字节数,并且需要写入的字节数小于可写的最大字节数时,对缓冲区进行动态扩展。无论缓冲区是否进行了动态扩展,从功能角度看使用者并不感知,这样就简化了上层的应用。

    ByteBuf源码思路

    由于NIO的Channel读写的参数都是ByteBuffer,因此,Netty的ByteBuf接口必须提供API方便的将ByteBuf转换成ByteBuffer,或者将ByteBuffer包装成ByteBuf。考虑到性能,应该尽量避免缓冲区的复制,内部实现的时候可以考虑聚合一个ByteBuffer的私有指针用来代表ByteBuffer。

    从内存分配的角度看,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提供了灵活的策略供使用者来做选择。

    内存池原理分析 

    Arena本身是指一块区域,在内存管理中,Memory Arena是指内存中的一大块连续的区域,PoolArena就是Netty的内存池实现类。

    为了集中管理内存的分配和释放,同时提高分配和释放内存时候的性能,很多框架和应用都会通过预先申请一大块内存,然后通过提供相应的分配和释放接口来使用内存。这样一来,对内存的管理就被集中到几个类或者函数中,由于不再频繁使用系统调用来申请和释放内存,应用或者系统的性能也会大大提高。在这种设计思路下,预先申请的那一大块内存就被称为Memory Arena。

    不同的框架,Memory Arena的实现不同,Netty的PoolArena是由多个Chunk组成的大块内存区域,而每个Chunk则由一个或者多个Page组成,因此,对内存的组织和管理也就主要集中在如何管理和组织Chunk和Page了。

    PoolChunk 

    Chunk主要用来组织和管理多个Page的内存分配和释放,在Netty中,Chunk中的Page被构建成一棵二叉树。假设一个Chunk由16个Page组成,那么这些Page将会被按照下图所示的形式组织起来。 

    Page的大小是4个字节,Chunk的大小是64个字节(4×16)。整棵树有5层,第1层(也就是叶子节点所在的层)用来分配所有Page的内存,第4层用来分配2个Page的内存,依次类推。

    每个节点都记录了自己在整个Memory Arena中的偏移地址,当一个节点代表的内存区域被分配出去之后,这个节点就会被标记为已分配,自这个节点以下的所有节点在后面的内存分配请求中都会被忽略。举例来说,当我们请求一个16字节的存储区域时,上面这个树中的第3层中的4个节点中的一个就会被标记为已分配,这就表示整个Memroy Arena中有16个字节被分配出去了,新的分配请求只能从剩下的3个节点及其子树中寻找合适的节点。

    对树的遍历采用深度优先的算法,但是在选择哪个子节点继续遍历时则是随机的,并不像通常的深度优先算法中那样总是访问左边的子节点。

    PoolSubpage 

    对于小于一个Page的内存,Netty在Page中完成分配。每个Page会被切分成大小相等的多个存储块,存储块的大小由第一次申请的内存块大小决定。假如一个Page是8个字节,如果第一次申请的块大小是4个字节,那么这个Page就包含2个存储块;如果第一次申请的是8个字节,那么这个Page就被分成1个存储块。 

    一个Page只能用于分配与第一次申请时大小相同的内存,比如,一个4字节的Page,如果第一次分配了1字节的内存,那么后面这个Page只能继续分配1字节的内存,如果有一个申请2字节内存的请求,就需要在一个新的Page中进行分配。

    Page中存储区域的使用状态通过一个long数组来维护,数组中每个long的每一位表示一个块存储区域的占用情况:0表示未占用,1表示以占用。对于一个4字节的Page来说,如果这个Page用来分配1个字节的存储区域,那么long数组中就只有一个long类型的元素,这个数值的低4位用来指示各个存储区域的占用情况。对于一个128字节的Page来说,如果这个Page也是用来分配1个字节的存储区域,那么long数组中就会包含2个元素,总共128位,每一位代表一个区域的占用情况。

    内存回收策略

    无论是Chunk还是Page,都通过状态位来标识内存是否可用,不同之处是Chunk通过在二叉树上对节点进行标识实现,Page是通过维护块的使用状态标识来实现。

    对于使用者来说,不需要关心内存池的实现细节,也不需要与这些类库打交道,只需要按照API说明正常使用即可。

    回到顶部

    辅助类功能介绍

    ByteBufHolder 

    ByteBufHolder是ByteBuf的容器,在Netty中,它非常有用,例如HTTP协议的请求消息和应答消息都可以携带消息体,这个消息体在NIO ByteBuffer中就是个ByteBuffer对象,在Netty中就是ByteBuf对象。由于不同的协议消息体可以包含不同的协议字段和功能,因此,需要对ByteBuf进行包装和抽象,不同的子类可以有不同的实现。为了满足这些定制化的需求,Netty抽象出了ByteBufHolder对象,它包含了一个ByteBuf,另外还提供了一些其他实用的方法,使用者继承ByteBufHolder接口后可以按需封装自己的实现。

    ByteBufAllocator 

    ByteBufAllocator是字节缓冲区分配器,按照Netty的缓冲区实现不同,共有两种不同的分配器:基于内存池的字节缓冲区分配器和普通的字节缓冲区分配器。

    CompositeByteBuf

    CompositeByteBuf允许将多个ByteBuf的实例组装到一起,形成一个统一的视图,有点类似于数据库将多个表的字段组装到一起统一用视图展示。

    CompositeByteBuf在一些场景下非常有用,例如某个协议POJO对象包含两部分:消息头和消息体,它们都是ByteBuf对象。当需要对消息进行编码的时候需要进行整合,如果使用JDK的默认能力,有以下两种方式:

     (1)将某个ByteBuffer复制到另一个ByteBuffer中,或者创建一个新的ByteBuffer,将两者复制到新建的ByteBuffer中;

    (2)通过List或数组等容器,将消息头和消息体放到容器中进行统一维护和处理。

    上面的做法非常别扭,实际上我们遇到的问题跟数据库中视图解决的问题一致——缓冲区有多个,但是需要统一展示和处理,必须有存放它们的统一容器。为了解决这个问题,Netty提供了CompositeByteBuf。

    ByteBufUtil 

    ByteBufUtil是一个非常有用的工具类,它提供了一系列静态方法用于操作ByteBuf对象。

    其中最有用的方法就是对字符串的编码和解码,具体如下。

    (1)encodeString(ByteBufAllocator alloc, CharBuffer src, Charset charset):对需要编码的字符串src按照指定的字符集charset进行编码,利用指定的ByteBufAllocator生成一个新的ByteBuf;

    (2)decodeString(ByteBuffer src, Charset charset):使用指定的ByteBuffer和charset进行对ByteBuffer进行解码,获取解码后的字符串。

    还有一个非常有用的方法就是hexDump,它能够将参数ByteBuf的内容以十六进制字符串的方式打印出来,用于输出日志或者打印码流,方便问题定位,提升系统的可维护性。hexDump包含了一系列的方法,参数不同,输出的结果也不同。

    转载自:https://www.cnblogs.com/wade-luffy/p/6196481.html

  • 相关阅读:
    sql2slack alash3al 开源的又个轻量级工具
    pgspider fetchq 扩展docker镜像
    godns 集成coredns 的demo
    godns 简单dnsmasq 的dns 替换方案
    aviary.sh 一个基于bash的分布式配置管理工具
    使用coredns 的template plugin实现一个xip 服务
    nginx 代理 coredns dns 服务
    基于nginx proxy dns server
    几个不错的geodns server
    spring boot rest api 最好添加servlet.context-path
  • 原文地址:https://www.cnblogs.com/heshana/p/13970091.html
Copyright © 2011-2022 走看看