1. 程式人生 > 其它 >NIO核心之選擇器(Selector)與NIO網路通訊原理

NIO核心之選擇器(Selector)與NIO網路通訊原理

選擇器(Selector)

選擇器(Selector) 是 SelectableChannle 物件的多路複用器,Selector 可以同時監控多個 SelectableChannel 的 IO 狀況,也就是說,利用 Selector可使一個單獨的執行緒管理多個 Channel。Selector 是非阻塞 IO 的核心。

  • Java 的 NIO,用非阻塞的 IO 方式。可以用一個執行緒,處理多個的客戶端連線,就會使用到 Selector(選擇器)

  • Selector 能夠檢測多個註冊的通道上是否有事件發生(注意:多個 Channel 以事件的方式可以註冊到同一個Selector,如果有事件發生,便獲取事件然後針對每個事件進行相應的處理。這樣就可以只用一個單執行緒去管理多個通道,也就是管理多個連線和請求。

  • 只有在 連線/通道 真正有讀寫事件發生時,才會進行讀寫,就大大地減少了系統開銷,並且不必為每個連線都建立一個執行緒,不用去維護多個執行緒

  • 避免了多執行緒之間的上下文切換導致的開銷

  • 選擇 器(Selector)的應用

    建立 Selector :通過呼叫 Selector.open() 方法建立一個 Selector。

Selector selector = Selector.open();

   向選擇器註冊通道:SelectableChannel.register(Selector sel, int ops)

//1. 獲取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
//2. 切換非阻塞模式 ssChannel.configureBlocking(false); //3. 繫結連線 ssChannel.bind(new InetSocketAddress(9898)); //4. 獲取選擇器 Selector selector = Selector.open(); //5. 將通道註冊到選擇器上, 並且指定“監聽接收事件” ssChannel.register(selector, SelectionKey.OP_ACCEPT);

當呼叫 register(Selector sel, int ops) 將通道註冊選擇器時,選擇器對通道的監聽事件,需要通過第二個引數 ops 指定。可以監聽的事件型別(用 可使用 SelectionKey 的四個常量 表示):

  • 讀 : SelectionKey.OP_READ (1)

  • 寫 : SelectionKey.OP_WRITE (4)

  • 連線 : SelectionKey.OP_CONNECT (8)

  • 接收 : SelectionKey.OP_ACCEPT (16)

若註冊時不止監聽一個事件,則可以使用“位或”操作符連線。

int interestSet = SelectionKey.OP_READ|SelectionKey.OP_WRITE 

NIO網路通訊原理

Selector 示意圖和特點說明

Selector可以實現: 一個 I/O 執行緒可以併發處理 N 個客戶端連線和讀寫操作,這從根本上解決了傳統同步阻塞 I/O 一連線一執行緒模型,架構的效能、彈性伸縮能力和可靠性都得到了極大的提升。

服務端流程

public class Server {
    public static void main(String[] args) throws Exception {
        System.out.println("----服務端啟動---");
        // 1、獲取通道
        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        // 2、切換為非阻塞模式
        ssChannel.configureBlocking(false);
        // 3、繫結連線的埠
        ssChannel.bind(new InetSocketAddress(9999));
        // 4、獲取選擇器Selector
        Selector selector = Selector.open();
        // 5、將通道都註冊到選擇器上去,並且開始指定監聽接收事件
        ssChannel.register(selector , SelectionKey.OP_ACCEPT);
        // 6、使用Selector選擇器輪詢已經就緒好的事件
        while (selector.select() > 0){
            System.out.println("開始新一輪事件處理");
            // 7、獲取選擇器中的所有註冊的通道中已經就緒好的事件 迭代器的方式獲得:selectedKeys獲取當前選擇器輪詢到的所有事件 iterator()迭代器
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            // 8、開始遍歷這些準備好的事件 如果有下一個 就提取
            while (it.hasNext()){
                // 提取當前這個事件
                SelectionKey sk = it.next();
                // 9、判斷這個事件具體是什麼 是接入事件?是 則接入客戶端通道
                if(sk.isAcceptable()){
                    // 10、直接獲取當前接入的客戶端通道
                    SocketChannel schannel = ssChannel.accept();
                    // 11 、切換成非阻塞模式
                    schannel.configureBlocking(false);
                    // 12、將本客戶端通道註冊到選擇器 進行讀的監聽(客戶端寫過來 服務端負責讀)
                    schannel.register(selector , SelectionKey.OP_READ);
                    //如果是讀事件
                }else if(sk.isReadable()){
                    // 13、獲取當前選擇器上的讀就緒事件  先獲得通道 獲得客戶端連線 有通道則說明有資料過來
                    SocketChannel sChannel = (SocketChannel) sk.channel();
                    // 14、設定buffer讀取資料
                    ByteBuffer buf = ByteBuffer.allocate(1024);
                    int len = 0;
                    //迴圈讀取通道中的緩衝區 >0 還有資料未讀 迴圈讀完
                    while((len = sChannel.read(buf)) > 0){
                        buf.flip();//歸位
                        System.out.println(new String(buf.array() , 0, len));
                        buf.clear();// 清除之前的資料
                    }
                }
                it.remove(); // 處理完畢之後需要移除當前事件
            }
        }
    }
}

客戶端流程

public class Client {
    public static void main(String[] args) throws Exception {
        // 1、獲取通道
        SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));
        // 2、切換成非阻塞模式
        sChannel.configureBlocking(false);
        // 3、分配指定緩衝區大小
        ByteBuffer buf = ByteBuffer.allocate(1024);
        // 4、傳送資料給服務端
        Scanner sc = new Scanner(System.in);
        while (true){
            System.out.println("請說:");
            String msg = sc.nextLine();
            buf.put(("theone:"+msg).getBytes());
            buf.flip();
            sChannel.write(buf);
            buf.clear();
        }
    }
}

總結:

BIO、NIO:

  • Java BIO : 同步並阻塞,伺服器實現模式為一個連線一個執行緒,即客戶端有連線請求時伺服器端就需要啟動一個執行緒進行處理,如果這個連線不做任何事情會造成不必要的執行緒開銷,當然可以通過執行緒池機制改善。

  • Java NIO : 同步非阻塞,伺服器實現模式為一個請求一個執行緒,即客戶端傳送的連線請求都會註冊到多路複用器上,多路複用器輪詢到連線有I/O請求時才啟動一個執行緒進行處理。

BIO、NIO適用場景分析:

  • BIO方式適用於連線數目比較小且固定的架構,這種方式對伺服器資源要求比較高,併發侷限於應用中,JDK1.4以前的唯一選擇,但程式直觀簡單易理解。

  • NIO方式適用於連線數目多且連線比較短(輕操作)的架構,比如聊天伺服器,併發侷限於應用中,程式設計比較複雜,JDK1.4開始支援。