一挽唉、回顧
上一篇文章 JAVA NIO編程入門(一)我們學習了NIO編程的基礎知識,并通過一個小demo實戰(zhàn)幫助了解NIO編程的channel藐翎,buffer等概念橘忱。本文會繼續(xù)學習JAVA NIO編程,并通過一個小示例來幫助理解相關知識赡鲜,通過本文你將可以學習到
- buffer的聚集和分散(Scatter/Gather)
- SocketChannel和ServerSocketChannel的使用
- 選擇器的使用
二空厌、什么是聚集和分散(Scatter/Gather)
- 分散(scatter)從Channel中讀取是指在讀操作時將讀取的數(shù)據(jù)寫入多個buffer中。因此银酬,Channel將從Channel中讀取的數(shù)據(jù)“分散(scatter)”到多個Buffer中嘲更。
- 聚集(gather)寫入Channel是指在寫操作時將多個buffer的數(shù)據(jù)寫入同一個Channel,因此揩瞪,Channel 將多個Buffer中的數(shù)據(jù)“聚集(gather)”后發(fā)送到Channel赋朦。
分散(Scatter)示意圖
從通道填充buffer,必須填充完前一個buffer才會填充后面的buffer李破,這也意味著不能動態(tài)調整每個buffer的接受大小宠哄。
聚集(Gather)示意圖
聚集和分散是相反的形式,從buffer寫入數(shù)據(jù)到通道嗤攻,只會寫入buffer的positon位置到limit位置的內容毛嫉,也就是意味著可以動態(tài)的寫入內容到通道中。
三妇菱、選擇器
什么是選擇器
Selector(選擇器)是Java NIO中能夠檢測多個NIO通道承粤,并能夠知道通道是否為諸如讀寫事件做好準備的組件。這樣闯团,一個單獨的線程可以管理多個channel辛臊,從而管理多個網絡連接,提高效率房交。
為什么要用選擇器
使用了選擇器就可以用一個線程管理多個channel彻舰,如果多個channel由多個線程管理,線程之前的切換是消耗資源的,而單個線程就避免了線程之間切換的消耗刃唤。
選擇器常用方法
方法名 | 功能 |
---|---|
register(Selector sel, int ops) | 向選擇器注冊通道口猜,并且可以選擇注冊指定的事件,目前事件分為4種透揣;1.Connect济炎,2.Accept,3.Read辐真,4.Write须尚,一個通道可以注冊多個事件 |
select() | 阻塞到至少有一個通道在你注冊的事件上就緒了 |
selectNow() | 不會阻塞,不管什么通道就緒都立刻返回 |
select(long timeout) | 和select()一樣侍咱,除了最長會阻塞timeout毫秒(參數(shù)) |
selectedKeys() | 一旦調用了select()方法耐床,并且返回值表明有一個或更多個通道就緒了,然后可以通過調用selector的selectedKeys()方法楔脯,訪問“已選擇鍵集(selected key set)”中的就緒通道 |
wakeUp() | 可以使調用select()阻塞的對象返回撩轰,不阻塞。 |
close() | 用完Selector后調用其close()方法會關閉該Selector昧廷,且使注冊到該Selector上的所有SelectionKey實例無效堪嫂。通道本身并不會關閉 |
四、實戰(zhàn)
實戰(zhàn)需求說明
編碼客戶端和服務端木柬,服務端可以接受客戶端的請求皆串,并返回一個報文,客戶端接受報文并解析輸出眉枕。
服務端代碼
try {
//創(chuàng)建一個服socket并打開
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//監(jiān)聽綁定8090端口
serverSocketChannel.socket().bind(new InetSocketAddress(8090));
//設置為非阻塞模式
serverSocketChannel.configureBlocking(false);
while(true){
//獲取請求連接
SocketChannel socketChannel = serverSocketChannel.accept();
if (socketChannel!=null){
ByteBuffer buf1 = ByteBuffer.allocate(1024);
socketChannel.read(buf1);
buf1.flip();
if(buf1.hasRemaining())
System.out.println(">>>服務端收到數(shù)據(jù):"+new String(buf1.array()));
buf1.clear();
//構造返回的報文恶复,分為頭部和主體,實際情況可以構造復雜的報文協(xié)議速挑,這里只演示谤牡,不做特殊設計。
ByteBuffer header = ByteBuffer.allocate(6);
header.put("[head]".getBytes());
ByteBuffer body = ByteBuffer.allocate(1024);
body.put("i am body!".getBytes());
header.flip();
body.flip();
ByteBuffer[] bufferArray = { header, body };
socketChannel.write(bufferArray);
socketChannel.close();
}else{
Thread.sleep(1000);
}
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
服務端selector(選擇器版本)
try {
//打開選擇器
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(8090));
serverSocketChannel.configureBlocking(false);
//向通道注冊選擇器姥宝,并且注冊接受事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
//獲取已經準備好的通道數(shù)量
int readyChannels = selector.selectNow();
//如果沒準備好翅萤,重試
if (readyChannels == 0) continue;
//獲取準備好的通道中的事件集合
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = (SelectionKey) keyIterator.next();
if (key.isAcceptable()) {
//在自己注冊的事件中寫業(yè)務邏輯,
//我這里注冊的是accept事件伶授,
//這部分邏輯和上面非選擇器服務端代碼一樣断序。
ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverSocketChannel1.accept();
ByteBuffer buf1 = ByteBuffer.allocate(1024);
socketChannel.read(buf1);
buf1.flip();
if (buf1.hasRemaining())
System.out.println(">>>服務端收到數(shù)據(jù):" + new String(buf1.array()));
buf1.clear();
ByteBuffer header = ByteBuffer.allocate(6);
header.put("[head]".getBytes());
ByteBuffer body = ByteBuffer.allocate(1024);
body.put("i am body!".getBytes());
header.flip();
body.flip();
ByteBuffer[] bufferArray = {header, body};
socketChannel.write(bufferArray);
socketChannel.close();
} else if (key.isConnectable()) {
} else if (key.isReadable()) {
} else if (key.isWritable()) {
}
//注意每次迭代末尾的keyIterator.remove()調用流纹。
//Selector不會自己從已選擇鍵集中移除SelectionKey實例糜烹。必須在處理完通道時自己移除。
//下次該通道變成就緒時漱凝,Selector會再次將其放入已選擇鍵集中
keyIterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
客戶端代碼
try {
//打開socket連接疮蹦,連接本地8090端口,也就是服務端
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8090));
//請求服務端茸炒,發(fā)送請求
ByteBuffer buf1 = ByteBuffer.allocate(1024);
buf1.put("來著客戶端的請求".getBytes());
buf1.flip();
if (buf1.hasRemaining())
socketChannel.write(buf1);
buf1.clear();
//接受服務端的返回愕乎,構造接受緩沖區(qū)阵苇,我們定義頭6個字節(jié)為頭部,后續(xù)其他字節(jié)為主體內容感论。
ByteBuffer header = ByteBuffer.allocate(6);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
socketChannel.read(bufferArray);
header.flip();
body.flip();
if (header.hasRemaining())
System.out.println(">>>客戶端接收頭部數(shù)據(jù):" + new String(header.array()));
if (body.hasRemaining())
System.out.println(">>>客戶端接收body數(shù)據(jù):" + new String(body.array()));
header.clear();
body.clear();
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
運行結果
服務端:
客戶端:
這里給出了服務端代碼的兩個版本绅项,一個是非選擇器的版本,一個是選擇器的版本比肄。查看最后運行結果快耿,發(fā)現(xiàn)客戶端根據(jù)雙方約定的協(xié)議格式,正確解析到了頭部和body的內容芳绩,其實這也是聚集和分散最主要的作用和應用場景掀亥,在網絡交互中,進行協(xié)議報文格式的定義和實現(xiàn)妥色。后續(xù)學完NIO編程入門后我們最后進行總結性的實戰(zhàn)搪花,編寫一個RPC的demo框架,實現(xiàn)分布式系統(tǒng)的遠程調用嘹害,有興趣的同學可以關注筆者和后續(xù)的文章撮竿。
參考
《JAVA NIO》