文章目录
- 前言
- ByteBuf
- 结构
- 池化与非池化
- 创建(直接内存/堆内存)
- 写入和读取
- 释放
- 零拷贝,slice,duplicate, copy,Composite
- 结语
前言
ByteBuf
这部分视频讲的更为详细。
主要参考视频。
ByteBuf
结构
首先这个东西的结构与ByteBuffer十分相似。
就是一个带指针的字节容器。
有读指针(read index
),写指针(write index
),和容量(capacity
)。
我们每读取1字节,read index
就加1
每写1字节,write index
加1
可读范围就是write index - read index
,读指针到了写指针的位置就表明没有数据可读。
read index
之前一般就是已读字节
write index
如果没到capacity
,则write index
到capacity
之间表示可写字节。
还有就是ByteBuf
可以扩容。内部有变量maxCapacity
,若扩容时会超过这个值会报错。
池化与非池化
池化,听着有点陌生。
数据库连接池,线程池。都是池化来达到重用的思路。
ByteBuf
也一样。
正常非池化意味着每次使用都要创建ByteBuf
实例。
而池化则可以直接重用池中的ByteBuf
实例。
直接内存的申请与释放是比较消耗性能的。所以采用池化可以省掉这部分开销。且省掉申请空间的时间。
创建(直接内存/堆内存)
可以创建基于堆的和基于直接内存的ByteBuf
。
//默认池化,基于直接内存
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(10);
//池化,基于堆内存
ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer(10);
//池化,基于直接内存
ByteBuf buffer = ByteBufAllocator.DEFAULT.directBuffer(10);
直接内存不受JVM GC管理,需要手动释放。
直接内存申请释放比较消耗性能(所以推荐池化)。
直接内存的优势在于读写性能高
写入和读取
这里的读取写入不需要切换读写模式
读取的话同样有mark和reset方法
buffer.markReaderIndex();
buffer.resetReaderIndex();
buffer.getInt();//读取一个int值,不改变指针位置
基本使用和ByteBuffer一样,读写指针的变化这里类似于NIO的ByteBuffer。不再赘述
这里主要说一个问题。
比如我们使用writeInt方法,int我们知道是32位的,4个字节。例如写入一个数字8,writeInt(8),那么我们如果读取使用readByte(),表示读一个字节,前三次就会读出0,第四次才会读出8,相当于你把32位的int值分四次,每次读8位即一个字节。使用readInt就会直接读32位,不会出现上面的问题。
所以一般怎么写怎么读。
下面是老师讲义里的API表格
读取的API类似
方法签名 | 含义 | 备注 |
---|---|---|
writeBoolean(boolean value) | 写入 boolean 值 | 用一字节 01|00 代表 true|false |
writeByte(int value) | 写入 byte 值 | |
writeShort(int value) | 写入 short 值 | |
writeInt(int value) | 写入 int 值 | Big Endian,即 0x250,写入后 00 00 02 50 |
writeIntLE(int value) | 写入 int 值 | Little Endian,即 0x250,写入后 50 02 00 00 |
writeLong(long value) | 写入 long 值 | |
writeChar(int value) | 写入 char 值 | |
writeFloat(float value) | 写入 float 值 | |
writeDouble(double value) | 写入 double 值 | |
writeBytes(ByteBuf src) | 写入 netty 的 ByteBuf | |
writeBytes(byte[] src) | 写入 byte[] | |
writeBytes(ByteBuffer src) | 写入 nio 的 ByteBuffer | |
int writeCharSequence(CharSequence sequence, Charset charset) | 写入字符串 |
释放
我们使用直接内存的ByteBuf
需要我们自己来释放
Netty 这里采用了引用计数法来控制回收内存,每个 ByteBuf 都实现了 ReferenceCounted 接口
- 每个 ByteBuf 对象的初始计数为 1
- 调用 release 方法计数减 1,如果计数为 0,ByteBuf 内存被回收
- 调用 retain 方法计数加 1,表示调用者没用完之前,其它 handler 即使调用了 release 也不会造成回收
- 当计数为 0 时,底层内存会被回收,这时即使 ByteBuf 对象还在,其各个方法均无法正常使用
我们一般ByteBuf会经过多个ChannelHandler的传递,所以我们不能直接
release。而是在最后使用ByteBuf的地方释放。
老师这里说的是谁是最后使用者,谁负责 release
讲义的详细分析
- 起点,对于 NIO 实现来讲,在 io.netty.channel.nio.AbstractNioByteChannel.NioByteUnsafe#read 方法中首次创建 ByteBuf 放入 pipeline(line 163 pipeline.fireChannelRead(byteBuf))
- 入站 ByteBuf 处理原则
- 对原始 ByteBuf 不做处理,调用 ctx.fireChannelRead(msg) 向后传递,这时无须 release
- 将原始 ByteBuf 转换为其它类型的 Java 对象,这时 ByteBuf 就没用了,必须 release
- 如果不调用 ctx.fireChannelRead(msg) 向后传递,那么也必须 release
- 注意各种异常,如果 ByteBuf 没有成功传递到下一个 ChannelHandler,必须 release
- 假设消息一直向后传,那么 TailContext 会负责释放未处理消息(原始的 ByteBuf)
- 出站 ByteBuf 处理原则
- 出站消息最终都会转为 ByteBuf 输出,一直向前传,由 HeadContext flush 后 release
- 异常处理原则
- 有时候不清楚 ByteBuf 被引用了多少次,但又必须彻底释放,可以循环调用 release 直到返回 true
零拷贝,slice,duplicate, copy,Composite
结语
ByteBuf是最后一部分。
还有一个就是Netty提供了拆包器了来解决粘包半包问题。下篇来说
协议设计放在下下篇。
再后面就是Netty的应用了。这块不一定会写,想看可以去看视频
比如实现简单的客户端服务端消息收发,通信协议,实现一个聊天等应用。就是说基本的使用来实现我们的需求。
然后就是其实NIO部分的IO模型没写,因为这个属于网上能找到很多资料。
NIO的零拷贝的逐步优化可能写一下,老师讲义里写的挺好的。
Netty ByteBuf的零拷贝这里仅提了概念,后续写了NIO的和ByteBuf的还有直接内存一块写一下,加深下理解。