Netty ByteToMessageDecoder 中 ByteBuf 的readerIndex 详解

在Netty的ByteToMessageDecoder中,ByteBuf扮演了“原始数据的临时容器”和“解码操作的对象”的双重角色。它既缓存了不完整的网络数据包,也是进行解码操作时直接操作的对象。

一. ByteBuf的内部结构

二. 影响 readerIndex 的操作

(1) 前移 readerIndex(消耗数据):

byteBuf.readByte() // +1
byteBuf.readShort() // +2
byteBuf.readInt() // +4
byteBuf.readLong() // +8
byteBuf.readBytes(dst) // +dst.length
byteBuf.readBytes(dst, 0, 5) // +5
byteBuf.skipBytes(n) // +n
byteBuf.readCharSequence(n, charset) // +n

(2) 不改变 readerIndex:

byteBuf.getByte(index) // 绝对读取,不改 index
byteBuf.getInt(index) // 绝对读取,不改 index
byteBuf.getBytes(index, dst) // 绝对读取,不改 index
byteBuf.readableBytes() // 只查询
byteBuf.capacity() // 只查询
byteBuf.duplicate() // 共享数据,独立 index
byteBuf.slice() // 共享数据,独立 index
byteBuf.copy() // 深拷贝,完全独立

(3) 直接设置 readerIndex:

byteBuf.readerIndex(0) // 直接设置为 0
byteBuf.resetReaderIndex() // 回退到上次 markReaderIndex() 的位置
byteBuf.markReaderIndex() // 记录当前 readerIndex 位置(不改值)

(4) mark / reset 机制

byteBuf.markReaderIndex(); // 记住当前位置,比如 0
byteBuf.readByte(); // readerIndex: 0→1
byteBuf.readByte(); // readerIndex: 1→2
byteBuf.resetReaderIndex(); // 回到 0(必须先 mark,否则行为不确定)

没有 mark 就 reset 的行为:
- Netty 4.x 默认回退到 0(AbstractByteBuf 的默认 mark 是 0)
- 但这不是规范行为,依赖默认值是坏习惯

三. ByteToMessageDecoder 中的规范用法

ByteToMessageDecoder 的 decode() 方法有严格约定:

规则 1:添加了消息到 list,必须消费字节

// ❌ 错误:list.add() 了但 readerIndex 没动 → "did not read anything but decoded a message"
protected void decode(ctx, byteBuf, list) {
byteBuf.resetReaderIndex(); // readerIndex 回到 0
list.add(byteBuf); // 加了消息但没消费字节
}

// ✅ 正确:添加消息后标记字节已消费
protected void decode(ctx, byteBuf, list) {
ByteBuf data = byteBuf.retainedDuplicate();
list.add(data);
byteBuf.skipBytes(byteBuf.readableBytes()); // 标记已消费
}

规则 2:不处理就不要动 readerIndex,直接 return

// ✅ 正确:数据不够,等下次
protected void decode(ctx, byteBuf, list) {
if (byteBuf.readableBytes() < 2) {
return; // 不读字节,不改 readerIndex
}
...
}

规则 3:不要把内部 ByteBuf 直接传给下游

// ❌ 错误:传内部 buffer,下游释放后 decoder 再释放 → 双重释放
channelHandlerContext.fireChannelRead(byteBuf);

// ✅ 正确:用 retainedDuplicate() 创建副本
ByteBuf data = byteBuf.retainedDuplicate();
list.add(data);
byteBuf.skipBytes(byteBuf.readableBytes());

规则 4:mark 和 reset 必须配对

// ❌ 错误:没有 mark 就 reset
byteBuf.readByte();
byteBuf.resetReaderIndex(); // 行为不确定

// ✅ 正确:先 mark 再 reset
byteBuf.markReaderIndex();
byteBuf.readByte();
byteBuf.resetReaderIndex();

总结速查表

四. ByteBuf读取和消费有的区别

● 好问题。在大多数场景下,读取 = 消费。但在 Netty 中它们有区别:

读取 ≠ 消费 的场景

1. 读了但"退回" — 不算消费

byteBuf.markReaderIndex(); // readerIndex = 0,记住位置
byte b = byteBuf.readByte(); // readerIndex = 1,读了
byteBuf.resetReaderIndex(); // readerIndex = 0,退回去

// ByteToMessageDecoder 视角:readerIndex 没动 → 没消费任何字节

读了,但退回去等于没读。

2. 窥探但不移动 — 不算消费

byte b = byteBuf.getByte(0); // 看了一眼,readerIndex 还是 0

3. 读了但传给别人 — 谁消费?

byte[] data = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(data); // readerIndex 到底,自己消费了
channelHandlerContext.fireChannelRead(byteBuf); // 传空的给别人,没意义

// vs

ByteBuf copy = byteBuf.retainedDuplicate(); // 副本,readerIndex = 0
list.add(copy); // 副本给下游消费
byteBuf.skipBytes(byteBuf.readableBytes()); // 原 buffer 标记已消费

从 ByteToMessageDecoder 的视角理解

ByteToMessageDecoder 只看一件事:decode() 调用前后,readerIndex 有没有移动。

// 情况 A:readerIndex 移动了 → "消费了"
readerIndex before: 0
decode() { byteBuf.readByte(); } // readerIndex: 0→1
readerIndex after: 1
→ 消耗了 1 字节 ✓

// 情况 B:readerIndex 没动 → "没消费"
readerIndex before: 0
decode() { byteBuf.markReaderIndex(); byteBuf.readByte(); byteBuf.resetReaderIndex(); }
readerIndex after: 0
→ 消耗了 0 字节 ✗

// 情况 C:readerIndex 没动,但 list 有消息 → 报错!
readerIndex before: 0
decode() { byteBuf.resetReaderIndex(); list.add(byteBuf); }
readerIndex after: 0
→ "did not read anything but decoded a message" ❌

一句话总结

- 读取:访问数据的行为(readByte()、getByte() 都算)
- 消费:readerIndex 净前移了多少

mark + read + reset = 读了但没消费,就像你翻了一下书但合回去说"我没看"。