《netty入门与实战》笔记-03:数据传输载体 ByteBuf 介绍

ByteBuf结构

首先,咱们先来了解一下 ByteBuf 的结构数组

以上就是一个 ByteBuf 的结构图,从上面这幅图能够看到:微信

  1. ByteBuf 是一个字节容器,容器里面的的数据分为三个部分,第一个部分是已经丢弃的字节,这部分数据是无效的;第二部分是可读字节,这部分数据是 ByteBuf 的主体数据, 从 ByteBuf 里面读取的数据都来自这一部分;最后一部分的数据是可写字节,全部写到 ByteBuf 的数据都会写到这一段。最后一部分虚线表示的是该 ByteBuf 最多还能扩容多少容量
  2. 以上三段内容是被两个指针给划分出来的,从左到右,依次是读指针(readerIndex)、写指针(writerIndex),而后还有一个变量 capacity,表示 ByteBuf 底层内存的总容量
  3. 从 ByteBuf 中每读取一个字节,readerIndex 自增1,ByteBuf 里面总共有 writerIndex-readerIndex 个字节可读, 由此能够推论出当 readerIndex 与 writerIndex 相等的时候,ByteBuf 不可读
  4. 写数据是从 writerIndex 指向的部分开始写,每写一个字节,writerIndex 自增1,直到增到 capacity,这个时候,表示 ByteBuf 已经不可写了
  5. ByteBuf 里面其实还有一个参数 maxCapacity,当向 ByteBuf 写数据的时候,若是容量不足,那么这个时候能够进行扩容,直到 capacity 扩容到 maxCapacity,超过 maxCapacity 就会报错。

Netty 使用 ByteBuf 这个数据结构能够有效地区分可读数据和可写数据,读写之间相互没有冲突,固然,ByteBuf 只是对二进制数据的抽象,具体底层的实现咱们在下面的小节会讲到,在这一小节,咱们 只须要知道 Netty 关于数据读写只认 ByteBuf,下面,咱们就来学习一下 ByteBuf 经常使用的 API数据结构

容量 API

capacity()jvm

表示 ByteBuf 底层占用了多少字节的内存(包括丢弃的字节、可读字节、可写字节),不一样的底层实现机制有不一样的计算方式,后面咱们讲 ByteBuf 的分类的时候会讲到函数

maxCapacity()学习

表示 ByteBuf 底层最大可以占用多少字节的内存,当向 ByteBuf 中写数据的时候,若是发现容量不足,则进行扩容,直到扩容到 maxCapacity,超过这个数,就抛异常指针

readableBytes() 与 isReadable()code

readableBytes() 表示 ByteBuf 当前可读的字节数,它的值等于 writerIndex-readerIndex,若是二者相等,则不可读,isReadable() 方法返回 false对象

writableBytes()、 isWritable() 与 maxWritableBytes()blog

writableBytes() 表示 ByteBuf 当前可写的字节数,它的值等于 capacity-writerIndex,若是二者相等,则表示不可写,isWritable() 返回 false,可是这个时候,并不表明不能往 ByteBuf 中写数据了, 若是发现往 ByteBuf 中写数据写不进去的话,Netty 会自动扩容 ByteBuf,直到扩容到底层的内存大小为 maxCapacity,而 maxWritableBytes() 就表示可写的最大字节数,它的值等于 maxCapacity-writerIndex

读写指针相关的 API

readerIndex() 与 readerIndex(int)

前者表示返回当前的读指针 readerIndex, 后者表示设置读指针

writeIndex() 与 writeIndex(int)

前者表示返回当前的写指针 writerIndex, 后者表示设置写指针

markReaderIndex() 与 resetReaderIndex()

前者表示把当前的读指针保存起来,后者表示把当前的读指针恢复到以前保存的值,下面两段代码是等价的

// 代码片断1
int readerIndex = buffer.readerIndex();
// .. 其余操做
buffer.readerIndex(readerIndex);

// 代码片断二
buffer.markReaderIndex();
// .. 其余操做
buffer.resetReaderIndex();

但愿你们多多使用代码片断二这种方式,不须要本身定义变量,不管 buffer 看成参数传递到哪里,调用 resetReaderIndex() 均可以恢复到以前的状态,在解析自定义协议的数据包的时候很是常见,推荐你们使用这一对 API

markWriterIndex() 与 resetWriterIndex()

这一对 API 的做用与上述一对 API 相似,这里再也不 赘述

读写 API

本质上,关于 ByteBuf 的读写均可以看做从指针开始的地方开始读写数据

writeBytes(byte[] src) 与 buffer.readBytes(byte[] dst)

writeBytes() 表示把字节数组 src 里面的数据所有写到 ByteBuf,而 readBytes() 指的是把 ByteBuf 里面的数据所有读取到 dst,这里 dst 字节数组的大小一般等于 readableBytes(),而 src 字节数组大小的长度一般小于等于 writableBytes()

writeByte(byte b) 与 buffer.readByte()

writeByte() 表示往 ByteBuf 中写一个字节,而 buffer.readByte() 表示从 ByteBuf 中读取一个字节,相似的 API 还有 writeBoolean()、writeChar()、writeShort()、writeInt()、writeLong()、writeFloat()、writeDouble() 与 readBoolean()、readChar()、readShort()、readInt()、readLong()、readFloat()、readDouble() 这里就不一一赘述了,相信读者应该很容易理解这些 API

