1. ByteBuf的创建

在Netty中,有一个比较常见的对象ByteBuf,它其实等同于Java Nio中的ByteBuffer,但是ByteBuf对Nio中的ByteBuffer的功能做了很作增强,下面我们来简单了解一下ByteBuf。

下面这段代码演示了ByteBuf的创建以及内容的打印,这里显示出了和普通ByteBuffer最大的区别之一,就是ByteBuf可以自动扩容,默认长度是256,如果内容长度超过阈值时,会自动触发扩容。

public class ByteBufExample {public static void main(String[] args) {ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();//可自动扩容log(buf); //打印StringBuilder sb = new StringBuilder();for (int i = 0; i < 111; i++) { //演示的时候,可以把循环的值扩大,就能看到扩容效果sb.append(" - " + i);}buf.writeBytes(sb.toString().getBytes());log(buf);}private static void log(ByteBuf buf) {StringBuilder builder = new StringBuilder().append(" read index:").append(buf.readerIndex()) //获取读索引.append(" write index:").append(buf.writerIndex()) //获取写索引.append(" capacity:").append(buf.capacity()) //获取容量.append(StringUtil.NEWLINE); //把ByteBuf中的内容,dump到StringBuilder中ByteBufUtil.appendPrettyHexDump(builder, buf);System.out.println(builder.toString());}
}

ByteBuf创建的方法有两种

  • 创建基于堆内存的ByteBuf

    ByteBuf byteBuf = ByteBufAllocator.DEFAULT.heapBuffer();
    
