In Netty, there is another more common object ByteBuf, which is actually equivalent to ByteBuffer in Java Nio, but ByteBuf has made a lot of enhancements to the function of ByteBuffer in Nio. Let's take a brief look at ByteBuf.
The following code demonstrates the creation of ByteBuf and the printing of content. Here is one of the biggest differences from ordinary ByteBuffer, that is, ByteBuf can be automatically expanded. The default length is 256. If the content length exceeds the threshold, the expansion will be automatically triggered.
public class ByteBufExample {
public static void main(String[] args) {
ByteBuf buf= ByteBufAllocator.DEFAULT.buffer();//可自动扩容
log(buf);
StringBuilder sb=new StringBuilder();
for (int i = 0; i < 32; i++) { //演示的时候,可以把循环的值扩大,就能看到扩容效果
sb.append(" - "+i);
}
buf.writeBytes(sb.toString().getBytes());
log(buf);
}
private static void log(ByteBuf buf){
StringBuilder builder=new StringBuilder()
.append(" read index:").append(buf.readerIndex()) //获取读索引
.append(" write index:").append(buf.writerIndex()) //获取写索引
.append(" capacity:").append(buf.capacity()) //获取容量
.append(StringUtil.NEWLINE);
//把ByteBuf中的内容,dump到StringBuilder中
ByteBufUtil.appendPrettyHexDump(builder,buf);
System.out.println(builder.toString());
}
}
There are two ways to create
The first is to create a ByteBuf based on heap memory
ByteBuf buffer=ByteBufAllocator.DEFAULT.heapBuffer(10);
The second is to create a ByteBuf based on direct memory (out-of-heap memory) ( uses this )
Memory in Java is divided into two parts, one part is direct memory that does not require jvm management, also known as off-heap memory. Off-heap memory is to allocate memory objects in unexpected memory areas of the JVM heap. This part of memory is not managed by the virtual machine, but by the operating system, which can reduce the impact of garbage collection on the application.
ByteBufAllocator.DEFAULT.directBuffer(10);
The advantage of direct memory is that the read and write performance will be higher. If the data is stored in the heap, the data in the Java heap space needs to be sent to the remote server. First, the data inside the heap needs to be copied to the direct memory (out-of-heap memory). Then send again. If the data is directly stored in the off-heap memory, there is one less copy step when sending.
But it also has shortcomings. Due to the lack of JVM memory management, we need to maintain off-heap memory by ourselves to prevent memory overflow.
In addition, it should be noted that ByteBuf uses pooling technology to create it by default. The pooling technology has been repeated in the previous courses. Its core idea is to realize the reuse of objects, thereby reducing the performance overhead caused by frequent creation and destruction of objects.
Whether the pooling function is turned on can be controlled by the following environment variables, where unpooled means not to turn on.
-Dio.netty.allocator.type={unpooled|pooled}
public class NettyByteBufExample {
public static void main(String[] args) {
ByteBuf buf= ByteBufAllocator.DEFAULT.buffer();
System.out.println(buf);
}
}
ByteBuf storage structure
The storage structure of ByteBuf is shown in Figure 3-1. From this figure, you can see that ByteBuf is actually a byte container, which contains three parts.
- Bytes that have been discarded, this part of the data is invalid
- Readable byte, this part of the data is the main data of ByteBuf, the data read from ByteBuf comes from this part; Writable byte, all data written to ByteBuf will be stored in this section
- The expandable byte indicates how much capacity the ByteBuf can expand at most.
<center>Figure 3-1</center>
In ByteBuf, there are two pointers
- readerIndex: Read pointer, readerIndex increases by 1 every time a byte is read. There are a total of witeIndex-readerIndex bytes readable in ByteBuf. When readerIndex and writeIndex are equal, ByteBuf is not readable
- writeIndex: Write pointer, every time a byte is written, writeIndex increases by 1 until capacity is increased, and then the expansion can be triggered to continue writing.
- There is also a maxCapacity maximum capacity in ByteBuf. The default value is
Integer.MAX_VALUE
. When ByteBuf writes data, if the capacity is insufficient, expansion will be triggered until the capacity is expanded to maxCapacity.
Commonly used methods in ByteBuf
For ByteBuf, the common method is to write and read
Write related methods
For the write method, ByteBuf provides writes for a variety of different data types, such as
- writeChar, write char type
- writeInt, write int type
- writeFloat, write float type
- writeBytes, write to nio's ByteBuffer
- writeCharSequence, write string
public class ByteBufExample {
public static void main(String[] args) {
ByteBuf buf= ByteBufAllocator.DEFAULT.heapBuffer();//可自动扩容
buf.writeBytes(new byte[]{1,2,3,4}); //写入四个字节
log(buf);
buf.writeInt(5); //写入一个int类型,也是4个字节
log(buf);
}
private static void log(ByteBuf buf){
System.out.println(buf);
StringBuilder builder=new StringBuilder()
.append(" read index:").append(buf.readerIndex())
.append(" write index:").append(buf.writerIndex())
.append(" capacity:").append(buf.capacity())
.append(StringUtil.NEWLINE);
//把ByteBuf中的内容,dump到StringBuilder中
ByteBufUtil.appendPrettyHexDump(builder,buf);
System.out.println(builder.toString());
}
}
Expansion
When writing data to ByteBuf, when the capacity is found to be insufficient, the expansion will be triggered, and the specific expansion rules are
Assume that the initial capacity of ByteBuf is 10.
- If the data size after writing does not exceed 512 bytes, the next integer multiple of 16 is selected for storage. For example, after the data is written, the size is 12, and the capacity after expansion is 16.
- If the data size exceeds 512 bytes after writing, the next 2^n^ is selected. For example, the size after writing is 512 bytes, and the capacity after expansion is 2^10^=1024. (Because 2^9^=512, the length is not enough)
- The expansion cannot exceed max capacity, otherwise an error will be reported.
Reader related methods
The reader method also provides different operation methods for different data types.
- readByte, read a single byte
- readInt, read an int type
- readFloat, read a float type
public class ByteBufExample {
public static void main(String[] args) {
ByteBuf buf= ByteBufAllocator.DEFAULT.heapBuffer();//可自动扩容
buf.writeBytes(new byte[]{1,2,3,4});
log(buf);
System.out.println(buf.readByte());
log(buf);
}
private static void log(ByteBuf buf){
StringBuilder builder=new StringBuilder()
.append(" read index:").append(buf.readerIndex())
.append(" write index:").append(buf.writerIndex())
.append(" capacity:").append(buf.capacity())
.append(StringUtil.NEWLINE);
//把ByteBuf中的内容,dump到StringBuilder中
ByteBufUtil.appendPrettyHexDump(builder,buf);
System.out.println(builder.toString());
}
}
As can be seen from the results below, after reading a byte, this byte becomes the discarded part, and only the unread part of the data can be read when it is read again.
read index:0 write index:7 capacity:256
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 05 06 07 |....... |
+--------+-------------------------------------------------+----------------+
1
read index:1 write index:7 capacity:256
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 02 03 04 05 06 07 |...... |
+--------+-------------------------------------------------+----------------+
Process finished with exit code 0
In addition, if you want to repeatedly read the data that has been read, here are two methods to achieve mark and reset
public static void main(String[] args) {
ByteBuf buf= ByteBufAllocator.DEFAULT.heapBuffer();//可自动扩容
buf.writeBytes(new byte[]{1,2,3,4,5,6,7});
log(buf);
buf.markReaderIndex(); //标记读取的索引位置
System.out.println(buf.readInt());
log(buf);
buf.resetReaderIndex();//重置到标记位
System.out.println(buf.readInt());
log(buf);
}
In addition, if you want to obtain data without changing the position of the read pointer, get
. This method is based on the index position and allows repeated reading.
Zero copy mechanism of ByteBuf
It needs to be explained that the zero-copy mechanism of ByteBuf is different from the zero-copy at the operating system level we mentioned earlier. The zero-copy at the operating system level means that when we want to send a file to a remote server, we need to copy from the kernel space to the user space. , And then copy from the user space to the network card buffer of the kernel space and send, resulting in an increase in the number of copies.
The zero-copy idea in ByteBuf is also the same, which is to reduce data replication and improve performance. As shown in Figure 3-2, suppose there is an original ByteBuf, and we want to operate on the data of two parts of this ByteBuf. According to the normal thinking, we will create two new ByteBuf, and then copy part of the data in the original ByteBuf to the two new ByteBuf, but this will involve data copying, in the case of a large amount of concurrency, will Affect performance.
<center>Figure 3-2</center>
ByteBuf provides a slice method, this method can split the original ByteBuf without copying the data. The method of use is as follows
public static void main(String[] args) {
ByteBuf buf= ByteBufAllocator.DEFAULT.buffer();//可自动扩容
buf.writeBytes(new byte[]{1,2,3,4,5,6,7,8,9,10});
log(buf);
ByteBuf bb1=buf.slice(0,5);
ByteBuf bb2=buf.slice(5,5);
log(bb1);
log(bb2);
System.out.println("修改原始数据");
buf.setByte(2, 5); //修改原始buf数据
log(bb1);//再打印bb1的结果,发现数据发生了变化
}
In the above code, the original buf is sliced through slices, and each slice is 5 bytes.
In order to prove that the slice has no data copy, we modify the value of the index 2 of the original buf, and then print the first slice bb1, we can find that the result of bb1 has changed. Explain that the data pointed to by the two fragments and the original buf is the same.
Unpooled
In the previous case, we often use the Unpooled tool class, which is the same as the creation, combination, and copying of the non-pooled ByteBuf.
Suppose there is a protocol data, which consists of a header and a message body, and these two parts are placed in two ByteBufs respectively
ByteBuf header=...
ByteBuf body= ...
We want to merge the header and body into a ByteBuf, the usual approach is
ByteBuf allBuf=Unpooled.buffer(header.readableBytes()+body.readableBytes());
allBuf.writeBytes(header);
allBuf.writeBytes(body);
In this process, we copied the header and body to the new allBuf. This process virtually added two data copy operations. Is there a more efficient way to reduce the number of copies to achieve the same goal?
In Netty, a CompositeByteBuf component is provided, which provides this function.
public class ByteBufExample {
public static void main(String[] args) {
ByteBuf header= ByteBufAllocator.DEFAULT.buffer();//可自动扩容
header.writeCharSequence("header", CharsetUtil.UTF_8);
ByteBuf body=ByteBufAllocator.DEFAULT.buffer();
body.writeCharSequence("body", CharsetUtil.UTF_8);
CompositeByteBuf compositeByteBuf=Unpooled.compositeBuffer();
//其中第一个参数是 true, 表示当添加新的 ByteBuf 时, 自动递增 CompositeByteBuf 的 writeIndex.
//默认是false,也就是writeIndex=0,这样的话我们不可能从compositeByteBuf中读取到数据。
compositeByteBuf.addComponents(true,header,body);
log(compositeByteBuf);
}
private static void log(ByteBuf buf){
StringBuilder builder=new StringBuilder()
.append(" read index:").append(buf.readerIndex())
.append(" write index:").append(buf.writerIndex())
.append(" capacity:").append(buf.capacity())
.append(StringUtil.NEWLINE);
//把ByteBuf中的内容,dump到StringBuilder中
ByteBufUtil.appendPrettyHexDump(builder,buf);
System.out.println(builder.toString());
}
}
The reason why CompositeByteBuf can achieve zero copy is because when header and body are combined, these two data are not copied, but a logical whole is constructed through CompositeByteBuf, which is still two real objects, that is, there is a pointer Point to the same object, so here is similar to the implementation of shallow copy.
wrappedBuffer
In the Unpooled tool class, a wrappedBuffer method is provided to implement the CompositeByteBuf zero copy function. The method of use is as follows.
public static void main(String[] args) {
ByteBuf header= ByteBufAllocator.DEFAULT.buffer();//可自动扩容
header.writeCharSequence("header", CharsetUtil.UTF_8);
ByteBuf body=ByteBufAllocator.DEFAULT.buffer();
body.writeCharSequence("body", CharsetUtil.UTF_8);
ByteBuf allBb=Unpooled.wrappedBuffer(header,body);
log(allBb);
//对于零拷贝机制,修改原始ByteBuf中的值,会影响到allBb
header.setCharSequence(0,"Newer0",CharsetUtil.UTF_8);
log(allBb);
}
copiedBuffer
copiedBuffer, and wrappedBuffer biggest difference is that this method can achieve data replication, the following code shows the difference between copiedBuffer and wrappedbuffer can be seen in case
label position, changes the value of the original ByteBuf, did not affect the allBb.
public static void main(String[] args) {
ByteBuf header= ByteBufAllocator.DEFAULT.buffer();//可自动扩容
header.writeCharSequence("header", CharsetUtil.UTF_8);
ByteBuf body=ByteBufAllocator.DEFAULT.buffer();
body.writeCharSequence("body", CharsetUtil.UTF_8);
ByteBuf allBb=Unpooled.copiedBuffer(header,body);
log(allBb);
header.setCharSequence(0,"Newer0",CharsetUtil.UTF_8); //case
log(allBb);
}
Memory release
For different ByteBuf creation, the method of memory release is different.
- UnpooledHeapByteBuf, use JVM memory, just wait for GC to recycle
- UnpooledDirectByteBuf, the use of external memory requires a special method to reclaim the memory
- PooledByteBuf and the like use a pooling mechanism, which requires more complex rules to reclaim memory
If ByteBuf is created using off-heap memory, then try to manually release the memory, then how to release it?
Netty uses a reference counting method to control memory recycling, and each ByteBuf implements the ReferenceCounted interface.
- The initial count of each ByteBuf object is 1
- When the release method is called, the counter is decremented by one, if the counter is 0, ByteBuf is recycled
- When the retain method is called, the counter is incremented by one, which means that before the caller runs out, other handlers immediately call release and will not cause recycling.
- When the counter is 0, the underlying memory will be recycled. At this time, even if the ByteBuf object still exists, its various methods cannot be used normally
Copyright statement: All articles in this blog, except for special statements, adopt the CC BY-NC-SA 4.0 license agreement. Please indicate the reprint from Mic takes you to learn architecture!
If this article is helpful to you, please help me to follow and like. Your persistence is the motivation for my continuous creation. Welcome to follow the WeChat public account of the same name for more technical dry goods!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。