与读写 API 相似的 API 还有 getBytes、getByte() 与 setBytes()、setByte() 系列,惟一的区别就是 get/set 不会改变读写指针,而 read/write 会改变读写指针,这点在解析数据的时候千万要注意

release() 与 retain()

因为 Netty 使用了堆外内存,而堆外内存是不被 jvm 直接管理的,也就是说申请到的内存没法被垃圾回收器直接回收,因此须要咱们手动回收。有点相似于c语言里面,申请到的内存必须手工释放,不然会形成内存泄漏。

Netty 的 ByteBuf 是经过引用计数的方式管理的,若是一个 ByteBuf 没有地方被引用到,须要回收底层内存。默认状况下,当建立完一个 ByteBuf,它的引用为1,而后每次调用 retain() 方法, 它的引用就加一, release() 方法原理是将引用计数减一,减完以后若是发现引用计数为0,则直接回收 ByteBuf 底层的内存。

slice()、duplicate()、copy()

这三个方法一般状况会放到一块儿比较,这三者的返回值都是一个新的 ByteBuf 对象

  1. slice() 方法从原始 ByteBuf 中截取一段,这段数据是从 readerIndex 到 writeIndex,同时,返回的新的 ByteBuf 的最大容量 maxCapacity 为原始 ByteBuf 的 readableBytes()
  2. duplicate() 方法把整个 ByteBuf 都截取出来,包括全部的数据,指针信息
  3. slice() 方法与 duplicate() 方法的相同点是:底层内存以及引用计数与原始的 ByteBuf 共享,也就是说通过 slice() 或者 duplicate() 返回的 ByteBuf 调用 write 系列方法都会影响到 原始的 ByteBuf,可是它们都维持着与原始 ByteBuf 相同的内存引用计数和不一样的读写指针
  4. slice() 方法与 duplicate() 不一样点就是:slice() 只截取从 readerIndex 到 writerIndex 之间的数据,它返回的 ByteBuf 的最大容量被限制到 原始 ByteBuf 的 readableBytes(), 而 duplicate() 是把整个 ByteBuf 都与原始的 ByteBuf 共享
  5. slice() 方法与 duplicate() 方法不会拷贝数据,它们只是经过改变读写指针来改变读写的行为,而最后一个方法 copy() 会直接从原始的 ByteBuf 中拷贝全部的信息,包括读写指针以及底层对应的数据,所以,往 copy() 返回的 ByteBuf 中写数据不会影响到原始的 ByteBuf
  6. slice() 和 duplicate() 不会改变 ByteBuf 的引用计数,因此原始的 ByteBuf 调用 release() 以后发现引用计数为零,就开始释放内存,调用这两个方法返回的 ByteBuf 也会被释放,这个时候若是再对它们进行读写,就会报错。所以,咱们能够经过调用一次 retain() 方法 来增长引用,表示它们对应的底层的内存多了一次引用,引用计数为2,在释放内存的时候,须要调用两次 release() 方法,将引用计数降到零,才会释放内存
  7. 这三个方法均维护着本身的读写指针,与原始的 ByteBuf 的读写指针无关,相互之间不受影响

retainedSlice() 与 retainedDuplicate()

相信读者应该已经猜到这两个 API 的做用了,它们的做用是在截取内存片断的同时,增长内存的引用计数,分别与下面两段代码等价

// retainedSlice 等价于
slice().retain();

// retainedDuplicate() 等价于
duplicate().retain()

使用到 slice 和 duplicate 方法的时候,千万要理清内存共享,引用计数共享,读写指针不共享几个概念,下面举两个常见的易犯错的例子

1.屡次释放

Buffer buffer = xxx;
doWith(buffer);
// 一次释放
buffer.release();


public void doWith(Bytebuf buffer) {
// ...    
    
// 没有增长引用计数
Buffer slice = buffer.slice();

foo(slice);

}


public void foo(ByteBuf buffer) {
    // read from buffer
    
    // 重复释放
    buffer.release();
}

这里的 doWith 有的时候是用户自定义的方法,有的时候是 Netty 的回调方法,好比 channelRead() 等等

2.不释放形成内存泄漏

Buffer buffer = xxx;
doWith(buffer);
// 引用计数为2,调用 release 方法以后,引用计数为1,没法释放内存 
buffer.release();


public void doWith(Bytebuf buffer) {
// ...    
    
// 增长引用计数
Buffer slice = buffer.retainedSlice();

foo(slice);

// 没有调用 release

}


public void foo(ByteBuf buffer) {
    // read from buffer
}

想要避免以上两种状况发生,你们只须要记得一点,在一个函数体里面,只要增长了引用计数(包括 ByteBuf 的建立和手动调用 retain() 方法),就必须调用 release() 方法。

总结

  1. 本小节,咱们分析了 Netty 对二进制数据的抽象 ByteBuf 的结构,本质上它的原理就是,它引用了一段内存,这段内存能够是堆内也能够是堆外的,而后用引用计数来控制这段内存是否须要被释放,使用读写指针来控制对 ByteBuf 的读写,能够理解为是外观模式的一种使用
  2. 基于读写指针和容量、最大可扩容容量,衍生出一系列的读写方法,要注意 read/write 与 get/set 的区别
  3. 多个 ByteBuf 能够引用同一段内存,经过引用计数来控制内存的释放,遵循谁 retain() 谁 release() 的原则

以上内容来源于掘金小册《Netty 入门与实战:仿写微信 IM 即时通信系统》,若想得到更多,更详细的内容,请用微信扫码订阅:

相关文章
相关标签/搜索