1.Selector(多路複用)
原先的bio中,一個客戶端連接,就爲它分配一個線程。這樣的問題,當用戶激增時候,線程會增加很多,增加服務器開銷。
所以後來使用了線程池進行管理線程,但是有個弊端,如果線程池有100個線程,這個時候第101個就會等待。傳統的bio(Server/Client)如下圖:
有這個弊端,Nio就用selector解決。
NIO中非阻塞I/O 採用了基於Reactor模式的工作方式,I/O 調用不會被阻塞,相反是註冊感興趣的特定I/O 事件,如可讀數據到
達,新的套接字連接等等,在發生特定事件時,系統再通知我們。NIO中實現非阻塞I/O的核心對象就是Selector,Selector 就是
註冊各種I/O 事件地方,而且當那些事件發生時,就是這個對象告訴我們所發生的事件,如下圖所示:
從圖中可以看出,當有讀或寫等任何註冊的事件發生時,可以從Selector 中獲得相應的SelectionKey,同時從 SelectionKey中可
以找到發生的事件和該事件所發生的具體的SelectableChannel,以獲得客戶端發送過來的數據。
使用NIO中非阻塞I/O 編寫服務器處理程序,大體上可以分爲下面三個步驟:
1. 向Selector 對象註冊感興趣的事件。
2. 從Selector 中獲取感興趣的事件。
3. 根據不同的事件進行相應的處理。
/* * 註冊事件 */
private Selector getSelector() throws IOException {
// 創建 Selector 對象
Selector sel = Selector.open();
// 創建可選擇通道,並配置爲非阻塞模式
ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false);
// 綁定通道到指定端口
ServerSocket socket = server.socket();
InetSocketAddress address = new InetSocketAddress(port);
socket.bind(address);
// 向 Selector 中註冊感興趣的事件
server.register(sel, SelectionKey.OP_ACCEPT); return sel;
}
創建了ServerSocketChannel對象,並調用 configureBlocking()方法,配置爲非阻塞模式,接下來的三行代碼把該通道綁定到指定端口,最後向Selector 中註冊事件,此處指定的是參數是OP_ACCEPT,即指定我們想要監聽accept 事件,也就是新的連接發 生時所產生的事件,對於ServerSocketChannel 通道來說,我們唯一可以指定的參數就是OP_ACCEPT。
當Selector 中獲取感興趣的事件,即開始監聽,進入內部循環:
public void listen(){ System.out.println("listen on " + this.port + "."); try { //輪詢主線程 while (true){ //大堂經理再叫號 selector.select(); //每次都拿到所有的號子 Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> iter = keys.iterator(); //不斷地迭代,就叫輪詢 //同步體現在這裏,因爲每次只能拿一個key,每次只能處理一種狀態 while (iter.hasNext()){ SelectionKey key = iter.next(); iter.remove(); //每一個key代表一種狀態 //沒一個號對應一個業務 //數據就緒、數據可讀、數據可寫 等等等等 process(key); } } } catch (IOException e) { e.printStackTrace(); } }
在非阻塞I/O 中,內部循環模式基本都是遵循這種方式。首先調用select()方法,該方法會阻塞,直到至少有一個事件發生,然後
再使用selectedKeys()方法獲取發生事件的SelectionKey,再使用迭代器進行循環。
最後根據不同事件進行不同處理:
private void process(SelectionKey key) throws IOException { //針對於每一種狀態給一個反應 if(key.isAcceptable()){ ServerSocketChannel server = (ServerSocketChannel)key.channel(); //這個方法體現非阻塞,不管你數據有沒有準備好 //你給我一個狀態和反饋 SocketChannel channel = server.accept(); //一定一定要記得設置爲非阻塞 channel.configureBlocking(false); //當數據準備就緒的時候,將狀態改爲可讀 key = channel.register(selector,SelectionKey.OP_READ); } else if(key.isReadable()){ //key.channel 從多路複用器中拿到客戶端的引用 SocketChannel channel = (SocketChannel)key.channel(); int len = channel.read(buffer); if(len > 0){ buffer.flip(); String content = new String(buffer.array(),0,len); key = channel.register(selector,SelectionKey.OP_WRITE); //在key上攜帶一個附件,一會再寫出去 key.attach(content); System.out.println("讀取內容:" + content); } } else if(key.isWritable()){ SocketChannel channel = (SocketChannel)key.channel(); String content = (String)key.attachment(); channel.write(ByteBuffer.wrap(("輸出:" + content).getBytes())); channel.close(); } }
2.Channel
通道是一個對象。我們用來讀取和輸出對象。裏面的數據我們不是用bio中的字節流處理,而是用buffer緩衝區。是將數據從通
道讀入緩衝區,再從緩衝區獲取這個字節。
在NIO 中,提供了多種通道對象,而所有的通道對象都實現了 Channel 接口。它們之間的繼承關係如下圖所示: