即使您了解Java NIO非阻塞功能如何工作(選擇器刑赶,通道谆膳,緩沖區(qū)等),設(shè)計(jì)非阻塞服務(wù)器仍然很難。與阻塞IO相比实辑,非阻塞IO包含幾個(gè)挑戰(zhàn)臣疑。這個(gè)無阻塞的服務(wù)器教程將討論非阻塞服務(wù)器的主要挑戰(zhàn),并為他們描述一些潛在的解決方案徙菠。
本教程中介紹的想法是圍繞Java NIO設(shè)計(jì)的讯沈。但是,我相信這些想法可以在其他語言中重用婿奔,只要它們具有某種類似于Selector的構(gòu)造缺狠。據(jù)我所知,這樣的結(jié)構(gòu)是由底層操作系統(tǒng)提供的萍摊,所以很有可能你也可以用其他語言訪問它挤茄。
非阻塞服務(wù)器- GitHub庫
I have created a simple proof-of-concept of the ideas presented in this tutorial and put it in a GitRebu repository for you to look at. Here is the GitHub repository:
https://github.com/jjenkov/java-nio-server
感謝國外作者Jakob Jenkov
非阻塞IO管道
非阻塞IO管道是處理非阻塞IO的組件鏈。 這包括以非阻塞方式讀取和寫入IO冰木。 下面是一個(gè)簡化的非阻塞IO管道的例子:
一個(gè)組件使用一個(gè)Selector來檢查一個(gè)Channel何時(shí)有數(shù)據(jù)讀取穷劈。然后,組件讀取輸入數(shù)據(jù)并根據(jù)輸入生成一些輸出踊沸。輸出再次寫入通道歇终。
非阻塞IO管道不需要讀取和寫入數(shù)據(jù)。有些流水線只能讀取數(shù)據(jù)逼龟,有些流水線只能寫數(shù)據(jù)评凝。
上圖僅顯示單個(gè)組件。非阻塞IO管道可能有多個(gè)組件處理傳入數(shù)據(jù)腺律。非阻塞IO管道的長度取決于管道需要做什么奕短。
非阻塞IO管道也可能同時(shí)從多個(gè)通道讀取。例如匀钧,從多個(gè)SocketChannel讀取數(shù)據(jù)翎碑。
上圖中的控制流程也被簡化了。它是通過選擇器啟動(dòng)從通道讀取數(shù)據(jù)的組件之斯。不是Channel將數(shù)據(jù)推送到Selector日杈,而是從那里進(jìn)入組件,即使這是上圖所示吊圾。
大概這個(gè)組件就是 Buffer吧
非阻塞與阻塞IO管道
非阻塞和阻塞IO管道之間的最大區(qū)別在于如何從底層通道(套接字或文件)讀取數(shù)據(jù)达椰。
IO管道通常從某個(gè)流(從一個(gè)套接字或文件)讀取數(shù)據(jù),并將該數(shù)據(jù)拆分為一致的消息项乒。 這與使用分詞器將數(shù)據(jù)流分解為令牌進(jìn)行分析類似啰劲。 相反,您將數(shù)據(jù)流分解為更大的消息檀何。 我將調(diào)用將消息流分解成消息讀取器的消息蝇裤。 下面是一個(gè)消息閱讀器將消息分解成消息的圖示:
阻塞IO管道可以使用類似于InputStream的接口廷支,一次一個(gè)字節(jié)可以從底層通道讀取,而類似于InputStream的接口將阻塞栓辜,直到有數(shù)據(jù)準(zhǔn)備好讀取恋拍。 這導(dǎo)致阻塞消息閱讀器實(shí)現(xiàn)。
使用阻塞IO接口來簡化消息閱讀器的實(shí)現(xiàn)藕甩。 阻塞消息閱讀器從不必處理沒有從流中讀取數(shù)據(jù)的情況施敢,或者只從流中讀取部分消息并且需要稍后恢復(fù)消息解析的情況。
同樣狭莱,一個(gè)阻塞消息編寫器(一個(gè)將消息寫入流的組件)從來不必處理只寫入消息的一部分的情況僵娃,以及稍后必須恢復(fù)消息寫入的情況。
阻塞IO管道缺陷
雖然攔截消息閱讀器更容易實(shí)現(xiàn)腋妙,但它有一個(gè)不幸的缺點(diǎn)是需要一個(gè)單獨(dú)的線程為每個(gè)流需要被拆分成消息默怨。這是必要的原因是每個(gè)流的IO接口阻塞,直到有一些數(shù)據(jù)要從中讀取骤素。這意味著單個(gè)線程不能嘗試從一個(gè)流中讀取匙睹,如果沒有數(shù)據(jù),則從另一個(gè)流中讀取济竹。只要線程試圖從流中讀取數(shù)據(jù)痕檬,線程就會(huì)阻塞,直到實(shí)際上有一些數(shù)據(jù)要讀取规辱。
如果IO管道是必須處理大量并發(fā)連接的服務(wù)器的一部分谆棺,則服務(wù)器將需要每個(gè)活動(dòng)的入口連接一個(gè)線程栽燕。如果服務(wù)器在任何時(shí)候只有幾百個(gè)并發(fā)連接罕袋,這可能不成問題。但是碍岔,如果服務(wù)器有數(shù)百萬并發(fā)連接浴讯,這種類型的設(shè)計(jì)不能很好地?cái)U(kuò)展。每個(gè)線程將為其堆棧提供320K(32位JVM)和1024K(64位JVM)內(nèi)存蔼啦。那么榆纽,1.000.000個(gè)線程將占用1TB的內(nèi)存!也就是說捏肢,在服務(wù)器已經(jīng)使用任何存儲(chǔ)器來處理傳入消息(例如分配給在消息處理期間使用的對(duì)象的存儲(chǔ)器)之前奈籽。
為了減少線程的數(shù)量,許多服務(wù)器使用一種設(shè)計(jì)鸵赫,其中服務(wù)器保持一個(gè)線程池(例如100個(gè))衣屏,該線程池一次從入站連接讀取消息。入站連接保留在隊(duì)列中辩棒,并且線程按順序處理來自每個(gè)入站連接的消息狼忱,并將入站連接放入隊(duì)列中膨疏。這里說明這個(gè)設(shè)計(jì):但是,這種設(shè)計(jì)要求入站連接經(jīng)常合理地發(fā)送數(shù)據(jù)钻弄。 如果入站連接可能處于非活動(dòng)狀態(tài)的時(shí)間較長佃却,大量的非活動(dòng)連接實(shí)際上可能會(huì)阻塞線程池中的所有線程。 這意味著服務(wù)器響應(yīng)緩慢甚至無響應(yīng)窘俺。
一些服務(wù)器設(shè)計(jì)試圖通過線程池中的線程數(shù)量具有一定彈性來緩解這個(gè)問題饲帅。 例如,如果線程池用完線程瘤泪,線程池可能會(huì)啟動(dòng)更多的線程來處理負(fù)載洒闸。 此解決方案意味著需要更多數(shù)量的慢連接才能使服務(wù)器無響應(yīng)。 但請(qǐng)記住均芽,您可以運(yùn)行多少個(gè)線程的上限仍然存在丘逸。 所以,這不能很好地?cái)U(kuò)大與1000.000慢速連接掀宋。
基本的非阻塞IO管道設(shè)計(jì)
非阻塞IO管道可以使用單個(gè)線程來讀取來自多個(gè)流的消息深纲。 這要求流可以切換到非阻塞模式。 處于非阻塞模式時(shí)劲妙,當(dāng)您嘗試從中讀取數(shù)據(jù)時(shí)湃鹊,流可能會(huì)返回0個(gè)或更多字節(jié)。 如果流沒有要讀取的數(shù)據(jù)镣奋,則返回0字節(jié)币呵。 當(dāng)流實(shí)際上有一些數(shù)據(jù)要讀取時(shí),將返回1+字節(jié)侨颈。
為了避免檢查有0字節(jié)的流余赢,我們使用Java NIO Selector。 一個(gè)或多個(gè)SelectableChannel實(shí)例可以使用Selector進(jìn)行注冊(cè)哈垢。 當(dāng)你在選擇器上調(diào)用select()或selectNow()時(shí)妻柒,它只給你實(shí)際上有數(shù)據(jù)讀取的SelectableChannel實(shí)例。 這里說明這個(gè)設(shè)計(jì):閱讀部分消息
當(dāng)我們從一個(gè)SelectableChannel讀取一個(gè)數(shù)據(jù)塊時(shí)耘分,我們不知道這個(gè)數(shù)據(jù)塊是否包含少于或多于一條消息举塔。 數(shù)據(jù)塊可能包含部分消息(少于消息),完整消息或超過消息求泰,例如1.5或2.5個(gè)消息央渣。 這里說明了各種部分消息的可能性:
處理部分消息有兩個(gè)挑戰(zhàn):
- 檢測(cè)數(shù)據(jù)塊中是否有完整的消息。
- 如何處理部分消息渴频,直到消息的其余部分到達(dá)芽丹。
檢測(cè)完整消息需要消息閱讀器查看數(shù)據(jù)塊中的數(shù)據(jù),以查看數(shù)據(jù)是否至少包含一條完整消息枉氮。 如果數(shù)據(jù)塊包含一個(gè)或多個(gè)完整的消息志衍,則這些消息可以沿管道向下發(fā)送以進(jìn)行處理暖庄。 尋找完整的消息的過程將會(huì)重復(fù)很多,所以這個(gè)過程必須盡可能快楼肪。
只要數(shù)據(jù)塊中有部分消息培廓,或者是一個(gè)或多個(gè)完整消息,就需要存儲(chǔ)該消息春叫,直到該消息的其余部分從該通道到達(dá)肩钠。
檢測(cè)完整消息和存儲(chǔ)部分消息都是消息閱讀器的責(zé)任。 為避免來自不同渠道實(shí)例的消息數(shù)據(jù)混合暂殖,我們將為每個(gè)渠道使用一個(gè)消息閱讀器 設(shè)計(jì)如下所示:
在檢索到一個(gè)具有要從Selector中讀取數(shù)據(jù)的Channel實(shí)例之后价匠,與該Channel關(guān)聯(lián)的Message Reader將讀取數(shù)據(jù)并嘗試將其分解為消息。 如果這導(dǎo)致讀取任何完整的消息呛每,則這些消息可以通過讀取管道傳遞給需要處理它們的任何組件踩窖。
消息閱讀器當(dāng)然是協(xié)議特定的。 消息閱讀器需要知道它正在嘗試閱讀的消息的消息格式晨横。 如果我們的服務(wù)器實(shí)現(xiàn)可以跨協(xié)議重用洋腮,則需要能夠插入消息讀取器實(shí)現(xiàn) - 可能通過以某種方式接受消息讀取器工廠作為配置參數(shù)。
存儲(chǔ)部分信息
現(xiàn)在我們已經(jīng)確定消息閱讀器負(fù)責(zé)存儲(chǔ)部分消息手形,直到收到完整的消息啥供,我們需要弄清楚這個(gè)部分消息存儲(chǔ)應(yīng)該如何實(shí)現(xiàn)。
我們應(yīng)該考慮兩個(gè)設(shè)計(jì)考慮因素:
- 我們希望盡可能少地復(fù)制消息數(shù)據(jù)库糠。 越復(fù)制伙狐,性能越低。
- 我們希望完整的消息被存儲(chǔ)在連續(xù)的字節(jié)序列中瞬欧,以使解析消息更容易贷屎。
每個(gè)消息讀取器的緩沖區(qū)
顯然,部分消息需要存儲(chǔ)在某種緩沖區(qū)中黍判。 直接的實(shí)現(xiàn)是在每個(gè)消息閱讀器內(nèi)部簡單地使用一個(gè)緩沖區(qū)豫尽。 但是,這個(gè)緩沖區(qū)應(yīng)該有多大顷帖? 它需要足夠大才能存儲(chǔ)最大允許的消息。 因此渤滞,如果允許的最大消息是1MB贬墩,則每個(gè)消息讀取器中的內(nèi)部緩沖區(qū)至少需要1MB。
當(dāng)我們達(dá)到數(shù)百萬的連接時(shí)妄呕,每個(gè)連接使用1MB并不是真正的工作陶舞。 1.000.000 x 1MB仍然是1TB的內(nèi)存! 而如果最大消息大小是16MB绪励? 還是128MB肿孵?
可調(diào)整大小的緩沖區(qū)
另一個(gè)選擇是在每個(gè)消息讀取器中實(shí)現(xiàn)可調(diào)整大小的緩沖區(qū)唠粥。 一個(gè)可調(diào)整大小的緩沖區(qū)將從小的地方開始,如果緩沖區(qū)的消息太大停做,緩沖區(qū)將被擴(kuò)展晤愧。 這樣,每個(gè)連接不一定需要例如 1MB緩沖區(qū)蛉腌。 每個(gè)連接只占用盡可能多的內(nèi)存官份,因?yàn)樗鼈冃枰4嫦乱粭l消息。
有幾種方法來實(shí)現(xiàn)可調(diào)整大小的緩沖區(qū)烙丛。 所有這些都有優(yōu)點(diǎn)和缺點(diǎn)舅巷,所以我將在下面的部分進(jìn)行討論。
通過復(fù)制調(diào)整大小
實(shí)現(xiàn)可調(diào)整大小的緩沖器的第一種方法是從例如小緩沖器開始河咽。 4KB钠右。如果消息不能放入4KB緩沖器中,可以分配8KB忘蟹,并將來自4KB緩沖區(qū)的數(shù)據(jù)復(fù)制到較大的緩沖區(qū)中爬舰。
通過調(diào)整復(fù)制大小的緩沖區(qū)實(shí)現(xiàn)的優(yōu)點(diǎn)是消息的所有數(shù)據(jù)都保存在一個(gè)連續(xù)的字節(jié)數(shù)組中。這使解析消息變得更容易寒瓦。
調(diào)整大小的副本緩沖區(qū)實(shí)現(xiàn)的缺點(diǎn)是情屹,它會(huì)導(dǎo)致大量的數(shù)據(jù)復(fù)制更大的消息。
為了減少數(shù)據(jù)復(fù)制杂腰,您可以分析流經(jīng)系統(tǒng)的消息的大小垃你,以查找會(huì)減少復(fù)制量的緩沖區(qū)大小。例如喂很,您可能會(huì)看到大多數(shù)消息都小于4KB惜颇,因?yàn)樗鼈冎话浅P〉恼?qǐng)求/響應(yīng)。這意味著第一個(gè)緩沖區(qū)大小應(yīng)該是4KB少辣。
那么你可能會(huì)看到凌摄,如果消息大于4KB,通常是因?yàn)樗粋€(gè)文件漓帅。您可能會(huì)注意到锨亏,流經(jīng)系統(tǒng)的大部分文件都小于128KB。那么使第二個(gè)緩沖區(qū)大小為128KB是有意義的忙干。
最后你可能會(huì)看到器予,一旦消息超過128KB,消息的大小就沒有真正的模式捐迫,所以也許最后的緩沖區(qū)大小應(yīng)該是最大的消息大小乾翔。
有了這3種緩沖區(qū)大小,可以根據(jù)流經(jīng)系統(tǒng)的消息的大小施戴,減少數(shù)據(jù)復(fù)制的次數(shù)反浓。低于4KB的郵件將永遠(yuǎn)不會(huì)被復(fù)制萌丈。對(duì)于1.000.000個(gè)并發(fā)連接,導(dǎo)致1.000.000 x 4KB = 4GB雷则,這在今天的大多數(shù)服務(wù)器(2015)中都是可能的辆雾。 4KB和128KB之間的消息將被復(fù)制一次,只有4KB的數(shù)據(jù)將被復(fù)制到128KB緩沖區(qū)中巧婶。 128KB和最大消息大小之間的消息將被復(fù)制兩次乾颁。第一次4KB將被復(fù)制,第二次128KB將被復(fù)制艺栈,所以總共132KB復(fù)制為最大的消息英岭。假設(shè)沒有那么多的128KB以上的消息,這可能是可以接受的湿右。
一旦消息被完全處理诅妹,分配的內(nèi)存應(yīng)該被釋放。這樣毅人,從同一連接接收到的下一個(gè)消息再次以最小的緩沖區(qū)大小開始吭狡。這是確保內(nèi)存可以在連接之間更有效地共享的必要條件。很可能不是所有的連接都需要同時(shí)使用大緩沖區(qū)丈莺。
我有一個(gè)關(guān)于如何實(shí)現(xiàn)這樣一個(gè)支持可調(diào)整大小的數(shù)組的內(nèi)存緩沖區(qū)的完整教程:可調(diào)整大小的數(shù)組划煮。本教程還包含指向GitHub存儲(chǔ)庫的鏈接,其中的代碼顯示了一個(gè)工作實(shí)現(xiàn)缔俄。
通過追加來調(diào)整大小
調(diào)整緩沖區(qū)大小的另一種方法是使緩沖區(qū)由多個(gè)數(shù)組組成弛秋。當(dāng)您需要調(diào)整緩沖區(qū)大小時(shí),只需分配另一個(gè)字節(jié)數(shù)組并將數(shù)據(jù)寫入該數(shù)組俐载。
有兩種方式來增長這樣一個(gè)緩沖區(qū)蟹略。一種方法是分配單獨(dú)的字節(jié)數(shù)組并保留這些字節(jié)數(shù)組的列表。另一種方法是分配更大的共享字節(jié)數(shù)組的片遏佣,然后保存分配給緩沖區(qū)的片的列表挖炬。就我個(gè)人而言,我覺得切片方法稍好一些状婶,但差別不大意敛。
通過附加單獨(dú)的數(shù)組或片來增長緩沖區(qū)的好處是在寫入時(shí)不需要復(fù)制任何數(shù)據(jù)。所有數(shù)據(jù)可以直接從套接字(Channel)直接復(fù)制到數(shù)組或片中太抓。
以這種方式增長緩沖區(qū)的缺點(diǎn)是數(shù)據(jù)不會(huì)存儲(chǔ)在單個(gè)連續(xù)的數(shù)組中空闲。這使得消息解析變得更加困難,因?yàn)榻馕銎餍枰瑫r(shí)查找每個(gè)數(shù)組的末尾和所有數(shù)組的末尾走敌。由于您需要在寫入的數(shù)據(jù)中查找消息的結(jié)尾,因此此模型不太容易處理逗噩。
TLV編碼消息 Type-length-value
一些協(xié)議消息格式使用TLV格式(類型掉丽,長度跌榔,值)進(jìn)行編碼。這意味著捶障,當(dāng)消息到達(dá)時(shí)僧须,消息的總長度被存儲(chǔ)在消息的開頭。這樣你就可以立即知道為整個(gè)消息分配多少內(nèi)存项炼。
TLV編碼使得內(nèi)存管理變得更容易担平。您立即知道要為消息分配多少內(nèi)存。沒有內(nèi)存被浪費(fèi)在僅部分使用的緩沖區(qū)的末尾锭部。
TLV編碼的一個(gè)缺點(diǎn)是暂论,在消息的所有數(shù)據(jù)到達(dá)之前,為消息分配所有內(nèi)存拌禾。一些發(fā)送較大消息的緩慢連接可以分配所有可用的內(nèi)存取胎,從而使服務(wù)器無響應(yīng)。
解決此問題的方法是使用包含多個(gè)TLV字段的消息格式湃窍。因此闻蛀,內(nèi)存分配給每個(gè)字段,而不是整個(gè)消息您市,內(nèi)存只在字段到達(dá)時(shí)才分配觉痛。盡管如此,一個(gè)大的領(lǐng)域可能會(huì)對(duì)你的內(nèi)存管理產(chǎn)生同樣的影響茵休。
另一個(gè)解決方法是超時(shí)例如沒有收到的消息10-15秒薪棒。這可以使您的服務(wù)器從許多大消息的同時(shí)到達(dá)的同時(shí)恢復(fù),但它仍然會(huì)使服務(wù)器一段時(shí)間無響應(yīng)泽篮。此外盗尸,有意的DoS(拒絕服務(wù))攻擊仍然可以導(dǎo)致您的服務(wù)器的內(nèi)存完全分配。
TLV編碼存在不同的變化帽撑。究竟使用了多少字節(jié)泼各,所以指定字段的類型和長度取決于每個(gè)單獨(dú)的TLV編碼。也有TLV編碼亏拉,首先是字段的長度扣蜻,然后是類型,然后是值(LTV編碼)及塘。雖然字段的順序是不同的莽使,但它仍然是一個(gè)TLV變化。
TLV編碼使內(nèi)存管理更容易的事實(shí)是HTTP 1.1是如此糟糕的協(xié)議的原因之一笙僚。這是他們?cè)噲D在HTTP 2.0中解決數(shù)據(jù)以LTV編碼幀傳輸?shù)膯栴}之一芳肌。這也是為什么我們?yōu)槭褂肨LV編碼的VStack.co項(xiàng)目設(shè)計(jì)了自己的網(wǎng)絡(luò)協(xié)議。
編寫部分消息
在一個(gè)非阻塞的IO管道中寫數(shù)據(jù)也是一個(gè)挑戰(zhàn)。當(dāng)您以非阻塞模式在通道上調(diào)用寫入(ByteBuffer)時(shí)亿笤,無法保證正在寫入ByteBuffer中有多少個(gè)字節(jié)翎迁。寫(ByteBuffer)方法返回寫了多少字節(jié),所以可以跟蹤寫入的字節(jié)數(shù)净薛。這就是挑戰(zhàn):跟蹤部分編寫的消息汪榔,以便最終發(fā)送消息的所有字節(jié)。
為了管理將部分消息寫入頻道肃拜,我們將創(chuàng)建一個(gè)消息編寫器痴腌。就像使用消息讀取器一樣,我們需要每個(gè)通道寫消息的消息寫入器燃领。在每個(gè)Message Writer內(nèi)部士聪,我們都記錄了它正在寫入的消息的字節(jié)數(shù)。
如果有更多的消息到達(dá)消息寫入器柿菩,而不是直接寫入通道戚嗅,則消息需要在消息寫入器內(nèi)部排隊(duì)。消息編寫器然后將消息盡可能快地寫入通道枢舶。
下面是一個(gè)圖表懦胞,展示了如何設(shè)計(jì)部分消息:為了使消息編寫器能夠發(fā)送僅部分被發(fā)送的消息,消息編寫器需要不時(shí)地被調(diào)用凉泄,所以它可以發(fā)送更多的數(shù)據(jù)躏尉。
如果你有很多的連接,你會(huì)有很多消息編寫器實(shí)例后众。檢查例如一百萬個(gè)Message Writer實(shí)例來查看它們是否可以寫入任何數(shù)據(jù)的速度很慢胀糜。首先,許多Message Writer實(shí)例很多沒有任何消息要發(fā)送蒂誉。我們不想檢查那些Message Writer實(shí)例教藻。其次,并不是所有的Channel實(shí)例都可以準(zhǔn)備好寫入數(shù)據(jù)右锨。我們不想浪費(fèi)時(shí)間試圖寫數(shù)據(jù)到無法接受任何數(shù)據(jù)的頻道括堤。
要檢查通道是否準(zhǔn)備好寫入,可以使用選擇器注冊(cè)通道绍移。但是悄窃,我們不想使用Selector注冊(cè)所有Channel實(shí)例。試想一下蹂窖,如果你有1.000.000連接轧抗,大部分是空閑的,所有1.000.000連接都被注冊(cè)到了Selector瞬测。然后横媚,當(dāng)你調(diào)用select()時(shí)纠炮,這些Channel實(shí)例中的大部分都將準(zhǔn)備好寫入(它們大部分是空閑的,記得嗎分唾?)抗碰。然后您必須檢查所有這些連接的消息編寫器狮斗,看看他們是否有任何數(shù)據(jù)要寫入绽乔。
為了避免檢查消息的所有消息編寫器實(shí)例,以及所有通道實(shí)例碳褒,它們沒有任何消息發(fā)送給它們折砸,我們使用這個(gè)兩步法:
當(dāng)消息寫入消息寫入器時(shí),消息寫入器向選擇器(如果它尚未注冊(cè))注冊(cè)其關(guān)聯(lián)的通道沙峻。
當(dāng)你的服務(wù)器有時(shí)間的時(shí)候睦授,它會(huì)檢查選擇器來查看哪些已注冊(cè)的通道實(shí)例準(zhǔn)備好寫入。 對(duì)于每個(gè)寫入就緒通道摔寨,其相關(guān)的消息寫入器被要求將數(shù)據(jù)寫入通道去枷。 如果消息寫入器將其所有消息寫入其通道,則該通道將再次從選擇器中注銷是复。
這個(gè)小小的兩步方法確保只有Channel消息被寫入到它們的實(shí)例才會(huì)真正注冊(cè)到Selector中删顶。
總結(jié):Putting it All Together
如您所見,非阻塞服務(wù)器需要不時(shí)檢查傳入數(shù)據(jù)淑廊,以查看是否收到任何新的完整郵件逗余。 服務(wù)器可能需要多次檢查,直到收到一個(gè)或多個(gè)完整的消息季惩。 檢查一次是不夠的录粱。
同樣,非阻塞服務(wù)器需要不時(shí)檢查是否有數(shù)據(jù)要寫入画拾。 如果是啥繁,服務(wù)器需要檢查是否有相應(yīng)的連接準(zhǔn)備好寫入數(shù)據(jù)。 僅在消息第一次排隊(duì)時(shí)檢查是不夠的青抛,因?yàn)橄⒖赡苁遣糠謱懭氲摹?/p>
- 總而言之旗闽,一個(gè)非阻塞服務(wù)器最終需要定期執(zhí)行三個(gè)“管道”:
- 讀取管道,用于檢查來自打開的連接的新輸入數(shù)據(jù)脂凶。
- 處理任何收到的完整消息的流程管道宪睹。
寫入管道檢查是否可以將任何傳出消息寫入任何打開的連接。
這三條管道是循環(huán)重復(fù)執(zhí)行的蚕钦。 你可能會(huì)稍微優(yōu)化執(zhí)行亭病。 例如,如果沒有排隊(duì)的消息嘶居,則可以跳過寫入管道罪帖。 或者促煮,如果我們沒有收到新的,完整的消息整袁,也許可以跳過流程管道菠齿。
以下是一個(gè)說明整個(gè)服務(wù)器循環(huán)的圖表:
如果您仍然覺得這有點(diǎn)復(fù)雜,請(qǐng)記住查看GitHub存儲(chǔ)庫:https://github.com/jjenkov/java-nio-server
也許看到代碼在行動(dòng)可能會(huì)幫助你了解如何實(shí)現(xiàn)這一點(diǎn)坐昙。