NIO和傳統IO

N1ce2cu發表於2024-08-17

傳統 IO 基於位元組流或字元流(如 FileInputStream、BufferedReader 等)進行檔案讀寫,以及使用 Socket 和 ServerSocket 進行網路傳輸。

NIO 使用通道(Channel)和緩衝區(Buffer)進行檔案操作,以及使用 SocketChannel 和 ServerSocketChannel 進行網路傳輸。

傳統 IO 採用阻塞式模型,對於每個連線,都需要建立一個獨立的執行緒來處理讀寫操作。當一個執行緒在等待 I/O 操作時,無法執行其他任務。這會導致大量執行緒的建立和銷燬,以及上下文切換,降低了系統效能。

NIO 使用非阻塞模型,允許執行緒在等待 I/O 時執行其他任務。這種模式透過使用選擇器(Selector)來監控多個通道(Channel)上的 I/O 事件,實現了更高的效能和可伸縮性。

NIO 和傳統 IO 在操作檔案時的差異

JDK 1.4 中,java.nio.*包引入新的 Java I/O 庫,其目的是提高速度。實際上,“舊”的 I/O 包已經使用 NIO重新實現過,即使我們不顯式的使用 NIO 程式設計,也能從中受益

class SimpleFileTransferTest {

    // 使用傳統的 I/O 方法傳輸檔案
    private long transferFile(File source, File des) throws IOException {
        long startTime = System.currentTimeMillis();

        if (!des.exists()) des.createNewFile();

        // 建立輸入輸出流
        BufferedInputStream bis = new BufferedInputStream(Files.newInputStream(source.toPath()));
        BufferedOutputStream bos = new BufferedOutputStream(Files.newOutputStream(des.toPath()));

        // 使用陣列傳輸資料
        byte[] bytes = new byte[1024 * 1024];
        int len;
        while ((len = bis.read(bytes)) != -1) {
            bos.write(bytes, 0, len);
        }

        long endTime = System.currentTimeMillis();
        return endTime - startTime;
    }

    // 使用 NIO 方法傳輸檔案
    private long transferFileWithNIO(File source, File des) throws IOException {
        long startTime = System.currentTimeMillis();

        if (!des.exists()) des.createNewFile();

        // 建立隨機存取檔案物件
        RandomAccessFile read = new RandomAccessFile(source, "rw");
        RandomAccessFile write = new RandomAccessFile(des, "rw");

        // 獲取檔案通道
        FileChannel readChannel = read.getChannel();
        FileChannel writeChannel = write.getChannel();

        // 建立並使用 ByteBuffer 傳輸資料
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024 * 1024);
        while (readChannel.read(byteBuffer) > 0) {
            byteBuffer.flip();
            writeChannel.write(byteBuffer);
            byteBuffer.clear();
        }

        // 關閉檔案通道
        writeChannel.close();
        readChannel.close();
        long endTime = System.currentTimeMillis();
        return endTime - startTime;
    }

    public static void main(String[] args) throws IOException {
        SimpleFileTransferTest simpleFileTransferTest = new SimpleFileTransferTest();
        File sourse = new File("hello.txt");
        File des = new File("io.txt");
        File nio = new File("nio.txt");

        // 比較傳統的 I/O 和 NIO 傳輸檔案的時間
        long time = simpleFileTransferTest.transferFile(sourse, des);
        System.out.println("普通位元組流時間=" + time);

        long timeNio = simpleFileTransferTest.transferFileWithNIO(sourse, nio);
        System.out.println("NIO時間=" + timeNio);
    }
}

NIO(New I/O)的設計目標是解決傳統 I/O(BIO,Blocking I/O)在處理大量併發連線時的效能瓶頸。傳統 I/O 在網路通訊中主要使用阻塞式 I/O,為每個連線分配一個執行緒。當連線數量增加時,系統效能將受到嚴重影響,執行緒資源成為關鍵瓶頸。而 NIO 提供了非阻塞 I/O 和 I/O 多路複用,可以在單個執行緒中處理多個併發連線,從而在網路傳輸中顯著提高效能。

以下是 NIO 在網路傳輸中優於傳統 I/O 的原因:

①、NIO 支援非阻塞 I/O,這意味著在執行 I/O 操作時,執行緒不會被阻塞。這使得在網路傳輸中可以有效地管理大量併發連線(數千甚至數百萬)。而在操作檔案時,這個優勢沒有那麼明顯,因為檔案讀寫通常不涉及大量併發操作。

②、NIO 支援 I/O 多路複用,這意味著一個執行緒可以同時監視多個通道(如套接字),並在 I/O 事件(如可讀、可寫)準備好時處理它們。這大大提高了網路傳輸中的效能,因為單個執行緒可以高效地管理多個併發連線。操作檔案時這個優勢也無法提現出來。

③、NIO 提供了 ByteBuffer 類,可以高效地管理緩衝區。這在網路傳輸中很重要,因為資料通常是以位元組流的形式傳輸。操作檔案的時候,雖然也有緩衝區,但優勢仍然不夠明顯。

NIO 和傳統 IO 在網路傳輸中的差異

IOSever

