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