🌟我的其他文章也讲解的比较有趣😁,如果喜欢博主的讲解方式,可以多多支持一下,感谢🤗!
🌟了解 Netty 的 线程模型 请看 : 【Netty篇】Netty的线程模型
其他优质专栏: 【🎇SpringBoot】【🎉多线程】【🎨Redis】【✨设计模式专栏(已完结)】…等
如果喜欢作者的讲解方式,可以点赞收藏加关注,你的支持就是我的动力
✨更多文章请看个人主页: 码熔burning
各位观众,欢迎再次回到我们精彩纷呈的“Netty奇妙夜”!🌙 今晚,我们不仅要深入了解Netty的超级英雄——ByteBuf,还要一起探索它的内部构造和神奇的“膨胀术”!🤩
一、ByteBuf的“五脏六腑”大揭秘!🧠❤️💪🦵🦶
一个ByteBuf实例,就像一个拥有神奇能力的“字节容器”,它主要由以下几个关键的“器官”组成:
-
Capacity(容量): 这就像我们水桶的最大容积,表示这个ByteBuf总共能装多少字节的数据。一旦分配,在不重新分配内存的情况下,这个值通常是固定的。就像我们买了一个10升的水桶,它最多就能装10升水。
-
Reader Index(读指针): 这就像我们从水桶里舀水时,当前的“水位”标记。它指向ByteBuf中下一个可读的字节的索引位置。初始值为0。随着数据的读取,这个指针会不断向后移动。就像我们舀了一部分水后,水面会下降,读指针就指向了新的水面。👇💧
-
Writer Index(写指针): 这就像我们往水桶里倒水时,当前的“水位”标记。它指向ByteBuf中下一个可写的字节的索引位置。初始值为0。随着数据的写入,这个指针会不断向后移动。就像我们倒了一些水后,水面会上升,写指针就指向了新的水面。👆💧
-
Readable Bytes(可读字节数): 这表示从读指针到写指针之间的字节数,也就是我们水桶里实际装了多少“有效的水”。计算公式是:
writerIndex - readerIndex
。我们只能读取这个范围内的字节。📏 -
Writable Bytes(可写字节数): 这表示从写指针到容量之间的字节数,也就是我们水桶还能装多少“水”。计算公式是:
capacity - writerIndex
。我们只能在这个范围写入数据。📏
形象比喻:
你可以把ByteBuf想象成一个卷尺 📏。
- Capacity 是卷尺的总长度。
- Reader Index 是你当前读数的起始位置。
- Writer Index 是你已经测量到的长度。
- Readable Bytes 是你已经测量到的有效长度(从起始位置到当前测量位置)。
- Writable Bytes 是卷尺剩余的未测量长度。
二、ByteBuf的“十八般武艺”写入方法!✍️📜
我们的ByteBuf不仅能装东西,还能以各种“花式”写入数据!下面是它的常用写入方法表格:
方法名 | 参数类型 | 功能描述 |
---|---|---|
writeByte(int value) | int (实际写入一个 byte) | 写入一个字节。传入的是 int 类型,但只会取低 8 位写入。写指针 writerIndex 加 1。 |
writeBoolean(boolean value) | boolean | 写入一个布尔值(true 写入 1,false 写入 0)。写指针 writerIndex 加 1。 |
writeShort(int value) | int (实际写入一个 short) | 写入一个短整型(2 个字节)。写指针 writerIndex 加 2。 |
writeMedium(int value) | int (实际写入一个 medium) | 写入一个中等长度的整型(3 个字节)。写指针 writerIndex 加 3。 |
writeInt(int value) | int | 写入一个整型(4 个字节)。写指针 writerIndex 加 4。 |
writeLong(long value) | long | 写入一个长整型(8 个字节)。写指针 writerIndex 加 8。 |
writeFloat(float value) | float | 写入一个浮点型(4 个字节)。写指针 writerIndex 加 4。 |
writeDouble(double value) | double | 写入一个双精度浮点型(8 个字节)。写指针 writerIndex 加 8。 |
writeBytes(byte[] src) | byte[] | 写入一个字节数组。写指针 writerIndex 增加 src.length 。 |
writeBytes(ByteBuf src) | ByteBuf | 将另一个 ByteBuf 中的可读字节写入当前 ByteBuf。写指针 writerIndex 增加 src.readableBytes() ,并且源 ByteBuf 的读指针也会相应移动。 |
writeBytes(ByteBuf src, int length) | ByteBuf , int | 将另一个 ByteBuf 中从当前读指针开始的指定长度的字节写入当前 ByteBuf。 |
writeBytes(ByteBuf src, int readerIndex, int length) | ByteBuf , int , int | 将另一个 ByteBuf 中从指定的 readerIndex 开始的指定长度的字节写入当前 ByteBuf。注意:这不会改变源 ByteBuf 的读指针。 |
writeCharSequence(CharSequence sequence, Charset charset) | CharSequence , Charset | 将一个字符序列按照指定的字符集编码后写入 ByteBuf。写指针 writerIndex 相应增加。 |
“花式写入”效果演示!🎬✍️
现在,让我们来演示一些常用的写入方法,并看看ByteBuf内部的“水位”是如何变化的!
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import java.nio.charset.StandardCharsets;
public class ByteBufWriteExample {
public static void main(String[] args) {
// 创建一个初始容量为 16 字节的堆缓冲区
ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer(16);
System.out.println("初始状态:" + buffer);
printBufferDetails(buffer); // 打印初始状态
// 写入一个字节
buffer.writeByte(100); // 写入 ASCII 码为 100 的字符 'd'
System.out.println("\n\n写入一个字节 (100): " + buffer);
printBufferDetails(buffer);
// 写入一个布尔值
buffer.writeBoolean(true);
System.out.println("\n\n写入一个布尔值 (true): " + buffer);
printBufferDetails(buffer);
// 写入一个短整型
buffer.writeShort(256);
System.out.println("\n\n写入一个短整型 (256): " + buffer);
printBufferDetails(buffer);
// 写入一个整型
buffer.writeInt(65535);
System.out.println("\n\n写入一个整型 (65535): " + buffer);
printBufferDetails(buffer);
// 写入一个字符串
String text = "Netty";
buffer.writeCharSequence(text, StandardCharsets.UTF_8);
System.out.println("\n\n写入一个字符串 (\"Netty\"): " + buffer);
printBufferDetails(buffer);
// 写入一个字节数组
byte[] bytes = {'b', 'u', 'f'};
buffer.writeBytes(bytes);
System.out.println("\n\n写入一个字节数组 (['b', 'u', 'f']): " + buffer);
printBufferDetails(buffer);
// 别忘了释放!
buffer.release();
}
private static void printBufferDetails(ByteBuf buffer) {
System.out.print(" 容量: " + buffer.capacity());
System.out.print(" \t读指针: " + buffer.readerIndex());
System.out.print(" \t写指针: " + buffer.writerIndex());
System.out.print(" \t可读字节数: " + buffer.readableBytes());
System.out.print(" \t可写字节数: " + buffer.writableBytes());
}
}
运行结果
你就能清晰地看到每次写入操作后,ByteBuf的 writerIndex
和 readableBytes
是如何一步步增长的,而 writableBytes
则相应减少。就像我们往水桶里不断加水,水位线(writerIndex
)就会上升,能装的水量(writableBytes
)就会减少,而我们已经装的水量(readableBytes
)就会增加!
记住,readerIndex
在我们进行写入操作时是不会改变的,它只有在进行读取操作时才会移动。
三、ByteBuf的“长大成人”——扩容机制!👶➡️👦➡️👨
我们都知道,ByteBuf在创建的时候会被分配一个初始容量。但是,万一我们写入的数据超过了这个初始容量怎么办呢?难道ByteBuf会像一个装满水的气球,“嘭”的一声爆炸吗?💥
当然不会!Netty的ByteBuf可是个聪明的家伙,它有一套自己的“长大成人”的机制——扩容(Resizing)!当ByteBuf发现自己快要装不下更多的数据时,它会尝试自动增加自己的容量,以便容纳更多的新内容。
ByteBuf的“增肥秘籍”——扩容规则!📜✨
ByteBuf的扩容并不是随心所欲、漫无目的的,它遵循着一定的规则,就像我们人类的成长一样,也是有规律可循的:
-
检查是否需要扩容: 在每次写入操作之前,ByteBuf会先检查当前的
writableBytes
是否足够写入本次需要写入的数据量。如果不够,它就会触发扩容机制。这就像我们吃饭前看看碗里的饭够不够吃,不够就得再添点。🍚➡️🍚🍚 -
尝试按照需求扩容: ByteBuf首先会尝试将容量增加到足以容纳当前数据和即将写入的数据所需的最小容量。这就像我们肚子饿了,就先添够吃的就行,不会一下子撑到爆炸。🤰➡️😋
-
扩容因子(默认策略): 如果仅仅按照所需容量扩容还不够“慷慨”,ByteBuf通常会采用一个扩容因子来决定最终扩容的大小。默认情况下,Netty的
AbstractByteBuf
会尝试将容量翻倍,直到达到maxCapacity
的限制。也就是说,如果当前容量是 C C C,需要写入 W W W 字节,而 C < r e a d e r I n d e x + W C < readerIndex + W C<readerIndex+W,那么新的容量通常会变成 max ( C × 2 , r e a d e r I n d e x + W ) \max(C \times 2, readerIndex + W) max(C×2,readerIndex+W),当然这个新容量也不能超过maxCapacity
。这就像我们添饭的时候,如果碗还空着不少,就干脆多添点,以防等下又饿了。🍚➡️🍚🍚🍚 -
最大容量限制(
maxCapacity
): ByteBuf在创建的时候,有些类型是可以指定最大容量(maxCapacity
)的。无论如何扩容,ByteBuf的最终容量都不会超过这个maxCapacity
。这就像我们水桶再怎么吹气,它的体积也是有限度的,不会无限膨胀。(有限体积) -
内存分配开销: 扩容涉及到重新分配内存和拷贝原有数据,这会带来一定的性能开销。因此,Netty在扩容时会尽量“智能”,避免过于频繁的小幅度扩容,而是采取翻倍等策略,减少扩容的次数。这就像我们搬家,一次性搬多点东西总比来回跑好几趟要省力。🚚➡️🏠
“膨胀”过程模拟!🧪➡️
让我们用一个简单的例子来模拟一下ByteBuf的扩容过程:
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
public class ByteBufExpandExample {
public static void main(String[] args) {
// 创建一个初始容量为 8 字节,最大容量为 32 字节的堆缓冲区
ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer(8, 32);
System.out.println("初始状态:" + buffer);
printBufferDetails(buffer);
// 写入 5 个字节
buffer.writeBytes(new byte[]{1, 2, 3, 4, 5});
System.out.println("\n\n写入 5 字节后:" + buffer);
printBufferDetails(buffer);
// 再次写入 5 个字节,此时剩余可写空间不足,触发扩容
buffer.writeBytes(new byte[]{6, 7, 8, 9, 10});
System.out.println("\n\n再次写入 5 字节后(触发扩容):" + buffer);
printBufferDetails(buffer);
// 继续写入更多字节,可能会再次触发扩容
buffer.writeBytes(new byte[]{11, 12, 13, 14, 15, 16, 17, 18, 19, 20});
System.out.println("\n\n继续写入 10 字节后(可能再次扩容):" + buffer);
printBufferDetails(buffer);
// 写入超过最大容量会发生什么?(会抛出异常)
try {
byte[] overflow = new byte[50];
buffer.writeBytes(overflow);
} catch (Throwable t) {
System.err.println("\n\n尝试写入超出最大容量的数据,发生异常:" + t.getMessage());
} finally {
buffer.release();
}
}
private static void printBufferDetails(ByteBuf buffer) {
System.out.print(" 容量: " + buffer.capacity());
System.out.print(" \t读指针: " + buffer.readerIndex());
System.out.print(" \t写指针: " + buffer.writerIndex());
System.out.print(" \t可读字节数: " + buffer.readableBytes());
System.out.print(" \t可写字节数: " + buffer.writableBytes());
System.out.print(" \t最大容量: " + buffer.maxCapacity());
}
}
运行结果
你会观察到当 writableBytes
不足以容纳新写入的数据时,capacity
的值会增加(通常是翻倍),而 maxCapacity
则限制了 capacity
的上限。一旦尝试写入超过 maxCapacity
的数据,ByteBuf就会“生气”,抛出异常!😠
温馨提示:
虽然ByteBuf会自动扩容,但频繁的扩容仍然会带来性能损耗。因此,在知道数据的大致大小时,建议在创建ByteBuf时就指定一个合适的初始容量,尽量减少扩容的次数,提高程序的运行效率。这就像我们出门旅行,尽量带上足够用的行李,避免路上不停地购买。🎒➡️🧳
好了,今晚的“Netty奇妙夜”就到此告一段落!我们一起探索了ByteBuf的内部构造和神奇的扩容机制。希望这些生动有趣的讲解能帮助大家更好地理解这位Netty世界的超级英雄!下次再见!👋😊