Netty专栏 ( 三)——— Netty的ByteBuf

2024-03-31 06:48
文章标签 netty 专栏 bytebuf

本文主要是介绍Netty专栏 ( 三)——— Netty的ByteBuf,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

@author 鲁伟林
记录《Netty 实战》中各章节学习过程,写下一些自己的思考和总结,帮助使用Netty框架的开发技术人员们,能够有所得,避免踩坑。
本博客目录结构将严格按照书本《Netty 实战》,省略与Netty无关的内容,可能出现跳小章节。
本博客中涉及的完整代码:
GitHub地址: https://github.com/thinkingfioa/netty-learning/tree/master/netty-in-action。
本人博客地址: https://blog.csdn.net/thinking_fioa

第5章 ByteBuf

Netty提供的ByteBuf与JDK的ByteBuffer相比,前者具有卓越的功能性和灵活性。

5.1 ByteBuf的API

ByteBuf提供读访问索引(readerIndex)和写访问索引(writerIndex)来控制字节数组。ByteBuf API具有以下优点:

  1. 允许用户自定义缓冲区类型扩展
  2. 通过内置的复合缓冲区类型实现透明的零拷贝
  3. 容量可按需增长
  4. 读写这两种模式之间不需要调用类似于JDK的ByteBuffer的flip()方法进行切换
  5. 读和写使用不同的索引
  6. 支持方法的链式调用
  7. 支持引用计数
  8. 支持池化

5.2 ByteBuf类 ----- Netty的数据容器

5.2.1 ByteBuf如何工作的

ByteBuf维护两个不同的索引: 读索引(readerIndex)和写索引(writerIndex)。如下图: 

  1. ByteBuf维护了readerIndex和writerIndex索引
  2. 当readerIndex > writerIndex时,则抛出IndexOutOfBoundsException
  3. ByteBuf容量 = writerIndex。
  4. ByteBuf可读容量 = writerIndex - readerIndex
  5. readXXX()和writeXXX()方法将会推进其对应的索引。自动推进
  6. getXXX()和setXXX()方法将对writerIndex和readerIndex无影响

5.2.2 ByteBuf的使用模式

ByteBuf本质是: 一个由不同的索引分别控制读访问和写访问的字节数组。请记住这句话。ByteBuf共有三种模式: 堆缓冲区模式(Heap Buffer)、直接缓冲区模式(Direct Buffer)和复合缓冲区模式(Composite Buffer)

1. 堆缓冲区模式(Heap Buffer)

堆缓冲区模式又称为:支撑数组(backing array)。将数据存放在JVM的堆空间,通过将数据存储在数组中实现

  • 堆缓冲的优点: 由于数据存储在Jvm堆中可以快速创建和快速释放,并且提供了数组直接快速访问的方法
  • 堆缓冲的缺点: 每次数据与I/O进行传输时,都需要将数据拷贝到直接缓冲区

代码:

public static void heapBuffer() {// 创建Java堆缓冲区ByteBuf heapBuf = Unpooled.buffer(); if (heapBuf.hasArray()) { // 是数组支撑byte[] array = heapBuf.array();int offset = heapBuf.arrayOffset() + heapBuf.readerIndex();int length = heapBuf.readableBytes();handleArray(array, offset, length);}
}

2. 直接缓冲区模式(Direct Buffer)

Direct Buffer属于堆外分配的直接内存,不会占用堆的容量。适用于套接字传输过程,避免了数据从内部缓冲区拷贝到直接缓冲区的过程,性能较好

  • Direct Buffer的优点: 使用Socket传递数据时性能很好,避免了数据从Jvm堆内存拷贝到直接缓冲区的过程。提高了性能
  • Direct Buffer的缺点: 相对于堆缓冲区而言,Direct Buffer分配内存空间和释放更为昂贵
  • 对于涉及大量I/O的数据读写,建议使用Direct Buffer。而对于用于后端的业务消息编解码模块建议使用Heap Buffer

代码:

public static void directBuffer() {ByteBuf directBuf = Unpooled.directBuffer();if (!directBuf.hasArray()) {int length = directBuf.readableBytes();byte[] array = new byte[length];directBuf.getBytes(directBuf.readerIndex(), array);handleArray(array, 0, length);}
}

