即使你了解了Java NIO非阻塞功能的工作(怎么樣SelectorChannel, Buffer等等),設(shè)計(jì)一個(gè)無(wú)阻塞服務(wù)器仍然很難。非阻塞IO包含了相比阻塞IO的要有難度。本章非阻塞服務(wù)器教程將討論非阻塞服務(wù)器的主要挑戰(zhàn),并為他們描述一些潛在的解決方案。

  本教程中描述的想法是圍繞Java NIO設(shè)計(jì)的。但是,我認(rèn)為,只要有這樣的構(gòu)造,這些想法可以用其他語(yǔ)言重復(fù)使用Selector。據(jù)我所知,這樣的結(jié)構(gòu)是由底層操作系統(tǒng)提供的,所以很有可能您可以使用其他語(yǔ)言訪問(wèn)此操作。

非阻塞服務(wù)器 - GitHub存儲(chǔ)庫(kù)

  我創(chuàng)建了一個(gè)簡(jiǎn)單的概念驗(yàn)證概念,在本教程中提出的想法,并將其放在GitRebu存儲(chǔ)庫(kù)中,可供查看。這是GitHub信息庫(kù):

https://github.com/jjenkov/java-nio-server

非阻塞IO管道

  非阻塞IO管道是處理非阻塞IO組件的鏈。這包括以非阻塞方式讀寫IO。以下是簡(jiǎn)化的非阻塞IO管道的流程圖:

  組件使用選擇器來(lái)檢測(cè)通道何時(shí)讀取數(shù)據(jù)。然后組件讀取輸入數(shù)據(jù),并根據(jù)輸入生成一些輸出。輸出Channel再次寫入。

  非阻塞IO管道不需要同時(shí)讀寫數(shù)據(jù)。一些管道只能讀取數(shù)據(jù),而一些管道只能寫入數(shù)據(jù)。

  上圖僅顯示單個(gè)組件。非阻塞IO管道可能具有多個(gè)組件進(jìn)程傳入數(shù)據(jù)。非阻塞IO管道的長(zhǎng)度取決于管道需要做什么。

  非阻塞IO管道也可能同時(shí)從多個(gè)Channels 讀取。例如,從多個(gè)SocketChannels 讀取數(shù)據(jù)。

  上圖中的控制流程也簡(jiǎn)化了。它是發(fā)起數(shù)據(jù)的讀出從該部件Channel經(jīng)過(guò)Selector。事實(shí)不是圖中所展示的Channel將數(shù)據(jù)推Selector進(jìn)入到組件中。

非阻塞與阻塞IO管道

  非阻塞和阻塞IO管道之間的最大區(qū)別是如何從底層Channel(套接字或文件)讀取數(shù)據(jù) 。

  IO管道通常從一些流(從套接字或文件)讀取數(shù)據(jù),并將該數(shù)據(jù)分解成一致的消息。這類似于將數(shù)據(jù)流打破使用標(biāo)記器進(jìn)行解析。相反,您將數(shù)據(jù)流分解成更大的消息。我們把將流打破成消息的組件稱為消息讀取器

消息閱讀器將流分成消息的簡(jiǎn)易流程圖:

  阻塞IO管道可以使用InputStream接口,其中一次可以從底層Channel讀取一個(gè)字節(jié),并且其中的InputStream接口處于阻塞狀態(tài),直到有數(shù)據(jù)準(zhǔn)備好讀取。這導(dǎo)致阻塞Message Reader實(shí)現(xiàn)。

  使用阻塞IO接口流大大簡(jiǎn)化了Message Reader的實(shí)現(xiàn)。阻塞消息讀取器不必處理數(shù)據(jù)流中沒(méi)有讀取數(shù)據(jù)的情況,或者僅部分消息是從流中讀取,并且消息需要稍后恢復(fù)解析。

  類似地,阻塞消息寫入器(將消息寫入流的組件)不必處理僅寫入部分消息的,以及稍后需要恢復(fù)消息寫入的情況。

