Netty ByteBuf 核心详解(简化版)
ByteBuf 是 Netty 对 ByteBuffer 的优化实现
一、为什么需要 ByteBuf?
1.1 ByteBuffer 的痛点
// ByteBuffer 使用繁琐
ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put("data".getBytes());
buffer.flip(); // 必须手动切换模式!
byte[] data = new byte[buffer.limit()];
buffer.get(data);
buffer.clear(); // 必须手动清理!
问题:
- 读写模式需要手动切换(flip/clear/compact)
- 只有一个 position 指针,不能同时读写
- 容量固定,无法动态扩展
- API 不友好,容易出错
1.2 ByteBuf 的优势
// ByteBuf 使用简单
ByteBuf buffer = Unpooled.buffer(10);
buffer.writeBytes("data".getBytes()); // 直接写
byte[] data = new byte[buffer.readableBytes()];
buffer.readBytes(data); // 直接读,无需 flip!
buffer.clear(); // 清理
优势:
- ✅ 读写索引分离(readerIndex / writerIndex)
- ✅ 无需手动切换读写模式
- ✅ 支持自动扩容
- ✅ 零拷贝支持
- ✅ 引用计数管理内存
- ✅ 链式调用,API 友好
二、ByteBuf 核心原理
2.1 双指针设计
ByteBuf 使用两个独立的索引:
0 <= readerIndex <= writerIndex <= capacity
| readerIndex | 读取位置指针 |
| writerIndex | 写入位置指针 |
| capacity | 缓冲区总容量 |
2.2 内存区域划分
+——————-+——————+——————+
| discardable bytes | readable bytes | writable bytes |
| (已读废弃区域) | (可读区域) | (可写区域) |
+——————-+——————+——————+
| | | |
0 <= readerIndex <= writerIndex <= capacity
三个区域:
三、ByteBuf 使用流程图解
3.1 初始状态
创建: ByteBuf buffer = Unpooled.buffer(10);
[_][_][_][_][_][_][_][_][_][_]
↑
readerIndex=0
writerIndex=0
capacity=10
3.2 写入数据
buffer.writeBytes("ABCD".getBytes());
[A][B][C][D][_][_][_][_][_][_]
↑ ↑
readerIndex=0 writerIndex=4
关键点:
- writerIndex 自动移动到 4
- readerIndex 保持不变
- 无需调用 flip()
3.3 读取数据
byte b1 = buffer.readByte(); // 读取 'A'
byte b2 = buffer.readByte(); // 读取 'B'
[A][B][C][D][_][_][_][_][_][_]
↑ ↑
readerIndex=2 writerIndex=4
关键点:
- readerIndex 自动移动到 2
- writerIndex 保持不变
- 可读数据:writerIndex – readerIndex = 2
3.4 丢弃已读数据
buffer.discardReadBytes(); // 类似 ByteBuffer.compact()
[C][D][_][_][_][_][_][_][_][_]
↑ ↑
readerIndex=0 writerIndex=2
作用:
- 将未读数据移到开头
- 释放已读空间
- 增加可写空间
四、ByteBuf vs ByteBuffer 对比
4.1 API 对比
| 创建 | ByteBuffer.allocate(10) | Unpooled.buffer(10) |
| 写入 | put(data) | writeBytes(data) |
| 切换模式 | flip() ⚠️ | 无需切换 ✅ |
| 读取 | get(data) | readBytes(data) |
| 清空 | clear() | clear() |
| 压缩 | compact() | discardReadBytes() |
4.2 完整示例对比
ByteBuffer 方式:
ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put("ABCD".getBytes());
buffer.flip(); // ⚠️ 必须切换模式
byte[] data = new byte[buffer.limit()];
buffer.get(data);
System.out.println(new String(data)); // ABCD
buffer.clear(); // ⚠️ 必须清理
ByteBuf 方式:
ByteBuf buffer = Unpooled.buffer(10);
buffer.writeBytes("ABCD".getBytes());
// ✅ 无需 flip()
byte[] data = new byte[buffer.readableBytes()];
buffer.readBytes(data);
System.out.println(new String(data)); // ABCD
buffer.clear();
五、实战示例
5.1 基础读写示例
public class ByteBufBasicDemo {
public static void main(String[] args) {
// 1. 创建缓冲区
ByteBuf buffer = Unpooled.buffer(10);
System.out.println("初始状态:");
printBuffer(buffer);
// 输出: readerIndex=0, writerIndex=0, capacity=10
// 2. 写入数据
buffer.writeBytes("love".getBytes());
System.out.println("\\n写入 'love' 后:");
printBuffer(buffer);
// 输出: readerIndex=0, writerIndex=4, capacity=10
// 3. 读取数据(逐字节)
System.out.println("\\n逐字节读取:");
while (buffer.isReadable()) {
byte b = buffer.readByte();
System.out.print((char) b + " "); // l o v e
}
System.out.println("\\n\\n读取完成后:");
printBuffer(buffer);
// 输出: readerIndex=4, writerIndex=4, capacity=10
// 4. 丢弃已读数据
buffer.discardReadBytes();
System.out.println("\\ndiscardReadBytes 后:");
printBuffer(buffer);
// 输出: readerIndex=0, writerIndex=0, capacity=10
// 5. 清空缓冲区
buffer.clear();
System.out.println("\\nclear 后:");
printBuffer(buffer);
// 输出: readerIndex=0, writerIndex=0, capacity=10
}
private static void printBuffer(ByteBuf buffer) {
System.out.println("readerIndex: " + buffer.readerIndex());
System.out.println("writerIndex: " + buffer.writerIndex());
System.out.println("capacity: " + buffer.capacity());
System.out.println("readableBytes: " + buffer.readableBytes());
System.out.println("writableBytes: " + buffer.writableBytes());
}
}
5.2 常用方法示例
public class ByteBufMethodsDemo {
public static void main(String[] args) {
ByteBuf buffer = Unpooled.buffer(10);
// 写入方法
buffer.writeByte(65); // 写入单字节 'A'
buffer.writeInt(100); // 写入 int (4字节)
buffer.writeBytes("Hi".getBytes()); // 写入字节数组
// 查询方法
int readable = buffer.readableBytes(); // 可读字节数
int writable = buffer.writableBytes(); // 可写字节数
boolean canRead = buffer.isReadable(); // 是否可读
boolean canWrite = buffer.isWritable(); // 是否可写
// 读取方法
byte b = buffer.readByte(); // 读取单字节
int i = buffer.readInt(); // 读取 int
byte[] data = new byte[2];
buffer.readBytes(data); // 读取到数组
// 标记和重置
buffer.markReaderIndex(); // 标记读位置
buffer.readByte();
buffer.resetReaderIndex(); // 重置到标记位置
buffer.markWriterIndex(); // 标记写位置
buffer.writeByte(66);
buffer.resetWriterIndex(); // 重置到标记位置
// 跳过字节
buffer.skipBytes(2); // 跳过2个字节
// 释放资源
buffer.release(); // 引用计数-1
}
}
六、ByteBuf 三种模式
6.1 堆缓冲区(Heap Buffer)
特点:数据存储在 JVM 堆内存中
public class HeapBufferDemo {
public static void main(String[] args) {
// 创建堆缓冲区
ByteBuf buffer = Unpooled.buffer(10);
buffer.writeBytes("netty".getBytes());
// 检查是否有支撑数组
if (buffer.hasArray()) {
byte[] array = buffer.array(); // 获取底层数组
int offset = buffer.arrayOffset() + buffer.readerIndex();
int length = buffer.readableBytes();
System.out.println("底层数组: " + new String(array, offset, length));
// 输出: netty
}
}
}
优点:
- 快速分配和释放
- 可直接访问底层数组
缺点:
- Socket IO 时需要复制到直接内存
适用场景:后端业务逻辑、编解码
6.2 直接缓冲区(Direct Buffer)
特点:数据存储在堆外直接内存
public class DirectBufferDemo {
public static void main(String[] args) {
// 创建直接缓冲区
ByteBuf buffer = Unpooled.directBuffer(10);
buffer.writeBytes("netty".getBytes());
// 直接缓冲区没有支撑数组
if (!buffer.hasArray()) {
int length = buffer.readableBytes();
byte[] array = new byte[length];
buffer.getBytes(buffer.readerIndex(), array);
System.out.println("数据: " + new String(array));
// 输出: netty
}
}
}
优点:
- Socket IO 性能高,无需复制
- 避免 GC 压力
缺点:
- 分配和释放成本高
- 不能直接访问底层数组
适用场景:网络 IO、大量数据传输
6.3 复合缓冲区(Composite Buffer)
特点:多个 ByteBuf 的逻辑视图,零拷贝
public class CompositeBufferDemo {
public static void main(String[] args) {
// 创建堆缓冲区
ByteBuf heapBuf = Unpooled.buffer(3);
heapBuf.writeBytes("net".getBytes());
// 创建直接缓冲区
ByteBuf directBuf = Unpooled.directBuffer(2);
directBuf.writeBytes("ty".getBytes());
// 创建复合缓冲区(零拷贝)
CompositeByteBuf compositeBuf = Unpooled.compositeBuffer();
compositeBuf.addComponents(true, heapBuf, directBuf);
// 遍历所有组件
for (ByteBuf buf : compositeBuf) {
int length = buf.readableBytes();
byte[] array = new byte[length];
buf.getBytes(buf.readerIndex(), array);
System.out.print(new String(array)); // netty
}
System.out.println("\\n总可读字节: " + compositeBuf.readableBytes());
// 输出: 5
}
}
优点:
- 零拷贝组合多个缓冲区
- 避免内存复制和分配
缺点:
- 不支持直接访问支撑数组
- 需要先复制到堆内存
适用场景:HTTP 消息聚合、协议组装
七、ByteBuf 高级特性
7.1 自动扩容
ByteBuf buffer = Unpooled.buffer(4); // 初始容量 4
buffer.writeBytes("Hello World".getBytes()); // 写入 11 字节
// ✅ 自动扩容,无需手动处理
System.out.println("capacity: " + buffer.capacity()); // > 11
7.2 引用计数
ByteBuf buffer = Unpooled.buffer(10);
System.out.println("refCnt: " + buffer.refCnt()); // 1
buffer.retain(); // 引用计数 +1
System.out.println("refCnt: " + buffer.refCnt()); // 2
buffer.release(); // 引用计数 -1
System.out.println("refCnt: " + buffer.refCnt()); // 1
buffer.release(); // 引用计数 -1,释放内存
// buffer 已释放,不能再使用
7.3 零拷贝
// slice() – 切片,共享底层数据
ByteBuf buffer = Unpooled.buffer(10);
buffer.writeBytes("0123456789".getBytes());
ByteBuf slice = buffer.slice(0, 5); // 切片 [0,5)
System.out.println(slice.toString(CharsetUtil.UTF_8)); // 01234
// ✅ 零拷贝,共享底层数据
// duplicate() – 复制,共享底层数据
ByteBuf duplicate = buffer.duplicate();
// ✅ 零拷贝,独立的读写索引
// copy() – 深拷贝,独立数据
ByteBuf copy = buffer.copy();
// ⚠️ 有拷贝,独立的数据和索引
八、实战:Netty 中的使用
8.1 ChannelHandler 中使用
public class MyHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf buf = (ByteBuf) msg;
try {
// 读取数据
int readableBytes = buf.readableBytes();
byte[] data = new byte[readableBytes];
buf.readBytes(data);
System.out.println("收到: " + new String(data));
// 写回响应
ByteBuf response = Unpooled.copiedBuffer("OK", CharsetUtil.UTF_8);
ctx.writeAndFlush(response);
} finally {
// ⚠️ 重要:释放资源
buf.release();
}
}
}
8.2 编解码器中使用
public class MyDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
// 检查是否有足够的数据
if (in.readableBytes() < 4) {
return; // 等待更多数据
}
// 标记读位置
in.markReaderIndex();
// 读取消息长度
int length = in.readInt();
// 检查是否有完整消息
if (in.readableBytes() < length) {
in.resetReaderIndex(); // 重置,等待完整消息
return;
}
// 读取消息体
byte[] body = new byte[length];
in.readBytes(body);
// 输出解码后的消息
out.add(new String(body));
}
}
九、核心要点总结
9.1 ByteBuf vs ByteBuffer
| 读写指针 | 单指针 position | 双指针 readerIndex/writerIndex |
| 模式切换 | 需要 flip() | 无需切换 |
| 容量 | 固定 | 可自动扩容 |
| 内存管理 | JVM GC | 引用计数 |
| 零拷贝 | 不支持 | 支持 slice/duplicate |
| API | 复杂 | 简洁友好 |
9.2 使用建议
选择合适的模式:
- 网络 IO → 直接缓冲区
- 业务逻辑 → 堆缓冲区
- 消息组装 → 复合缓冲区
注意内存释放:
- 使用完毕后调用 release()
- 或使用 try-finally 确保释放
善用零拷贝:
- 使用 slice() 切片
- 使用 CompositeByteBuf 组合
标记和重置:
- 使用 mark/reset 实现回退
- 避免重复读取
十、常见问题
Q1: 什么时候需要 release()?
A: 当你是 ByteBuf 的最后一个使用者时。
// 场景1: ChannelHandler 中
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf buf = (ByteBuf) msg;
try {
// 处理数据
} finally {
buf.release(); // ✅ 必须释放
}
}
// 场景2: 传递给下一个 Handler
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ctx.fireChannelRead(msg); // ✅ 无需释放,由下一个 Handler 负责
}
Q2: readBytes() 和 getBytes() 的区别?
A:
- readBytes(): 移动 readerIndex
- getBytes(): 不移动 readerIndex
ByteBuf buf = Unpooled.copiedBuffer("ABCD", CharsetUtil.UTF_8);
byte[] data1 = new byte[2];
buf.readBytes(data1); // 读取 AB,readerIndex 移动到 2
byte[] data2 = new byte[2];
buf.getBytes(0, data2); // 读取 AB,readerIndex 不变
Q3: 如何避免内存泄漏?
A:
// 启用泄漏检测
–Dio.netty.leakDetection.level=PARANOID
下一步:学习 Netty 的编解码器框架