3. 复合缓冲区模式(Composite Buffer)

Composite Buffer是Netty特有的缓冲区。本质上类似于提供一个或多个ByteBuf的组合视图,可以根据需要添加和删除不同类型的ByteBuf。

  • 想要理解Composite Buffer,请记住:它是一个组合视图。它提供一种访问方式让使用者自由的组合多个ByteBuf,避免了拷贝和分配新的缓冲区。
  • Composite Buffer不支持访问其支撑数组。因此如果要访问,需要先将内容拷贝到堆内存中,再进行访问
  • 下图是将两个ByteBuf:头部+Body组合在一起,没有进行任何复制过程。仅仅创建了一个视图

代码:

public static void byteBufComposite() {// 复合缓冲区,只是提供一个视图CompositeByteBuf messageBuf = Unpooled.compositeBuffer();ByteBuf headerBuf = Unpooled.buffer(); // can be backing or directByteBuf bodyBuf = Unpooled.directBuffer();   // can be backing or directmessageBuf.addComponents(headerBuf, bodyBuf);messageBuf.removeComponent(0); // remove the headerfor (ByteBuf buf : messageBuf) {System.out.println(buf.toString());}
}

5.3 字节级操作

5.3.1 随机访问索引

ByteBuf的索引与普通的Java字节数组一样。第一个字节的索引是0,最后一个字节索引总是capacity()-1。请记住下列两条,非常有用:

  • readXXX()和writeXXX()方法将会推进其对应的索引readerIndex和writerIndex。自动推进
  • getXXX()和setXXX()方法用于访问数据,对writerIndex和readerIndex无影响

代码:

public static void byteBufRelativeAccess() {ByteBuf buffer = Unpooled.buffer(); //get reference form somewherefor (int i = 0; i < buffer.capacity(); i++) {byte b = buffer.getByte(i);// 不改变readerIndex值System.out.println((char) b);}
}

5.3.2 顺序访问索引

Netty的ByteBuf同时具有读索引和写索引,但JDK的ByteBuffer只有一个索引,所以JDK需要调用flip()方法在读模式和写模式之间切换。

  •  ByteBuf被读索引和写索引划分成3个区域:可丢弃字节区域,可读字节区域和可写字节区域 

5.3.3 可丢弃字节区域

可丢弃字节区域是指:[0,readerIndex)之间的区域。可调用discardReadBytes()方法丢弃已经读过的字节。

  1. discardReadBytes()效果 ----- 将可读字节区域(CONTENT)[readerIndex, writerIndex)往前移动readerIndex位,同时修改读索引和写索引。

  2. discardReadBytes()方法会移动可读字节区域内容(CONTENT)。如果频繁调用,会有多次数据复制开销,对性能有一定的影响

5.3.4 可读字节区域

可读字节区域是指:[readerIndex, writerIndex)之间的区域。任何名称以read和skip开头的操作方法,都会改变readerIndex索引。

5.3.5 可写字节区域

可写字节区域是指:[writerIndex, capacity)之间的区域。任何名称以write开头的操作方法都将改变writerIndex的值。

5.3.6 索引管理

1. markReaderIndex()+resetReaderIndex() ----- markReaderIndex()是先备份当前的readerIndex,resetReaderIndex()则是将刚刚备份的readerIndex恢复回来。常用于dump ByteBuf的内容,又不想影响原来ByteBuf的readerIndex的值

2. readerIndex(int) ----- 设置readerIndex为固定的值

3. writerIndex(int) ----- 设置writerIndex为固定的值

4. clear() ----- 效果是: readerIndex=0, writerIndex(0)。不会清除内存

5. 调用clear()比调用discardReadBytes()轻量的多。仅仅重置readerIndex和writerIndex的值,不会拷贝任何内存,开销较小。

5.3.7 查找操作(indexOf)

查找ByteBuf指定的值。类似于,String.indexOf("str")操作

1. 最简单的方法 ----- indexOf()

2. 利用ByteProcessor作为参数来查找某个指定的值。

代码:

