Java Nio Socket-IO复制过程

271 阅读2分钟

Java NIO Socket通信

传统Java应用程序使用NIO进行网络通信时大概会有6次数据复制过程,其中包括4次CPU复制和2次DMA复制。大概过程如下图:

截屏2022-10-03 23.02.29.png 其中用户缓冲区到内核缓冲区的复制是由C代码完成的,内核缓冲区到网卡是由DMA完成的,因此在这里不讨论

OpenJDK 11源码分析

Socket 写

SocketChannel类写实现如下:

public int write(ByteBuffer buf) throws IOException {
    Objects.requireNonNull(buf);

    writeLock.lock();
    try {
        boolean blocking = isBlocking();
        int n = 0;
        try {
            beginWrite(blocking);
            if (blocking) {
                do {
                    n = IOUtil.write(fd, buf, -1, nd);
                } while (n == IOStatus.INTERRUPTED && isOpen());
            } else {
                n = IOUtil.write(fd, buf, -1, nd);
            }
        } finally {
            endWrite(blocking, n > 0);
            if (n <= 0 && isOutputClosed)
                throw new AsynchronousCloseException();
        }
        return IOStatus.normalize(n);
    } finally {
        writeLock.unlock();
    }
}

完成数据写入的是IOUtil.write(fd, buf, -1, nd),源码如下:

static int write(FileDescriptor fd, ByteBuffer src, long position,
                 NativeDispatcher nd)
    throws IOException
{
    return write(fd, src, position, false, -1, nd);
}

static int write(FileDescriptor fd, ByteBuffer src, long position,
                 boolean directIO, int alignment, NativeDispatcher nd)
    throws IOException
{
    if (src instanceof DirectBuffer) {
        return writeFromNativeBuffer(fd, src, position, directIO, alignment, nd);
    }

    // Substitute a native buffer
    int pos = src.position();
    int lim = src.limit();
    assert (pos <= lim);
    int rem = (pos <= lim ? lim - pos : 0);
    ByteBuffer bb;
    if (directIO) {
        Util.checkRemainingBufferSizeAligned(rem, alignment);
        bb = Util.getTemporaryAlignedDirectBuffer(rem, alignment);
    } else {
        bb = Util.getTemporaryDirectBuffer(rem);
    }
    try {
        bb.put(src);
        bb.flip();
        // Do not update src until we see how many bytes were written
        src.position(pos);

        int n = writeFromNativeBuffer(fd, bb, position, directIO, alignment, nd);
        if (n > 0) {
            // now update src
            src.position(pos + n);
        }
        return n;
    } finally {
        Util.offerFirstTemporaryDirectBuffer(bb);
    }
}

write方法一共有6个参数,其中前两个参数分别是socket文件描述符,需要写入的数据信息

在write方法中会先判断src是否是堆外内存,如果是堆外内存则不需要将数据从Java Heap复制到堆外内存中,因为数据已经在堆外内存中了

如果src不是堆外内存则会先通过bb = Util.getTemporaryDirectBuffer(rem)申请一个临时的堆外内存,并且通过bb.put(src)将数据复制到堆外内存中

Socket 读

SocketChannel类读实现如下:

public int read(ByteBuffer buf) throws IOException {
    Objects.requireNonNull(buf);

    readLock.lock();
    try {
        boolean blocking = isBlocking();
        int n = 0;
        try {
            beginRead(blocking);

            // check if input is shutdown
            if (isInputClosed)
                return IOStatus.EOF;

            if (blocking) {
                do {
                    n = IOUtil.read(fd, buf, -1, nd);
                } while (n == IOStatus.INTERRUPTED && isOpen());
            } else {
                n = IOUtil.read(fd, buf, -1, nd);
            }
        } finally {
            endRead(blocking, n > 0);
            if (n <= 0 && isInputClosed)
                return IOStatus.EOF;
        }
        return IOStatus.normalize(n);
    } finally {
        readLock.unlock();
    }
}

完成数据读的是IOUtil.read(fd, buf, -1, nd),源码如下:

static int read(FileDescriptor fd, ByteBuffer dst, long position,
                NativeDispatcher nd)
    throws IOException
{
    return read(fd, dst, position, false, -1, nd);
}

static int read(FileDescriptor fd, ByteBuffer dst, long position,
                boolean directIO, int alignment, NativeDispatcher nd)
    throws IOException
{
    if (dst.isReadOnly())
        throw new IllegalArgumentException("Read-only buffer");
    if (dst instanceof DirectBuffer)
        return readIntoNativeBuffer(fd, dst, position, directIO, alignment, nd);

    // Substitute a native buffer
    ByteBuffer bb;
    int rem = dst.remaining();
    if (directIO) {
        Util.checkRemainingBufferSizeAligned(rem, alignment);
        bb = Util.getTemporaryAlignedDirectBuffer(rem, alignment);
    } else {
        bb = Util.getTemporaryDirectBuffer(rem);
    }
    try {
        int n = readIntoNativeBuffer(fd, bb, position, directIO, alignment,nd);
        bb.flip();
        if (n > 0)
            dst.put(bb);
        return n;
    } finally {
        Util.offerFirstTemporaryDirectBuffer(bb);
    }
}

read 方法一共有6个参数,其中前两个参数分别是socket文件描述符,数据存放buffer

在 read 方法中会判断dst是否是堆外内存,如果是堆外内存则直接读取到dst中

如果 dst 不是堆外内存则会先通过bb = Util.getTemporaryDirectBuffer(rem)申请一个临时的堆外内存,将数据读取到 bb 中,然后再复制到 dst

总结

根据 OpenJDK 源码分析,Socket的读写操作都会先将Java Heap内存中的数据复制到堆外内存中