阻止IO管道缺點(diǎn)

  雖然阻塞消息閱讀器容易實(shí)現(xiàn),但是對(duì)于需要分割成消息的每個(gè)流需要單獨(dú)的線程是不幸的缺點(diǎn)。因此每個(gè)流的IO接口阻塞,直到有一些數(shù)據(jù)從中讀取是必要的。這意味著單個(gè)線程不能嘗試從一個(gè)流中讀取,如果沒(méi)有數(shù)據(jù),則從另一個(gè)流中讀取。一旦線程嘗試從流中讀取數(shù)據(jù),線程將阻塞,直到實(shí)際上有一些數(shù)據(jù)要讀取。

  如果IO管道是處理大量并發(fā)連接的服務(wù)器的一部分,則服務(wù)器每個(gè)活動(dòng)進(jìn)入連接的將需要一個(gè)線程。如果服務(wù)器在任何時(shí)間只有幾個(gè)并發(fā)連接,這可能不是一個(gè)問(wèn)題。但是,如果服務(wù)器具有數(shù)百萬(wàn)個(gè)并發(fā)連接,則這種類型的設(shè)計(jì)不會(huì)很好地?cái)U(kuò)展。每個(gè)線程的堆棧將占用320K(32位JVM)和1024K(64位JVM)內(nèi)存。所以,1000000線程將占用1 TB內(nèi)存!而在這之前,服務(wù)器已經(jīng)使用內(nèi)存來(lái)處理傳入的消息(例如,為消息處理期間使用的對(duì)象分配的內(nèi)存)。

  為了保持線程數(shù)量的減少,許多服務(wù)器使用一種設(shè)計(jì),其中服務(wù)器保留一個(gè)線程池(例如100),從而一次從入站連接中讀取消息。入站連接保留在隊(duì)列中,并且線程按入站連接放入隊(duì)列的順序處理來(lái)自每個(gè)入站連接的消息。此設(shè)計(jì)如下圖所示:

  但是,此設(shè)計(jì)要求入站連接能夠合理地發(fā)送數(shù)據(jù)。如果入站連接可能在較長(zhǎng)時(shí)間內(nèi)處于非活動(dòng)狀態(tài),則大量非活動(dòng)連接實(shí)際上可能會(huì)阻塞線程池中的所有線程。這意味著服務(wù)器響應(yīng)遲緩,甚至無(wú)響應(yīng)。

  一些服務(wù)器設(shè)計(jì)嘗試通過(guò)在線程池中的線程彈性數(shù)量數(shù)量來(lái)緩解這個(gè)問(wèn)題。例如,如果線程池用盡線程,則線程池可能會(huì)啟動(dòng)更多的線程來(lái)處理負(fù)載。這方案意味著需要更多數(shù)量的慢速連接才能使服務(wù)器無(wú)響應(yīng)。注意,您可以運(yùn)行多少線程仍然有上限。所以,這將不會(huì)適應(yīng)于1.000.000慢速連接。

基本無(wú)阻塞IO管道設(shè)計(jì)

  非阻塞IO管道可以使用單個(gè)線程來(lái)讀取多個(gè)流的消息。這要求流可以切換到非阻塞模式。當(dāng)處于非阻塞模式時(shí),如果你嘗試從其讀取數(shù)據(jù)時(shí),流可能返回0個(gè)或更多字節(jié)。如果流沒(méi)有要讀取的數(shù)據(jù),則返回0個(gè)字節(jié)。當(dāng)流實(shí)際上有一些要讀取的數(shù)據(jù)時(shí),返回1+字節(jié)。

  為了避免檢查有0個(gè)字節(jié)的流,我們使用Java NIO選擇器。一個(gè)或多個(gè)SelectableChannel實(shí)例可以用Selector注冊(cè)。當(dāng)調(diào)用select()selectNow()在其上Selector只給出SelectableChannel實(shí)例,實(shí)際上有數(shù)據(jù)讀取的。此設(shè)計(jì)如下圖所示:

閱讀部分消息

  當(dāng)我們從一個(gè)SelectableChannel數(shù)據(jù)塊中讀取數(shù)據(jù)時(shí),我們不知道該數(shù)據(jù)塊是否包含少于或者多于一個(gè)消息。數(shù)據(jù)塊可能潛在地包含一個(gè)部分消息(小于一條消息),一條完整消息,或者多于一條消息,例如1.5或2.5條消息。各種部分消息的可能性如下所示:

