簡單的聊聊網路請求中的記憶體拷貝

CoderJiAi發表於2019-02-23

掘金是自己剛發現不久的平臺,原本一些學習筆記都是記錄在有道,因為正好兩邊都支援markdown,現在打算把一些整理後的筆記分享出來。這篇主要來簡單的聊聊網路請求中的記憶體拷貝。

網路請求中資料傳輸過程圖

資料傳輸型別一(read)

簡單的聊聊網路請求中的記憶體拷貝

該資料傳輸模型正是傳統的IO進行網路通訊時所採用的方式,資料在使用者空間(JVM記憶體)與核心空間進行多次拷貝和上下文切換,對於沒有對資料進行業務處理的時候,這樣拷貝顯得很沒有必要。

資料傳輸型別二(sendFile)

簡單的聊聊網路請求中的記憶體拷貝

該資料傳輸模型是的NIO進行網路通訊時所採用的方式,它依賴於作業系統是否支援這種對於核心的操作(圖中第4個過程),這個模型對比第一種減少了兩次不必要的使用者空間和核心之間的資料拷貝過程。

資料傳輸型別三(支援聚集的sendFile)

簡單的聊聊網路請求中的記憶體拷貝

從中我們可以發現這種真正實現了零拷貝,這種傳輸模型它依賴於作業系統是否支援這種對於核心的操作(圖中4過程),圖中4過程看著很難理解,下面把四過程裡面的奧祕分解下。

簡單的聊聊網路請求中的記憶體拷貝

四過程其實是將核心中檔案資訊(檔案地址、大小等資訊)appendStr到Sokcet Buffer中,這樣Sokcet Buffer中存有很少的資訊,然後在協議引擎傳輸之前使用Gather將兩個Buffer聚集。

資料傳輸型別四(mmap,本文先不介紹)

程式碼實現

模型一(BIO)

/**
 * @Author CoderJiA
 * @Description TransferModel1Client
 * @Date 23/2/19 下午3:01
 **/
public class TransferModel1Client {

    private static final String HOST = "localhost";
    private static final int PORT = 8899;
    private static final String FILE_PATH = "/Users/coderjia/Documents/gradle-5.2.1-all.zip";
    private static final int MB = 1024 * 1024;

    public static void main(String[] args) throws Exception{
        Socket socket = new Socket(HOST, PORT);
        InputStream input = new FileInputStream(FILE_PATH);
        DataOutputStream output = new DataOutputStream(socket.getOutputStream());
        byte[] bytes = new byte[MB];
        long start = System.currentTimeMillis();
        int len;
        while ((len = input.read(bytes)) != -1) {
            output.write(bytes, 0, len);
        }
        long end = System.currentTimeMillis();
        System.out.println("耗時:" + (end - start) + "ms");
        output.close();
        input.close();
        socket.close();
    }
}
複製程式碼
/**
 * @Author CoderJiA
 * @Description TransferModel1Server
 * @Date 23/2/19 下午3:01
 **/
public class TransferModel1Server {

    private static final int PORT = 8899;
    private static final int MB = 1024 * 1024;

    public static void main(String[] args) throws Exception{
        ServerSocket serverSocket = new ServerSocket(PORT);
        for (;;) {
            Socket socket = serverSocket.accept();
            DataInputStream input = new DataInputStream(socket.getInputStream());
            byte[] bytes = new byte[MB];
            for (;;) {
                int readSize = input.read(bytes, 0, MB);
                if (-1 == readSize) {
                    break;
                }
            }
        }
    }
}
複製程式碼

模型二(NIO)

/**
 * @Author CoderJiA
 * @Description TransferModel2Client
 * @Date 23/2/19 下午3:36
 **/
public class TransferModel2Client {

    private static final String HOST = "localhost";
    private static final int PORT = 8899;
    private static final String FILE_PATH = "/Users/coderjia/Documents/gradle-5.2.1-all.zip";

    public static void main(String[] args) throws Exception {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress(HOST, PORT));
        socketChannel.configureBlocking(true);
        FileChannel fileChannel = new FileInputStream(FILE_PATH).getChannel();
        long start = System.currentTimeMillis();
        fileChannel.transferTo(0, fileChannel.size(), socketChannel);
        long end = System.currentTimeMillis();
        System.out.println("耗時:" + (end - start) + "ms");
        fileChannel.close();
    }

}
複製程式碼
/**
 * @Author CoderJiA
 * @Description TransferModel2Server
 * @Date 23/2/19 下午3:36
 **/
public class TransferModel2Server {

    private static final int PORT = 8899;
    private static final int MB = 1024 * 1024;

    public static void main(String[] args) throws Exception {

        InetSocketAddress address = new InetSocketAddress(PORT);

        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        ServerSocket serverSocket = serverSocketChannel.socket();
        serverSocket.setReuseAddress(true);
        serverSocket.bind(address);

        ByteBuffer byteBuffer = ByteBuffer.allocate(MB);
        for (;;) {
            SocketChannel socketChannel = serverSocketChannel.accept();
            socketChannel.configureBlocking(true);
            int readSize = 0;
            while (-1 != readSize) {
                readSize = socketChannel.read(byteBuffer);
                byteBuffer.rewind();
            }

        }

    }
}
複製程式碼

fileChannel.transferTo(0, fileChannel.size(), socketChannel)

transferto方法的文件註釋: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.

這句話簡單的理解就是:該方法比傳統的簡單輪詢(指的就是IO中的拷貝過程)更加高效,tranferto的拷貝方式依賴於底層作業系統,目前很多作業系統支援像模型二拷貝過程。在核心版本2.4中,修改了套接字緩衝區描述符以適應這些要求——在Linux下稱為零拷貝。 這種方法不僅減少了多個上下文切換,還完全消除了處理器的資料複製操作。

效能對比

在同一臺機器上,相同環境下測試結果如圖。

簡單的聊聊網路請求中的記憶體拷貝

參考文章地址

www.jianshu.com/p/e9f422586…

原始碼地址

github.com/coderjia061…

相關文章