參考:http://ifeve.com/non-blocking-server/
原文地址
目錄
- Java NIO教程
- Java NIO 教程(一) 概述
- Java NIO 教程(二) Channel
- Java NIO 教程(三) Buffer
- Java NIO 教程(四) Scatter/Gather
- Java NIO 教程(五) 通道之間的數(shù)據(jù)傳輸
- Java NIO 教程(六) Selector
- Java NIO 教程(七) FileChannel
- Java NIO 教程(八) SocketChannel
- Java NIO 教程(九) ServerSocketChannel
- Java NIO 教程(十) 非阻塞式服務(wù)器
- Java NIO 教程(十一) Java NIO DatagramChannel
- Java NIO 教程(十二) Pipe
- Java NIO 教程(十三) Java NIO vs. IO
- Java NIO 教程(十四) Java NIO Path
- Java NIO 教程(十五) Java NIO Files
- Java NIO 教程(十六) Java NIO AsynchronousFileChannel
即使你知道Java NIO非阻塞的工作特性(如Selector,Channel,Buffer等組件),但是想要設(shè)計(jì)一個(gè)非阻塞的服務(wù)器仍然是一件很困難的事。非阻塞式服務(wù)器相較于阻塞式來(lái)說(shuō)要多上許多挑戰(zhàn)。本文將會(huì)討論非阻塞式服務(wù)器的主要幾個(gè)難題,并針對(duì)這些難題給出一些可能的解決方案。
查找關(guān)于非阻塞式服務(wù)器設(shè)計(jì)方面的資料實(shí)在不太容易,所以本文提供的解決方案都是基于本人工作和想法上的。如果各位有其他的替代方案或者更好的想法,我會(huì)很樂(lè)意聽(tīng)取這些方案和想法!你可以在文章下方留下你的評(píng)論,或者發(fā)郵件給我(郵箱為:info@jenkov.com )。
本文的設(shè)計(jì)思路想法都是基于Java NIO的。但是我相信如果某些語(yǔ)言中也有像Selector之類的組件的話,文中的想法也能用于該語(yǔ)言。據(jù)我所知,類似的組件底層操作系統(tǒng)會(huì)提供,所以對(duì)你來(lái)說(shuō)也可以根據(jù)其中的思想運(yùn)用在其他語(yǔ)言上。
非阻塞式服務(wù)器– GitHub 倉(cāng)庫(kù)
我已經(jīng)創(chuàng)建了一些簡(jiǎn)單的這些思想的概念驗(yàn)證呈現(xiàn)在這篇教程中,并且為了讓你可以看到,我把源碼放到了github資源庫(kù)上了。這里是GitHub資源庫(kù)地址:
https://github.com/jjenkov/java-nio-server
非阻塞式IO管道(Pipelines)
一個(gè)非阻塞式IO管道是由各個(gè)處理非阻塞式IO組件組成的鏈。其中包括讀/寫(xiě)IO。下圖就是一個(gè)簡(jiǎn)單的非阻塞式IO管道組成:

一個(gè)組件使用Selector監(jiān)控Channel什么時(shí)候有可讀數(shù)據(jù)。然后這個(gè)組件讀取輸入并且根據(jù)輸入生成相應(yīng)的輸出。最后輸出將會(huì)再次寫(xiě)入到一個(gè)Channel中。
一個(gè)非阻塞式IO管道不需要將讀數(shù)據(jù)和寫(xiě)數(shù)據(jù)都包含,有一些管道可能只會(huì)讀數(shù)據(jù),另一些可能只會(huì)寫(xiě)數(shù)據(jù)。
上圖僅顯示了一個(gè)單一的組件。一個(gè)非阻塞式IO管道可能擁有超過(guò)一個(gè)以上的組件去處理輸入數(shù)據(jù)。一個(gè)非阻塞式管道的長(zhǎng)度是由他的所要完成的任務(wù)決定。
一個(gè)非阻塞IO管道可能同時(shí)讀取多個(gè)Channel里的數(shù)據(jù)。舉個(gè)例子:從多個(gè)SocketChannel管道讀取數(shù)據(jù)。
其實(shí)上圖的控制流程還是太簡(jiǎn)單了。這里是組件從Selector開(kāi)始從Channel中讀取數(shù)據(jù),而不是Channel將數(shù)據(jù)推送給Selector進(jìn)入組件中,即便上圖畫(huà)的就是這樣。
非阻塞式 vs 阻塞式管道
非阻塞和阻塞IO管道兩者之間最大的區(qū)別在于他們?nèi)绾螐牡讓?code>Channel(Socket或者file)讀取數(shù)據(jù)。
IO管道通常從流中讀取數(shù)據(jù)(來(lái)自socket或者file)并且將這些數(shù)據(jù)拆分為一系列連貫的消息。這和使用tokenizer(這里估計(jì)是解析器之類的意思)將數(shù)據(jù)流解析為token(這里應(yīng)該是數(shù)據(jù)包的意思)類似。相反你只是將數(shù)據(jù)流分解為更大的消息體。我將拆分?jǐn)?shù)據(jù)流成消息這一組件稱為“消息讀取器”(Message Reader)下面是Message Reader拆分流為消息的示意圖:

