NIO零拷贝

java的 transformTo transformFrom

java是平台无关的,但是JVM是平台相关的。

磁盘上文件拿出来发给用户

最原始的实现(c实现)

内核空间系统调用--上下文切换--直接内存访问DMA 数据拷贝到内核空间缓冲区(页缓存)--上下文切换--又拷贝到了用户空间的缓冲区

内核空间系统调用--上下文切换--用户空间 数据拷贝到内核空间缓冲区(页缓存)--又拷贝到了网卡的socket缓冲区--上下文切换--返回用户空间

用户空间没有对数据修改

  • 4次上下文切换
  • 2次系统调用
  • 4次数据拷贝

1624802954524-9524161a-3848-4e1b-88a2-8e68e9834ea3.png

零拷贝(c实现)

完全依赖操作系统的,不会有数据在用户内核之间拷贝

sendfile() 内核空间系统调用--上下文切换--直接内存访问DMA 数据拷贝到内核空间缓冲区(页缓存)--数据拷贝到目标socket的缓冲区--缓冲区到DMA网卡发送--上下文切换--返回用户空间

1624803392683-d2000245-9018-4793-b038-3ad022e6bbea.png

真正的零拷贝(c实现)

文件描述符可以描述数据的一些大小偏移,socket缓存里面只是存这些,

DMA 拷贝内核缓冲区(页缓存)之后--Linux 2.4 之后,文件描述符gather操作,数据不会kernel buffer到socketbuffer,****只有文件描述符的信息(kernel buffer 的内存地址在什么地方;kernel buffer 多长)会到socket buffer里面。原来的数据本身

真正的数据直接去网卡缓冲区

1624803704100-2f64a8cc-43a4-46ea-8829-f5672a8c7bd4.png

1624806298348-b4c0cb19-e103-4de7-808b-bf452ed17132.png

1624806344586-ac506341-e8ce-4941-b626-1391cb23c1a1.png

零拷贝案例

服务端读取完就丢弃掉

客户端打印需要的时间

传统方式


import java.io.DataInputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class OldIOServer {

    public static void main(String[] args) throws Exception {
        ServerSocket serverSocket = new ServerSocket(8899);

        while (true) {
            // 连接 建立起来才会不阻塞
            Socket socket = serverSocket.accept();
            DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());

            try {
                byte[] byteArray = new byte[4096];

                while (true) {
                    // 实际读到的字节数
                    int readCount = dataInputStream.read(byteArray, 0, byteArray.length);

                    // 读完就返回-1
                    if (-1 == readCount) {
                        break;
                    }
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }
}


import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.Socket;

public class OldIOClient {

    public static void main(String[] args) throws Exception {
        Socket socket = new Socket("localhost", 8899);

        String fileName = "~/Desktop/spark-2.2.0-bin-hadoop2.7.tgz";
        InputStream inputStream = new FileInputStream(fileName);

        DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());

        byte[] buffer = new byte[4096];
        long readCount;
        long total = 0;

        long startTime = System.currentTimeMillis();

        while ((readCount = inputStream.read(buffer)) >= 0) {
            total += readCount;
            dataOutputStream.write(buffer);
        }

        // 200多MB
        // 400多ms
        System.out.println("发送总字节数: " + total + ", 耗时: " + (System.currentTimeMillis() - startTime));

        dataOutputStream.close();
        socket.close();
        inputStream.close();
    }
}

零拷贝


import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

public class NewIOServer {

    public static void main(String[] args) throws Exception{
        InetSocketAddress address = new InetSocketAddress(8899);

        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        ServerSocket serverSocket = serverSocketChannel.socket();
        // 连接关闭之后会有超时状态,超时时候就可以重用了
        // 端口号处于time wait的时候是不可以绑定连接的,true就可以绑定,即便是time out状态
        // 处于time wait就可以重用
        serverSocket.setReuseAddress(true);
        serverSocket.bind(address);

        // 字节数数组的大小是4096
        ByteBuffer byteBuffer = ByteBuffer.allocate(4096);

        while (true) {
            // false 非阻塞,没有Selector,所以就要用阻塞的
            SocketChannel socketChannel = serverSocketChannel.accept();
            socketChannel.configureBlocking(true);

            int readCount = 0;

            while (-1 != readCount) {
                try {
                    // position到最后没法read了
                    readCount = socketChannel.read(byteBuffer);
                } catch (Exception ex) {
                    ex.printStackTrace();
                }

                //
                // 磁带倒带,Mark丢弃,position变为0.
                // rewind与flip的用法差异
                // https://blog.csdn.net/yiifaa/article/details/77652914
                byteBuffer.rewind();
            }
        }

    }
}

import java.io.FileInputStream;
import java.net.InetSocketAddress;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;

public class NewIOClient {

    public static void main(String[] args) throws Exception {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("localhost", 8899));
        socketChannel.configureBlocking(true);

        String fileName = "~/Desktop/spark-2.2.0-bin-hadoop2.7.tgz";

        FileChannel fileChannel = new FileInputStream(fileName).getChannel();

        long startTime = System.currentTimeMillis();

        // 使用 FileChannel  transferTo 就是写到 socketChannel
        // 传递多少就是   0, fileChannel.size(),
        // transferTo 比简单的循环效率更高,很多OS会直接从文件系统缓存直接写到目标Channel。不会从源Channel拷贝到目标
        long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);


        // 149ms
        System.out.println("发送总字节数:" + transferCount + ",耗时: " + (System.currentTimeMillis() - startTime));

        fileChannel.close();
    }
}

磁盘上文件修改一下发给用户

内存映射

文件内容映射,用户空间直接操作内核空间的文件

java里面的MappedByteBuffer就是这个在支持

posted on 2025-10-13 17:42  chuchengzhi  阅读(12)  评论(0)    收藏  举报

导航

杭州技术博主,专注分享云计算领域实战经验、技术教程与行业洞察, 打造聚焦云计算技术的垂直博客,助力开发者快速掌握云服务核心能力。

褚成志 云计算 技术博客