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