一個(gè)阻塞IO管道可以使用類似InputStream的接口每次一個(gè)字節(jié)地從底層Channel讀取數(shù)據(jù),并且這個(gè)接口阻塞直到有數(shù)據(jù)可以讀取。這就是阻塞式Message Reader的實(shí)現(xiàn)過(guò)程。
使用阻塞式IO接口簡(jiǎn)化了Message Reader的實(shí)現(xiàn)。阻塞式Message Reader從不用處理在流沒(méi)有數(shù)據(jù)可讀的情況,或者它只讀取流中的部分?jǐn)?shù)據(jù)并且對(duì)于消息的恢復(fù)也要延遲處理的情況。
同樣,阻塞式Message Writer(一個(gè)將數(shù)據(jù)寫(xiě)入流中組件)也從不用處理只有部分?jǐn)?shù)據(jù)被寫(xiě)入和寫(xiě)入消息要延遲恢復(fù)的情況。
阻塞式IO管道的缺陷
雖然阻塞式Message Reader容易實(shí)現(xiàn),但是也有一個(gè)不幸的缺點(diǎn):每一個(gè)要分解成消息的流都需要一個(gè)獨(dú)立的線程。必須要這樣做的理由是每一個(gè)流的IO接口會(huì)阻塞,直到它有數(shù)據(jù)讀取。這就意味著一個(gè)單獨(dú)的線程是無(wú)法嘗試從一個(gè)沒(méi)有數(shù)據(jù)的流中讀取數(shù)據(jù)轉(zhuǎn)去讀另一個(gè)流。一旦一個(gè)線程嘗試從一個(gè)流中讀取數(shù)據(jù),那么這個(gè)線程將會(huì)阻塞直到有數(shù)據(jù)可以讀取。
如果IO管道是必須要處理大量并發(fā)鏈接服務(wù)器的一部分的話,那么服務(wù)器就需要為每一個(gè)鏈接維護(hù)一個(gè)線程。對(duì)于任何時(shí)間都只有幾百條并發(fā)鏈接的服務(wù)器這確實(shí)不是什么問(wèn)題。但是如果服務(wù)器擁有百萬(wàn)級(jí)別的并發(fā)鏈接量,這種設(shè)計(jì)方式就沒(méi)有良好收放。每個(gè)線程都會(huì)占用棧32bit-64bit的內(nèi)存。所以一百萬(wàn)個(gè)線程占用的內(nèi)存將會(huì)達(dá)到1TB!不過(guò)在此之前服務(wù)器將會(huì)把所有的內(nèi)存用以處理傳過(guò)來(lái)的消息(例如:分配給消息處理期間使用對(duì)象的內(nèi)存)
為了將線程數(shù)量降下來(lái),許多服務(wù)器使用了服務(wù)器維持線程池(例如:常用線程為100)的設(shè)計(jì),從而一次一個(gè)地從入站鏈接(inbound connections)地讀取。入站鏈接保存在一個(gè)隊(duì)列中,線程按照進(jìn)入隊(duì)列的順序處理入站鏈接。這一設(shè)計(jì)如下圖所示:(譯者注:Tomcat就是這樣的)