處理部分消息有兩個(gè)難點(diǎn):

  A.檢測(cè)數(shù)據(jù)塊中是否有完整消息。

  B.在部分消息到達(dá)消息的其余部分之前應(yīng)該如何處理。

  檢測(cè)完整消息要求消息讀取器查看數(shù)據(jù)塊中的數(shù)據(jù),查看數(shù)據(jù)是否至少包含一條完整消息。如果數(shù)據(jù)塊包含一個(gè)或多個(gè)完整消息,則可以將這些消息發(fā)送到管道中進(jìn)行處理。尋找完整信息的過(guò)程將會(huì)重復(fù),所以這個(gè)過(guò)程必須盡可能快。

  無(wú)論何時(shí)在數(shù)據(jù)塊中存在部分消息,無(wú)論是本身還是在一個(gè)或多個(gè)完整消息之后,需要存儲(chǔ)該部分消息,直到該消息的其余部分從該消息到達(dá)Channel。

  檢測(cè)完整消息和存儲(chǔ)部分消息都是消息讀取器的責(zé)任。為了避免混淆來(lái)自不同Channel實(shí)例的消息數(shù)據(jù),我們將使用一個(gè)Message Reader Channel。設(shè)計(jì)如下所示:

  在檢索到Channel具有要從中讀取的數(shù)據(jù)的實(shí)例之后,與之相關(guān)聯(lián)Selector的消息讀取器Channel讀取數(shù)據(jù)并嘗試將其分解成消息。如果這樣導(dǎo)致任何完整的消息被讀取,這些消息可以被傳遞到讀取流水線到需要處理它們的任何組件。

  消息閱讀器當(dāng)然是協(xié)議特定的。消息讀取器需要知道其嘗試讀取的消息的消息格式。如果我們的服務(wù)器實(shí)現(xiàn)可以通過(guò)協(xié)議重復(fù)使用,則需要能夠?qū)⑾⒆x取器實(shí)現(xiàn)插入 - 可能通過(guò)以某種方式接受消息讀取器工廠作為配置參數(shù)。

存儲(chǔ)部分消息

  現(xiàn)在我們已經(jīng)確定消息讀取器有責(zé)任存儲(chǔ)部分消息,直到收到完整的消息,我們需要弄清楚這個(gè)部分消息存儲(chǔ)應(yīng)該如何實(shí)現(xiàn)。

我們應(yīng)該考慮兩個(gè)設(shè)計(jì)考慮因素:

  1.我們要盡可能少地復(fù)制信息數(shù)據(jù)??截愒蕉?,性能越差。

  2.我們希望將完整的消息存儲(chǔ)在連續(xù)的字節(jié)序列中,使解析消息更容易。

每個(gè)消息讀取器的緩沖區(qū)

  顯然,部分消息需要存儲(chǔ)在緩沖器中。簡(jiǎn)單的實(shí)現(xiàn)將是在每個(gè)消息讀取器中內(nèi)部簡(jiǎn)單地具有一個(gè)緩沖區(qū)。但是,緩沖區(qū)應(yīng)該有多大?它將需要足夠大,以便能夠存儲(chǔ)甚至最大的允許的消息。所以,如果最大的允許消息是1MB,那么每個(gè)消息讀取器中的內(nèi)部緩沖區(qū)將需要至少為1MB。

  當(dāng)我們達(dá)到數(shù)以百萬(wàn)計(jì)的連接時(shí),每個(gè)連接使用1MB并不會(huì)奏效。1.000.000 x 1MB還是1TB內(nèi)存!如果最大郵件大小是16MB呢?或者128MB?

可調(diào)整緩沖區(qū)

  另一個(gè)選擇是實(shí)現(xiàn)一個(gè)可調(diào)整大小的緩沖區(qū)用于每個(gè)消息讀取器內(nèi)。一個(gè)可調(diào)整大小的緩沖區(qū)將開(kāi)始小,如果一個(gè)消息對(duì)于緩沖區(qū)太大,緩沖區(qū)將被擴(kuò)展。這樣一來(lái),每個(gè)連接不一定需要一個(gè)例如1MB的緩沖區(qū)。每個(gè)連接只需要占用大量?jī)?nèi)存,因?yàn)樗鼈冃枰4嫦乱粭l消息?! ?/p>

  實(shí)現(xiàn)緩存區(qū)的可調(diào)我們將在后面的幾章討論.