public static void byteProcessor() {ByteBuf buffer = Unpooled.buffer(); //get reference form somewhere// 使用indexOf()方法来查找buffer.indexOf(buffer.readerIndex(), buffer.writerIndex(), (byte)8);// 使用ByteProcessor查找给定的值int index = buffer.forEachByte(ByteProcessor.FIND_CR);
}

5.3.8 派生缓冲区 ----- 视图

派生缓冲区为ByteBuf提供了一个访问的视图。视图仅仅提供一种访问操作,不做任何拷贝操作。下列方法,都会呈现给使用者一个视图,以供访问:

1. duplicate() 

2. slice()

3. slice(int, int)

4. Unpooled.unmodifiableBuffer(...)

5. Unpooled.wrappedBuffer(...)

6. order(ByteOrder)

7. readSlice(int)

理解

1. 上面的6中方法,都会返回一个新的ByteBuf实例,具有自己的读索引和写索引。但是,其内部存储是与原对象是共享的。这就是视图的概念

2. 请注意:如果你修改了这个新的ByteBuf实例的具体内容,那么对应的源实例也会被修改,因为其内部存储是共享的

3. 如果需要拷贝现有缓冲区的真实副本,请使用copy()或copy(int, int)方法。

4. 使用派生缓冲区,避免了复制内存的开销,有效提高程序的性能

代码:

public static void byteBufSlice() {Charset utf8 = Charset.forName("UTF-8");ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);ByteBuf sliced = buf.slice(0, 15);System.out.println(sliced.toString(utf8));buf.setByte(0, (byte)'J');assert buf.getByte(0) == sliced.getByte(0); // return true
}public static void byteBufCopy() {Charset utf8 = Charset.forName("UTF-8");ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);ByteBuf copy = buf.copy(0, 15);System.out.println(copy.toString(utf8));buf.setByte(0, (byte)'J');assert buf.getByte(0) != copy.getByte(0); // return true
}

5.3.9 读/写操作

如上文所提到的,有两种类别的读/写操作:

1. get()和set()操作 ----- 从给定的索引开始,并且保持索引不变

2. read()和write()操作 ----- 从给定的索引开始,并且根据已经访问过的字节数对索引进行访问

3. 下图给出get()操作API,对于set()操作、read()操作和write操作可参考书籍或API

5.3.10 更多的操作

下面的两个方法操作字面意思较难理解,给出解释:

1. hasArray() ----- 如果ByteBuf由一个字节数组支撑,则返回true。通俗的讲:ByteBuf是堆缓冲区模式,则代表其内部存储是由字节数组支撑的。如果还没理解,可参考5.2.2章节

2. array() ----- 如果ByteBuf是由一个字节数组支撑泽返回数组,否则抛出UnsupportedOperationException异常。也就是,ByteBuf是堆缓冲区模式

5.4 ByteBufHolder接口

ByteBufHolder为Netty的高级特性提供了支持,如缓冲区池化,可以从池中借用ByteBuf,并且在需要时自动释放。

1. ByteBufHolder是ByteBuf的容器,可以通过子类实现ByteBufHolder接口,根据自身需要添加自己需要的数据字段。可以用于自定义缓冲区类型扩展字段。

2. Netty提供了一个默认的实现DefaultByteBufHolder。

代码

public class CustomByteBufHolder extends DefaultByteBufHolder{private String protocolName;public CustomByteBufHolder(String protocolName, ByteBuf data) {super(data);this.protocolName = protocolName;}@Overridepublic CustomByteBufHolder replace(ByteBuf data) {return new CustomByteBufHolder(protocolName, data);}@Overridepublic CustomByteBufHolder retain() {super.retain();return this;}@Overridepublic CustomByteBufHolder touch() {super.touch();return this;}@Overridepublic CustomByteBufHolder touch(Object hint) {super.touch(hint);return this;}...
}

5.5 ByteBuf分配

创建和管理ByteBuf实例的多种方式:按需分配(ByteBufAllocator)、Unpooled缓冲区和ByteBufUtil类

 

5.5.1 按序分配: ByteBufAllocator接口

Netty通过接口ByteBufAllocator实现了(ByteBuf的)池化。Netty提供池化和非池化的ButeBufAllocator: 

1. ctx.channel().alloc().buffer() ----- 本质就是: ByteBufAllocator.DEFAULT

