4. 非阻塞I/O(NonBlocking I/O)
上文花了太多的筆墨描述BIO,接下來的非阻塞IO我們只抓主要矛盾,其余參考BIO即可。
如果你看過其他介紹非阻塞IO的文章,下面這個圖片你多少會有點眼熟。
NIO模型
非阻塞IO指的是進程發起系統調用之后,內核不會將進程投入睡眠,而是會立即返回一個結果,這個結果可能恰好是我們需要的數據,又或者是某些錯誤。
你可能會想,這種非阻塞帶來的輪詢有什么用呢?大多數都是空輪詢,白白浪費CPU而已,還不如讓進程休眠來的合適。
4.1 Java的非阻塞實現
這個問題暫且擱置一下,我們先看Java在語法層面是如何提供非阻塞功能的,細節慢慢聊。
public class NoBlockingServer {
public static List<SocketChannel> channelList = new ArrayList<>();
public static void main(String[] args) throws InterruptedException {
try {
// 相當于serverSocket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 將監聽socket設置為非阻塞
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(8099));
while (true) {
// 這里將不再阻塞
SocketChannel socketChannel = serverSocketChannel.accept();
if (socketChannel != null) {
// 將連接socket設置為非阻塞
socketChannel.configureBlocking(false);
channelList.add(socketChannel);
} else {
System.out.println("沒有客戶端連接!!!");
}
for (SocketChannel client : channelList) {
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// read也不阻塞
int num = client.read(byteBuffer);
if (num > 0) {
System.out.println("收到客戶端【" + client.socket().getPort() + "】數據:" + new String(byteBuffer.array()));
} else {
System.out.println("等待客戶端【" + client.socket().getPort() + "】寫數據");
}
}
// 加個睡眠是為了避免strace產生大量日志,否則不好追蹤
Thread.sleep(1000);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
Java提供了新的API,ServerSocketChannel
以及SocketChannel
,相當于BIO中的ServerSocket
和Socket
。此外,通過下面兩行的配置,將監聽socket和連接socket設置為非阻塞。
// 將監聽socket設置為非阻塞
serverSocketChannel.configureBlocking(false);
// 將連接socket設置為非阻塞
socketChannel.configureBlocking(false);
我們上文強調過, Java自身并沒有將socket設置為非阻塞的本事,一定是在某個時間點上,操作系統內核提供了這個功能,才使得Java設計出了新的API來提供非阻塞功能 。
之所以需要上面兩行代碼的顯式設置,也恰好說明了內核是默認將socket設置為阻塞狀態的,需要非阻塞,就得額外調用其他系統調用。我們通過man
命令查看一下socket()
這個方法(截圖的中間省略了一部分內容):
man 2 socket
image-20221225144028751
我們可以看到socket()
函數提供了SOCK_NONBLOCK
這個類型,可以通過fcntl()
這個方法將socket從默認的阻塞修改為非阻塞,不管是對監聽socket還是連接socket都是一樣的。
4.2 Java的非阻塞解釋
現在解釋上面提到的問題:這種非阻塞帶來的輪詢有什么用?觀察一下上面的代碼就可以發現,我們全程只使用了1個main線程就解決了所有客戶端的連接以及所有客戶端的讀寫操作。
serverSocketChannel.accept();
會立即返回調用結果。
返回的結果如果是一個SocketChannel
對象(系統調用底層就是個socket描述符),說明有客戶端連接,這個SocketChannel
就表示了這個連接;然后利用socketChannel.configureBlocking(false);
將這個連接socket設置為非阻塞。這個設置非常重要,設置之后對連接socket所有的讀寫操作都變成了非阻塞,因此接下來的client.read(byteBuffer);
并不會阻塞while循環,導致新的客戶端無法連接。再之后將該連接socket加入到channelList
隊列中。
如果返回的結果為空(底層系統調用返回了錯誤),就說明現在還沒有新的客戶端要連接監聽socket,因此程序繼續向下執行,遍歷channelList
隊列中的所有連接socket,對連接socket進行讀操作。而讀操作也是非阻塞的,會理解返回一個整數,表示讀到的字節數,如果>0
,則繼續進行下一步的邏輯處理;否則繼續遍歷下一個連接socket。
下面給出一張accept()
返回一個連接socket情況下的動圖,希望對大家理解整個流程有幫助。
4.3 掀開非阻塞IO的底褲
我將上面的程序在CentOS下再次用strace
程序追蹤一下,具體步驟不再贅述,下面是out日志文件的內容(我忽略了絕大多數沒用的)。
非阻塞IO的系統調用分析
4.4 非阻塞IO總結
NIO模型
再放一遍這個圖,有一個細節需要大家注意,系統調用向內核要數據時,內核的動作分成兩步:
- 等待數據(從網卡緩沖區拷貝到內核緩沖區)
- 拷貝數據(數據從內核緩沖區拷貝到用戶空間)
只有在第1步時,系統調用是非阻塞的,第2步進程依然需要等待這個拷貝過程,然后才能返回,這一步是阻塞的。
非阻塞IO模型僅用一個線程就能處理所有操作,對比BIO的一個客戶端需要一個線程而言進步還是巨大的。但是他的致命問題在于會不停地進行系統調用,不停的進行accept()
,不停地對連接socket進行read()
操作,即使大部分時間都是白忙活。要知道,系統調用涉及到用戶空間和內核空間的多次轉換,會嚴重影響整體性能。
所以,一個自然而言的想法就是,能不能別讓進程瞎輪詢。
比如有人告訴進程監聽socket是不是被連接了,有的話進程再執行accept()
;比如有人告訴進程哪些連接socket有數據從客戶端發送過來了,然后進程只對有數據的連接socket進行read()
。
這個方案就是 I/O多路復用 。
-
IO
+關注
關注
0文章
435瀏覽量
39080 -
非阻塞
+關注
關注
0文章
13瀏覽量
2165 -
Redis
+關注
關注
0文章
371瀏覽量
10846
發布評論請先 登錄
相關推薦
評論