class IOServer {
    public static void main(String[] args) {
        try {
            ServerSocket serverSocket = new ServerSocket(9527);
            while (true) {
                Socket client = serverSocket.accept();
                InputStream in = client.getInputStream();
                OutputStream out = client.getOutputStream();

                byte[] buffer = new byte[1024];
                int bytesRead = in.read(buffer);
                out.write(buffer, 0, bytesRead);

                in.close();
                out.close();
                client.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Socket 和 ServerSocket 是傳統的阻塞式 I/O 程式設計方式,用於建立和管理 TCP 連線。

  • Socket:表示客戶端套接字,負責與伺服器端建立連線並進行資料的讀寫。
  • ServerSocket:表示伺服器端套接字,負責監聽客戶端連線請求。當有新的連線請求時,ServerSocket 會建立一個新的 Socket 例項,用於與客戶端進行通訊。

在傳統阻塞式 I/O 程式設計中,每個連線都需要一個單獨的執行緒進行處理,這導致了在高併發場景下的效能問題。

NIOSever

class NIOServer {
    public static void main(String[] args) {
        try {
            // 建立 ServerSocketChannel
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            // 繫結埠
            serverSocketChannel.bind(new InetSocketAddress(4399));
            // 設定為非阻塞模式
            serverSocketChannel.configureBlocking(false);

            // 建立 Selector
            Selector selector = Selector.open();
            // 將 ServerSocketChannel 註冊到 Selector,關注 OP_ACCEPT 事件
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

            // 無限迴圈,處理事件
            while (true) {
                // 阻塞直到有事件發生
                selector.select();
                // 獲取發生事件的 SelectionKey
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    // 處理完後,從 selectedKeys 集合中移除
                    iterator.remove();

                    // 判斷事件型別
                    if (key.isAcceptable()) {
                        // 有新的連線請求
                        ServerSocketChannel server = (ServerSocketChannel) key.channel();
                        // 接受連線
                        SocketChannel client = server.accept();
                        // 設定為非阻塞模式
                        client.configureBlocking(false);
                        // 將新的 SocketChannel 註冊到 Selector,關注 OP_READ 事件
                        client.register(selector, SelectionKey.OP_READ);
                    } else if (key.isReadable()) {
                        // 有資料可讀
                        SocketChannel client = (SocketChannel) key.channel();
                        // 建立 ByteBuffer 緩衝區
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        // 從 SocketChannel 中讀取資料並寫入 ByteBuffer
                        client.read(buffer);
                        // 準備讀取
                        buffer.flip();
                        // 將資料從 ByteBuffer 寫回到 SocketChannel
                        client.write(buffer);
                        // 關閉連線
                        client.close();
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

非阻塞 I/O,可以在單個執行緒中處理多個連線。

  • ServerSocketChannel:類似於 ServerSocket,表示伺服器端套接字通道。它負責監聽客戶端連線請求,並可以設定為非阻塞模式,這意味著在等待客戶端連線請求時不會阻塞執行緒。
  • SocketChannel:類似於 Socket,表示客戶端套接字通道。它負責與伺服器端建立連線並進行資料的讀寫。SocketChannel 也可以設定為非阻塞模式,在讀寫資料時不會阻塞執行緒。

Selector 是 Java NIO 中的一個關鍵元件,用於實現 I/O 多路複用。它允許在單個執行緒中同時監控多個 ServerSocketChannel 和 SocketChannel,並透過 SelectionKey 標識關注的事件。當某個事件發生時,Selector 會將對應的 SelectionKey 新增到已選擇的鍵集合中。透過使用 Selector,可以在單個執行緒中同時處理多個連線,從而有效地提高 I/O 操作的效能,特別是在高併發場景下。

客戶端測試用例

class TestClient {
    public static void main(String[] args) throws InterruptedException {
        int clientCount = 10000;
        ExecutorService executorServiceIO = Executors.newFixedThreadPool(10);
        ExecutorService executorServiceNIO = Executors.newFixedThreadPool(10);

        // 使用傳統 IO 的客戶端
        Runnable ioClient = () -> {
            try {
                Socket socket = new Socket("localhost", 9527);
                OutputStream out = socket.getOutputStream();
                InputStream in = socket.getInputStream();
                out.write("Hello, IO!".getBytes());
                byte[] buffer = new byte[1024];
                in.read(buffer);
                in.close();
                out.close();
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        };

        // 使用 NIO 的客戶端
        Runnable nioClient = () -> {
            try {
                SocketChannel socketChannel = SocketChannel.open();
                socketChannel.connect(new InetSocketAddress("localhost", 4399));
                ByteBuffer buffer = ByteBuffer.wrap("Hello, NIO!".getBytes());
                socketChannel.write(buffer);
                buffer.clear();
                socketChannel.read(buffer);
                socketChannel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        };

        // 分別測試 NIO 和傳統 IO 的伺服器效能
        long startTime, endTime;

        startTime = System.currentTimeMillis();
        for (int i = 0; i < clientCount; i++) {
            executorServiceIO.execute(ioClient);
        }
        executorServiceIO.shutdown();
        executorServiceIO.awaitTermination(1, TimeUnit.MINUTES);
        endTime = System.currentTimeMillis();
        System.out.println("傳統 IO 伺服器處理 " + clientCount + " 個客戶端耗時: " + (endTime - startTime) + "ms");

        startTime = System.currentTimeMillis();
        for (int i = 0; i < clientCount; i++) {
            executorServiceNIO.execute(nioClient);
        }
        executorServiceNIO.shutdown();
        executorServiceNIO.awaitTermination(1, TimeUnit.MINUTES);
        endTime = System.currentTimeMillis();
        System.out.println("NIO 伺服器處理 " + clientCount + " 個客戶端耗時: " + (endTime - startTime) + "ms");
    }
}

相關文章