2. ByteBufAllocator.DEFAULT.buffer() ----- 返回一个基于堆或者直接内存存储的Bytebuf。默认是堆内存

3. ByteBufAllocator.DEFAULT ----- 有两种类型: UnpooledByteBufAllocator.DEFAULT(非池化)和PooledByteBufAllocator.DEFAULT(池化)。对于Java程序,默认使用PooledByteBufAllocator(池化)。对于安卓,默认使用UnpooledByteBufAllocator(非池化)

4. 可以通过BootStrap中的Config为每个Channel提供独立的ByteBufAllocator实例

解释:

1. 上图中的buffer()方法,返回一个基于堆或者直接内存存储的Bytebuf ----- 缺省是堆内存。源码: AbstractByteBufAllocator() { this(false); }

2. ByteBufAllocator.DEFAULT ----- 可能是池化,也可能是非池化。默认是池化(PooledByteBufAllocator.DEFAULT)

5.5.2 Unpooled缓冲区 ----- 非池化

Unpooled提供静态的辅助方法来创建未池化的ByteBuf。

注意:

1. 上图的buffer()方法,返回一个未池化的基于堆内存存储的ByteBuf

2. wrappedBuffer() ----- 创建一个视图,返回一个包装了给定数据的ByteBuf。非常实用

创建ByteBuf代码:

 public void createByteBuf(ChannelHandlerContext ctx) {// 1. 通过Channel创建ByteBufByteBuf buf1 = ctx.channel().alloc().buffer();// 2. 通过ByteBufAllocator.DEFAULT创建ByteBuf buf2 =  ByteBufAllocator.DEFAULT.buffer();// 3. 通过Unpooled创建ByteBuf buf3 = Unpooled.buffer();
}

5.5.3 ByteBufUtil类

ByteBufUtil类提供了用于操作ByteBuf的静态的辅助方法: hexdump()和equals

1. hexdump() ----- 以十六进制的表示形式打印ByteBuf的内容。非常有价值 

2. equals() ----- 判断两个ByteBuf实例的相等性

5.6 引用计数

Netty4.0版本中为ButeBuf和ButeBufHolder引入了引用计数技术。请区别引用计数和可达性分析算法(jvm垃圾回收)

1. 谁负责释放: 一般来说,是由最后访问(引用计数)对象的那一方来负责将它释放

2. buffer.release() ----- 引用计数减1 

3. buffer.retain() ----- 引用计数加1

4. buffer.refCnt() ----- 返回当前对象引用计数值

5. buffer.touch() ----- 记录当前对象的访问位置,主要用于调试。

6. 引用计数并非仅对于直接缓冲区(direct Buffer)。ByteBuf的三种模式: 堆缓冲区(heap Buffer)、直接缓冲区(dirrect Buffer)和复合缓冲区(Composite Buffer)都使用了引用计数,某些时候需要程序员手动维护引用数值

代码:

public static void releaseReferenceCountedObject(){ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();// 引用计数加1buffer.retain();// 输出引用计数buffer.refCnt();// 引用计数减1buffer.release();
}

5.7 建议

1. 如果使用了Netty的ByteBuf,建议功能测试时,打开内存检测: -Dio.netty.leakDetectionLevel=paranoid

2. ByteBuf的三种模式: 堆缓冲区(heap Buffer)、直接缓冲区(dirrect Buffer)和复合缓冲区(Composite Buffer)都使用了引用计数,某些时候需要程序员手动维护引用数值。

附录

1. 完整代码地址

2. netty-in-action书籍下载地址

这篇关于Netty专栏 ( 三)——— Netty的ByteBuf的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/863685

相关文章

欢迎大家关注我的【白话算法和数据结构】专栏

学习ACM也有一年半了,曾经对什么算法都不懂,现在对很多算法都有一定的了解,我们acm集训队都是学长学姐带学弟学妹,其实我们将的学弟学妹大部分都不能理解,当初我听杨大神讲课也是一样,听和没听一样,但是有学长告诉你有这个算法也是好的,只是你知道哦,原来这道题要用这道算法,我以前傻逼的暴力解决~~~然后他告诉你有这个算法,你自己去学,去网上搜资料学,所有人都是这么走过来的,但是网上能把算法将的跟白话一

