1. 使用DirectBuffer
以下是NioEventLoop#processSelectedKey方法的部分代码
1 if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) { 2 unsafe.read(); 3 if (!ch.isOpen()) { 4 // Connection already closed - no need to handle write. 5 return; 6 } 7 }
unsafe.read()方法对应AbstractNioByteChannel.NioByteUnsafe#read方法。
该方法的部分代码如下
1 RecvByteBufAllocator.Handle allocHandle = this.allocHandle; 2 if (allocHandle == null) { 3 this.allocHandle = allocHandle = config.getRecvByteBufAllocator().newHandle(); 4 } 5 // 省略部分代码... 6 byteBuf = allocator.ioBuffer(byteBufCapacity); 7 int writable = byteBuf.writableBytes(); 8 int localReadAmount = doReadBytes(byteBuf);
在执行读操作之前先获取了一个RecvByteBufAllocator.Handle的对象,该对象是用于获取ByteBuf的工具类,可以跟踪代码看到这个工具类是AdaptiveRecvByteBufAllocator#HandleImpl。
再看工具类的申请byteBuf的方法ioBuffer
1 public ByteBuf ioBuffer(int initialCapacity) { 2 if (PlatformDependent.hasUnsafe()) { 3 return directBuffer(initialCapacity); 4 } 5 return heapBuffer(initialCapacity); 6 }
这里默认会返回directBuffer。
获取到byteBuf之后就可以调用doReadBytes方法将数据包读取到byteBuf中。该方法最终会调用到java的socketChannel的read方法,该方法调用IOUtil#read方法来读取数据,来看下该方法的部分代码
1 if (dst instanceof DirectBuffer) 2 return readIntoNativeBuffer(fd, dst, position, nd); 3 // Substitute a native buffer 4 ByteBuffer bb = Util.getTemporaryDirectBuffer(dst.remaining());
在开始读取之前先判断byteBuffer是不是DirectBuffer,如果是则直接读取数据,否则需要将byteBuffer拷贝到JVM内存中。
从以上流程可知netty使用directBuf来实现零拷贝。
2. 使用CompositeByteBuf
该类其实就是一个装饰器,将多个byteBuf组装成逻辑上的一个byteBuf,并不存在内存拷贝。详细介绍https://segmentfault.com/a/1190000007560884。
3. 使用FileRegion
该类是netty的文件传输类,实际上底层是使用的java的NIO提供的transferTo方法将一个channel的数据传输到另一个channel中。
ps:需要特别注意的是,netty的零拷贝与系统的零拷贝是不同概念,系统层面的零拷贝指的是避免在用户态与内核态间拷贝数据;而netty作为一个应用程序,它只存在于用户态,即使是申请的directBuffer同样是位于用户态。
socketChannel的数据位于内核态,要把该数据读取到用户态必定会拷贝一次,如果使用directBuffer,则可以直接把数据读取到buffer;如果使用heapBuff,则需要先创建一个directBuffer,然后读取到directBuffer中,然后再从directBuffer读取到目标heapBuff。
深入了解java directBuffer:
https://www.zhihu.com/question/57374068/answer/152691891
netty的directBuf与heapBuf的区别: