前言
上一章節我們理解了Java NIO三大核心,以及重點講解了Buffer的原理和幾個使用場景,其中也用到了channel,這一章我們來理解一下selector,結合channel來做一個c/s通信,
理解Selector 和 Channel
Selector 選擇器,也叫多路復用器,可以同時處理多個客戶端連接,多路復用器采用輪詢機制來選擇有讀寫事件的客戶端鏈接進行處理,
通過 Selector ,一個 I/O 執行緒可以并發處理 N 個客戶端連接和讀寫操作,這解決了傳統同步阻塞 I/O 一連接一執行緒模型,架構的性能、彈性伸縮能力和可靠性都得到了極大的提升,
由于它的讀寫操作都是非阻塞的,這就可以充分提升 IO 執行緒的運行效率,避免由于頻繁 I/O 阻塞導致的執行緒掛起,
這里在整理一下它的作業原理,如圖:

Selector :多路復用器(也叫選擇器)的作用就是提供給SocketChannel通道來注冊,然后Selector會輪詢的去監聽通道通道的讀寫事件從而做出相應的IO處理,它 的作業流程如下:
-
首先需要創建一個ServerSocketChannel,它類似于(ServerSocket)需要指定一個監聽的IP和Port,需要注意的是ServerSocketChannel為了兼容BIO,默認是阻塞的,可以通過ServerSocketChannel#configureBlocking(false)來指定為NIO模式,
通過ServerSocketChannel 可以獲取一個客戶端的SocketChannel ,客戶端的SocketChannel 需要注冊到Selector上,然后每個通道都會對應一個SelectionKey
-
選擇器可以通過Selector.open() 創建,然后將 ServerSocketChannel 注冊給Selector ,選擇器的 Selector#select() 方法 可以選擇有事件的通道(SocketChannel ),并回傳已就緒的通道數量.事件型別包括:“連接”,“接收” ,“讀”,“寫”,
-
如果 Selector#select() 回傳值大于0代表某些通道有事件發生,可以通過 selector.selectedKeys() 來得到所有有事件通道的SelectionKey,
然后可以通過SelectionKey方向拿到SocketChannel , 從而將SocketChannel 中的資料讀取到Buffer中,完成IO操作,
ServerSocketChannel
ServerSocketChannel 是服務端用來用來監聽客戶端Socket鏈接,通過accept方法可以獲取客戶端SocketChannel,從而將 SocketChannel 注冊到Selector,相關方法如下
- open : 創建一個 ServerSocketChannel 通道
- bind(SocketAddress local):設定服務器端監聽的地址和埠號
- configureBlocking(boolean block) : 設定阻塞或非阻塞模式,取值 false 表示采用非阻塞模式
- accept() :接受一個連接,回傳代表這個連接的通道物件SocketChannel
- register(Selector sel, int ops): 把當前通道注冊到選擇器,并設定監聽事件
SocketChannel
一個客戶端鏈接服務端就會產生通道:ServerChannel ,需要注冊到Selector,被Selector監聽通道的讀寫事件,ServerChannel 負責具體的讀寫,把緩沖區的資料寫入通道,或者把通道中的資料寫入緩沖區,
- open() : 得到一個 SocketChannel 通道
- configureBlocking(boolean block):設定阻塞或非阻塞模式,取值 false 表示采用非阻塞模式
- connect(SocketAddress remote) :連接遠程服務器,通過SocketAddress 指定IP和埠
- finishConnect(): 如果connect連接失敗,就要通過finishConnect方法完成連接操作
- write(ByteBuffer src) : 把ByteBuffer中的資料,往通道里寫
- read(ByteBuffer dst) :從通道里讀資料,寫入ByteBuffer
- register(Selector sel, int ops, Object att): 把該通道注冊到selector,并設定監聽事件(OPS),最后一個引數可以設定共享資料,該方法會回傳一個 SelectionKey ,這個key對應了該通道,
- close() :關閉通道
SelectionKey
當 ServerChannel 注冊到Selector就會產生SelectionKey , 通過SelectionKey可以反向獲得SocketChannel通道物件,從而進行IO讀寫操作,
- selector(): 通過SelectionKey 獲取得到與之關聯的 Selector 物件
- channel():得到與之關聯的通道
- attachment():得到與之關聯的共享資料
- interestOps(int ops):設定或改變監聽事件
- isAcceptable():是否可以 accept,“接收就緒”事件
- isReadable():是否可以讀
- isWritable():是否可以寫
事件包括:
SelectionKey.OP_CONNECT : 值 16,連接就緒 ,比如:一個 channel連接到一個服務器
SelectionKey.OP_ACCEPT : 值 8,接收就緒,比如:ServerSocketChannel準備好接入新的連接
SelectionKey.OP_READ :值 1,“讀就緒”,通道有可以讀資料可以說是
SelectionKey.OP_WRITE : 值 4 ,“寫就緒” ,等待寫資料的通道可以說
Selector
負責采用輪詢方式監聽通道,當通道有讀寫事件就進行IO操作,
- open() :得到一個選擇器物件
- select(long timeout):選擇有事件的通道,將其對應的 SelectionKey 加入到內部集合中,回傳通道的數量
- selectedKeys() : 從內部集合中得到所有有事件 SelectionKey
- keys() : 從內部集合中得到所有SelectionKey
- close:關閉選擇器
綜合案例
使用selector ,ServerSocketChannel,SocketChannel完成一個 C/S 案例,
服務端代碼
//通道
@Test
public void serverSocketChannelTest() throws IOException {
//創建服務端通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//socket監聽地址和埠
SocketAddress socketAddress = new InetSocketAddress("127.0.0.1",5000);
//和某個SocketAddress系結
serverSocketChannel.bind(socketAddress);
//NIO默認采用阻塞,為了兼容BIO
serverSocketChannel.configureBlocking(false);
//創建選擇器
Selector selector = Selector.open();
//通道注冊到選擇器,事件型別為:OP_ACCEPT “接受”
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//==選擇器輪詢=======================================================================
while(true){
//select,選擇有事件的通道,回傳有事件發生通道的key的個數 ,超時時間 1s
if(selector.select(1000) == 0){
System.out.println("無連接...輪詢等待...\");
continue;
}
//有事件發生,得到有事件的通道的key的集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
//遍歷key的集合
while (iterator.hasNext()){
//拿到每個通道的key
SelectionKey key = iterator.next();
//如果當前通道事件是: OP_ACCEPT ,就注冊通道
if(key.isAcceptable()){
//接收一個socketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("客戶端鏈接成功...");
socketChannel.configureBlocking(false);
//把socketChannel注冊到選擇器 ,并給通道系結一個buffer
socketChannel.register(selector, SelectionKey.OP_READ,ByteBuffer.allocate(1024));
}
//如果通道事件是: OP_READ,說明通道有資料
if(key.isReadable()){
//通過key得到SocketChannel
SocketChannel channel = (SocketChannel)key.channel();
//得到channel系結的buffer
ByteBuffer byteBuffer = (ByteBuffer)key.attachment();
//從通道把資料讀取到buffer
channel.read(byteBuffer);
System.out.println(new String(byteBuffer.array()));
}
//洗掉當前key
iterator.remove();
}
}
}
客戶端代碼
//通道
@Test
public void socketChannelTest() throws IOException {
//創建一個SocketChannel
SocketChannel socketChannel = SocketChannel.open();
//使用非阻塞模式
socketChannel.configureBlocking(false);
//鏈接的地址和埠
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 5000);
//嘗試鏈接,如果使用的異步,那么需要使用 socketChannel.finishConnect() 來確保連接成功,
if(!socketChannel.connect(inetSocketAddress)){
//如果沒鏈接成功,會通過while回圈,直到 finishConnect 鏈接成功,跳出while
while(!socketChannel.finishConnect()){
System.out.println("還未完成鏈接...等待中...");
}
}
//鏈接成功,把資料寫出去
socketChannel.write(ByteBuffer.wrap("你好".getBytes()));
System.out.println("向服務端發送資料...");
//防止客戶端結束,所以使用read()阻塞
System.in.read();
}
總結
本篇文章介紹了Selector ,ServerSocketChannel ,SocketChannel的作用,場景API,和三種的作業原理,并通過一個C/S綜合案例來演示三者之間的關系,
文章結束,希望對你有所幫助,你的鼓勵是我最大的動力,如果喜歡,請不要吝嗇你的一鍵三連哦!!!
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/291533.html
標籤:其他
下一篇:C語言分支與回圈的那些事