然而,這一設(shè)計(jì)需要入站鏈接合理地發(fā)送數(shù)據(jù)。如果入站鏈接長(zhǎng)時(shí)間不活躍,那么大量的不活躍鏈接實(shí)際上就造成了線程池中所有線程阻塞。這意味著服務(wù)器響應(yīng)變慢甚至是沒(méi)有反應(yīng)。
一些服務(wù)器嘗試通過(guò)彈性控制線程池的核心線程數(shù)量這一設(shè)計(jì)減輕這一問(wèn)題。例如,如果線程池線程不足時(shí),線程池可能開(kāi)啟更多的線程處理請(qǐng)求。這一方案意味著需要大量的長(zhǎng)時(shí)鏈接才能使服務(wù)器不響應(yīng)。但是記住,對(duì)于并發(fā)線程數(shù)任然是有一個(gè)上限的。因此,這一方案仍然無(wú)法很好地解決一百萬(wàn)個(gè)長(zhǎng)時(shí)鏈接。
基礎(chǔ)非阻塞式IO管道設(shè)計(jì)
一個(gè)非阻塞式IO管道可以使用一個(gè)單獨(dú)的線程向多個(gè)流讀取數(shù)據(jù)。這需要流可以被切換到非阻塞模式。在非阻塞模式下,當(dāng)你讀取流信息時(shí)可能會(huì)返回0個(gè)字節(jié)或更多字節(jié)的信息。如果流中沒(méi)有數(shù)據(jù)可讀就返回0字節(jié),如果流中有數(shù)據(jù)可讀就返回1+字節(jié)。
為了避免檢查沒(méi)有可讀數(shù)據(jù)的流我們可以使用 Java NIO Selector. 一個(gè)或多個(gè)SelectableChannel實(shí)例可以同時(shí)被一個(gè)Selector注冊(cè).。當(dāng)你調(diào)用Selector的select()或者selectNow()方法它只會(huì)返回有數(shù)據(jù)讀取的SelectableChannel的實(shí)例. 下圖是該設(shè)計(jì)的示意圖:

讀取部分消息
當(dāng)我們從一個(gè)SelectableChannel讀取一個(gè)數(shù)據(jù)包時(shí),我們不知道這個(gè)數(shù)據(jù)包相比于源文件是否有丟失或者重復(fù)數(shù)據(jù)(原文是:When we read a block of data from a SelectableChannel we do not know if that data block contains less or more than a message)。一個(gè)數(shù)據(jù)包可能的情況有:缺失數(shù)據(jù)(比原有消息的數(shù)據(jù)少)、與原有一致、比原來(lái)的消息的數(shù)據(jù)更多(例如:是原來(lái)的1.5或者2.5倍)。數(shù)據(jù)包可能出現(xiàn)的情況如下圖所示:

在處理類似上面這樣部分信息時(shí),有兩個(gè)問(wèn)題:
- 判斷你是否能在數(shù)據(jù)包中獲取完整的消息。
- 在其余消息到達(dá)之前如何處理已到達(dá)的部分消息。
判斷消息的完整性需要消息讀取器(Message Reader)在數(shù)據(jù)包中尋找是否存在至少一個(gè)完整消息體的數(shù)據(jù)。如果一個(gè)數(shù)據(jù)包包含一個(gè)或多個(gè)完整消息體,這些消息就能夠被發(fā)送到管道進(jìn)行處理。尋找完整消息體這一處理可能會(huì)重復(fù)多次,因此這一操作應(yīng)該盡可能的快。
判斷消息完整性和存儲(chǔ)部分消息都是消息讀取器(Message Reader)的責(zé)任。為了避免混合來(lái)自不同Channel的消息,我們將對(duì)每一個(gè)Channel使用一個(gè)Message Reader。設(shè)計(jì)如下圖所示:

