|
從之前的通道(1)--基礎接口 大概知道了通道的基本特點,open/close/selectable/流式通道可以通過工廠方法open創(chuàng)建,文件通道比如在一個文件對象上調(diào)用getChannel來獲取,下面來深入看下SocketChannel
這一篇比較偏向理論
如果想知道大概的代碼怎么寫,可以參考 SocketChannel續(xù)1---基本操作API
如果想知道更多的坑,可以參考 SocketChannel續(xù)2---很多注意點
如果想知道一個nio框架的演變,可以參考SocketChannel續(xù)3--io框架模型演化
1 SocketIO
1.1 阻塞式的
之前的Socket/ServerSocket/DatagramSocket他們是阻塞式的,java IO的性能瓶頸所在
1.2 不便的讀寫操作
另外讀寫也相對不便,分別是getInputStream() 和getOutputStream() 然后使用他們的read和write方法,或者用XXReader稍微包裝一下,比如下面的
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
PrintWriter out = new PrintWriter(client.getOutputStream());
while (true) {
String str = in.readLine();//從socket讀入數(shù)據(jù)
out.println("has receive...."+str);
out.flush();
if (str.equals("end"))
break;
}
2 SocketChannel
通道是新增的IO服務管道,并且提供交互方法,但是socket已有的協(xié)議API并不會重新實現(xiàn),大多可以復用
全部Socket通道類(DatagramChannel/SocketChannel/ServerSocketChannel)在創(chuàng)建的時候都會創(chuàng)建一個對等的Socket對象,可以通過socket()方法獲取,這種方式獲取到的socket可以調(diào)用getChannel()獲取對應的channel
2.1 可選擇的通道
從最基礎的層面來看,選擇器提供了詢問通道是否準備好執(zhí)行每個I/O操作的能力,比如了解一個SocketChannel對象是否有更多的字節(jié)需要讀取,或者需要知道ServerSocketChannel是否有需要準備接受的連接。他們都實現(xiàn)了SelectableChannel接口,因此可以實現(xiàn)就緒選擇,這種方式的價值在于潛在的大量的通道同時進行就緒狀態(tài)的檢查。
2.2 自己實現(xiàn)選擇通道?
假設自己實現(xiàn)對是否就緒通道的輪訓,那么從效率上來說會有幾個問題:1.檢查每個通道都需要一次系統(tǒng)調(diào)用,代價昂貴;2.檢查不是原子性的,列表中的每一個通道都有可能在它被檢查之后就緒,直到下一次輪訓為止;3.不斷地遍歷,無法在某個感興趣的通道就緒時得到通知;
傳統(tǒng)的監(jiān)控多個socket的方案是為每一個socket創(chuàng)建一個線程并使得線程在read中阻塞,直到數(shù)據(jù)可用。這里的被阻塞線程被當做了socket監(jiān)控器,java虛擬機的線程調(diào)度當做了通知機制,這種方式在線程數(shù)量的增長失控的時候會造成巨大的壓力。
因此真正的就緒選擇操作必須由操作系統(tǒng)來做,它會處理I/O請求并通知各個線程他們的數(shù)據(jù)已經(jīng)準備好了。
2.3 SocketChananel建立
Socket和SocketChannel類封裝點對點、有序的網(wǎng)絡連接;每個SocketChannel對象創(chuàng)建時都是和一個對等的Socket對象關聯(lián)的,靜態(tài)的open方法可以創(chuàng)建一個新的SocketChannel(注意Socket的通道都是通過工廠方法open創(chuàng)建的),在Channel上調(diào)用socket方法能返回對等的Socket對象
新創(chuàng)建的Channel都是未連接的,可以調(diào)用connect方法去連接,連接之前嘗試IO操作會導致NotYetConnectedException異常。如果說阻塞模式下,線程在連接建立好或超時之前會保持阻塞;在非阻塞模式下(沒有超時的參數(shù)),他會發(fā)起連接請求,并且立即返回,如果是true,則說明連接已經(jīng)建立(本地環(huán)回連接);如果不能連接,立即返回false,并且異步的繼續(xù)嘗試連接,這時候isConnectPending()會返回true,這時候可以調(diào)用finishConnect()來安全的完成連接過程:
1.connect還未被調(diào)用,拋出NoConnectionPendingException
2.正在進行連接,未完成,那么finishConnect會立即返回false
3.非阻塞模式下,調(diào)用connect之后,SocketChannel可以調(diào)用configureBlocking()切換回阻塞模式,這時候調(diào)用finishConnect方法會阻塞直到連接建立完成
4.如果連接已經(jīng)建立,那么調(diào)用finishConnect()方法會返回true,什么也不發(fā)生
當通道處于中間的連接等待(connectio n-pending)狀態(tài)時,只可以調(diào)用 finishConnect( ) 、isConnectPending( )或isConnected( ) 方法。一旦連接建立過程成功完成,isConnected( ) 將返回 true值。
InetSocketAddress addr = new InetSocketAddress (host, port); SocketChannel sc = SocketChannel.open( ); sc.configureBlocking (false); sc.connect (addr); while ( ! sc.finishConnect( )) { doSomethingElse( ); } doSomethingWithChannel (sc); sc.close( );
3 三個基本元素 (Selector、SelectionKey、SocketChannel之間的關系) (在使用nio的情況下,常會接觸的API)
3.1 Selector
它管理著一組被注冊的通道集合的信息和他們的就緒狀態(tài)
3.2 SelectableChannel
這個抽象類提供了實現(xiàn)通道的可選擇性所需要的公共方法。它是所有支持就緒檢查的通道類的父類;一個通道可以被注冊到多個選擇器上,可以通過isRegistered來檢查一個通道是否被注冊到任何一個選擇器上,但對每個選擇器而言只能被注冊一次。
(注冊之前要確保通道是非阻塞的,否則拋出異常)
SelectionKey wKey = channel.register(selector, SelectionKey.OP_WRITE);
3.3 SelectorKey
選擇鍵封裝了特定的通道與特定的選擇器的注冊關系,可以調(diào)用cancel方法終結這種關系。在通道的特定事件注冊到選擇器上之后,該選擇鍵對象被返回并提供一個表示這種注冊關系的標記,通過它可以得到channel,包括一些狀態(tài)等。
選擇鍵是基于位的操作,比如判斷是否有寫事件,如下代碼
public final boolean isWritable() {
return (readyOps() & OP_WRITE) != 0;
}
4 深入
4.1 selector創(chuàng)建和注冊
ServerSocketChannel server = ServerSocketChannel.open();
Selector sel = Selector.open();// 創(chuàng)建,使用完畢之后調(diào)用close關閉
server.socket().bind(new InetSocketAddress(port));
server.configureBlocking(false); // 設置成非阻塞
// 注冊感興趣的事件,可以關聯(lián)一個對象,下次通過SelectorKey獲?。ńoServer注冊read事件其實是無效的)
SelectionKey sk=server.register(sel, SelectionKey.OP_ACCEPT);
SelectionKey sk1=server.register(sel, SelectionKey.OP_READ&SelectionKey.OP_ACCEPT);
//任意時刻只有一種注冊關系是有效的,實際上只是更新selectionkey的感興趣集合,并不是新創(chuàng)建
//keyFor返回channel和該Selector的選擇鍵
Assert.assertTrue(server.keyFor(sel)==sk);
Assert.assertTrue(server.keyFor(sel)==sk1);
int count=selector.select(100);
Select是一個阻塞操作,他會等待直到有感興趣的事件,或者超過100m
注意到Selector是通過open這個工廠方法創(chuàng)建的,他會通過SelectorProvider來獲取一個新的實例,關于SPI機制,請參考:Java SPI機制簡介
4.2 selector的選擇鍵集合以及選擇過程
前面我們知道了,選擇鍵(SelectionKey)代表了channel和Selector的注冊關系,可以調(diào)用cancel()取消:
//取消channel和Selector的注冊關系
sk1.cancel();
不過這種注銷關系并不是立即生效的,實際上,選擇器(Selector)會維護三種選擇鍵(SelectionKey)的集合 
1.已注冊的鍵的集合(Registered key set),可能包括已經(jīng)取消的鍵,通過keys()方法返回,無法修改
2.已準備好的選擇鍵集合(Selected key set),1的子集,每個成員都是選擇器判斷相關的通道已經(jīng)準備好,并且包含于鍵的interest集合中的某種操作(比如read),通過selectedKeys方法返回。如果要確定是具體某種操作,請使用readyOps確定。
3.已取消的鍵集合,但是還沒被注銷
當一個selector操作被調(diào)用的時候,會發(fā)生下面的事情:
1.檢查已取消的鍵集合,如果非空,則將這鍵從另外兩個集合中移出
2.檢查已注冊的鍵集合,每個鍵的interest事件集合會被檢查,之后修改interest也不會影響后面的過程,然后會執(zhí)行底層的查詢,直到有感興趣的事件或者超時
a) 在操作系統(tǒng)確定一個鍵的某個interest事件發(fā)生的時候,會確定這個鍵有沒有在已選擇的鍵集合中,如果沒,則鍵加入到選擇的集合,清空ready集合,并且設置該感興趣的事件到ready
b) 如果已經(jīng)在已選擇的鍵中,那么更新ready(位操作)
c) 之后會重新執(zhí)行1,這樣可以取消那些在選擇過程中有變更的通道
3.返回的是從上一個select之后,處于就緒狀態(tài)的通道數(shù)量,如果已經(jīng)在就緒集合中,不會累計;比如我們注冊read,并且在使用之后SelectionKey不移出,那么下次再有read事件過來,select方法可能會返回0就不會被記錄,但是不代表沒有感興趣的事件
延遲注銷鍵的操作,是為了在選擇的過程中減少不必要的同步,不然注銷和選擇就要形成一定的互斥,因為注銷潛在的代價比較高,可能需要釋放各種資源。關于SelectionKey的read和interest集合可以參考4.4
4.3 喚醒正在阻塞的selector操作
調(diào)用wakeup可以使阻塞在select方法的線程返回,當然如果當前沒有阻塞的select方法,那么會讓下一次select直接返回,他調(diào)用多次和一次的效果是一樣的
不過很多時候,我們并不能確定是否有線程阻塞在select方法,也不想影響到下一次select(只是因為某些事件,臨時喚醒一下),那可以在wakeup之后調(diào)用selectNow,他會立即返回,也抵消了wakeup的影響
4.4 支持的事件
在使用Selector的時候,需要將一個通道注冊到上面,即:(通道在注冊之前需要設置為非阻塞)
Selector selector = Selector.open( );
server.register(channel, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
第2個參數(shù)表示關心的通道操作,有4種:讀(read)、寫(write)、連接(connect)、接受(accept);
不是所有的Channel都支持這些事件,比如SocketChannel不支持accept,可以通過validOps()來驗證特定的通道所支持的操作集合。
Socket channels support connecting, reading, and writing, so this method returns (SelectionKey.OP_CONNECT | SelectionKey.OP_READ | SelectionKey.OP_WRITE).
Server-socket channels only support the accepting of new connections, so this method returns SelectionKey.OP_ACCEPT.
4.5 SelectionKey使用
4.5.1 interest和ready集合
一個Key包含兩個以整數(shù)形式進行編碼的比特掩碼,
interest(集合):指示哪些通道/選擇器組合所關心的操作
ready(集合):表示通道準備好要執(zhí)行的操作
當前的interest集合可以通過調(diào)用鍵對象的interestOps()方法來獲取,這個值不會被選擇器改變,但是可以調(diào)用帶參數(shù)的interestOps()方法改變,和上面一樣,他會在下一次selector的時候生效;
key.interestOps()返回的是注冊到該channel上的感興趣的動作,key.readyOps()是該channel已經(jīng)就緒的操作,ready集合是interest集合的子集,并且表示了interest集合中從上次調(diào)用select()以來已經(jīng)就緒的那些操作,例如:注冊channel感興趣的動作是OP_READ,OP_WRITE,
sc.register(sel, SelectionKey.OP_WRITE|SelectionKey.OP_READ);
調(diào)用interestOps()可以得到他對read和write感興趣,但是如果該channel中沒有數(shù)據(jù),則只能是key.readyOps()==SelectionKey.OP_WRITE。
比如下面的方法檢查通道是否已經(jīng)可讀,并且讀取
if ((key.readyOps( ) & SelectionKey.OP_READ) != 0){
myBuffer.clear( );
key.channel( ).read (myBuffer);
doSomethingWithBuffer (myBuffer.flip( ));
另外key也有便捷的方式可以檢查:
if (key.isWritable( )) 等價于: if ((key.readyOps( ) & SelectionKey.OP_WRITE) != 0)
另外,SelectionKey中還有一個attach方法啊,可以獲得鍵對象中保存所提供的對象的引用
4.5.2 移除SelectionKey
為了表示已經(jīng)處理了ready,只能將該SelectionKey從已選擇的鍵集合中移出。
Iterator<SelectionKey> iter = selector.selectedKeys().iterator(); while (iter.hasNext()) { /** SelectionKey中的ready集合表示了他感興趣的事件,他只能在選擇的時候由底層修改,其他時候無 法修改,他表示的是合法的就緒信息(而不是可以任意設置的), 所以為了表示已經(jīng)處理了ready,只能將該SelectionKey從已選擇的鍵集合中移出
另外,如果通道關閉,因為SelectionKey表示通道和selector的關系, 所以永遠都會發(fā)生“關閉”事件,除非通道從slector移除(key.cancel()) */ SelectionKey key = iter.next(); iter.remove(); handleKey(key); }
// 處理事件,Key可以同時表示多個事件到達 protected void handleKey(SelectionKey key) throws IOException { if (key.isAcceptable()) { // 允許網(wǎng)絡連接事件 ServerSocketChannel server = (ServerSocketChannel) key.channel(); SocketChannel channel = server.accept(); channel.configureBlocking(false);
// 網(wǎng)絡管道準備處理讀事件 channel.register(selector, SelectionKey.OP_READ); } else if (key.isReadable()) { } else if (key.isWritable()) { SocketChannel channel = (SocketChannel) key.channel(); } }
|