netty的零拷贝

本文详细解析了Netty如何通过使用DIRECTBUFFERS、组合Buffer和transferTo()方法实现'零拷贝',提高Socket I/O性能,包括创建接收Buffer的过程、CompositeByteBuf的使用以及文件传输的直接传输机制。
摘要由CSDN通过智能技术生成

Netty 的“零拷贝”主要体现在如下三个方面:

1) Netty 的接收和发送 ByteBuffer 采用 DIRECT BUFFERS,使用堆外直接内存进行 Socket 读写,不需要进行字节缓冲 区的二次拷贝。如果使用传统的堆内存(HEAP BUFFERS)进行 Socket 读写,JVM 会将堆内存 Buffer 拷贝一份到直接 内存中,然后才写入 Socket 中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。

2) Netty 提供了组合 Buffer 对象,可以聚合多个 ByteBuffer 对象,用户可以像操作一个 Buffer 那样方便的对组合 Buffer 进行操作,避免了传统通过内存拷贝的方式将几个小 Buffer 合并成一个大的 Buffer。

3) Netty 的文件传输采用了 transferTo()方法,它可以直接将文件缓冲区的数据发送到目标 Channel,避免了传统通过 循环 write()方式导致的内存拷贝问题。

下面,我们对上述三种“零拷贝”进行说明,先看 Netty 接收 Buffer 的创建:

打开 AbstractNioByteChannel$NioByteUnsafe

@Override
        public final void read() {
            final ChannelConfig config = config();
            if (shouldBreakReadReady(config)) {
                clearReadPending();
                return;
            }
            final ChannelPipeline pipeline = pipeline();
            final ByteBufAllocator allocator = config.getAllocator();
            final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
            allocHandle.reset(config);
​
            ByteBuf byteBuf = null;
            boolean close = false;
            try {
                do {
                    byteBuf = allocHandle.allocate(allocator);
                    allocHandle.lastBytesRead(doReadBytes(byteBuf));
                    if (allocHandle.lastBytesRead() <= 0) {
                        // nothing was read. release the buffer.
                        byteBuf.release();
                        byteBuf = null;
                        close = allocHandle.lastBytesRead() < 0;
                        if (close) {
                            // There is nothing left to read as we received an EOF.
                            readPending = false;
                        }
                        break;
                    }
​
                    allocHandle.incMessagesRead(1);
                    readPending = false;
                    pipeline.fireChannelRead(byteBuf);
                    byteBuf = null;
                } while (allocHandle.continueReading());
​
                allocHandle.readComplete();
                pipeline.fireChannelReadComplete();
​
                if (close) {
                    closeOnRead(pipeline);
                }
            } catch (Throwable t) {
                handleReadException(pipeline, byteBuf, t, close, allocHandle);
            } finally {
                // Check if there is a readPending which was not processed yet.
                // This could be for two reasons:
                // * The user called Channel.read() or ChannelHandlerContext.read() in channelRead(...) method
                // * The user called Channel.read() or ChannelHandlerContext.read() in channelReadComplete(...) method
                //
                // See https://github.com/netty/netty/issues/2254
                if (!readPending && !config.isAutoRead()) {
                    removeReadOp();
                }
            }
        }

再找到 do while 中的 allocHandle.allocate(allocator) 方法,实际上调用的是

DefaultMaxMessageRecvByteBufAllocator$MaxMessageHandle 的 allocate 方法

public ByteBuf allocate(ByteBufAllocator alloc) { 
    return alloc.ioBuffer(guess()); 
}

相当于是每次循环读取一次消息,就通过 ByteBufferAllocator 的 ioBuffer 方法获取 ByteBuf 对象,下面继续看它的接口定义

public abstract class ByteBuf implements ReferenceCounted, Comparable<ByteBuf> { ... }

当进行 Socket IO 读写的时候,为了避免从堆内存拷贝一份副本到直接内存,Netty 的 ByteBuf 分配器直接创建非堆内存避免缓冲区的二次拷贝,通过“零拷贝”来提升读写性能。

下面我们继续看第二种“零拷贝”的实现 CompositeByteBuf,它对外将多个 ByteBuf 封装成一个 ByteBuf,对外提供统一封装后的 ByteBuf 接口,它的类定义如下:

通过继承关系我们可以看出 CompositeByteBuf 实际就是个 ByteBuf 的包装器,它将多个 ByteBuf 组合成一个集合,然后对外提供统一的 ByteBuf 接口,相关定义如下:

private static final ByteBuffer EMPTY_NIO_BUFFER = Unpooled.EMPTY_BUFFER.nioBuffer(); private static final Iterator<ByteBuf> EMPTY_ITERATOR = Collections.<ByteBuf>emptyList().iterator(); 
private final ByteBufAllocator alloc;
private final boolean direct;
private final List<Component> components;
private final int maxNumComponents; 
private boolean freed;
添加 ByteBuf,不需要做内存拷贝,相关代码如下:

private int addComponent0(boolean increaseWriterIndex, int cIndex, ByteBuf buffer) {
        assert buffer != null;
        boolean wasAdded = false;
        try {
            checkComponentIndex(cIndex);
​
            // No need to consolidate - just add a component to the list.
            Component c = newComponent(ensureAccessible(buffer), 0);
            int readableBytes = c.length();
​
            // Check if we would overflow.
            // See https://github.com/netty/netty/issues/10194
            checkForOverflow(capacity(), readableBytes);
​
            addComp(cIndex, c);
            wasAdded = true;
            if (readableBytes > 0 && cIndex < componentCount - 1) {
                updateComponentOffsets(cIndex);
            } else if (cIndex > 0) {
                c.reposition(components[cIndex - 1].endOffset);
            }
            if (increaseWriterIndex) {
                writerIndex += readableBytes;
            }
            return cIndex;
        } finally {
            if (!wasAdded) {
                buffer.release();
            }
        }
    }
最后,我们看下文件传输的“零拷贝”:

public long transferTo(WritableByteChannel target, long position) throws IOException { 
    long count = this.count - position; 
    if (count < 0 || position < 0) { 
        throw new IllegalArgumentException( 
        "position out of range: " + position + 
        " (expected: 0 - " + (this.count - 1) + ')'); 
    }
    if (count == 0) { 
        return 0L; 
    }
    if (refCnt() == 0) { 
     throw new IllegalReferenceCountException(0); 
    }
    // Call open to make sure fc is initialized. This is a no-oop if we called it before. 
    open(); 
    long written = file.transferTo(this.position + position, count, target); 
    if (written > 0) { 
    transferred += written; 
    }
return written; 
}

Netty 文件传输 DefaultFileRegion 通过 transferTo()方法将文件发送到目标 Channel 中,下面重点看 FileChannel 的transferTo()方法,它的 API DOC 说明如下:

/*** Transfers bytes from this channel's file to the given writable byte * channel. ** <p> An attempt is made to read up to <tt>count</tt> bytes starting at * the given <tt>position</tt> in this channel's file and write them to the * target channel. An invocation of this method may or may not transfer * all of the requested bytes; whether or not it does so depends upon the * natures and states of the channels. Fewer than the requested number of * bytes are transferred if this channel's file contains fewer than * <tt>count</tt> bytes starting at the given <tt>position</tt>, or if the * target channel is non-blocking and it has fewer than <tt>count</tt> * bytes free in its output buffer. ** <p> This method does not modify this channel's position. If the given * position is greater than the file's current size then no bytes are * transferred. If the target channel has a position then bytes are * written starting at that position and then the position is incremented * by the number of bytes written. ** <p> This method is potentially much more efficient than a simple loop * that reads from this channel and writes to the target channel. Many * operating systems can transfer bytes directly from the filesystem cache * to the target channel without actually copying them. </p> */
public abstract long transferTo(long position, long count,WritableByteChannel target) throws IOException;

对于很多操作系统它直接将文件缓冲区的内容发送到目标 Channel 中,而不需要通过拷贝的方式,这是一种更加高效的传输方式,它实现了文件传输的“零拷贝”。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值