NIO非阻塞通信簡介及基於TCP的非阻塞通信

NIO非阻塞通信簡介及基於TCP的非阻塞通信

通信方式簡述

阻塞式的通信

以基於TCP的通信方式來說明:創建ServerSocket(或ServerSocketChannel)、調用accept()方法獲取客戶端的Socket連接、通過Socket通信、關閉Socket連接。

對於阻塞式的通信,最影響效率的其實是SocketChannel.read()或通過Socket獲得的輸入流的InputStream.read()。當服務端調用這些輸入方法時,線程就會進入阻塞狀態,直到客戶端發來數據才被喚醒。

如果用單線程的方式實現上述流程,資源的利用率極低。線程調用read()方法後,進入阻塞狀態,如果客戶端遲遲沒有數據,服務端就會一直被阻塞,而沒辦法進行其他操作。

第一種解決方法就是使用多線程的方式,一個線程僅負責accept()接收請求,將接收到的請求分配給其他線程,這樣,調用read()時,處理請求的線程不會被阻塞,這樣就還能接收新的請求,提高了併發量與資源利用率。

第二種解決方法就是使用非阻塞的通信方式。

非阻塞式的通信

先來考慮一下爲什麼會有阻塞通信?和本地I/O一樣,本地I/O也是需要阻塞,當通過系統調用I/O完成後,喚醒被阻塞的進程(線程)。問題就在於:什麼時候I/O完成,對於阻塞式的I/O來說,當進程被喚醒時就說明I/O完成。

現在再來想一下本地I/O最低效的方式,循環的訪問I/O完成的狀態標誌,當狀態標誌指示I/O完成後,說明I/O結束。這就是一個非阻塞的過程,當調度到這個進程(線程),CPU在循環的訪問一個標誌位來判斷I/O是否完成

把這個思想遷移到網絡通信中,現在有多個連接,怎樣判斷哪個連接有數據過來?給這些連接一個標誌,用來表示是否有數據,服務端循環的訪問每個連接的這個標誌,就可以知道哪個連接的數據已經準備好了

問題貌似解決了,但是想一下,如果所有連接都沒有數據,那CPU一直再做“無用功”,浪費資源。

爲了避免這種資源浪費,可以使用一個選擇器,這個選擇器的作用就是把有數據的連接都挑出來,輪詢的時候保證每個連接都是有數據需要處理的;如果所有連接都沒有數據,選擇器被阻塞,直到有某個連接的數據準備好。

關於非阻塞式的通信的基本思想,已經闡述完成,帶着這些簡單的理解,繼續瞭解NIO提供的非阻塞通信。

Java提供的抽象

上面分析非阻塞I/O時,有兩個很重要的點:被選擇器選擇的連接選擇器

NIO提供了它們的抽象:java.nio.channels.SelectableChannelSelector

SelectableChannel

下面給出一段官方文檔描述的翻譯:
SelectableChannel可以通過Selector複用。
SelectableChannel的實例爲了可以被Selector使用,必須使用register()方法註冊到Selector的實例;該方法返回一個SelectionKey對象,這個對象代表了這個通道註冊到一個選擇器中。
SelectableChannel是線程安全的。

看完這些解釋如果不懂,可以先接着往下看,看到後面就會理解。

Selector

再談談向Selector註冊,在註冊時,有兩個參數,一個是Selector,另一個是一個int類型,它表示事件

在上面一直用“讀”/“輸入”舉例子,如果要讓Selector判斷一個Selectable是否可讀,註冊時僅需要調用SelectableChannel.register(Selector, SelectionKey.OP_READ)

SelectionKey.OP_READ表示一個讀事件,或者說讓選擇器判斷通道是否可讀。

非阻塞通信的使用-TCP

SelectableChannel是一個抽象類,在使用時,沒辦法直接實例化,查看一下它的子類,巧了,有ServerSocketChannelSocketChannel

看到這兩個,就想到了它們可以進行阻塞的通信,那怎麼進行非阻塞通信?

1. 創建ServerSocketChannel

這一步和阻塞式的通信一致。

severSocketChannel = ServerSocketChannel.open();

2. 設置使用非阻塞式通信

severSocketChannel.configureBlocking(false);

默認情況下,爲阻塞通信,也就是參數爲true的情況。

3. 設置使用非阻塞通信

這一步和阻塞式的通信一致。

severSocketChannel.bind(new InetSocketAddress(PORT));

4. 創建Selector

selector = Selector.open();

5. 將ServerSocketChannelOP_Accept事件註冊到Selector

severSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

當有連接到來(accept隊列中有連接請求),事件被觸發,選擇器選擇時就會把這個通道“挑選”出來,之後就可以輪詢所有“挑選”出來的通道,並處理這些通道出發的事件。

這裏要注意,註冊的通道必須是非阻塞的,否則會出錯拋出異常。

在這裏插入圖片描述

6. 選擇器選擇已經觸發了事件的通道:select()

通過使用Selector.select()方法,可以“挑選”出已經觸發了事件的通道。

請看官方的解釋

Selects a set of keys whose corresponding channels are ready for I/O operations.

選擇出一組健,它們對應的通道已經準備好I/O操作

