1. 概述
从本文开始,我们来分享 Netty ByteBuf 相关的内容。它在 buffer
模块中实现,在功能定位上,它和 NIO ByteBuffer 是一致的,但是强大非常多。如下是 《Netty 实战》 对它的优点总结:
- A01. 它可以被用户自定义的缓冲区类型扩展
- A02. 通过内置的符合缓冲区类型实现了透明的零拷贝
- A03. 容量可以按需增长
- A04. 在读和写这两种模式之间切换不需要调用
#flip()
方法- A05. 读和写使用了不同的索引
- A06. 支持方法的链式调用
- A07. 支持引用计数
- A08. 支持池化
- 特别是第 A04 这点,相信很多胖友都被 NIO ByteBuffer 反人类的读模式和写模式给坑哭了。在 《精尽 Netty 源码分析 —— NIO 基础(三)之 Buffer》 中,我们也吐槽过了。😈
- 当然,可能胖友看着这些优点,会一脸懵逼,不要紧,边读源码边理解落。
老艿艿,从下文开始,Netty ByteBuf ,我们只打 ByteBuf 。相比 NIO ByteBuffer ,它少
"fer"
三个字母。
ByteBuf 的代码实现挺有趣的,但是会略有一点点深度,所以笔者会分成三大块来分享:
- ByteBuf 相关,主要是它的核心 API 和核心子类实现。
- ByteBufAllocator 相关,用于创建 ByteBuf 对象。
- Jemalloc 相关,内存管理算法,Netty 基于该算法,实现对内存高效和有效的管理。😈 这块是最最最有趣的。
每一块,我们会分成几篇小的文章。而本文,我们就来对 ByteBuf 有个整体的认识,特别是核心 API 部分。
2. ByteBuf
io.netty.buffer.ByteBuf
,实现 ReferenceCounted 、Comparable 接口,ByteBuf 抽象类。注意,ByteBuf 是一个抽象类,而不是一个接口。当然,实际上,它主要定义了抽象方法,很少实现对应的方法。
关于 io.netty.util.ReferenceCounted
接口,对象引用计数器接口。
- 对象的初始引用计数为 1 。
- 当引用计数器值为 0 时,表示该对象不能再被继续引用,只能被释放。
- 本文暂时不解析,我们会在 TODO 1011
2.1 抽象方法
因为 ByteBuf 的方法非常多,所以笔者对它的方法做了简单的归类。Let’s Go 。
2.1.1 基础信息
public abstract int capacity(); // 容量 |
主要是如下四个属性:
readerIndex
,读索引。writerIndex
,写索引。capacity
,当前容量。maxCapacity
,最大容量。当writerIndex
写入超过capacity
时,可自动扩容。每次扩容的大小,为capacity
的 2 倍。当然,前提是不能超过maxCapacity
大小。
所以,ByteBuf 通过 readerIndex
和 writerIndex
两个索引,解决 ByteBuffer 的读写模式的问题。
四个大小关系很简单:readerIndex
<= writerIndex
<= capacity
<= maxCapacity
。如下图所示:分段
- 图中一共有三段,实际是四段,省略了
capacity
到maxCapacity
之间的一段。 - discardable bytes ,废弃段。一般情况下,可以理解成已读的部分。
- readable bytes ,可读段。可通过
#readXXX()
方法,顺序向下读取。 - writable bytes ,可写段。可通过
#writeXXX()
方法,顺序向下写入。
另外,ByteBuf 还有 markReaderIndex
和 markWriterIndex
两个属性:
- 通过对应的
#markReaderIndex()
和#markWriterIndex()
方法,分别标记读取和写入位置。 - 通过对应的
#resetReaderIndex()
和#resetWriterIndex()
方法,分别读取和写入位置到标记处。
3.1.2 读取 / 写入操作
// Boolean 1 字节 |
虽然方法比较多,总结下来是不同数据类型的四种读写方法:
#getXXX(index)
方法,读取指定位置的数据,不改变readerIndex
索引。#readXXX()
方法,读取readerIndex
位置的数据,会改成readerIndex
索引。#setXXX(index, value)
方法,写入数据到指定位置,不改变writeIndex
索引。#writeXXX(value)
方法,写入数据到指定位置,会改变writeIndex
索引。
2.1.3 查找 / 遍历操作
public abstract int indexOf(int fromIndex, int toIndex, byte value); // 指定值( value ) 在 ByteBuf 中的位置 |
3.1.4 释放操作
public abstract ByteBuf discardReadBytes(); // 释放已读的字节空间 |
discardReadBytes
#discardReadBytes()
方法,释放【所有的】废弃段的空间内存。
- 优点:达到重用废弃段的空间内存。
- 缺点:释放的方式,是通过复制可读段到 ByteBuf 的头部。所以,频繁释放会导致性能下降。
- 总结:这是典型的问题:选择空间还是时间。具体的选择,需要看对应的场景。😈 后续的文章,我们会看到对该方法的调用。
discardSomeReadBytes
#discardSomeReadBytes()
方法,释放【部分的】废弃段的空间内存。
这是对 #discardSomeReadBytes()
方法的这种方案,具体的实现,见 「4. AbstractByteBuf」 中。
clear
#clear()
方法,清空字节空间。实际是修改 readerIndex = writerIndex = 0
,标记清空。
- 优点:通过标记来实现清空,避免置空 ByteBuf ,提升性能。
- 缺点:数据实际还存在,如果错误修改
writerIndex
时,会导致读到“脏”数据。
3.1.5 拷贝操作
public abstract ByteBuf copy(); // 拷贝可读部分的字节数组。独立,互相不影响。 |
3.1.6 转换 NIO ByteBuffer 操作
// ByteBuf 包含 ByteBuffer 数量。 |
3.1.7 Heap 相关方法
// 适用于 Heap 类型的 ByteBuf 对象的 byte[] 字节数组 |
3.1.8 Unsafe 相关方法
// 适用于 Unsafe 类型的 ByteBuf 对象 |
3.1.9 Object 相关
|
3.1.10 引用计数相关
本文暂时不解析,我们会在 TODO 1011 。
来自 ReferenceCounted
https://skyao.gitbooks.io/learning-netty/content/buffer/interface_ReferenceCounted.html 可参考
|
3.2 子类类图
ByteBuf 的子类灰常灰常灰常多,胖友点击 传送门 可以进行查看。
本文仅分享 ByteBuf 的五个直接子类实现,如下图所示:传送门
- 【重点】AbstractByteBuf ,ByteBuf 抽象实现类,提供 ByteBuf 的默认实现类。可以说,是 ByteBuf 最最最重要的子类。详细解析,见 「4. AbstractByteBuf」 。
- EmptyByteBuf ,用于构建空 ByteBuf 对象,
capacity
和maxCapacity
均为 0 。详细解析,见 「5. EmptyByteBuf」 。 - WrappedByteBuf ,用于装饰 ByteBuf 对象。详细解析,见 「6. WrappedByteBuf」 。
- SwappedByteBuf ,用于构建具有切换字节序功能的 ByteBuf 对象。详细解析,见 「7. SwappedByteBuf」 。
- ReplayingDecoderByteBuf ,用于构建在 IO 阻塞条件下实现无阻塞解码的特殊 ByteBuf 对象,当要读取的数据还未接收完全时,抛出异常,交由 ReplayingDecoder处理。详细解析,见 「8. ReplayingDecoderByteBuf」 。
4. AbstractByteBuf
io.netty.buffer.AbstractByteBuf
,实现 ByteBuf 抽象类,ByteBuf 抽象实现类。官方注释如下:
/** |
因为 AbstractByteBuf 实现类 ByteBuf 超级多的方法,所以我们还是按照 ByteBuf 的归类,逐个分析过去。
4.1 基础信息
4.1.1 构造方法
/** |
capacity
属性,在 AbstractByteBuf 未定义,而是由子类来实现。为什么呢?在后面的文章,我们会看到,ByteBuf 根据内存类型分成 Heap 和 Direct ,它们获取capacity
的值的方式不同。maxCapacity
属性,相关的方法:
public int maxCapacity() {
return maxCapacity;
}
protected final void maxCapacity(int maxCapacity) {
this.maxCapacity = maxCapacity;
}
4.1.2 读索引相关的方法
获取和设置读位置
|
是否可读
|
标记和重置读位置
|
4.1.3 写索引相关的方法
获取和设置写位置
|
是否可写
|
标记和重置写位置
|
保证可写
#ensureWritable(int minWritableBytes)
方法,保证有足够的可写空间。若不够,则进行扩容。代码如下:
1: |
第 13 行:调用
#ensureAccessible()
方法,检查是否可访问。代码如下:/**
* Should be called by every method that tries to access the buffers content to check
* if the buffer was released before.
*/
protected final void ensureAccessible() {
if (checkAccessible && refCnt() == 0) { // 若指向为 0 ,说明已经释放,不可继续写入。
throw new IllegalReferenceCountException(0);
}
}
private static final String PROP_MODE = "io.netty.buffer.bytebuf.checkAccessible";
/**
* 是否检查可访问
*
* @see #ensureAccessible()
*/
private static final boolean checkAccessible;
static {
checkAccessible = SystemPropertyUtil.getBoolean(PROP_MODE, true);
if (logger.isDebugEnabled()) {
logger.debug("-D{}: {}", PROP_MODE, checkAccessible);
}
}第 14 至 17 行:目前容量可写,直接返回。
- 第 19 至 24 行:超过最大上限,抛出 IndexOutOfBoundsException 异常。
- 第 28 行:调用
ByteBufAllocator#calculateNewCapacity(int minNewCapacity, int maxCapacity)
方法,计算新的容量。默认情况下,2 倍扩容,并且不超过最大容量上限。注意,此处仅仅是计算,并没有扩容内存复制等等操作。- 第 32 行:调用
#capacity(newCapacity)
方法,设置新的容量大小。
- 第 32 行:调用
#ensureWritable(int minWritableBytes, boolean force)
方法,保证有足够的可写空间。若不够,则进行扩容。代码如下:
|
和 #ensureWritable(int minWritableBytes)
方法,有两点不同:
- 超过最大容量的上限时,不会抛出 IndexOutOfBoundsException 异常。
- 根据执行的过程不同,返回不同的返回值。
比较简单,胖友自己看下代码。
4.1.4 setIndex
|
4.1.5 读索引标记位相关的方法
|
4.1.6 写索引标记位相关的方法
|
4.1.7 是否只读相关
#isReadOnly()
方法,返回是否只读。代码如下:
|
- 默认返回
false
。子类可覆写该方法,根据情况返回结果。
#asReadOnly()
方法,转换成只读 ByteBuf 对象。代码如下:
"deprecation") ( |
- 如果已是只读,直接返回该 ByteBuf 对象。
如果不是只读,调用
Unpooled#unmodifiableBuffer(Bytebuf)
方法,转化成只读 Buffer 对象。代码如下:/**
* Creates a read-only buffer which disallows any modification operations
* on the specified {@code buffer}. The new buffer has the same
* {@code readerIndex} and {@code writerIndex} with the specified
* {@code buffer}.
*
* @deprecated Use {@link ByteBuf#asReadOnly()}.
*/
public static ByteBuf unmodifiableBuffer(ByteBuf buffer) {
ByteOrder endianness = buffer.order();
// 大端
if (endianness == BIG_ENDIAN) {
return new ReadOnlyByteBuf(buffer);
}
// 小端
return new ReadOnlyByteBuf(buffer.order(BIG_ENDIAN)).order(LITTLE_ENDIAN);
}- 注意,返回的是新的
io.netty.buffer.ReadOnlyByteBuf
对象。并且,和原 ByteBuf 对象,共享readerIndex
和writerIndex
索引,以及相关的数据。仅仅是说,只读,不能写入。
- 注意,返回的是新的
4.1.8 ByteOrder 相关的方法
#order()
方法,获得字节序。由子类实现,因为 AbstractByteBuf 的内存类型,不确定是 Heap 还是 Direct 。
#order(ByteOrder endianness)
方法,设置字节序。代码如下:
|
- 如果字节序未修改,直接返回该 ByteBuf 对象。
- 如果字节序有修改,调用
#newSwappedByteBuf()
方法,TODO SwappedByteBuf
4.1.9 未实现方法
和 「2.1.1 基础信息」 相关的方法,有三个未实现,如下:
public abstract ByteBufAllocator alloc(); // 分配器,用于创建 ByteBuf 对象。 |
4.2 读取 / 写入操作
我们以 Int 类型为例子,来看看它的读取和写入操作的实现代码。
4.2.1 getInt
|
调用
#checkIndex(index, fieldLength)
方法,校验读取是否会超过容量。注意,不是超过writerIndex
位置。因为,只是读取指定位置开始的 Int 数据,不会改变readerIndex
。代码如下:protected final void checkIndex(int index, int fieldLength) {
// 校验是否可访问
ensureAccessible();
// 校验是否会超过容量
checkIndex0(index, fieldLength);
}
final void checkIndex0(int index, int fieldLength) {
if (isOutOfBounds(index, fieldLength, capacity())) {
throw new IndexOutOfBoundsException(String.format(
"index: %d, length: %d (expected: range(0, %d))", index, fieldLength, capacity()));
}
}
// MathUtil.java
/**
* Determine if the requested {@code index} and {@code length} will fit within {@code capacity}.
* @param index The starting index.
* @param length The length which will be utilized (starting from {@code index}).
* @param capacity The capacity that {@code index + length} is allowed to be within.
* @return {@code true} if the requested {@code index} and {@code length} will fit within {@code capacity}.
* {@code false} if this would result in an index out of bounds exception.
*/
public static boolean isOutOfBounds(int index, int length, int capacity) {
// 只有有负数,或运算,就会有负数。
// 另外,此处的越界,不仅仅有 capacity - (index + length < 0 ,例如 index < 0 ,也是越界
return (index | length | (index + length) | (capacity - (index + length))) < 0;
}
调用
#_getInt(index)
方法,读取 Int 数据。这是一个抽象方法,由子类实现。代码如下:protected abstract int _getInt(int index);
关于 #getIntLE(int index)
/ getUnsignedInt(int index)
/ getUnsignedIntLE(int index)
方法的实现,胖友自己去看。
4.2.2 readInt
|
调用
#checkReadableBytes0(fieldLength)
方法,校验读取是否会超过可读段。代码如下:private void checkReadableBytes0(int minimumReadableBytes) {
// 是否可访问
ensureAccessible();
// 是否超过写索引,即超过可读段
if (readerIndex > writerIndex - minimumReadableBytes) {
throw new IndexOutOfBoundsException(String.format(
"readerIndex(%d) + length(%d) exceeds writerIndex(%d): %s",
readerIndex, minimumReadableBytes, writerIndex, this));
}
}调用
#_getInt(index)
方法,读取 Int 数据。- 读取完成,修改
readerIndex
【重要 😈】,加上已读取字节数 4 。
关于 #readIntLE()
/ readUnsignedInt()
/ readUnsignedIntLE()
方法的实现,胖友自己去看。
4.2.3 setInt
|
- 调用
#checkIndex(index, fieldLength)
方法,校验写入是否会超过容量。 调用
#_setInt(index,value )
方法,写入 Int 数据。这是一个抽象方法,由子类实现。代码如下:protected abstract int _setInt(int index, int value);
关于 #setIntLE(int index, int value)
方法的实现,胖友自己去看。
public abstract ByteBuf writeInt(int value);
public abstract ByteBuf writeIntLE(int value);
4.2.4 writeInt
|
- 调用
#ensureWritable0(int minWritableBytes)
方法,保证可写入。 - 调用
#_setInt(index, int value)
方法,写入Int 数据。 - 写入完成,修改
writerIndex
【重要 😈】,加上已写入字节数 4 。
4.2.5 其它方法
其它类型的读取和写入操作的实现代码,胖友自己研究落。还是有一些有意思的方法,例如:
#writeZero(int length)
方法。原本以为是循环length
次写入 0 字节,结果发现会基于long
=>int
=>byte
的顺序,尽可能合并写入。#skipBytes((int length)
方法
4.3 查找 / 遍历操作
查找 / 遍历操作相关的方法,实现比较简单。所以,感兴趣的胖友,可以自己去看。
4.4 释放操作
4.4.1 discardReadBytes
#discardReadBytes()
方法,代码如下:
1: |
- 第 4 行:调用
#ensureAccessible()
方法,检查是否可访问。 - 第 5 至 8 行:无废弃段,直接返回。
第 10 至 19 行:未读取完,即还有可读段。
- 第 13 行:调用
#setBytes(int index, ByteBuf src, int srcIndex, int length)
方法,将可读段复制到 ByteBuf 头开始。如下图所示:discardReadBytes
- 第 15 行:写索引
writerIndex
减小。 第 19 行:调用
#adjustMarkers(int decrement)
方法,调整标记位。代码如下:protected final void adjustMarkers(int decrement) {
int markedReaderIndex = this.markedReaderIndex;
// 读标记位小于减少值(decrement)
if (markedReaderIndex <= decrement) {
// 重置读标记位为 0
this.markedReaderIndex = 0;
// 写标记位小于减少值(decrement)
int markedWriterIndex = this.markedWriterIndex;
if (markedWriterIndex <= decrement) {
// 重置写标记位为 0
this.markedWriterIndex = 0;
// 减小写标记位
} else {
this.markedWriterIndex = markedWriterIndex - decrement;
}
// 减小读写标记位
} else {
this.markedReaderIndex = markedReaderIndex - decrement;
this.markedWriterIndex -= decrement;
}
}- 代码虽然比较多,但是目的很明确,减小读写标记位。并且,通过判断,最多减小至 0 。
- 第 19 行:仅读索引重置为 0 。
- 第 13 行:调用
- 第 20 至 26 行:全部读取完,即无可读段。
- 第 23 行:调用
#adjustMarkers(int decrement)
方法,调整标记位。 - 第 25 行:读写索引都重置为 0 。
- 第 23 行:调用
4.4.2 discardSomeReadBytes
#discardSomeReadBytes()
方法,代码如下:
|
整体代码和 #discardReadBytes()
方法是一致的。差别在于,readerIndex >= capacity() >>> 1
,读取超过容量的一半时,进行释放。也就是说,在空间和时间之间,做了一个平衡。
😈 后续,我们来看看,Netty 具体在什么时候,调用 #discardSomeReadBytes()
和 #discardReadBytes()
方法。
4.4.3 clear
#clear()
方法,代码如下:
|
- 读写索引都重置为 0 。
- 读写标记位不会重置。
4.5 拷贝操作
4.5.1 copy
#copy()
方法,拷贝可读部分的字节数组。代码如下:
|
- 调用
#readableBytes()
方法,获得可读的字节数。 - 调用
#copy(int index, int length)
方法,拷贝指定部分的字节数组。独立,互相不影响。具体的实现,需要子类中实现,原因是做深拷贝,需要根据内存类型是 Heap 和 Direct 会有不同。
4.5.2 slice
#slice()
方法,拷贝可读部分的字节数组。代码如下:
|
- 调用
#readableBytes()
方法,获得可读的字节数。 调用
#slice(int index, int length)
方法,拷贝指定部分的字节数组。共享,互相影响。代码如下:
public ByteBuf slice(int index, int length) {
// 校验可访问
ensureAccessible();
// 创建 UnpooledSlicedByteBuf 对象
return new UnpooledSlicedByteBuf(this, index, length);
}- 返回的是创建的 UnpooledSlicedByteBuf 对象。在它内部,会调用当前 ByteBuf 对象,所以这也是为什么说是共享的。或者说,我们可以认为这是一个浅拷贝。
#retainedSlice()
方法,在 #slice()
方法的基础上,引用计数加 1 。代码如下:
|
- 调用
#slice(int index, int length)
方法,拷贝指定部分的字节数组。也就说,返回 UnpooledSlicedByteBuf 对象。 - 调用
UnpooledSlicedByteBuf#retain()
方法,,引用计数加 1 。本文暂时不解析,我们会在 TODO 1011 。
4.5.3 duplicate
#duplicate()
方法,拷贝整个的字节数组。代码如下:
|
- 创建的 UnpooledDuplicatedByteBuf 对象。在它内部,会调用当前 ByteBuf 对象,所以这也是为什么说是共享的。或者说,我们可以认为这是一个浅拷贝。
- 它和
#slice()
方法的差别在于,前者是整个,后者是可写段。
#retainedDuplicate()
方法,在 #duplicate()
方法的基础上,引用计数加 1 。代码如下:
|
- 调用
#duplicate()
方法,拷贝整个的字节数组。也就说,返回 UnpooledDuplicatedByteBuf 对象。 - 调用
UnpooledDuplicatedByteBuf#retain()
方法,,引用计数加 1 。本文暂时不解析,我们会在 TODO 1011 。
4.6 转换 NIO ByteBuffer 操作
4.6.1 nioBuffer
#nioBuffer()
方法,代码如下:
|
在方法内部,会调用
#nioBuffer(int index, int length)
方法。而该方法,由具体的子类实现。FROM 《深入研究Netty框架之ByteBuf功能原理及源码分析》
将当前 ByteBuf 的可读缓冲区(
readerIndex
到writerIndex
之间的内容) 转换为 ByteBuffer 对象,两者共享共享缓冲区的内容。对 ByteBuffer 的读写操作不会影响 ByteBuf 的读写索引。注意:ByteBuffer 无法感知 ByteBuf 的动态扩展操作。ByteBuffer 的长度为
readableBytes()
。
4.6.2 nioBuffers
#nioBuffers()
方法,代码如下:
|
- 在方法内部,会调用
#nioBuffers(int index, int length)
方法。而该方法,由具体的子类实现。 - 😈 为什么会产生数组的情况呢?例如 CompositeByteBuf 。当然,后续文章,我们也会具体分享。
4.7 Heap 相关方法
Heap 相关方法,在子类中实现。详细解析,见 《精尽 Netty 源码解析 —— Buffer 之 ByteBuf(二)核心子类》
4.8 Unsafe 相关方法
Unsafe,在子类中实现。详细解析,见 《精尽 Netty 源码解析 —— Buffer 之 ByteBuf(二)核心子类》
4.9 Object 相关
Object 相关的方法,主要调用 io.netty.buffer.ByteBufUtil
进行实现。而 ByteUtil 是一个非常有用的工具类,它提供了一系列静态方法,用于操作 ByteBuf 对象:ByteUtil
😈 因为 Object 相关的方法,实现比较简单。所以,感兴趣的胖友,可以自己去看。
4.10 引用计数相关
本文暂时不解析,我们会在 TODO 1011 。
5. EmptyByteBuf
io.netty.buffer.EmptyByteBuf
,继承 ByteBuf 抽象类,用于构建空 ByteBuf 对象,capacity
和 maxCapacity
均为 0 。
😈 代码实现超级简单,感兴趣的胖友,可以自己去看。
6. WrappedByteBuf
io.netty.buffer.WrappedByteBuf
,继承 ByteBuf 抽象类,用于装饰 ByteBuf 对象。构造方法如下:
/** |
buf
属性,被装饰的 ByteBuf 对象。每个实现方法,是对
buf
的对应方法的调用。例如:
public final int capacity() {
return buf.capacity();
}
public ByteBuf capacity(int newCapacity) {
buf.capacity(newCapacity);
return this;
}
7. SwappedByteBuf
io.netty.buffer.SwappedByteBuf
,继承 ByteBuf 抽象类,用于构建具有切换字节序功能的 ByteBuf 对象。构造方法如下:
/** |
buf
属性,原 ByteBuf 对象。order
属性,字节数。实际上,SwappedByteBuf 可以看成一个特殊的 WrappedByteBuf 实现,所以它除了读写操作外的方法,都是对
buf
的对应方法的调用。#capacity()
方法,代码如下:
public int capacity() {
return buf.capacity();
}- 直接调用
buf
的对应方法。
- 直接调用
#setInt(int index, int value)
方法,代码如下:
public ByteBuf setInt(int index, int value) {
buf.setInt(index, ByteBufUtil.swapInt(value));
return this;
}
// ByteBufUtil.java
/**
* Toggles the endianness of the specified 32-bit integer.
*/
public static int swapInt(int value) {
return Integer.reverseBytes(value);
}- 先调用
ByteBufUtil#swapInt(int value)
方法,将value
的值,转换成相反字节序的 Int 值。 - 后调用
buf
的对应方法。
- 先调用
通过 SwappedByteBuf 类,我们可以很方便的修改原 ByteBuf 对象的字节序,并且无需进行内存复制。但是反过来,一定要注意,这两者是共享的。
8. ReplayingDecoderByteBuf
io.netty.handler.codec.ReplayingDecoderByteBuf
,继承 ByteBuf 抽象类,用于构建在 IO 阻塞条件下实现无阻塞解码的特殊 ByteBuf对 象。当要读取的数据还未接收完全时,抛出异常,交由 ReplayingDecoder 处理。
细心的胖友,会看到 ReplayingDecoderByteBuf 是在 codec
模块,配合 ReplayingDecoder 使用。所以,本文暂时不会分享它,而是在 《TODO 2000 ReplayingDecoderByteBuf》 中,详细解析。
666. 彩蛋
每逢开篇,内容就特别啰嗦,哈哈哈哈。
推荐阅读如下文章: