nio 入門

nio的知識以操作系統,網絡原理,java的bio,socket,線程爲基礎,基礎知識的掌握程度決定了對nio,netty的掌握程度。

阻塞io

簡化後的bio示例代碼,客戶端向服務器發送字符串:QUERY TIME ORDER,服務器收到確認字符串後返回當前時間。

  • 服務器主線程負責等待接受請求,當收到一個請求後,開啓一個線程去處理。
  • 使用jdk 1.7的try-with-resources Statement來消除樣板代碼,即關閉資源相關代碼。
  • 服務器使用線程池來處理請求,避免耗盡系統資源。
  • 客戶端的 in.readLine();是阻塞的,直到服務器返回數據後,輸出響應內容。
  • 服務器每次處理完一個請求,關閉socket連接。
public class TimeClient {
    public static void main(String[] args) {
        try (Socket socket = new Socket("127.0.0.1", 8080);
             BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
             PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
            out.println("QUERY TIME ORDER");
            String resp = in.readLine();
            System.out.println("Now is : " + resp);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
public class TimeServer {
    private static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 20, 0L, TimeUnit.SECONDS, new LinkedBlockingDeque<>(100), r -> {
        Thread thread = new Thread(r);
        thread.setName("TimeServer thread pool");
        return thread;
    });

    public static void main(String[] args) {
        int port = 8080;
        try (ServerSocket server = new ServerSocket(port)) {
            System.out.println("The time server is start in port : " + port);
            while (true) {
                Socket socket = server.accept();
                threadPool.submit(() -> {
                    try (BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                         PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
                        String command = in.readLine();
                        String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(command) ? new Date().toString() : "BAD ORDER";
                        out.println(currentTime);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                });
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

之所以是阻塞的,是因爲:

  • ServerSocket 上的 accept()方法將會一直阻塞到一個連接建立 ,隨後返回一個 新的Socket 用於客戶端和服務器之間的通信。該 ServerSocket 將繼續監聽傳入的 連接。
  • readLine()方法將會阻塞,直到一個由換行符或者回車符結尾的字符串被讀取。

非阻塞io

首先不管是bio還是nio,都是大量的樣板代碼,基本都是固定的寫法。

nio的核心概念:Channel,Buffer,Selector。之所以是非阻塞的,是藉助了操作系統的能力,它使用了事件通知 API以確定在一組非阻塞套接字中有哪些已經就緒能夠進行 I/O 相關的操作。因爲可以在任何的時間檢查任意的讀操作或者寫操作的完成狀態,一個單一的線程便可以處理多個併發的連接。nio的核心是selector,一個selecter可以管理多個channel。nio是一組全新的api,與bio的對應關係應當如下:

  • Socket--->ServerSocket,SocketChannel-->ServerSocketChannel
  • xxxStream-->Buffer
  • 線程池-->Selector註冊,輪詢

服務器端是等待連接,接受連接,客戶端是主動發起連接。服務端開啓ServerSocketChannel,開啓Selector,並將channel註冊到select,並且要訂閱要關注的事件,事件包括:接受連接,已連接,可讀,可寫。

servChannel = ServerSocketChannel.open();
servChannel.configureBlocking(false);
servChannel.socket().bind(new InetSocketAddress(port), 1024);
selector = Selector.open();
servChannel.register(selector, SelectionKey.OP_ACCEPT);

然後就是死循環輪詢,是否有事件發生。

 Set<SelectionKey> selectedKeys = selector.selectedKeys();
  Iterator<SelectionKey> it = selectedKeys.iterator();

通過監聽Accept事件,建立連接後產生的SocketChannel對象也註冊到selector上。

 if (key.isAcceptable()) {
                // Accept the new connection
                ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                SocketChannel sc = ssc.accept();
                sc.configureBlocking(false);
                // Add the new connection to the selector
                sc.register(selector, SelectionKey.OP_READ);
            }

根據讀寫事件來處理每一條連接建立後的通信,使用Buffer的子類讀寫數據。

      if (key.isReadable()) {
                SocketChannel sc = (SocketChannel) key.channel();
                ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                int readBytes = sc.read(readBuffer);

nio避免創建多個線程,線程間切換的開銷,藉助操作系統異步通知機制,沒有 I/O 操作需要處理的時候,線程也可以被用於其他任務。

參考文獻:

  • 《netty權威指南》及源碼
  • 《netty實戰》
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章