Java NIO Socket通信
传统Java应用程序使用NIO进行网络通信时大概会有6次数据复制过程,其中包括4次CPU复制和2次DMA复制。大概过程如下图:
其中用户缓冲区到内核缓冲区的复制是由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内存中的数据复制到堆外内存中