通過(guò)復(fù)制調(diào)整大小

  實(shí)現(xiàn)可調(diào)整大小的緩沖區(qū)的第一種方法是從一個(gè)例如4KB的小緩沖區(qū)開(kāi)始。如果消息不能適應(yīng)4KB緩沖區(qū),則可以分配更大的緩沖區(qū),例如8KB,并將來(lái)自4KB緩沖區(qū)的數(shù)據(jù)復(fù)制到較大的緩沖區(qū)中。

通過(guò)復(fù)制調(diào)整大小的優(yōu)缺點(diǎn):

  優(yōu)點(diǎn):是消息的所有數(shù)據(jù)都保存在單個(gè)連續(xù)的字節(jié)數(shù)組中。這使得解析消息更容易。

  缺點(diǎn):是它會(huì)導(dǎo)致大量的數(shù)據(jù)復(fù)制用于更大的消息。

為了減少數(shù)據(jù)復(fù)制,您可以分析流過(guò)系統(tǒng)的消息的大小,以找到一些可以減少?gòu)?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ì)看到,一旦一個(gè)消息高于128KB,消息的大小就沒(méi)有真正的模式,所以也許最后的緩沖區(qū)大小應(yīng)該是最大的消息大小。

  根據(jù)流經(jīng)系統(tǒng)的消息大小,這3種緩沖區(qū)大小可以減少數(shù)據(jù)復(fù)制。4KB以下的訊息永遠(yuǎn)不會(huì)被復(fù)制。對(duì)于1.000.000并發(fā)連接,導(dǎo)致1.000.000 x 4KB = 4GB,這在今天(2015年)的大多數(shù)服務(wù)器中是可能的。4KB和128KB之間的消息將被復(fù)制一次,只有4KB數(shù)據(jù)將被復(fù)制到128KB緩沖區(qū)。128KB和最大郵件大小之間的郵件將被復(fù)制兩次。第一次4KB將被復(fù)制,第二次128KB將被復(fù)制,所以總共132KB復(fù)制最大的消息。假設(shè)沒(méi)有超過(guò)128KB的這么多信息可能是可以接受的。

  一旦消息完全處理完畢,分配的內(nèi)存應(yīng)該再次被釋放。這樣,從同一個(gè)連接接收到的下一條消息將以最小的緩沖區(qū)大小再次開(kāi)始。這是必要的,以確保在連接之間可以更有效地共享內(nèi)存。很可能并不是所有的連接都將同時(shí)需要大的緩沖區(qū)。

調(diào)整大小追加

  調(diào)整緩沖區(qū)大小的另一種方法是使緩沖區(qū)由多個(gè)數(shù)組組成。當(dāng)您需要調(diào)整緩沖區(qū)大小時(shí),您只需分配另一個(gè)字節(jié)數(shù)組并將數(shù)據(jù)寫入該數(shù)組。

  有兩種方式來(lái)增長(zhǎng)這樣一個(gè)緩沖區(qū)。一種方法是分配單獨(dú)的字節(jié)數(shù)組并保留這些字節(jié)數(shù)組的列表。另一種方法是分配較大的共享字節(jié)數(shù)組的片段,然后保留分配給緩沖區(qū)的片段的列表。就個(gè)人而言,我覺(jué)得切片方法稍好些,但差別很小。

  通過(guò)在其中附加單獨(dú)的數(shù)組或片來(lái)增加緩沖區(qū)的優(yōu)點(diǎn)是在寫入過(guò)程中不需要復(fù)制數(shù)據(jù)。所有數(shù)據(jù)可以直接從套接字(Channel)直接復(fù)制到數(shù)組或片中。

  以這種方式生長(zhǎng)緩沖區(qū)的缺點(diǎn)是數(shù)據(jù)不存儲(chǔ)在單個(gè)連續(xù)陣列中。這使得消息解析更加困難,因?yàn)榻馕銎餍枰瑫r(shí)查找每個(gè)單獨(dú)數(shù)組的末尾和所有數(shù)組的結(jié)束。由于您需要在寫入的數(shù)據(jù)中查找消息的結(jié)尾,所以該模型并不容易使用。