  • 创建基于直接内存(堆外内存)的ByteBuf(默认情况下用的是这种

    Java中的内存分为两个部分,一部分是不需要jvm管理的直接内存,也被称为堆外内存。堆外内存就是把内存对象分配在JVM堆意外的内存区域,这部分内存不是虚拟机管理,而是由操作系统来管理,这样可以减少垃圾回收对应用程序的影响。

    ByteBufAllocator.DEFAULT.directBuffer();
    

    直接内存的好处是读写性能会高一些,如果数据存放在堆中,此时需要把Java堆空间的数据发送到远程服务器,首先需要把堆内部的数据拷贝到直接内存(堆外内存),然后再发送。如果是把数据直接存储到堆外内存中,发送的时候就少了一个复制步骤。

    但是它也有缺点,由于缺少了JVM的内存管理,所以需要我们自己来维护堆外内存,防止内存溢出。

另外,需要注意的是,ByteBuf默认采用了池化技术来创建。关于池化技术(类似线程池),它的核心思想是实现对象的复用,从而减少对象频繁创建销毁带来的性能开销。

池化功能是否开启,可以通过下面的环境变量来控制,其中unpooled表示不开启。

-Dio.netty.allocator.type={unpooled|pooled}

2. ByteBuf的存储结构

ByteBuf的存储结构如下图所示,从这个图中可以看到ByteBuf其实是一个字节容器,该容器中包含三个部分:

  • 已经丢弃的字节,这部分数据是无效的

  • 可读字节,这部分数据是ByteBuf的主体数据,从ByteBuf里面读取的数据都来自这部分;

    可写字节,所有写到ByteBuf的数据都会存储到这一段;

  • 可扩容字节,表示ByteBuf最多还能扩容多少容量。

在ByteBuf中,有两个指针:

  • readerIndex: 读指针,每读取一个字节,readerIndex自增加1。ByteBuf里面总共有writeIndex-readerIndex个字节可读,当readerIndex和writeIndex相等的时候,ByteBuf不可读。
  • writeIndex: 写指针,每写入一个字节,writeIndex自增加1,直到增加到capacity后,可以触发扩容后继续写入。
  • ByteBuf中还有一个maxCapacity最大容量,默认的值是 Integer.MAX_VALUE ,当ByteBuf写入数据时,如果容量不足时,会触发扩容,直到capacity扩容到maxCapacity。

3. ByteBuf中常用的方法

对于ByteBuf来说,常见的方法就是写入和读取。

3.1 Write相关方法

对于write方法来说,ByteBuf提供了针对各种不同数据类型的写入,比如:

  • writeChar,写入char类型

  • writeInt,写入int类型

  • writeFloat,写入float类型

  • writeBytes, 写入nio的ByteBuffer

  • writeCharSequence, 写入字符串

public class ByteBufCreateExample {public static void main(String[] args) {ByteBuf buf = ByteBufAllocator.DEFAULT.heapBuffer();buf.writeBytes(new byte[]{1, 2, 3, 4});log(buf);buf.writeInt(5);log(buf);}private static void log(ByteBuf buf) {StringBuilder sb = new StringBuilder();sb.append(" read index:").append(buf.readerIndex());  //读索引sb.append(" write index:").append(buf.writerIndex()); //写索引sb.append(" capacity :").append(buf.capacity()); //容量ByteBufUtil.appendPrettyHexDump(sb, buf);System.out.println(sb.toString());}
}

3.2 扩容

当向ByteBuf写入数据时,发现容量不足时,会触发扩容,而具体的扩容规则是:

假设ByteBuf初始容量是10。

  • 如果写入后数据大小未超过512个字节,则选择下一个16的整数倍进行库容。 比如写入数据后大小为12,则扩容后的capacity是16。

  • 如果写入后数据大小超过512个字节,则选择下一个2.n。 比如写入后大小是512字节,则扩容后的capacity是2.10=1024 。(因为2.9次方=512,长度已经不够了)

  • 扩容不能超过max capacity,否则会报错。

3.3 Reader相关方法

reader方法也同样针对不同数据类型提供了不同的操作方法:

  • readByte ,读取单个字节

  • readInt , 读取一个int类型

  • readFloat ,读取一个float类型

public class ByteBufCreateExample {public static void main(String[] args) {ByteBuf buf = ByteBufAllocator.DEFAULT.heapBuffer();buf.writeBytes(new byte[]{1, 2, 3, 4});log(buf);System.out.println("开始进行读取操作");byte b = buf.readByte();System.out.println(b);log(buf);}private static void log(ByteBuf buf) {StringBuilder sb = new StringBuilder();sb.append(" read index:").append(buf.readerIndex());  //读索引sb.append(" write index:").append(buf.writerIndex()); //写索引sb.append(" capacity :").append(buf.capacity()); //容量ByteBufUtil.appendPrettyHexDump(sb, buf);System.out.println(sb.toString());}
}

从下面结果中可以看到,读完一个字节后,这个字节就变成了废弃部分,再次读取的时候只能读取未读取的部分数据。

另外,如果想重复读取哪些已经读完的数据,这里提供了两个方法来实现标记和重置。

    public static void main(String[] args) {ByteBuf buf = ByteBufAllocator.DEFAULT.heapBuffer();buf.writeBytes(new byte[]{1, 2, 3, 4});log(buf);System.out.println("开始进行读取操作");buf.markReaderIndex(); //标记索引位置.  markWriterIndex()byte b = buf.readByte();System.out.println(b);buf.resetReaderIndex(); //重新回到标记位置log(buf);}

另外,如果想不改变读指针位置来获得数据,在ByteBuf中提供了 get 开头的方法,这个方法基于索引位置读取,并且允许重复读取的功能。

3.4 ByteBuf的零拷贝机制

需要说明一下,ByteBuf的零拷贝机制和我们之前提到的操作系统层面的零拷贝不同,操作系统层面的零拷贝,是我们要把一个文件发送到远程服务器时,需要从内核空间拷贝到用户空间,再从用户空间拷贝到内核空间的网卡缓冲区发送,导致拷贝次数增加。

而ByteBuf中的零拷贝思想也是相同,都是减少数据复制提升性能。如下图所示,假设有一个原始ByteBuf,我们想对这个ByteBuf其中的两个部分的数据进行操作。按照正常的思路,我们会创建两个新的ByteBuf,然后把原始ByteBuf中的部分数据拷贝到两个新的ByteBuf中,但是这种会涉及到数据拷贝,在并发量较大的情况下,会影响到性能。

ByteBuf中提供了一个slice方法,这个方法可以在不做数据拷贝的情况下对原始ByteBuf进行拆分,使用方法如下:

public class ByteBufCopyExample {public static void main(String[] args) {ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();buf.writeBytes(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10});log(buf);//从buf这个总的数据中,分别拆分5个字节保存到两个ByteBuf中//拆分 零拷贝机制    (浅克隆) 索引及长度ByteBuf bb1 = buf.slice(0, 5);ByteBuf bb2 = buf.slice(5, 5);log(bb1);log(bb2);System.out.println("修改原始数据");buf.setByte(2, 8);log(bb1);}private static void log(ByteBuf buf) {StringBuilder sb = new StringBuilder();sb.append(" read index:").append(buf.readerIndex());  //读索引sb.append(" write index:").append(buf.writerIndex()); //写索引sb.append(" capacity :").append(buf.capacity()); //容量ByteBufUtil.appendPrettyHexDump(sb, buf);System.out.println(sb.toString());}}

在上面的代码中,通过slice对原始buf进行切片,每个分片是5个字节。

为了证明slice是没有数据拷贝,我们通过修改原始buf的索引2所在的值,然后再打印第一个分片bb1,可以发现bb1的结果发生了变化。说明两个分片和原始buf指向的数据是同一个。类似于我们的浅克隆。

3.5 Unpooled

在前面的案例中我们经常用到Unpooled工具类,它里面有非池化的ByteBuf的创建、组合、复制等操作。

假设有一个协议数据,它有头部和消息体组成,这两个部分分别放在两个ByteBuf中:

ByteBuf header=...
ByteBuf body= ...

我们希望把header和body合并成一个ByteBuf,通常的做法是:

ByteBuf total= Unpooled.buffer(header.readableBytes()+body.readableBytes());
total.writeBytes(header);
total.writeBytes(body);

在这个过程中,我们把header和body拷贝到了新的allBuf中,这个过程在无形中增加了两次数据拷贝操作。那有没有更高效的方法减少拷贝次数来达到相同目的呢?

在Netty中,提供了一个CompositeByteBuf组件,它提供了这个功能。

public class CompositeByteBufExample {// 合并 也是零拷贝的实现public static void main(String[] args) {ByteBuf header = ByteBufAllocator.DEFAULT.buffer();header.writeBytes(new byte[]{1, 2, 3, 4, 5});ByteBuf body = ByteBufAllocator.DEFAULT.buffer();body.writeBytes(new byte[]{6, 7, 8, 9, 10});CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer();//其中第一个参数是 true, 表示当添加新的 ByteBuf 时, 自动递增 CompositeByteBuf 的 writeIndex.//默认是false,也就是writeIndex=0,这样的话我们不可能从compositeByteBuf中读取到数据。compositeByteBuf.addComponents(true, header, body);log(compositeByteBuf);}private static void log(ByteBuf buf) {StringBuilder sb = new StringBuilder();sb.append(" read index:").append(buf.readerIndex());  //读索引sb.append(" write index:").append(buf.writerIndex()); //写索引sb.append(" capacity :").append(buf.capacity()); //容量ByteBufUtil.appendPrettyHexDump(sb, buf);System.out.println(sb.toString());}}

之所以CompositeByteBuf能够实现零拷贝,是因为在组合header和body时,并没有对这两个数据进行复制,而是通过CompositeByteBuf构建了一个逻辑整体,里面仍然是两个真实对象,也就是有一个指针指向了同一个对象,所以这里类似于浅拷贝的实现。

3.6 wrappedBuffer

在Unpooled工具类中,提供了一个wrappedBuffer方法,来实现CompositeByteBuf零拷贝功能。使用方法如下:

ByteBuf total = Unpooled.wrappedBuffer(header, body);

4. 内存释放

针对不同的ByteBuf创建,内存释放的方法不同。

  • UnpooledHeapByteBuf,使用JVM内存,只需要等待GC回收即可

  • UnpooledDirectByteBuf,使用对外内存,需要特殊方法来回收内存

  • PooledByteBuf和它的子类使用了池化机制,需要更复杂的规则来回收内存

如果ByteBuf是使用堆外内存来创建,那么尽量手动释放内存,那怎么释放呢?

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

  • 每个ByteBuf对象的初始计数为1

  • 调用release方法时,计数器减一,如果计数器为0,ByteBuf被回收

  • 调用retain方法时,计数器加一,表示调用者没用完之前,其他handler即时调用了release也不会造成回收。

  • 当计数器为0时,底层内存会被回收,这时即使ByteBuf对象还存在,但是它的各个方法都无法正常使用

Netty之ByteBuf详解相关推荐

  1. Netty 教程 – ByteBuf详解

    ByteBuffer存在的问题 ByteBuffer是JDK1.4中提供的java.nio.Buffer, 在内存中预留指定大小的存储空间来存放临时数据,其他Buffer的子类有:CharBuffer ...

  2. netty系列之:netty中的ByteBuf详解

    文章目录 简介 ByteBuf详解 创建一个Buff 随机访问Buff 序列读写 搜索 其他衍生buffer方法 和现有JDK类型的转换 总结 简介 netty中用于进行信息承载和交流的类叫做Byte ...

  3. java 检查bytebuf长度_Java学习笔记16-Netty缓冲区ByteBuf详解

    Java学习笔记16-Netty缓冲区ByteBuf详解 Netty自己的ByteBuf ByteBuf是为解决ByteBuffer的问题和满足网络应用程序开发人员的日常需求而设计的. JDK Byt ...

  4. netty源码之ByteBuf详解

    一.背景简介 ByteBuf,顾名思义,就是字节缓冲区,是Netty中非常重要的一个组件.熟悉jdk NIO的同学应该知道ByteBuffer,正是因为jdk原生ByteBuffer使用比较复杂,某些 ...

  5. Netty之Bootstrap详解

    本文来重点说下Bootstrap 文章目录 概述 Boostrap类 引导客户端和无连接协议Booststrap 引导服务端ServerBootstrap 本文小结 概述 在了解 ChanelPipe ...

  6. 消息 ByteBuf 详解

    Netty提供了ByteBuf来替代Java NIO的ByteBuffer缓冲区,以操纵内存缓冲区. 与Java NIO的ByteBuffer相比,ByteBuf的优势如下: · Pooling(池化 ...

  7. 六个月离职空档期获得足够时间反思自我+常见Netty面试题详解

    前言 疫情期我离职了,在这个时间离职也是很无奈.但这次的经历却让我有了一个认真反思的机会. 回顾前两年的工作经历,每天忙忙碌碌,看似解决了很多问题,也积累了很多经验,但实际都是一些浅层次的东西,只不过 ...

  8. Netty 教程 – 解码器详解

    TCP以流的方式进行数据传输,上层的应用为了对消息进行区分,往往采用如下方式 固定消息长度,累计读取到长度和定长LEN的报文后,就认为读取到了个完整的消息,然后将计数器位置重置在读取下一个报文内容 将 ...

  9. ByteBuf 详解

    进行数据传输 ,需要使用缓冲区.ByteBuf是一个Byte数组的缓冲区,通过两个指针来协助读写操作.以下ri标识readIndex wi标识writeIndex. 1.1 工作原理 工作原理 图示原 ...

最新文章

  1. centos 重启网络服务的方法
  2. 搭建SSH框架之一(资料准备)
  3. 微软等数据结构+算法面试100题全部答案集锦
  4. FastDFS设置开机启动
  5. linux 系统 安装 nginx 服务
  6. 台湾大学林轩田机器学习基石课程学习笔记6 -- Theory of Generalization
  7. 编写python程序、创建名为class的数据库_python面向对象编程class1
  8. 想法越多越贫穷,赚钱的人,都是少想多干
  9. C#之ActionBlock异步关闭死锁
  10. java 读取配置文件的几种方法
  11. 没有电脑却想运行代码?有手机就够了
  12. RadAsm + OD 搭配编写和调试汇编程序
  13. hadoop key和value 分隔符号设置
  14. 【哈士奇赠书活动 - 23期】-〖你好 ChatGPT〗
  15. View被遮挡的解决办法
  16. 金蝶EAS,序时簿界面ListUI数据不允许修改、删除
  17. python使用selenium + PhantomJs搭建的简单漫画爬虫工具
  18. 基于jaccard计算论文对的reference相似度的算法(2)
  19. 学习ARM开发(2)
  20. Java 之父:找Bug最浪费时间,现在不是开源的黄金时代!

热门文章

  1. [附源码]Node.js计算机毕业设计菜鸟驿站快递分发系统Express
  2. Java强密码正则表达式
  3. 刚到手!来看看这款10公里激光测距仪的效果咋样子吧!
  4. 使用canal实时同步MySQL数据到Elasticsearch
  5. 计算机组成原理学习笔记—— 高速缓冲存储器Cache
  6. Flutter、Dart实现农历(阴历)转公历(阳历)
  7. 根据ip获取服务器详细信息,服务器根据ip获取地址
  8. moveTo与moveBy的区别
  9. 2018年11月1日——产品面试总结
  10. 作文未来计算机400字,未来作文400字大全