其實,因該改成選擇出一組健,它們對應的通道的註冊事件已經觸發,因爲可註冊的事件除了I/O之外,還有accept

這裏說一下可以註冊的事件:

  • OP_ACCEPT:選擇器檢測到對應的ServerSocketChannel已經準備好接受(accept)另一個連接,則該事件被觸發(被“選中”)。
  • OP_CONNECT:選擇器檢測到對應的SocketChannel準備完成連接序列,則該事件觸發(被“選中”)。
  • OP_READ:選擇器檢測到對應的SocketChannel已經準備好讀取數據,則該事件被觸發(被“選中”)。
  • OP_WRITE:選擇器檢測到對應的SocketChannel已經準備好寫數據,則改時間觸發(被“選中”)。

7. 獲得選擇出來的集合:selectedKeys()

可以這樣理解:select()方法會遍歷已經註冊的通道,如果通道對應的註冊事件已經出發,則把這個通道放入一個selected集合,調用Selector.selectedKeys()即可獲得該集合

selected集合中存放的不是一個通道,而是對它進行了封裝,封裝成了一個SelectionKey,通過這個封裝好的類,不僅可以獲得觸發了時間的通道,還可以知道觸發了什麼事件,針對不同的事件,可以給出不同的處理。

8. 遍歷selectedKey集合,處理事件

示例:Echo服務端

public class Server {
	private static final int PORT = 8888;
	private ServerSocketChannel severSocketChannel;
	private Selector selector;
	
	public Server() throws IOException {
		//創建serverSocketChannel
		this.severSocketChannel = ServerSocketChannel.open();
		//開啓非阻塞方式
		this.severSocketChannel.configureBlocking(false);
		//綁定地址,開啓監聽
		this.severSocketChannel.bind(new InetSocketAddress(PORT));
		
		//創建selector
		this.selector = Selector.open();
		//註冊accepte事件
		this.severSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
	}
	
	
	public void service() {
		//是否有事件觸發
		try {
			while(selector.select() > 0) {
				//獲得已經觸發的事件
				Iterator<SelectionKey> selectionKeys = selector.selectedKeys().iterator();
				while(selectionKeys.hasNext()) {
					SelectionKey key = selectionKeys.next();
					//刪除即將處理過的事件,因爲使用多線程進行處理,避免多次處理
					selectionKeys.remove();
					if(key.isAcceptable()) {//可接受一個連接
						ServerSocketChannel serverSocketChannel = (ServerSocketChannel)key.channel();
						SocketChannel clientChannel = serverSocketChannel.accept();
						clientChannel.configureBlocking(false);
						clientChannel.register(this.selector, SelectionKey.OP_READ);
					}else if(key.isReadable()) {//可讀
						ByteBuffer buffer = ByteBuffer.allocate(1024);
						SocketChannel clientChannel = (SocketChannel) key.channel();
						InetSocketAddress address = (InetSocketAddress)clientChannel.getLocalAddress();
						String clientIp = address.getHostString();
						int clientPort = address.getPort();
						try {
							clientChannel.read(buffer);
							buffer.flip();
							//服務端輸出
							String content = new String(buffer.array(), 0, buffer.limit());
							System.out.println("收到信息 [" + clientIp + " : " + clientPort + "] " + content);
							//回送給客戶端				
							clientChannel.write(buffer);
							//收到exit,退出
							if("exit".equals(content)) {
								System.out.println("與 [ " + clientIp + " : " + clientPort + "]的連接已斷開");
								try {
									clientChannel.close();
								} catch (IOException e) {
									e.printStackTrace();
								}
							}					
						
							//準備下一次輸入
							buffer.clear();	
						} catch (IOException e) {
							e.printStackTrace();
						}
					}
				}
			}
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				this.severSocketChannel.close();
				this.selector.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	
	public static void main(String[] args) {
		try {
			(new Server()).service();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

關於非阻塞客戶端的坑

  1. 連接建立的不確定性:如果使用一個非阻塞的通道連接服務端(connect()),connect()可能會在連接建立完成之前返回false。因此,connect()之後,需要使用finishConnect()檢查連接是否建立成功。
  2. 在實現給Echo服務端發數據的客戶端時,對於阻塞通信,只需要在發送數據之後在調用read()接收服務端的會送即可,阻塞通信可以保證從通道內可以讀到數據;對於非阻塞通信,在發送數據之後不能直接讀取,因爲不能保證通道內的數據已經準備好了(服務端可能還沒把數據通過通道傳輸過來),直接讀取可能會出錯,對於這種情況,必須向選擇器註冊一個讀事件,可以通過選擇器來判斷通道什麼時候可讀。

關於Selector.select()的細節

Selector提供了三個select()方法:

  1. select():阻塞式,至少會選擇出一個。當沒有事件觸發時,進入阻塞狀態,當有事件被觸發是,喚醒。
  2. selectNow():非阻塞,如果有事件觸發,則選擇並返回;沒有事件觸發,立即返回,不會阻塞等待。
  3. select(long timeout):阻塞式,與select()相同,但是加了一個定時器,阻塞時間超過timeout將會返回。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章