TLV編碼消息

  一些協(xié)議消息格式使用TLV格式(類型,長(zhǎng)度,值)進(jìn)行編碼。這意味著當(dāng)消息到達(dá)時(shí),消息的總長(zhǎng)度被存儲(chǔ)在消息的開(kāi)頭。這樣你就可以立即知道為整個(gè)消息分配多少內(nèi)存。

  TLV編碼使內(nèi)存管理更加容易。您立即知道要為消息分配多少內(nèi)存。只有部分使用的緩沖區(qū)結(jié)束時(shí)才會(huì)浪費(fèi)內(nèi)存。

  TLV編碼的一個(gè)缺點(diǎn)是在消息的所有數(shù)據(jù)到達(dá)之前分配消息的所有內(nèi)存。因此,發(fā)送大郵件的慢速連接可以分配所有可用的內(nèi)存,從而使您的服務(wù)器無(wú)響應(yīng)。

  此問(wèn)題的解決方法是使用包含多個(gè)TLV字段的消息格式。因此,為每個(gè)字段分配存儲(chǔ)器,而不是為整個(gè)消息分配存儲(chǔ)器,并且僅當(dāng)字段到達(dá)時(shí)才分配存儲(chǔ)器。然而,一個(gè)大的字段可以對(duì)你的內(nèi)存管理具有相同的效果,作為一個(gè)大的消息。

  另一個(gè)解決方法是超時(shí)在10-15秒內(nèi)未收到的消息。這可以使您的服務(wù)器從巧合,同時(shí)到達(dá)許多大消息恢復(fù),但仍會(huì)使服務(wù)器反應(yīng)遲一段時(shí)間。另外,有意的DoS(拒絕服務(wù))攻擊仍然可以為您的服務(wù)器完全分配內(nèi)存。

  TLV編碼存在不同的變體。正是使用了多少字節(jié),因此指定字段的類型和長(zhǎng)度取決于每個(gè)單獨(dú)的TLV編碼。還有TLV編碼首先放置字段的長(zhǎng)度,然后是類型,然后是值(一個(gè)LTV編碼)。雖然字段的順序不同,但它仍然是TLV變體。

  TLV編碼使內(nèi)存管理更容易的事實(shí)是HTTP 1.1是如此可怕的協(xié)議的原因之一。這是他們?cè)噲D在HTTP 2.0中修復(fù)數(shù)據(jù)的問(wèn)題之一,數(shù)據(jù)在LTV編碼幀中傳輸。這也是為什么我們?yōu)?nbsp;使用TLV編碼的VStack.co項(xiàng)目設(shè)計(jì)了我們自己的網(wǎng)絡(luò)協(xié)議。

寫部分消息

  在非阻塞IO管道中寫入數(shù)據(jù)也是一個(gè)挑戰(zhàn)。當(dāng)您 以非阻塞模式調(diào)用write(ByteBuffer)時(shí)Channel,不能保證ByteBuffer正在寫入的字節(jié)數(shù)。該write(ByteBuffer)方法返回寫入多少個(gè)字節(jié),因此可以跟蹤寫入的字節(jié)數(shù)。這就是挑戰(zhàn):跟蹤部分寫入的消息,以便最終發(fā)送一條消息的所有字節(jié)。

  為了管理部分消息的寫入,Channel我們將創(chuàng)建一個(gè)Message Writer。就像消息閱讀器一樣,我們將需要一個(gè)Message Writer,每個(gè)Channel我們寫信息。在每個(gè)Message Writer中,我們跟蹤正在寫入的消息的字節(jié)數(shù)。

  如果消息寫入器的更多消息到達(dá)可以直接寫入到消息寫入器Channel的消息,消息需要在消息寫入器內(nèi)部排隊(duì)。消息寫入器然后將消息盡可能快地寫入Channel。

  這是一個(gè)圖表,顯示了部分消息寫作到目前為止的設(shè)計(jì):

  要使Message Writer能夠發(fā)送僅部分早期發(fā)送的消息,則需要不時(shí)調(diào)用Message Writer,因此可以發(fā)送更多數(shù)據(jù)。

  如果你有很多的連接,你將會(huì)有很多的Message Writer實(shí)例。檢查例如一百萬(wàn)個(gè)Message Writer實(shí)例,看看他們是否可以寫任何數(shù)據(jù)都很慢。首先,許多Message Writer實(shí)例很多都沒(méi)有任何消息要發(fā)送。我們不想檢查那些Message Writer實(shí)例。其次,并不是所有的Channel 實(shí)例都可以準(zhǔn)備好寫入數(shù)據(jù)。我們不想浪費(fèi)時(shí)間嘗試將數(shù)據(jù)寫入Channel 不能接受任何數(shù)據(jù)的數(shù)據(jù)。

  要檢查是否Channel準(zhǔn)備好寫作,您可以使用a注冊(cè)頻道Selector。但是,我們不想用所有Channel實(shí)例注冊(cè)Selector。想象一下,如果您有1.000.000個(gè)連接,大多是空閑的,并且所有的1.000.000個(gè)連接都已注冊(cè)Selector。那么當(dāng)你打電話時(shí),select()大部分這些Channel 實(shí)例都會(huì)被寫好(他們大都閑著,記得嗎?)。然后,您必須檢查所有這些連接的Message Writer,以查看他們是否有任何數(shù)據(jù)要寫入。