Java专栏介绍

专栏导读 在当今这个技术飞速发展的时代,Java作为一门成熟且广泛应用的编程语言,一直是软件开发领域的中坚力量。本“Java技术”专栏旨在帮助读者深入理解Java编程语言的精髓,掌握其核心概念与高级特性,并通过实战案例提升编程技能。 专栏目录 一、Java入门知识与基本使用二、Java变量三、运算符四、控制结构五、数组、排序和查找六、面向对象编程(基础)七、面向对象编程(中级)八、面向对

【Netty】netty中都是用了哪些设计模式

对于工程师来说,掌握并理解运用设计模式,是非常重要的,但是除了学习基本的概念之外,需要结合优秀的中间件、框架源码学习其中的优秀软件设计,这样才能以不变应万变。 单例模式 单例模式解决的对象的唯一性,一般来说就是构造方法私有化、然后提供一个静态的方法获取实例。 在netty中,select用于处理CONTINUE、SELECT、BUSY_WAIT 三种策略,通过DefaultSelectStra

Java语言的Netty框架+云快充协议1.5+充电桩系统+新能源汽车充电桩系统源码

介绍 云快充协议+云快充1.5协议+云快充1.6+云快充协议开源代码+云快充底层协议+云快充桩直连+桩直连协议+充电桩协议+云快充源码 软件架构 1、提供云快充底层桩直连协议,版本为云快充1.5,对于没有对接过充电桩系统的开发者尤为合适; 2、包含:启动充电、结束充电、充电中实时数据获取、报文解析、Netty通讯框架、包解析工具、调试器模拟器软件等; 源码合作 提供完整云快充协议源代码

Netty源码解析9-ChannelHandler实例之MessageToByteEncoder

MessageToByteEncoder框架可见用户使用POJO对象编码为字节数据存储到ByteBuf。用户只需定义自己的编码方法encode()即可。 首先看类签名: public abstract class MessageToByteEncoder<I> extends ChannelOutboundHandlerAdapter 可知该类只处理出站事件,切确的说是write事件

Netty源码解析8-ChannelHandler实例之CodecHandler

编解码处理器作为Netty编程时必备的ChannelHandler,每个应用都必不可少。Netty作为网络应用框架,在网络上的各个应用之间不断进行数据交互。而网络数据交换的基本单位是字节,所以需要将本应用的POJO对象编码为字节数据发送到其他应用,或者将收到的其他应用的字节数据解码为本应用可使用的POJO对象。这一部分,又和JAVA中的序列化和反序列化对应。幸运的是,有很多其他的开源工具(prot

Netty源码解析7-ChannelHandler实例之TimeoutHandler

请戳GitHub原文: https://github.com/wangzhiwubigdata/God-Of-BigData TimeoutHandler 在开发TCP服务时,一个常见的需求便是使用心跳保活客户端。而Netty自带的三个超时处理器IdleStateHandler,ReadTimeoutHandler和WriteTimeoutHandler可完美满足此需求。其中IdleSt

Netty源码解析6-ChannelHandler实例之LoggingHandler

LoggingHandler 日志处理器LoggingHandler是使用Netty进行开发时的好帮手,它可以对入站\出站事件进行日志记录,从而方便我们进行问题排查。首先看类签名: @Sharablepublic class LoggingHandler extends ChannelDuplexHandler 注解Sharable说明LoggingHandler没有状态相关变量,

Netty源码解析5-ChannelHandler

ChannelHandler并不处理事件,而由其子类代为处理:ChannelInboundHandler拦截和处理入站事件,ChannelOutboundHandler拦截和处理出站事件。ChannelHandler和ChannelHandlerContext通过组合或继承的方式关联到一起成对使用。事件通过ChannelHandlerContext主动调用如fireXXX()和write(msg)

Netty源码解析4-Handler综述

Netty中的Handler简介 Handler在Netty中,占据着非常重要的地位。Handler与Servlet中的filter很像,通过Handler可以完成通讯报文的解码编码、拦截指定的报文、 统一对日志错误进行处理、统一对请求进行计数、控制Handler执行与否。一句话,没有它做不到的只有你想不到的 Netty中的所有handler都实现自ChannelHandler接口。按照输入