介紹了nio中的channel概念以及FileChannel的使用: (netty極簡教程(三): nio Channel意義以及FileChannel使用)[https://www.jianshu.com/p/b8d08fa240e2],
接下來介紹下nio中的網絡channel,SocketChannel以及Selector
示例源碼: https://github.com/jsbintask22/netty-learning
SocketChannel
它類比bio中的Socket
. 與FileChannel相比,它實現了NetworkChannel
,SelectableChannel
接口。
1.NetworkChannel接口代表它是一個網絡字節流的連接,可以在綁定在本地網絡端口進行字節流的操作; 如可以 NetworkChannel bind(SocketAddress local)
方法用於綁定,
而NetworkChannel setOption(SocketOption<T> name, T value)
用於設置連接和進行io操作的選項,如SO_SNDBUF
選項用於標識發送緩衝池的大小,只有發送的字節大小達到這個值時纔會真正的發送字節流
-
SelectableChannel接口主要有兩個作用;
- 該連接支持多路複用,換句話說,它支持註冊到多個
Selector
(後面介紹)上,後面可由selector詢問操作系統是否有註冊的事件(連接,讀,寫)發生,這樣一個selector便可管理多個channel。
方法SelectionKey register(Selector sel, int ops)
註冊selector以及通知事件,SelectionKey
是一個註冊抽象類,可理解爲連接Channel以及Selector
,並且可使用該對象從selector上取消註冊:void cancel();
值得注意的是,當channel關閉後,該channel也會自動從selector上註銷,而當想要主動從selector註銷時,必須通過SelectionKey的cancel方法,它會等到selector下一次select
(詢問操作系統)操作時才正式註銷。
另外,它有一個
int validOps()
可以查看當前Channel主持的事件類型(註冊時需要指定),如SocketChannel支持的事件爲,讀,寫,連接
- 該連接支持多路複用,換句話說,它支持註冊到多個
- 該channel支持異步,連接,讀操作不會再阻塞當前線程:
SelectableChannel configureBlocking(boolean block)
值得注意的是,如果一個channel要註冊至Selector,它必須是異步的。
ServerSocketChannel
它類比bio中的ServerSocket
,用於服務端監聽指定的端口從而獲取對應的SocketChannel,它同樣實現了NetworkChannel接口以及SelectChannel代表可以綁定端口以及註冊到Selector
上,它只支持Accept事件(獲取連接),因爲它本身是無法直接發送讀取字節的 SelectionKey.OP_ACCEPT
用於監聽是否有新連接建立.
對於配置了異步選項的ServerSocketChannel來說,它的SocketChannel accept()
將不會再阻塞,而是直接返回null。
Selector
Selector
是整個nio實現非阻塞的關鍵,它是一個多路複用器,我們知道nio是基於事件驅動的,而這些事件從何獲取感知呢? 那就需要Selector來提供,它工作需要三部來完成事件驅動模型:
- 創建; 可以直接通過
open()
方法來創建操作系統類型的Selector,或者手動通過AbstractSelector openSelector()
來創建 - 註冊Channel, 只要實現了
SelectableChannel
接口都可向其註冊(必須是有效事件,見上) - 詢問操作系統,selector可通過
int select()
方法返回已經註冊的Channel的有效事件個數 - 如若在3中返回的有效事件不爲0,則可調用
Set<SelectionKey> selectedKeys();
返回所有的SelectionKey(可獲取Channel和Selector
),從而獲取知道具體的事件類型,這樣,我們不必再像bio一樣調用accept()
方法或者read
方法直接阻塞(因爲真正的讀寫操作還未到來),而是已經知道真正的讀寫buffer有效然後再進行後續操作,這樣就成了一個真正的非阻塞
知道注意的是,雖然selectedKeys()會返回真正有效的事件,但它是以來select方法的,所以select方法也提供了阻塞與非阻塞方法:
-
int select()
會一直阻塞直到至少有一個有效的事件 -
int select(long timeout)
可設置超時時間,否則直接返回0 -
int selectNow()
直接返回,不會阻塞當前線程
使用
我們結合上面的分析,將SocketChannel,ServerSocketChannel,Selector組件結合起來寫一個具體例子,關鍵在於服務端如何監聽
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(999));
Selector selector = Selector.open(); // 1
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); // 2
while (true) { // 3
if (selector.select(1000) == 0) { // 4
continue;
}
Set<SelectionKey> eventKeys = selector.selectedKeys(); // 5
Iterator<SelectionKey> iterator = eventKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
SelectableChannel channel = selectionKey.channel(); // 6
// 如果是 連接已就緒事件
if (selectionKey.isAcceptable()) { // 7
ServerSocketChannel server = ((ServerSocketChannel) channel);
SocketChannel clientChannel = server.accept();
clientChannel.configureBlocking(false);
// 再將 client 註冊到 selector
clientChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024)); // 8
// 如果是可讀事件 說明是客戶端的連接channel
} else if (selectionKey.isReadable()) { // 9
// 可將此處代碼放入先程序處理,不佔用 主線程循環監聽cpu時間片, 類比: netty 中的 EventLoop Work線程池
SocketChannel client = (SocketChannel) channel;
ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
int len = client.read(buffer);
buffer.flip();
byte[] data = new byte[buffer.remaining()];
int index = 0;
while (len != index) {
data[index++] = buffer.get();
}
String clientMsg = new String(data, StandardCharsets.UTF_8);
System.out.println("client: " + clientMsg);
buffer.clear();
client.write(ByteBuffer.wrap(("收到請求:" + clientMsg).getBytes(StandardCharsets.UTF_8)));
} else if (selectionKey.isWritable()) {
// System.out.println(selectionKey.readyOps());
} else {
System.out.println(selectionKey.readyOps());
}
iterator.remove(); // 10
}
}
- 將ServerSocketChannel綁定到本地端口,獲取Selector
- 將ServerSocketChannel註冊到Selector並且註冊事件是 accept
- 開始循環使用Selector,詢問操作系統
- 詢問操作系統,是否有註冊的事件發生
- 返回第4步中的有效的SelectionKey
- 從5中的key獲取對應的Channel
- 判斷事件類型,如若是accept事件 代表新的連接進來
- 獲取新的連接SocketChannel並將改Channel再次註冊到Selector,註冊事件是 READ
- 因爲代表客戶端的SocketChannel也註冊到了該Selector,所以該事件也可能是 READ 代表字節池現在可讀(read可直接讀取),隨後向改channel寫入數據表示響應
- 每次事件讀取完成後,需要把改事件剔除,否則下次會重複讀取到該事件
SocketChannel client = SocketChannel.open(); // 1
client.configureBlocking(false);
if (!client.connect(new InetSocketAddress("localhost", 999))) { // 2
if (!client.finishConnect()) { // 3
System.out.println("連接失敗,不佔用cpu資源,do other things.");
}
}
System.out.println("連接成功。.");
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
int len = client.read(buffer); // 4
buffer.flip();
byte[] data = new byte[buffer.remaining()];
int index = 0;
while (len != index) {
data[index++] = buffer.get();
}
System.out.println("server: " + new String(data, StandardCharsets.UTF_8));
buffer.clear();
client.write(ByteBuffer.wrap(("你好,我是客戶端:" + client.getLocalAddress() + "[" + client.hashCode() + "]" +
new Date()).getBytes(StandardCharsets.UTF_8)));
TimeUnit.SECONDS.sleep(2);
}
- 創建channel
- 綁定到服務端地址,因爲開啓了異步,所以可能連接尚在建立返回false
- 建立穩定連接
- 讀取數據,發送數據
運行效果:
雖然效果與bio一樣, 可是在accept與read 中確不在阻塞,其中的關鍵則在於 Selector
還記得之前分析的bio與aio之間的區別嗎, 對於同步非阻塞來說,由於Selector的事件模型使得當前線程不會在真正的有效連接或者有效數據到來之前阻塞當前線程,而Selector本身的select方法也可使用非阻塞,
這樣一個Selector便可管理多個Channel,相較於bio不斷開啓新線程處理連接及讀取事件, 它可節省很多的系統資源(線程)以及無用等待。
類似銀行取錢業務,對於bio而言,需要一直乖乖的排隊等待 無法合理利用cpu,而nio無需傻傻等待,如果當前櫃檯不可用則馬上走人做自己的事情,只是每隔一段時間便去諮詢前臺是否可用。
總結
- 介紹了SocketChannel作用以及用法
- 介紹ServerSocketChannel作用以及用法
- 講解Selector是如何實現事件驅動的
- 使用案例及類比