传统的server和client之间的数据传输就是用一个while循环不断的读取和发送。
server:
public class OldServer {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(8899);
while (true) {
Socket socket = serverSocket.accept();
if (socket != null) {
InputStream inputStream = socket.getInputStream();
DataInputStream dataInputStream = new DataInputStream(inputStream);
byte[] content = new byte[512];
long timeStart = System.currentTimeMillis();
while (true) {
int read = dataInputStream.read(content,0,content.length);
if (read == -1) {
break;
}
}
System.out.println("read data over: from server");
System.out.println("time spent: " + (System.currentTimeMillis() - timeStart) + "ms");
}
}
}
}
client:
public class OldClient {
public static void main(String[] args) throws Exception{
Socket socket = new Socket("localhost",8899);
FileInputStream fis = new FileInputStream("C:\\Users\\Administrator\\Downloads\\mysql-5.7.29-linux-glibc2.12-x86_64.tar.gz");
DataInputStream dataInputStream = new DataInputStream(fis);
DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
byte[] content = new byte[512];
while(dataInputStream.read(content)>=0){
dataOutputStream.write(content);
}
System.out.println("write data over: from client");
dataInputStream.close();
socket.close();
dataOutputStream.close();
}
}
在服务端,打印出的信息是:
read data over: from server
time spent: 216532ms
传统的网络数据传输模型如上图。
read()
函数,这时有一次上下文切换,即从用户空间切换到内核空间。write()
系统函数,这时又有一次上下文切换。我们看到,用户空间缓冲区根本没什么用,就是一个数据中转站。
所以,我们要把用户空间缓冲区干掉。
这是一种优化方案。
sendfile()
函数。于是从用户空间切换到内核空间。sendfile()
返回。看,这时我们就没有用户空间缓冲区了。
但是,这里还有一种拷贝,就是第4点,将数据从内核空间缓冲区拷贝到socket缓冲区,这在操作系统层面能不能优化呢?linux2.4之后就有优化了。
在看时序图之前,我们先看nio是如何进行数据传递的。
server:
public class NioServer {
public static void main(String[] args) throws Exception {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8899));
ByteBuffer buffer = ByteBuffer.allocate(512);
while (true) {
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(true);
if (socketChannel != null) {
long startTime = System.currentTimeMillis();
while (true) {
int read = socketChannel.read(buffer);
buffer.rewind();
if (read == -1) {
break;
}
}
System.out.println("time spent: " + (System.currentTimeMillis() - startTime) + "ms");
}
}
}
}
client:
public class NioClient {
public static void main(String[] args) throws Exception{
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost",8899));
socketChannel.configureBlocking(true);
FileChannel fileChannel = new FileInputStream("C:\\Users\\Administrator\\Downloads\\mysql-5.7.29-linux-glibc2.12-x86_64.tar.gz").getChannel();
fileChannel.transferTo(0, fileChannel.size(), socketChannel);
fileChannel.close();
socketChannel.close();
}
}
服务端打印文件传输时间:
time spent: 847ms
我们看一下transferTo
这个方法的javadoc:
* <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>
他说,支持该方法的操作系统能直接从文件系统缓存将数据传递到目标管道,而无需拷贝他们。
内核从磁盘读数据,这一步是无法避免的。
读到内核的数据直接被协议引擎拿到然后发送,这是如何做到的呢?
上图说,是通过gather/scatter。我们在nio操作的时候,也碰到过buffer的组合。
我们再看一张更清楚的图:
上面蓝色的虚线是省略掉的从内核空间buffer将数据拷贝到socket buffer的步骤。
这里,虽然socket buffer不再存有整个kernal buffer的数据,但是它存了kernal buffer的文件描述符和数据长度。
这样,协议引擎就会根据socket buffer中存的数据找到kernal buffer在内存中的位置并进行完整的读取。