Netty 基石,Java NIO 核心知識(shí)
本文轉(zhuǎn)載自微信公眾號(hào)「yes的練級(jí)攻略」,作者是Yes呀 。轉(zhuǎn)載本文請(qǐng)聯(lián)系yes的練級(jí)攻略公眾號(hào)。
你好,我是yes。
在深入 Netty 之前,我覺(jué)得有必要先對(duì)齊一下 Java NIO 的基礎(chǔ)知識(shí),因?yàn)?Netty 對(duì)底層網(wǎng)絡(luò) I/O 的操作就是基于 Java NIO 的,所以有必要了解一下。
到時(shí)候看源碼,會(huì)有很多概念,例如 Channel、Selector、SelectionKey、Buffer 等等,這篇我們就來(lái)了解下這些名詞到底代表著什么,分別是什么意思。
關(guān)于 Java NIO 相關(guān)的核心,總的來(lái)看包含以下三點(diǎn),分別是:
- Channel
- Buffer
- Selector
什么是 Channel
翻譯過(guò)來(lái)就是通道。
我們可以往通道里寫(xiě)數(shù)據(jù),也可以從通道里讀數(shù)據(jù),它是雙向的,而與之配套的是 Buffer,也就是你想要往一個(gè)通道里寫(xiě)數(shù)據(jù),必須要將數(shù)據(jù)寫(xiě)到一個(gè) Buffer 中,然后寫(xiě)到通道里。
從通道里讀數(shù)據(jù),必須將通道的數(shù)據(jù)先讀取到一個(gè) Buffer 中,然后再操作。
在 NIO 中 Channel 有多種類型:
- SocketChannel
- ServerSocketChannel
- DatagramChannel
- FileChannel
- SocketChannel
對(duì)標(biāo) Socket,我們可以直接將它當(dāng)做所建立的連接。
通過(guò) SocketChannel ,我們可以利用 TCP 協(xié)議進(jìn)行讀寫(xiě)網(wǎng)絡(luò)數(shù)據(jù)。
ServerSocketChannel
可以對(duì)標(biāo) ServerSocket,也就是服務(wù)端創(chuàng)建的 Socket。
它的作用就是監(jiān)聽(tīng)新建連的 TCP 連接,為新進(jìn)一個(gè)連接創(chuàng)建對(duì)應(yīng)的 SocketChannel。
之后,通過(guò)新建的 SocketChannel 就可以進(jìn)行網(wǎng)絡(luò)數(shù)據(jù)的讀寫(xiě),與對(duì)端交互。
可以看到它主要是用來(lái)接待新連接,這功能主要就是服務(wù)端做的,所以叫 ServerSocketChannel。
DatagramChannel
看到 Datagram 應(yīng)該就知道是 UDP 協(xié)議了,是無(wú)連接協(xié)議。
利用 DatagramChannel 可以直接通過(guò) UDP 進(jìn)行網(wǎng)絡(luò)數(shù)據(jù)的讀寫(xiě)。
FileChannel
文件通道,用來(lái)進(jìn)行文件的數(shù)據(jù)讀寫(xiě)。
我們?nèi)粘i_(kāi)發(fā)主要是基于 TCP 協(xié)議,所以我們把精力放在 SocketChannel 和 ServerSocketChannel 上即可。
我們?cè)倩剡^(guò)頭來(lái)繼續(xù)看看 SocketChannel 和 ServerSocketChannel。
SocketChannel 主要在兩個(gè)地方出現(xiàn):
- 客戶端,客戶端創(chuàng)建一個(gè) SocketChannel 用于連接至遠(yuǎn)程的服務(wù)端。
- 服務(wù)端,服務(wù)端利用 ServerSocketChannel 接收新連接之后,為其創(chuàng)建一個(gè) SocketChannel 。
隨后,客戶端和服務(wù)端就可以通過(guò)這兩個(gè) SocketChannel 相互發(fā)送和接收數(shù)據(jù)。
ServerSocketChannel 主要出現(xiàn)在一個(gè)地方:服務(wù)端。
服務(wù)端需要綁定一個(gè)端口,然后監(jiān)聽(tīng)新連接的到來(lái),這個(gè)活兒就由 ServerSocketChannel 來(lái)干。
服務(wù)端內(nèi)常常會(huì)利用一個(gè)線程,一個(gè)死循環(huán),不斷地接收新連接的到來(lái)。
- ServerSocketChannel serverSocketChannel
- = ServerSocketChannel.open();
- ......
- while(true){
- // 接收的新連接
- SocketChannel socketChannel =
- serverSocketChannel.accept();
- .......
- }
至此,想必你應(yīng)該清楚 ServerSocketChannel 和 SocketChannel 的區(qū)別和作用了。
Buffer
Buffer 說(shuō)白了就是內(nèi)存中可以讀寫(xiě)的一塊地方,叫緩沖區(qū),用于緩存數(shù)據(jù)。
其實(shí)還真沒(méi)啥好說(shuō)的,最多就講講 Java NIO Buffer 的 API。
但講 API 的太死板了,所以自己上網(wǎng)搜搜吧。我就告知一個(gè)結(jié)論,這個(gè) API 很不好用,稍微漏寫(xiě)了點(diǎn),就容易出 bug,而且還有很多優(yōu)化的之處,所以 Netty 沒(méi)用 Java NIO Buffer 而是自己實(shí)現(xiàn)了一個(gè) Buffer,叫 ByteBuf。
等我們之后分析 ByteBuf 的時(shí)候再來(lái)盤(pán)一盤(pán)?,F(xiàn)在你只需要知道 Buffer 主要用來(lái)緩存通道的讀寫(xiě)數(shù)據(jù)即可。
對(duì)了,看到這可能會(huì)有人提出疑問(wèn),為什么 Channel 必須和 Buffer 搭配使用?
其實(shí)網(wǎng)絡(luò)數(shù)據(jù)是面向字節(jié)的,但是我們讀寫(xiě)的數(shù)據(jù)往往是多字節(jié)的,假設(shè)不用 Buffer ,那我們就得一個(gè)字節(jié)一個(gè)字節(jié)的調(diào)用讀和調(diào)用寫(xiě),想想是不是很麻煩?
所以我們搞個(gè) Buffer,把數(shù)據(jù)攏一攏,這樣之后的調(diào)用才能更好地處理完整的數(shù)據(jù),方便異步的處理等等。
Selector
I/O多路復(fù)用的核心玩意。
一個(gè) Selector 上可以注冊(cè)多個(gè) Channel ,我們從上面得知一個(gè) Channel 就對(duì)應(yīng)了一個(gè)連接,因此一個(gè) Selector 可以管理多個(gè) Channel 。
具體管理什么?
當(dāng)任意 Channel 發(fā)生讀寫(xiě)事件的時(shí)候,通過(guò) Selector.select() 就可以捕捉到事件的發(fā)生,因此我們利用一個(gè)線程,死循環(huán)的調(diào)用 Selector.select(),這樣可以利用一個(gè)線程管理多個(gè)連接,減少了線程數(shù),減少了線程的上下文切換和節(jié)省了線程資源。
這就是 Selector 的核心功能,然后我們?cè)賮?lái)細(xì)說(shuō)具體是怎樣管理的。
首先,創(chuàng)建一個(gè) Selector。
- Selector selector = Selector.open();
然后,你需要將被管理的 Channel 注冊(cè)到 Selector 上,并聲明感興趣的事件。
- SelectionKey key = channel.register(selector, Selectionkey.OP_READ);
事件一共有以上四種類型,注冊(cè)的時(shí)候可以同時(shí)對(duì)多種類型的事件感興趣,例如:
- SelectionKey key
- = channel.register(selector,
- Selectionkey.OP_READ | SelectionKey.OP_WRITE);
這樣,當(dāng)這個(gè) Channel 發(fā)生讀或?qū)懯录?,我們調(diào)用 Selector.select() 就可以得知有事件發(fā)生。
具體 Selector.select() 有三個(gè)重載方法:
- int selectNow(),不論是否有無(wú)事件發(fā)生,立即返回
- int select(long timeout),至多阻塞 timeout 時(shí)間(或被喚醒),如果提早有事件發(fā)生,提早返回
- int select(),一直阻塞著,直到有事件發(fā)生(或被喚醒)
返回值就是就緒的通道數(shù),一般判斷大于 0 即可進(jìn)行后續(xù)的操作。
后續(xù)的操作就是調(diào)用:
- Set selectedKeys = selector.selectedKeys();
獲得了一個(gè)類型為 Set 的 selectedKeys 集合,那這個(gè) selectedKeys 又是啥玩意?
我們來(lái)看一下它的方法和成員:
看到這些成員,其實(shí)我們就很清晰了,我們可以通過(guò) selectedKey 得知當(dāng)前發(fā)生的是什么事件,有 isAcceptable、isReadable 等等。
然后還能獲得對(duì)應(yīng)的 channel 進(jìn)行相應(yīng)的讀寫(xiě)操作,還有獲取 attachment 等等。
所以得到了 selectedKeys 就可以通過(guò)迭代器遍歷所有發(fā)生事件的連接,然后進(jìn)行操作。
大致使用的代碼如下所示:
- while(true) {
- int readyNum = selector.select();
- if (readyNum == 0) {
- continue;
- }
- Set selectedKeys = selector.selectedKeys();
- Iterator keyIterator = selectedKeys.iterator();
- while(keyIterator.hasNext()) {
- SelectionKey key = keyIterator.next();
- if(key.isAcceptable()) {
- // a connection was accepted by a ServerSocketChannel.
- } else if (key.isConnectable()) {
- // a connection was established with a remote server.
- } else if (key.isReadable()) {
- // a channel is ready for reading
- } else if (key.isWritable()) {
- // a channel is ready for writing
- }
- keyIterator.remove(); //執(zhí)行完畢之后,需要在循環(huán)內(nèi)移除自己
- }
- }
還有個(gè)方法就是 Selector.wakeup(),可以喚醒阻塞著的 Selector。
對(duì)了還有一點(diǎn)沒(méi)說(shuō),就是如果 Channel 要和 Selector 搭配,那它必須得是非阻塞的,即配置
- channel.configureBlocking(false);
從上面的操作,我們可以得知 Selector 處理事件的時(shí)候必須快,如果長(zhǎng)時(shí)間處理某個(gè)事件,那么注冊(cè)到 Selector 上的其他連接的事件就不會(huì)被及時(shí)處理,造成客戶端阻塞。
至此,想必你應(yīng)該清晰 Selector 具體是如何管理這么多連接的了。
參考:https://ifeve.com/java-nio-all/