preface

The byte is the most basic and smallest unit in network transmission. JAVA NIO provides a ByteBuffer container to hold this data, but it can be a bit complicated to use, often switching between reads and writes, and does not support dynamic scaling. Netty provides us with a ByteBuf component, which is very powerful. This article mainly explains ByteBuf, interspersed with ByteBuffer for comparison.


advantage

Advantages of ByteBuf over ByteBuffer:

  1. Read and write using different indexes.
  2. Read and write can be switched at will, without calling the flip() method.
  3. Capacity can be dynamically expanded, just like StringBuilder.
  4. Transparent zero copy can be achieved with its built-in compound buffer.
  5. Support method chains.
  6. Support reference counting. The count = = 0, release.
  7. Support the pool.

Each advantage is explained in detail below.


Read and write the index

ByteBuffer uses position index for both reading and writing, and flip() is used to switch the read/write mode. ByteBuf uses readIndex for reading and writeIndex for writing, which makes it more convenient for us to operate without flip. The ByteBuffer and ByteBuf read-write models are illustrated below in graphical form.

bytebuffer.png

You can test it yourself with the following simple code:

1        ByteBuffer byteBuffer = ByteBuffer.allocate(8);

2        System.err.println("startPosition: " + byteBuffer.position() + ",limit: " + byteBuffer.limit() + ",capacity: " + byteBuffer.capacity());

3        byteBuffer.put("abc".getBytes());

4        System.err.println("writePosition: " + byteBuffer.position() + ",limit: " + byteBuffer.limit() + ",capacity: " + byteBuffer.capacity());

5        byteBuffer.flip();

6        System.err.println("readPosition: " + byteBuffer.position() + ",limit: " + byteBuffer.limit() + ",capacity: " + byteBuffer.capacity());

Copy the code

bytebuf.png

You can test it yourself with the following simple code:

 1        ByteBuf heapBuffer = Unpooled.buffer(8);

2        int startWriterIndex = heapBuffer.writerIndex();

3        System.err.println("startWriterIndex: " + startWriterIndex);

4        int startReadIndex = heapBuffer.readerIndex();

5        System.err.println("startReadIndex: " + startReadIndex);

6        System.err.println("capacity: " + heapBuffer.capacity());

7        System.err.println("= = = = = = = = = = = = = = = = = = = = = = = =");

8        for (int i = 0; i < 3; i++) {

9            heapBuffer.writeByte(i);

10        }

11        int writerIndex = heapBuffer.writerIndex();

12        System.err.println("writerIndex: " + writerIndex);

13        heapBuffer.readBytes(2);

14        int readerIndex = heapBuffer.readerIndex();

15        System.err.println("readerIndex: " + readerIndex);

16        System.err.println("capacity: " + heapBuffer.capacity());

Copy the code

The dynamic extension

ByteBuffer is does not support dynamic extensions, given a specific capacity, once put in data more than its capacity, will be thrown. Java nio. BufferOverflowException abnormalities, and ByteBuf perfectly solved this problem, support for dynamic to expand its capacity.


Zero copy

Netty provides the CompositeByteBuf class for zero copy. In most cases, during network data transmission, messages are divided into header and body, and there are even other parts. Here we simply divide the message into two parts:

Previous practice
 1        ByteBuffer header = ByteBuffer.allocate(1);

2        header.put("a".getBytes());

3        header.flip();

4        ByteBuffer body = ByteBuffer.allocate(1);

5        body.put("b".getBytes());

6        body.flip();

7        ByteBuffer message = ByteBuffer.allocate(header.remaining() + body.remaining());

8        message.put(header);

9        message.put(body);

10        message.flip();

11        while (message.hasRemaining()){

12            System.err.println((char)message.get());

13        }

Copy the code

In order to obtain the complete message body, it is equivalent to two redundant copies of memory, resulting in a great waste of resources.

Methods provided by Netty
 1        CompositeByteBuf messageBuf = Unpooled.compositeBuffer();

2        ByteBuf headerBuf = Unpooled.buffer(1);

3        headerBuf.writeByte('a');

4        ByteBuf bodyBuf = Unpooled.buffer(1);

5        bodyBuf.writeByte('b');

6        messageBuf.addComponents(headerBuf, bodyBuf);

7        for (ByteBuf buf : messageBuf) {

8            System.out.println((char)buf.readByte());

9            System.out.println(buf.toString());

10        }

Copy the code

Here headerBuf and bodyBuf are grouped together via the CompositeByteBuf object and the complete message body is also obtained, but no memory copy is made. Note that the buf.tostring () method I called in the above code snippet is still pointing to the original spatial address, which proves the zero-copy argument.


Support for reference counting

Take a look at a simple code snippet:

 1        ByteBuf buffer = Unpooled.buffer(1);

2        int i = buffer.refCnt();

3        System.err.println("refCnt : " + i);    //refCnt : 1

4        buffer.retain();

5        buffer.retain();

6        buffer.retain();

7        buffer.retain();

8        i = buffer.refCnt();

9        System.err.println("refCnt : " + i);      //refCnt : 5

10        boolean release = buffer.release();

11        i = buffer.refCnt();

12        System.err.println("refCnt : " + i + "= = = = =" + release);      //refCnt : 4 ===== false

13        release = buffer.release(4);

14        i = buffer.refCnt();

15        System.err.println("refCnt : " + i + "= = = = =" + release);      //refCnt : 0 ===== true

Copy the code

Retain is similar to lock, release is similar to unlock. It maintains an internal counter, and when the counter reaches 0, it indicates that it has been released. To a has been release to write data in the buffer, throws IllegalReferenceCountException: refCnt: 0.