為了避免檢查消息的所有Message Writer實(shí)例,以及所有Channel實(shí)例,無(wú)論如何都不會(huì)發(fā)送任何消息,我們使用這兩步的方法:

  1. 當(dāng)消息寫入消息寫入器時(shí),消息寫入器注冊(cè)它Channel 與Selector(如果尚未注冊(cè))相關(guān)聯(lián)。 

  2. 當(dāng)您的服務(wù)器有時(shí)間時(shí),它會(huì)檢查Selector哪些注冊(cè)的Channel 實(shí)例已準(zhǔn)備好進(jìn)行寫入。對(duì)于每個(gè)寫入就緒,Channel它的相關(guān)消息寫入器被請(qǐng)求寫入數(shù)據(jù)Channel。如果一個(gè)消息寫入器將其所有消息寫入其中ChannelChannel則從另一個(gè)消息 中注銷Selector。

這個(gè)小的兩步方法確保只有Channel具有寫入消息的實(shí)例實(shí)際上已被注冊(cè)Selector。

總結(jié): 

  非阻塞服務(wù)器需要不時(shí)檢查傳入的數(shù)據(jù),以查看是否收到新的完整消息。服務(wù)器可能需要多次檢查,直到收到一個(gè)或多個(gè)完整的消息。一次檢查是不夠的。

  同樣,非阻塞服務(wù)器需要不時(shí)檢查是否有任何數(shù)據(jù)要寫入。如果是,服務(wù)器需要檢查是否有任何相應(yīng)的連接準(zhǔn)備好將該數(shù)據(jù)寫入它們。只有在第一次排隊(duì)消息時(shí)才檢查是不夠的,因?yàn)橄⒖赡鼙徊糠謱懭搿?/p>

  所有這些非阻塞服務(wù)器最終都需要定期執(zhí)行的三個(gè)“管道”:

  • 讀取管道,用于從打開(kāi)的連接檢查新的傳入數(shù)據(jù)。

  • 處理任何收到的完整消息的進(jìn)程流程。

  • 寫入流水線檢查是否可以將任何傳出的消息寫入任何打開(kāi)的連接。

  這三條管道在循環(huán)中重復(fù)執(zhí)行。您可能可以稍微優(yōu)化執(zhí)行。例如,如果沒(méi)有排隊(duì)的消息可以跳過(guò)寫入管道。或者,如果我們沒(méi)有收到新的,完整的消息,也許您可以跳過(guò)流程管道。

  以下是說(shuō)明完整服務(wù)器循環(huán)的圖:

如果仍然發(fā)現(xiàn)這有點(diǎn)復(fù)雜,請(qǐng)記住查看GitHub資料庫(kù):

https://github.com/jjenkov/java-nio-server

服務(wù)器線程模型

GitHub存儲(chǔ)庫(kù)中的非阻塞服務(wù)器實(shí)現(xiàn)使用具有2個(gè)線程的線程模型。第一個(gè)線程接受來(lái)自a的傳入連接ServerSocketChannel。第二個(gè)線程處理接受的連接,意思是讀取消息,處理消息并將響應(yīng)寫回連接。這個(gè)2線程模型如下所示:

http://www.cnblogs.com/kuoAT/p/7018882.html