Netty的bytebuf详解

news/2024/10/17 14:30:11/

ByteBuf
ByteBuf是对nio中ByteBuffer的增强。主要的增强点就是ByteBuf它可以动态调整容量大小,当要存储的数据超过了当前容量的上限就会进行扩容,扩容的上限是多少?扩容机制是什么?请跟着本文往下看。对了,还有一个增强就是byteBuf不用和ByteBuffer一样进行读写模式的切换,ByteBuffer中读与写是共用一个指针,而ByteBuf既有读指针,也有写指针。


创建
ByteBufAllocator类中有一个默认实现,我们可以使用这个默认实现来创建一个Bytebuf

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;public class ByteBufCreateTest {public static void main(String[] args) {ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();}
}

这里可以指定ByteBuf的容量,可以传一个int,如果不指定的话默认是256。

这里比nio好的一个地方就是,netty的Bytebuf容量的可以动态扩容的,而nio的ByteBuffer指定了就不能动了。

然后往ByteBuf中添加数据,验证是否会扩容

public static void main(String[] args) {// 创建一个Bytebuf,默认创建的容量是256ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();System.out.println("添加数据前:" + buffer);// 往Bytebuf中写数据StringBuilder stringBuilder = new StringBuilder();// 故意超过初始容量,验证是否会自动扩容for (int i = 0; i < 300; i++) {stringBuilder.append("a");}// 将数据写入ByteBufbuffer.writeBytes(stringBuilder.toString().getBytes());System.out.println("添加数据后:" + buffer);
}

输出结果为:PooledUnsafeDirectByteBuf(读指针 写指针 容量)

添加数据前:PooledUnsafeDirectByteBuf(ridx: 0, widx: 0, cap: 256)

添加数据后:PooledUnsafeDirectByteBuf(ridx: 0, widx: 300, cap: 512)

上面的方式只是平时可以这样创建Bytebuf,我们一般情况下都是在Handler中创建ByteBuf,建议使用下面的方式创建ByteBuf
在这里插入图片描述

 

内存分配与池化
ByteBuf支持堆内存,也支持直接内存。

分配堆内存 ByteBufAllocator.DEFAULT.heapBuffer();

分配直接内存 ByteBufAllocator.DEFAULT.directBuffer();

堆内存的分配效率高,但是读写效率低。直接内存读写效率高。nettty默认采用的是直接内存,也就是上面创建ByteBuf时直接使用的

ByteBufAllocator.DEFAULT.buffer(); 获取的是直接内存。

netty中的ByteBuf支持一种池化的管理,netty默认情况下获取的ByteBuf都是从池中获取的,如果不想要从池中获取需要在idea启动配置的地方加一个-Dio.netty.allocator.type=upooled参数

在这里插入图片描述

 

ByteBuf组成

在这里插入图片描述

 

刚开始,读写两个指针都在0的位置。

ByteBuf有读指针和写指针,不像nio中的ByteBuffer公用一个指针 然后还要进行读写模式切换。

Bytebuf如果容量不够了可以动态扩容,但最大容量不能超过int的最大值。

上图中的四块区域分为:废弃区、可读区、可写区、可扩容区

四个属性为:读指针,写指针,容量,最大容量

扩容规则
如果写入的数据没有超过512,则选择的是下一个16的整数倍,例如现在容量是12,进行一次扩容会变为16,如果又不够了就会变为32

而如果写入后的数据大小超过了512,则选择下一个2n。例如写入后的大小为513,库容后的大小就变为210=1024,因为2^9=512不够

但是扩容不能超过max capacity,否则会报错
 

写入

写入的常用方法如下:

以下的方法会改变写指针位置,还有一类以set开头的方法,也可以写数据,但是不会改变写指针的位置

在这里插入图片描述

读取
buffer.readByte() 每次读取一个字节

buffer.readInt() 每次读取一个整数,也就是四个字节

buffer.markReaderIndex() 为读指针做一个标记,配合下面的方法可以实现重复读取某个数

buffer.resetReaderIndex() 将读指针跳到上一个标记过的地方实现重复读取某个数。

除了上面一些了read开头的方法以外,还有一系列get开头的方法也可以读取数据,只不过get开头的方法不会改变读指针位置。相当于是按索引去获取。

内存回收
ByteBuf有几种实现方式,针对这几种ByteBuf实现的机制也不同

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

 不同的ByteBuf的实现,内存回收也不一样,还好netty提供了统一的接口ReferenceCounted接口,该接口提供了通用的方法来进行上面几种ByteBuf的内存回收,该接口的工作方式是采用引用计数的规则

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

谁应该来调用release()方法嘞?

应该由最后一个使用ByteBuf的入站Handler来调用release()方法,因为可能出现前面一个handler释放掉了ByteBuf,下一个handler就使用不了了。
在这里插入图片描述

 如果入站Handler使用的ByteBuf最后传递给了tail,tail会自动释放掉ByteBuf,但是有可能前面的handler处理了数据,然后传递给下一个handler的不是ByteBuf而是处理后的数据,这样最后的tail由于接受的参数不是ByteBuf,所以也没办法释放。出站Handler最后传递给head也会自动自动释放ByteBuf。
头尾handler释放ByteBuf的源码分析
在TailContext类中的channelRead(),然后再跟进方法,多点几层,然后会找到如下所示的方法。
在这里插入图片描述