在從Selector得到可從中讀取數(shù)據(jù)的Channel實(shí)例之后,與該Channel相關(guān)聯(lián)的Message Reader讀取數(shù)據(jù)并嘗試將他們分解為消息。這樣讀出的任何完整消息可以被傳到讀取通道(read pipeline)任何需要處理這些消息的組件中。
一個(gè)Message Reader一定滿足特定的協(xié)議。Message Reader需要知道它嘗試讀取的消息的消息格式。如果我們的服務(wù)器可以通過(guò)協(xié)議來(lái)復(fù)用,那它需要有能夠插入Message Reader實(shí)現(xiàn)的功能 – 可能通過(guò)接收一個(gè)Message Reader工廠作為配置參數(shù)。
存儲(chǔ)部分消息
現(xiàn)在我們已經(jīng)確定Message Reader有責(zé)任存儲(chǔ)部分消息,直到收到完整的消息,我們需要弄清楚這些部分消息的存儲(chǔ)應(yīng)該如何實(shí)現(xiàn)。
有兩個(gè)設(shè)計(jì)因素我們要考慮:
- 我們想盡可能少地復(fù)制消息數(shù)據(jù)。復(fù)制越多,性能越低。
- 我們希望將完整的消息存儲(chǔ)在連續(xù)的字節(jié)序列中,使解析消息更容易。
每個(gè)Message Reader的緩沖區(qū)
很顯然部分消息需要存儲(chǔ)某些緩沖區(qū)中。簡(jiǎn)單的實(shí)現(xiàn)方式可以是每一個(gè)Message Reader內(nèi)部簡(jiǎn)單地有一個(gè)緩沖區(qū)。但是這個(gè)緩沖區(qū)應(yīng)該多大?它要大到足夠儲(chǔ)存最大允許儲(chǔ)存消息。因此,如果最大允許儲(chǔ)存消息是1MB,那么Message Reader內(nèi)部緩沖區(qū)將至少需要1MB。
當(dāng)我們的鏈接達(dá)到百萬(wàn)數(shù)量級(jí),每個(gè)鏈接都使用1MB并沒(méi)有什么作用。1,000,000 * 1MB仍然是1TB的內(nèi)存!那如果最大的消息是16MB甚至是128MB呢?
大小可調(diào)的緩沖區(qū)
另一個(gè)選擇是在Message Reader內(nèi)部實(shí)現(xiàn)一個(gè)大小可調(diào)的緩沖區(qū)。大小可調(diào)的緩沖區(qū)開(kāi)始的時(shí)候很小,如果它獲取的消息過(guò)大,那緩沖區(qū)會(huì)擴(kuò)大。這樣每一條鏈接就不一定需要如1MB的緩沖區(qū)。每條鏈接的緩沖區(qū)只要需要足夠儲(chǔ)存下一條消息的內(nèi)存就行了。
有幾個(gè)可實(shí)現(xiàn)可調(diào)大小緩沖區(qū)的方法。它們都各自有自己的優(yōu)缺點(diǎn),所以接下來(lái)的部分我將逐個(gè)討論。
通過(guò)復(fù)制調(diào)整大小
實(shí)現(xiàn)可調(diào)大小緩沖區(qū)的第一種方式是從一個(gè)大小(例如:4KB)的緩沖區(qū)開(kāi)始。如果4KB的緩沖區(qū)裝不下一個(gè)消息,則會(huì)分配一個(gè)更大的緩沖區(qū)(如:8KB),并將大小為4KB的緩沖區(qū)數(shù)據(jù)復(fù)制到這個(gè)更大的緩沖區(qū)中去。
通過(guò)復(fù)制實(shí)現(xiàn)大小可調(diào)緩沖區(qū)的優(yōu)點(diǎn)在于消息的所有數(shù)據(jù)被保存在一個(gè)連續(xù)的字節(jié)數(shù)組中,這就使得消息的解析更加容易。它的缺點(diǎn)就是在復(fù)制更大消息的時(shí)候會(huì)導(dǎo)致大量的數(shù)據(jù)。
為了減少消息的復(fù)制,你可以分析流進(jìn)你系統(tǒng)的消息的大小,并找出盡量減少?gòu)?fù)制量的緩沖區(qū)的大小。例如,你可能看到大多數(shù)消息都小于4KB,這是因?yàn)樗鼈兌純H包含很小的request/responses。這意味著緩沖區(qū)的初始值應(yīng)該設(shè)為4KB。
然后你可能有一個(gè)消息大于4KB,這通常是因?yàn)樗锩姘粋€(gè)文件。你可能注意到大多數(shù)流進(jìn)系統(tǒng)的文件都是小于128KB的。這樣第二個(gè)緩沖區(qū)的大小設(shè)置為128KB就較為合理。
最后你可能會(huì)發(fā)現(xiàn)一旦消息超過(guò)128KB之后,消息的大小就沒(méi)有什么固定的模式,因此緩沖區(qū)最終的大小可能就是最大消息的大小。
根據(jù)流經(jīng)系統(tǒng)的消息大小,上面三種緩沖區(qū)大小可以減少數(shù)據(jù)的復(fù)制。小于4KB的消息將不會(huì)復(fù)制。對(duì)于一百萬(wàn)個(gè)并發(fā)鏈接其結(jié)果是:1,000,000 * 4KB = 4GB,對(duì)于目前大多數(shù)服務(wù)器還是有可能的。介于4KB – 128KB的消息將只會(huì)復(fù)制一次,并且只有4KB的數(shù)據(jù)復(fù)制進(jìn)128KB的緩沖區(qū)中。介于128KB至最大消息大小的消息將會(huì)復(fù)制兩次。第一次復(fù)制4KB,第二次復(fù)制128KB,所以最大的消息總共復(fù)制了132KB。假設(shè)沒(méi)有那么多超過(guò)128KB大小的消息那還是可以接受的。
一旦消息處理完畢,那么分配的內(nèi)存將會(huì)被清空。這樣在同一鏈接接收到的下一條消息將會(huì)再次從最小緩沖區(qū)大小開(kāi)始算。這樣做的必要性是確保了不同連接間內(nèi)存的有效共享。所有的連接很有可能在同一時(shí)間并不需要打的緩沖區(qū)。
我有一篇介紹如何實(shí)現(xiàn)這樣支持可調(diào)整大小的數(shù)組的內(nèi)存緩沖區(qū)的完整文章:
文章包含一個(gè)GitHub倉(cāng)庫(kù)連接,其中的代碼演示了是如何實(shí)現(xiàn)的。
通過(guò)追加調(diào)整大小
調(diào)整緩沖區(qū)大小的另一種方法是使緩沖區(qū)由多個(gè)數(shù)組組成。當(dāng)你需要調(diào)整緩沖區(qū)大小時(shí),你只需要另一個(gè)字節(jié)數(shù)組并將數(shù)據(jù)寫(xiě)進(jìn)去就行了。
這里有兩種方法擴(kuò)張一個(gè)緩沖區(qū)。一個(gè)方法是分配單獨(dú)的字節(jié)數(shù)組,并將這些數(shù)組保存在一個(gè)列表中。另一個(gè)方法是分配較大的共享字節(jié)數(shù)組的片段,然后保留分配給緩沖區(qū)的片段的列表。就個(gè)人而言,我覺(jué)得片段的方式會(huì)好些,但是差別不大。
通過(guò)追加單獨(dú)的數(shù)組或片段來(lái)擴(kuò)展緩沖區(qū)的優(yōu)點(diǎn)在于寫(xiě)入過(guò)程中不需要復(fù)制數(shù)據(jù)。所有的數(shù)據(jù)可以直接從socket (Channel)復(fù)制到一個(gè)數(shù)組或片段中。
以這種方式擴(kuò)展緩沖區(qū)的缺點(diǎn)是在于數(shù)據(jù)不是存儲(chǔ)在單獨(dú)且連續(xù)的數(shù)組中。這將使得消息的解析更困難,因?yàn)榻馕銎餍枰瑫r(shí)查找每個(gè)單獨(dú)數(shù)組的結(jié)尾處和所有數(shù)組的結(jié)尾處。由于你需要在寫(xiě)入的數(shù)據(jù)中查找消息的結(jié)尾,所以該模型并不容易使用。
TLV編碼消息
一些協(xié)議消息格式是使用TLV格式(類型(Type)、長(zhǎng)度(Length)、值(Value))編碼。這意味著當(dāng)消息到達(dá)時(shí),消息的總長(zhǎng)度被存儲(chǔ)在消息的開(kāi)頭。這一方式你可以立即知道應(yīng)該對(duì)整個(gè)消息分配多大的內(nèi)存。
TLV編碼使得內(nèi)存管理變得更加容易。你可以立即知道要分配多大的內(nèi)存給這個(gè)消息。只有部分在結(jié)束時(shí)使用的緩沖區(qū)才會(huì)使得內(nèi)存浪費(fèi)。
TLV編碼的一個(gè)缺點(diǎn)是你要在消息的所有數(shù)據(jù)到達(dá)之前就分配好這個(gè)消息需要的所有內(nèi)存。一些慢連接可能因此分配完你所有可用內(nèi)存,從而使得你的服務(wù)器無(wú)法響應(yīng)。
此問(wèn)題的解決方法是使用包含多個(gè)TLV字段的消息格式。因此,服務(wù)器是為每個(gè)字段分配內(nèi)存而不是為整個(gè)消息分配內(nèi)存,并且是字段到達(dá)之后再分配內(nèi)存。然而,一個(gè)大消息中的一個(gè)大字段在你的內(nèi)存管理有同樣的影響。
另外一個(gè)方案就是對(duì)于還未到達(dá)的信息設(shè)置超時(shí)時(shí)間,例如10-15秒。當(dāng)恰好有許多大消息到達(dá)服務(wù)器時(shí),這個(gè)方案能夠使得你的服務(wù)器可以恢復(fù),但是仍然會(huì)造成服務(wù)器一段時(shí)間無(wú)法響應(yīng)。另外,惡意的DoS(Denial of Service拒絕服務(wù))攻擊仍然可以分配完你服務(wù)器的所有內(nèi)存。
TLV編碼存在許多不同的形式。實(shí)際使用的字節(jié)數(shù)、自定字段的類型和長(zhǎng)度都依賴于每一個(gè)TLV編碼。TLV編碼首先放置字段的長(zhǎng)度、然后是類型、然后是值(一個(gè)LTV編碼)。 雖然字段的順序不同,但它仍然是TLV的一種。
TLV編碼使內(nèi)存管理更容易這一事實(shí),其實(shí)是HTTP 1.1是如此可怕的協(xié)議的原因之一。 這是他們?cè)噲D在HTTP 2.0中修復(fù)數(shù)據(jù)的問(wèn)題之一,數(shù)據(jù)在LTV編碼幀中傳輸。 這也是為什么我們使用TLV編碼的VStack.co project 設(shè)計(jì)了我們自己的網(wǎng)絡(luò)協(xié)議。
寫(xiě)部分?jǐn)?shù)據(jù)
在非阻塞IO管道中寫(xiě)數(shù)據(jù)仍然是一個(gè)挑戰(zhàn)。當(dāng)你調(diào)用一個(gè)處于非阻塞式Channel對(duì)象的write(ByteBuffer)方法時(shí),ByteBuffer寫(xiě)入多少數(shù)據(jù)是無(wú)法保證的。write(ByteBuffer)方法會(huì)返回寫(xiě)入的字節(jié)數(shù),因此可以跟蹤寫(xiě)入的字節(jié)數(shù)。這就是挑戰(zhàn):跟蹤部分寫(xiě)入的消息,以便最終可以發(fā)送一條消息的所有字節(jié)。
為了管理部分消息寫(xiě)入Channel,我們將創(chuàng)建一個(gè)消息寫(xiě)入器(Message Writer)。就像Message Reader一樣,每一個(gè)要寫(xiě)入消息的Channel我們都需要一個(gè)Message Writer。在每個(gè)Message Writer中,我們跟蹤正在寫(xiě)入的消息的字節(jié)數(shù)。
如果達(dá)到的消息量超過(guò)Message Writer可直接寫(xiě)入Channel的消息量,消息就需要在Message Writer排隊(duì)。然后Message Writer盡快地將消息寫(xiě)入到Channel中。
下圖是部分消息如何寫(xiě)入的設(shè)計(jì)圖:

