【死磕NIO】— NIO基礎詳解

chenssy發表於2021-09-12

Netty 是基於Java NIO 封裝的網路通訊框架,只有充分理解了 Java NIO 才能理解好Netty的底層設計。Java NIO 由三個核心元件元件:

  • Buffer

  • Channel

  • Selector

緩衝區 Buffer

Buffer 是一個資料物件,我們可以把它理解為固定數量的資料的容器,它包含一些要寫入或者讀出的資料。

在 Java NIO 中,任何時候訪問 NIO 中的資料,都需要通過緩衝區(Buffer)進行操作。讀取資料時,直接從緩衝區中讀取,寫入資料時,寫入至緩衝區。NIO 最常用的緩衝區則是 ByteBuffer。下圖是 Buffer 繼承關係圖:

每一個 Java 基本型別都對應著一種 Buffer,他們都包含這相同的操作,只不過是所處理的資料型別不同而已。

通道 Channel

Channel 是一個通道,它就像自來水管一樣,網路資料通過 Channel 這根水管讀取和寫入。傳統的 IO 是基於流進行操作的,Channle 和類似,但又有些不同:

區別 通過Channel
支援非同步 不支援 支援
是否可雙向傳輸資料 不能,只能單向 可以,既可以從通道讀取資料,也可以向通道寫入資料
是否結合 Buffer 使用 必須結合 Buffer 使用
效能 較低 較高

正如上面說到的,Channel 必須要配合 Buffer 一起使用,我們永遠不可能將資料直接寫入到 Channel 中,同樣也不可能直接從 Channel 中讀取資料。都是通過從 Channel 讀取資料到 Buffer 中或者從 Buffer 寫入資料到 Channel 中,如下:

簡單點說,Channel 是資料的源頭或者資料的目的地,用於向 buffer 提供資料或者讀取 buffer 資料,並且對 I/O 提供非同步支援。

下圖是 Channel 的類圖

Channel 為最頂層介面,所有子 Channel 都實現了該介面,它主要用於 I/O 操作的連線。定義如下:

public interface Channel extends Closeable {

    /**

     * 判斷此通道是否處於開啟狀態。 

     */

    public boolean isOpen();

    /**

     *關閉此通道。

     */

    public void close() throws IOException;
}

最為重要的Channel實現類為:

  • FileChannel:一個用來寫、讀、對映和操作檔案的通道

  • DatagramChannel:能通過 UDP 讀寫網路中的資料

  • SocketChannel: 能通過 TCP 讀寫網路中的資料

  • ServerSocketChannel:可以監聽新進來的 TCP 連線,像 Web 伺服器那樣。對每一個新進來的連線都會建立一個 SocketChannel

多路複用器 Selector

多路複用器 Selector,它是 Java NIO 程式設計的基礎,它提供了選擇已經就緒的任務的能力。從底層來看,Selector 提供了詢問通道是否已經準備好執行每個 I/O 操作的能力。簡單來講,Selector 會不斷地輪詢註冊在其上的 Channel,如果某個 Channel 上面發生了讀或者寫事件,這個 Channel 就處於就緒狀態,會被 Selector 輪詢出來,然後通過 SelectionKey 可以獲取就緒 Channel 的集合,進行後續的 I/O 操作。

Selector 允許一個執行緒處理多個 Channel ,也就是說只要一個執行緒複雜 Selector 的輪詢,就可以處理成千上萬個 Channel ,相比於多執行緒來處理勢必會減少執行緒的上下文切換問題。下圖是一個 Selector 連線三個 Channel :

例項

服務端

public class NIOServer {
    /*接受資料緩衝區*/

    private ByteBuffer sendbuffer = ByteBuffer.allocate(1024);

    /*傳送資料緩衝區*/

    private  ByteBuffer receivebuffer = ByteBuffer.allocate(1024);

    private Selector selector;

    public NIOServer(int port) throws IOException {

        // 開啟伺服器套接字通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        // 伺服器配置為非阻塞
        serverSocketChannel.configureBlocking(false);

        // 檢索與此通道關聯的伺服器套接字
        ServerSocket serverSocket = serverSocketChannel.socket();

        // 進行服務的繫結
        serverSocket.bind(new InetSocketAddress(port));

        // 通過open()方法找到Selector
        selector = Selector.open();

        // 註冊到selector,等待連線
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        System.out.println("Server Start----:");

    }

    private void listen() throws IOException {

        while (true) {

            selector.select();

            Set<SelectionKey> selectionKeys = selector.selectedKeys();

            Iterator<SelectionKey> iterator = selectionKeys.iterator();

            while (iterator.hasNext()) {

                SelectionKey selectionKey = iterator.next();

                iterator.remove();

                handleKey(selectionKey);

            }

        }

    }

    private void handleKey(SelectionKey selectionKey) throws IOException {

        // 接受請求
        ServerSocketChannel server = null;

        SocketChannel client = null;

        String receiveText;

        String sendText;

        int count=0;

        // 測試此鍵的通道是否已準備好接受新的套接字連線。
        if (selectionKey.isAcceptable()) {

            // 返回為之建立此鍵的通道。
            server = (ServerSocketChannel) selectionKey.channel();

            // 接受到此通道套接字的連線。
            // 此方法返回的套接字通道(如果有)將處於阻塞模式。
            client = server.accept();

            // 配置為非阻塞
            client.configureBlocking(false);

            // 註冊到selector,等待連線
            client.register(selector, SelectionKey.OP_READ);

        } else if (selectionKey.isReadable()) {

            // 返回為之建立此鍵的通道。
            client = (SocketChannel) selectionKey.channel();

            //將緩衝區清空以備下次讀取
            receivebuffer.clear();

            //讀取伺服器傳送來的資料到緩衝區中
            count = client.read(receivebuffer);

            if (count > 0) {

                receiveText = new String( receivebuffer.array(),0,count);

                System.out.println("伺服器端接受客戶端資料--:"+receiveText);

                client.register(selector, SelectionKey.OP_WRITE);

            }

        } else if (selectionKey.isWritable()) {

            //將緩衝區清空以備下次寫入
            sendbuffer.clear();

            // 返回為之建立此鍵的通道。
            client = (SocketChannel) selectionKey.channel();

            sendText="message from server--";

            //向緩衝區中輸入資料
            sendbuffer.put(sendText.getBytes());

            //將緩衝區各標誌復位,因為向裡面put了資料標誌被改變要想從中讀取資料發向伺服器,就要復位
            sendbuffer.flip();

            //輸出到通道
            client.write(sendbuffer);

            System.out.println("伺服器端向客戶端傳送資料--:"+sendText);

            client.register(selector, SelectionKey.OP_READ);

        }

    }

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

        int port = 8080;

        NIOServer server = new NIOServer(port);

        server.listen();

    }

}

客戶端

public class NIOClient {
    /*接受資料緩衝區*/
    private static ByteBuffer sendbuffer = ByteBuffer.allocate(1024);

    /*傳送資料緩衝區*/
    private static ByteBuffer receivebuffer = ByteBuffer.allocate(1024);

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

        // 開啟socket通道
        SocketChannel socketChannel = SocketChannel.open();

        // 設定為非阻塞方式
        socketChannel.configureBlocking(false);

        // 開啟選擇器
        Selector selector = Selector.open();

        // 註冊連線服務端socket動作
        socketChannel.register(selector, SelectionKey.OP_CONNECT);

        // 連線

        socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));

        Set<SelectionKey> selectionKeys;

        Iterator<SelectionKey> iterator;

        SelectionKey selectionKey;

        SocketChannel client;

        String receiveText;

        String sendText;

        int count=0;

        while (true) {

            //選擇一組鍵,其相應的通道已為 I/O 操作準備就緒。
            //此方法執行處於阻塞模式的選擇操作。
            selector.select();

            //返回此選擇器的已選擇鍵集。
            selectionKeys = selector.selectedKeys();

            //System.out.println(selectionKeys.size());
            iterator = selectionKeys.iterator();

            while (iterator.hasNext()) {

                selectionKey = iterator.next();

                if (selectionKey.isConnectable()) {

                    System.out.println("client connect");

                    client = (SocketChannel) selectionKey.channel();

                    // 判斷此通道上是否正在進行連線操作。
                    // 完成套接字通道的連線過程。
                    if (client.isConnectionPending()) {

                        client.finishConnect();

                        System.out.println("完成連線!");

                        sendbuffer.clear();

                        sendbuffer.put("Hello,Server".getBytes());

                        sendbuffer.flip();

                        client.write(sendbuffer);

                    }

                    client.register(selector, SelectionKey.OP_READ);

                } else if (selectionKey.isReadable()) {

                    client = (SocketChannel) selectionKey.channel();

                    //將緩衝區清空以備下次讀取
                    receivebuffer.clear();

                    //讀取伺服器傳送來的資料到緩衝區中
                    count=client.read(receivebuffer);

                    if(count>0){

                        receiveText = new String( receivebuffer.array(),0,count);

                        System.out.println("客戶端接受伺服器端資料--:"+receiveText);

                        client.register(selector, SelectionKey.OP_WRITE);

                    }

                } else if (selectionKey.isWritable()) {

                    sendbuffer.clear();

                    client = (SocketChannel) selectionKey.channel();

                    sendText = "message from client--";

                    sendbuffer.put(sendText.getBytes());

                    //將緩衝區各標誌復位,因為向裡面put了資料標誌被改變要想從中讀取資料發向伺服器,就要復位
                    sendbuffer.flip();

                    client.write(sendbuffer);

                    System.out.println("客戶端向伺服器端傳送資料--:"+sendText);

                    client.register(selector, SelectionKey.OP_READ);

                }

            }

            selectionKeys.clear();

        }

    }
}

執行結果

相關文章