It is described in Netty in Action:

The idea behind reference counting isn’t particularly complex; mostly it involves

tracking the number of active references to a specified object. A ReferenceCounted

implementation instance will normally start out with an active reference count of 1. As long as the reference count is greater than 0, the object is guaranteed not to be released.When the number of active references decreases to 0, the instance will be released. Note that while the precise meaning of release may be implementation-specific, at the very least an object that has been released should no longer be available for use.

The implementation of a reference counter is not complicated. It simply involves an active reference to a specified object that is initialized with a reference count of 1. As long as the reference count is greater than 0, the object will not be freed. When the reference count is reduced to 0, the instance will be freed and the freed object should not be used again.


Support the pool

Netty provides pooling support for allocation of ByteBuf. The specific class is PooledByteBufAllocator. Using this allocator to allocate ByteBuf can improve performance and reduce memory fragmentation. In Netty, PooledByteBufAllocator is used as the ByteBuf allocator by default. The PooledByteBufAllocator object can be retrieved from a Channel or from a ChannelHandlerContext bound to a Channel.

1Channel channel = .;

2ByteBufAllocator allocator = channel.alloc();

3.

4ChannelHandlerContext ctx = .;

5ByteBufAllocator allocator2 = ctx.alloc();

Copy the code

Introduction to the API (some of the confusing ones)

Create ByteBuf
 1        // Create a heapBuffer that is allocated within the heap

2        ByteBuf heapBuf = Unpooled.buffer(5);

3        if (heapBuf.hasArray()) {

4            byte[] array = heapBuf.array(a);

5            int offset = heapBuf.arrayOffset() + heapBuf.readerIndex();

6            int length = heapBuf.readableBytes();

7            handleArray(array, offset, length);

8        }

9        // Create a directBuffer, which is allocated out-of-heap memory

10        ByteBuf directBuf = Unpooled.directBuffer();

11        if(! directBuf.hasArray()) {

12            int length = directBuf.readableBytes();

13            byte[] array = new byte[length];

14            directBuf.getBytes(directBuf.readerIndex(), array);

15            handleArray(array.0, length);

16        }

Copy the code

The main differences between the two are as follows: a. The allocated out-of-heap memory space is not copied during network transmission and is directly used by the network adapter. But for this space to be used by the JVM, it must be copied into heap memory. B. Allocating and freeing out-of-heap memory is quite expensive compared to heap memory. C. The two buffers are also used in slightly different ways, as shown in the code snippet above.


Read and write data (readByte writeByte)
1        ByteBuf heapBuf = Unpooled.buffer(5);

2        heapBuf.writeByte(1);

3        System.err.println("writeIndex : " + heapBuf.writerIndex());//writeIndex : 1

4        heapBuf.readByte();

5        System.err.println("readIndex : " + heapBuf.readerIndex());//readIndex : 1

6        heapBuf.setByte(2.2);

7        System.err.println("writeIndex : " + heapBuf.writerIndex());//writeIndex : 1

8        heapBuf.getByte(2);

9        System.err.println("readIndex : " + heapBuf.readerIndex());//readIndex : 1

Copy the code

The values of readIndex and writeIndex are changed when the readByte and writeByte methods are called. The values of readIndex and writeIndex are not changed when the set and GET methods are called. In the above test case, both writeIndex and readIndex are printed as 1 and are not changed after the set and GET methods are called.


DiscardReadBytes method

Let’s start with a picture:

discardReadBytes.png

As you can see from the figure above, after calling the discardReadBytes method, readIndex is set to 0 and writeIndex moves forward the discardReadBytes length to expand the writable area. But this is a very inefficient way of doing a lot of copying. To clear data, you are advised to use the clear method. Calling clear() sets both readIndex and writeIndex to 0, and does not copy memory. Note that the clear method does not clear memory, but only changes the index position.


Derived buffers

Here are three methods (shallow copy) :

Duplicate () : copies the entire buffer directly. Slice () : copies data already written to buffer. Slice (index,length): copies data from index to length in the buffer. ReadSlice (length): Reads the length data from the current readIndex.

Although I described the above methods as copying, these methods do not actually copy a new buffer, which shares data with the original buffer. Therefore, the consumption of calling these methods is very low, and no new space is opened for storage, but the modification will affect the original buffer. This method is also known as shallow copy. To make a deep copy, you can call the copy() and copy(index,length) methods, using the same method as above, but memory copy is very inefficient. Test the demo:

 1        ByteBuf heapBuf = Unpooled.buffer(5);

2        heapBuf.writeByte(1);

3        heapBuf.writeByte(1);

4        heapBuf.writeByte(1);

5        heapBuf.writeByte(1);

6        // Copy the entire buffer directly

7        ByteBuf duplicate = heapBuf.duplicate();

8        duplicate.setByte(0.2);

9        System.err.println("duplicate: " + duplicate.getByte(0) + "====heapBuf: " + heapBuf.getByte(0));//duplicate: 2====heapBuf: 2

10        // Copy the data already written to buffer

11        ByteBuf slice = heapBuf.slice();

12        System.err.println("slice capacity: " + slice.capacity());//slice capacity: 4

13        slice.setByte(2.5);

14        ByteBuf slice1 = heapBuf.slice(0.3);

15        System.err.println("slice1 capacity: "+slice1.capacity());//slice1 capacity: 3

16        System.err.println("duplicate: " + duplicate.getByte(2) + "====heapBuf: " + heapBuf.getByte(2));//duplicate: 5====heapBuf: 5

Copy the code

All of the above test code is available on my Github (The Buffer module in Netty).

End