為了使Message Writer能夠盡快發(fā)送數(shù)據(jù),Message Writer需要能夠不時(shí)被調(diào)用,這樣就能發(fā)送更多的消息。
如果你有大量的連接那你將需要大量的Message Writer實(shí)例。檢查Message Writer實(shí)例(如:一百萬(wàn)個(gè))看寫(xiě)任何數(shù)據(jù)時(shí)是否緩慢。 首先,許多Message Writer實(shí)例都沒(méi)有任何消息要發(fā)送,我們并不想檢查那些Message Writer實(shí)例。其次,并不是所有的Channel實(shí)例都可以準(zhǔn)備好寫(xiě)入數(shù)據(jù)。 我們不想浪費(fèi)時(shí)間嘗試將數(shù)據(jù)寫(xiě)入無(wú)法接受任何數(shù)據(jù)的Channel。
為了檢查Channel是否準(zhǔn)備好進(jìn)行寫(xiě)入,您可以使用Selector注冊(cè)Channel。然而我們并不想將所有的Channel實(shí)例注冊(cè)到Selector中去。想象一下,如果你有1,000,000個(gè)連接且其中大多是空閑的,并且所有的連接已經(jīng)與Selector注冊(cè)。然后當(dāng)你調(diào)用select()時(shí),這些Channel實(shí)例的大部分將被寫(xiě)入就緒(它們大都是空閑的,記得嗎?)然后你必須檢查所有這些連接的Message Writer,以查看他們是否有任何數(shù)據(jù)要寫(xiě)入。
為了避免檢查所有消息的Message Writer實(shí)例和所有不可能被寫(xiě)入任何信息的Channel實(shí)例,我們使用這兩步的方法:
- 當(dāng)一個(gè)消息被寫(xiě)入Message Writer,Message Writer向Selector注冊(cè)其相關(guān)Channel(如果尚未注冊(cè))。
- 當(dāng)你的服務(wù)器有時(shí)間時(shí),它檢查Selector以查看哪些注冊(cè)的Channel實(shí)例已準(zhǔn)備好進(jìn)行寫(xiě)入。 對(duì)于每個(gè)寫(xiě)就緒Channel,請(qǐng)求其關(guān)聯(lián)的Message Writer將數(shù)據(jù)寫(xiě)入Channel。 如果Message Writer將其所有消息寫(xiě)入其Channel,則Channel將再次從Selector注冊(cè)。
這兩個(gè)小步驟確保了有消息寫(xiě)入的Channel實(shí)際上已經(jīng)被Selector注冊(cè)了。
匯總
正如你所見(jiàn),一個(gè)非阻塞式服務(wù)器需要時(shí)不時(shí)檢查輸入的消息來(lái)判斷是否有任何的新的完整的消息發(fā)送過(guò)來(lái)。服務(wù)器可能會(huì)在一個(gè)或多個(gè)完整消息發(fā)來(lái)之前就檢查了多次。檢查一次是不夠的。
同樣,一個(gè)非阻塞式服務(wù)器需要時(shí)不時(shí)檢查是否有任何數(shù)據(jù)需要寫(xiě)入。如果有,服務(wù)器需要檢查是否有任何相應(yīng)的連接準(zhǔn)備好將該數(shù)據(jù)寫(xiě)入它們。只有在第一次排隊(duì)消息時(shí)才檢查是不夠的,因?yàn)橄⒖赡鼙徊糠謱?xiě)入。
所有這些非阻塞服務(wù)器最終都需要定期執(zhí)行的三個(gè)“管道”(pipelines)::
- 讀取管道(The read pipeline),用于檢查是否有新數(shù)據(jù)從開(kāi)放連接進(jìn)來(lái)的。
- 處理管道(The process pipeline),用于所有任何完整消息。
- 寫(xiě)入管道(The write pipeline),用于檢查是否可以將任何傳出的消息寫(xiě)入任何打開(kāi)的連接。
這三條管道在循環(huán)中重復(fù)執(zhí)行。你可能可以稍微優(yōu)化執(zhí)行。例如,如果沒(méi)有排隊(duì)的消息可以跳過(guò)寫(xiě)入管道。 或者,如果我們沒(méi)有收到新的,完整的消息,也許您可以跳過(guò)處理管道。
以下是說(shuō)明完整服務(wù)器循環(huán)的圖:

如果仍然發(fā)現(xiàn)這有點(diǎn)復(fù)雜,請(qǐng)記住查看GitHub資料庫(kù):
https://github.com/jjenkov/java-nio-server
也許看到正在執(zhí)行的代碼可能會(huì)幫助你了解如何實(shí)現(xiàn)這一點(diǎn)。
服務(wù)器線程模型
GitHub資源庫(kù)里面的非阻塞式服務(wù)器實(shí)現(xiàn)使用了兩個(gè)線程的線程模式。第一個(gè)線程用來(lái)接收來(lái)自ServerSocketChannel的傳入連接。第二個(gè)線程處理接受的連接,意思是讀取消息,處理消息并將響應(yīng)寫(xiě)回連接。這兩個(gè)線程模型的圖解如下:

上一節(jié)中說(shuō)到的服務(wù)器循環(huán)處理是由處理線程(Processor Thread)執(zhí)行。