零拷贝
应用的场景是我要分别处理一个大的ByteBuf中的几段数据,也就是将这一个ByteBuf拆分为几个小的ByteBuf。当然可以直接创建介个小的ByteBuf,然后进行赋值,但是这样有数据拷贝,有效率问题。而零拷贝就是直接将一个大的ByteBuf拆分为几个小的,这里只是逻辑上的拆分,他们共用同一块内存区域。各个小的ByteBuf有自己的读写指针。
在这里插入图片描述

 

其他几个不常用的方法

duplicate() 截取原始ByteBuf的所有内容,而slice()方法只是截取一部分。底层和原始ByteBuf也是使用的同一块内存,也有自己的读写指针。

copeXXXXX 与零拷贝对应的就是一系列以cope开头的方法,也是创建新的ByteBuf,只是会进行数据拷贝,读写都与原始ByteBuf无关

上面是将一个大的ByteBuf切分为几个小的ByteBuf,下面还有将几个小的ByteBuf零拷贝组合成一个大的ByteBuf。

也就是调用compositeBuffer()创建ByteBuf,然后调用addComponents()方法进行添加。
在这里插入图片描述

 

然后运行,发现数据并没有写入新的ByteBuf中,这是因为addComponents() 可变参数 或者是空参的这个方法默认不会去跳转写指针的位置。需要在该方法的参数1位置加一个boolean类型的参数true就可以了。

buffer.addComponents(true, buf1, buf2); 这个就表示写指针会自动增长。这样就可以正确的将两个ByteBuf组合到一起。

使用这个也需要注意release()的问题。

Unpooled是一个工具类,提供了非池化的 ByteBuf创建、组合、复制等操作。这里有一个关于零拷贝相关的方法

Unpooled.wrappedBuffer(ByteBuf buf...)方法,可以将多个Bytebuf组合成一个Bytebuf,底层使用的是compositeBuffer()方法
 

ByteBuf优势

  • 池化,可以重用池中的Bytebuf
  • 自动扩容,容量不够时会自动扩容,但是不会超过int的最大值
  • 读写指针分离,避免了nio中的切换读写模式
  • 支持链式调用
  • 很多地方体现零拷贝,比如slice、duplicate、compositeBuffer、

一个小知识点:java Socket是全双工的,在任意时刻读写都可以同时进行,即使的阻塞io,读写也可以同时进行,只要分别采用两个线程分别处理读写即可。
 


http://www.ppmy.cn/news/248379.html

相关文章

Linux命令su、sudo、sudo su、sudo -i使用和区别

sudo 与 su 两个命令的最大区别是&#xff1a; sudo 命令需要输入当前用户的密码&#xff0c;su 命令需要输入 root 用户的密码。另外一个区别是其默认行为&#xff0c;sudo 命令只允许使用提升的权限运行单个命令&#xff0c;而 su 命令会启动一个新的 shell&#xff0c;同时…

FPGA DAC AD9764调试

AD9764 时钟频率125M 14位数据位 数值电压/V8192016384-30313653.3-227302 实测8267 电压近似为0

MySQL学习12_rpm安装MySQL报** is needed by **错误

使用rpm -ivh MySQL-server-5.6.26-1.linux_glibc2.5.x86_64.rpm命令&#xff0c;安装MySQL时&#xff0c;遇到了下面的错误&#xff1a; [rootMaster mysql]# rpm -ivh MySQL-server-5.6.26-1.linux_glibc2.5.x86_64.rpm warning: MySQL-server-5.6.26-1.linux_glibc2.5.x86_6…

前端学习(2730):重读vue电商网站40之使用vue-table-with-tree-grid

安装新的依赖 vue-tabel-with-tree-gridvue-tabel-with-tree-grid 官方文档 安装完成后&#xff0c;在 main.js 入口文件内先导入 tree-tabel 然后全局注册组件 tree-tabel 页面中&#xff0c;我们使用了如下属性&#xff1a; data 确定我们的数据源&#xff0c;columns定义我…

百练:2729:求12以内n的阶乘 2730:求20以内n的阶乘 2731:求10000以内n的阶乘

2729:求12以内n的阶乘 #include<iostream> using namespace std; int main() { int n,sum1; scanf("%d",&n); for(int i1;i<n;i) sumsum*i; printf("%d",sum); return 0; } 2730:求20以内n的阶乘 #include<iostre…

oracle 27140,ORA-27140 ORA-27300 ORA-27301

查看节点1crs状态 [rootnode1 ~]# /oracle/app/grid/bin/crs_stat -t CRS-0184: Cannot communicate with the CRS daemon. 检查下ocr [rootnode1 bin]# ./ocrcheck PROT-602: Failed to retrieve data from the cluster registry PROC-26: Error while accessing the physical…

2020兰洽会VR全景展馆超千万人线上体验,签约总额2730亿元

万商云集&#xff0c;八方赴会。2020年7月2日-5日&#xff0c;以“深化经贸合作&#xff0c;共促绿色发展”为主题的第26届中国兰州投资贸易洽谈会在甘肃兰州如期举办。在商务部、国家市场监管总局、国务院台办、全国工商联、中国侨联、中国贸促会的大力推动下&#xff0c;此次…

PSP表格

PSP2.1Personal Software Process Stages预估耗时&#xff08;分钟&#xff09;实际耗时&#xff08;分钟&#xff09;Planning计划3060 Estimate 估计这个任务需要多少时间16902730Development开发7201040 Analysis 需求分析 (包括学习新技术)3030 Design Spec 生成设计文档30…