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