版權(quán)說(shuō)明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請(qǐng)進(jìn)行舉報(bào)或認(rèn)領(lǐng)
文檔簡(jiǎn)介
Netty4核心原理與手寫(xiě)RPC框架實(shí)戰(zhàn)與手寫(xiě)RPC框架實(shí)戰(zhàn)第1篇I/O基礎(chǔ)篇第1章網(wǎng)絡(luò)通信原理第2章JavaI/O演進(jìn)之路第1章網(wǎng)絡(luò)通信原理1.1網(wǎng)絡(luò)基礎(chǔ)架構(gòu)1.1.1C/S架構(gòu)有工作經(jīng)驗(yàn)的人都知道,C指的是Client(客戶端),S指的是Server(服務(wù)端),我們用Socket的目的就是實(shí)現(xiàn)C/S軟件架構(gòu)的服務(wù)端與客戶端之間的網(wǎng)絡(luò)通信。1.1.2C/S信息傳輸流程完成一次網(wǎng)絡(luò)通信,大致要經(jīng)過(guò)以下5個(gè)步驟。(1)客戶端產(chǎn)生數(shù)據(jù),存放于客戶端應(yīng)用的內(nèi)存中,然后調(diào)用接口將自己內(nèi)存中的數(shù)據(jù)發(fā)送/拷貝給操作系統(tǒng)內(nèi)存。(2)客戶端操作系統(tǒng)收到數(shù)據(jù)后,按照客戶端應(yīng)用指定的規(guī)則(即協(xié)議),調(diào)用網(wǎng)卡并發(fā)送數(shù)據(jù)。(3)網(wǎng)絡(luò)傳輸數(shù)據(jù)。(4)服務(wù)端應(yīng)用調(diào)用系統(tǒng)接口,想要將數(shù)據(jù)從操作系統(tǒng)內(nèi)存拷貝到自己的內(nèi)存中。(5)服務(wù)端操作系統(tǒng)收到指令后,使用與客戶端相同的規(guī)則(即協(xié)議)從網(wǎng)卡讀取數(shù)據(jù),然后拷貝給服務(wù)端應(yīng)用。1.2TCP/IP五層模型詳解計(jì)算機(jī)與計(jì)算機(jī)之間要有統(tǒng)一的連接標(biāo)準(zhǔn)才能夠完成相互通信,這個(gè)標(biāo)準(zhǔn)被稱為互聯(lián)網(wǎng)協(xié)議,而網(wǎng)絡(luò)就是物理鏈接介質(zhì)+互聯(lián)網(wǎng)協(xié)議。按照功能不同,人們將互聯(lián)網(wǎng)協(xié)議從不同維度分為OSI七層、TCP/IP五層或TCP/IP四層,如下圖所示。每層運(yùn)行的常見(jiàn)設(shè)備如下圖所示。1.2.1物理層物理層主要是基于電器特性發(fā)送高低電平信號(hào),電平即“電壓平臺(tái)”,指的是電路中某一點(diǎn)電壓的高低狀態(tài),在網(wǎng)絡(luò)信號(hào)中高電平用數(shù)字“1”表示,低電平用數(shù)字“0”表示。電平的高低是個(gè)相對(duì)概念,3V對(duì)于7V是低電平,但對(duì)于1V就是高電平。1.2.2數(shù)據(jù)鏈路層由于單純的電平信號(hào)“0”和“1”沒(méi)有任何意義,在實(shí)際應(yīng)用中,我們會(huì)將電平信號(hào)進(jìn)行分組處理,多少位一組、每組什么意思,這樣數(shù)據(jù)才有具體的含義。數(shù)據(jù)鏈路層的功能就是定義電平信號(hào)的分組方式。1.以太網(wǎng)協(xié)議數(shù)據(jù)鏈路層使用以太網(wǎng)協(xié)議進(jìn)行數(shù)據(jù)傳輸,基于MAC地址的廣播方式實(shí)現(xiàn)數(shù)據(jù)傳輸,只能在局域網(wǎng)內(nèi)廣播。早期各個(gè)公司都有自己的分組方式,后來(lái)形成了統(tǒng)一的標(biāo)準(zhǔn),即以太網(wǎng)協(xié)議Ethernet。2.Ethernet以太網(wǎng)由一組電平信號(hào)構(gòu)成一個(gè)數(shù)據(jù)包,叫作“幀”,每一數(shù)據(jù)幀由報(bào)頭Head和數(shù)Data兩部分組成,如下圖所示。Head:固定18字節(jié),其中發(fā)送者/源地址6字節(jié),接收者/目標(biāo)地址6字節(jié),數(shù)據(jù)類型6字節(jié)。Data:最短46字節(jié),最長(zhǎng)1500字節(jié)。數(shù)據(jù)包的具體內(nèi)容格式為:Head長(zhǎng)度+Data長(zhǎng)度=最短64字節(jié),最長(zhǎng)1518字節(jié)(超過(guò)最大限制就分片發(fā)送)。3.MAC地址Head中包含的源地址和目標(biāo)地址的由來(lái):Ethernet規(guī)定接入Internet的設(shè)備必須配有網(wǎng)卡,發(fā)送端和接收端的地址便是指網(wǎng)卡的地址,即MAC地址。MAC地址:每塊網(wǎng)卡出廠時(shí)都被印上一個(gè)世界唯一的MAC地址,它是一個(gè)長(zhǎng)度為48位的二進(jìn)制數(shù),通常用12位十六進(jìn)制數(shù)表示(前6位是廠商編號(hào),后6位是流水線號(hào))。4.Broadcast廣播有了MAC地址,同一網(wǎng)絡(luò)內(nèi)的兩臺(tái)主機(jī)就可以通信了(一臺(tái)主機(jī)通過(guò)ARP協(xié)議獲取另外一臺(tái)主機(jī)的MAC地址),下面是以太網(wǎng)通信數(shù)據(jù)幀的詳細(xì)示意圖。其實(shí)Ethernet采用非常原始的廣播方式進(jìn)行通信,也就是說(shuō)計(jì)算機(jī)之間的通信基本靠“吼”。例如,有多臺(tái)PC組成了一個(gè)網(wǎng)絡(luò),并通過(guò)硬件設(shè)施鏈接具備了通信條件,如下圖所示。上圖中,PC1按照固定協(xié)議格式以廣播的方式發(fā)送以太網(wǎng)包給PC4,然而,PC2、PC3、PC5都會(huì)收到PC1發(fā)來(lái)的數(shù)據(jù)包,拆開(kāi)后如果發(fā)現(xiàn)目標(biāo)MAC地址不是自己就會(huì)丟棄,如果是自己就響應(yīng)。1.2.3網(wǎng)絡(luò)層有了Ethernet、MAC地址、廣播的發(fā)送方式,世界上的計(jì)算機(jī)就可以彼此進(jìn)行通信了,問(wèn)題是世界范圍的互聯(lián)網(wǎng)是由一個(gè)個(gè)彼此隔離的小的局域網(wǎng)組成的(如下圖所示),如果所有的通信都采用以太網(wǎng)的廣播方式,那么一臺(tái)機(jī)器發(fā)送的數(shù)據(jù)包全世界都會(huì)收到,這就不僅僅是效率低的問(wèn)題了,這會(huì)是一種災(zāi)難。全世界的大網(wǎng)絡(luò)由一個(gè)個(gè)小的彼此隔離的局域網(wǎng)組成,以太網(wǎng)包只能在一個(gè)局域網(wǎng)內(nèi)發(fā)送,一個(gè)局域網(wǎng)是一個(gè)廣播域,跨廣播域通信只能通過(guò)路由轉(zhuǎn)發(fā)。由此得出結(jié)論:必須找出一種方法來(lái)區(qū)分哪些計(jì)算機(jī)屬于同一廣播域,哪些不是。如果是就采用廣播的方式發(fā)送,如果不是就采用路由的方式發(fā)送(向不同廣播域/子網(wǎng)分發(fā)數(shù)據(jù)包),MAC地址是無(wú)法區(qū)分的,它只跟廠商有關(guān),網(wǎng)絡(luò)層就是用來(lái)解決這一問(wèn)題的。網(wǎng)絡(luò)層的作用就是引入一套新的地址來(lái)區(qū)分不同的廣播域/子網(wǎng),這套地址叫作網(wǎng)絡(luò)地址。1.IP規(guī)定網(wǎng)絡(luò)地址的協(xié)議叫作IP(InternetProtocol,網(wǎng)際互聯(lián)協(xié)議),它定義的地址稱為IP地址。廣泛采用v4版本即IPv4,規(guī)定網(wǎng)絡(luò)地址由32位二進(jìn)制數(shù)表示。一個(gè)IP地址通常寫(xiě)成四段十進(jìn)制數(shù),例如,其取值范圍為:~55。IP地址由兩部分組成:網(wǎng)絡(luò)部分(用來(lái)標(biāo)識(shí)子網(wǎng))和主機(jī)部分(用來(lái)標(biāo)識(shí)主機(jī))。注意:?jiǎn)渭兊腎P地址段只是標(biāo)識(shí)了IP地址的種類,從網(wǎng)絡(luò)部分或主機(jī)部分都無(wú)法辨識(shí)一個(gè)IP地址所處的子網(wǎng)。例如,并不能確定與處于同一子網(wǎng)。因此,就需要子網(wǎng)掩碼。2.子網(wǎng)掩碼所謂“子網(wǎng)掩碼”,就是表示子網(wǎng)絡(luò)特征的一個(gè)參數(shù)。它在形式上等同于IP地址,也是一個(gè)32位二進(jìn)制數(shù)字,它的網(wǎng)絡(luò)部分全部為1,主機(jī)部分全部為0。比如,IP地址,如果已知網(wǎng)絡(luò)部分是前24位,主機(jī)部分是后8位,那么子網(wǎng)絡(luò)掩碼就是11111111.11111111.11111111.00000000,寫(xiě)成十進(jìn)制就是。我們根據(jù)“子網(wǎng)掩碼”就能判斷任意兩個(gè)IP地址是否處于同一個(gè)子網(wǎng)絡(luò)。方法是將兩個(gè)IP地址與子網(wǎng)掩碼分別進(jìn)行&運(yùn)算(兩個(gè)數(shù)位都為1,運(yùn)算結(jié)果為1,否則為0),然后比較結(jié)果是否相同,如果相同,就表明它們?cè)谕粋€(gè)子網(wǎng)絡(luò)中,否則就不在。比如,已知IP地址和的子網(wǎng)掩碼都是,請(qǐng)問(wèn)它們是否在同一個(gè)子網(wǎng)絡(luò)中?我們將二者IP地址與子網(wǎng)掩碼分別進(jìn)行&運(yùn)算,具體規(guī)則如下圖所示。運(yùn)算結(jié)果都是,因此它們?cè)谕粋€(gè)子網(wǎng)絡(luò)中??偨Y(jié)一下,IP的作用主要有兩個(gè),一個(gè)是為每一臺(tái)計(jì)算機(jī)分配IP地址,另一個(gè)是確定哪些地址在同一個(gè)子網(wǎng)絡(luò)中。3.IP數(shù)據(jù)包IP數(shù)據(jù)包也分為Head和Data兩部分,無(wú)須為IP數(shù)據(jù)包定義單獨(dú)的欄位,直接放入以太網(wǎng)包的Data部分即可。Head(IP頭部):長(zhǎng)度為20~60字節(jié)。Data(IP數(shù)據(jù)):最長(zhǎng)為65515字節(jié)。而以太網(wǎng)數(shù)據(jù)包的Data部分,最長(zhǎng)只有1500字節(jié)。因此,如果IP數(shù)據(jù)包超過(guò)1500字節(jié),它就需要分割成幾個(gè)以太網(wǎng)數(shù)據(jù)包,分開(kāi)發(fā)送。其具體結(jié)構(gòu)如下圖所示。4.ARP我們已經(jīng)知道計(jì)算機(jī)通信方式基本靠“吼”,也就是廣播的方式。所有上層的數(shù)據(jù)包到最后都要封裝到以太網(wǎng)頭,然后通過(guò)以太網(wǎng)協(xié)議發(fā)送。在談及以太網(wǎng)協(xié)議的時(shí)候,我們已經(jīng)了解到,通信基于MAC地址的廣播方式實(shí)現(xiàn)的,計(jì)算機(jī)在發(fā)送數(shù)據(jù)包時(shí),獲取自身的MAC地址是容易的,獲取目標(biāo)主機(jī)的MAC地址,需要通過(guò)ARP(AddressResolutionProtocol,地址解析協(xié)議)來(lái)實(shí)現(xiàn)。ARP用于實(shí)現(xiàn)從IP地址到MAC地址的映射,即詢問(wèn)目標(biāo)IP地址對(duì)應(yīng)的MAC地址,以廣播的方式發(fā)送數(shù)據(jù)包,獲取目標(biāo)主機(jī)的MAC地址。我們通過(guò)一個(gè)案例來(lái)說(shuō)明其具體通信原理,假設(shè)主機(jī)IP地址都已知。●主機(jī)A的IP地址為4,MAC地址為00:08:ca:xx:xx:xx;●主機(jī)B的IP地址為09,MAC地址為44:6d:57:xx:xx:xx。當(dāng)主機(jī)A要與主機(jī)B通信時(shí),ARP可以將主機(jī)B的IP地址(09)解析成主機(jī)B的MAC地址,以下為工作流程。第一步:通過(guò)IP地址和子網(wǎng)掩碼計(jì)算出自己所處的子網(wǎng),得出如下表所示的結(jié)果。第二步:分析主機(jī)A和B是否處于同一網(wǎng)絡(luò),如果不是同一網(wǎng)絡(luò),那么下表中目標(biāo)IP地址為09(訪問(wèn)路由器的路由表),通過(guò)ARP獲取的是網(wǎng)關(guān)的MAC地址。第三步:根據(jù)主機(jī)A上的路由表內(nèi)容,確定用于訪問(wèn)主機(jī)B的轉(zhuǎn)發(fā)IP地址是09。然后主機(jī)A在自己的本地ARP緩存中檢查主機(jī)B的匹配MAC地址。第四步:如果主機(jī)A在ARP緩存中沒(méi)有找到映射,它將詢問(wèn)4的硬件地址,從而將ARP請(qǐng)求幀廣播到本地網(wǎng)絡(luò)上的所有主機(jī)。源主機(jī)A的IP地址和MAC地址都包括在ARP請(qǐng)求中。本地網(wǎng)絡(luò)上的每臺(tái)主機(jī)都接收到ARP請(qǐng)求并且檢查是否與自己的IP地址匹配。如果主機(jī)發(fā)現(xiàn)請(qǐng)求的IP地址與自己的IP地址不匹配,它將丟棄ARP請(qǐng)求。第五步:主機(jī)B確定ARP請(qǐng)求中的IP地址與自己的IP地址匹配,將主機(jī)A的IP地址和MAC地址映射添加到本地ARP緩存中。第六步:主機(jī)B將包含其MAC地址的ARP回復(fù)消息直接發(fā)送回主機(jī)A。第七步:當(dāng)主機(jī)A接收到從主機(jī)B發(fā)來(lái)的ARP回復(fù)消息時(shí),會(huì)用主機(jī)B的IP地址和MAC地址映射更新ARP緩存。本機(jī)緩存是有生存期的,生存期結(jié)束后,將再次重復(fù)上面的過(guò)程。主機(jī)B的MAC地址一旦確定,主機(jī)A就能向主機(jī)B發(fā)送IP地址了。為了讓大家更好地理解ARP以及廣播和單播的概念,我們可以利用網(wǎng)絡(luò)抓包工具Wireshark來(lái)看一下抓取到的真實(shí)網(wǎng)絡(luò)中的ARP過(guò)程,通過(guò)數(shù)據(jù)包的方式來(lái)呈現(xiàn),部分MAC地址隱藏部分用xx代替。假如,主機(jī)A?主機(jī)B通信:●主機(jī)A:IP地址為4,MAC地址為00:08:ca:xx:xx:xx?!裰鳈C(jī)B:IP地址為09,MAC地址為44:6d:57:xx:xx:xx。ARP請(qǐng)求數(shù)據(jù)包的內(nèi)容如下。上面請(qǐng)求數(shù)據(jù)的含義是,我是主機(jī)A,IP地址為4,MAC地址為00:08:ca:xx:xx:xx。請(qǐng)問(wèn)主機(jī)B,IP地址為09,你的MAC地址是多少?00:00:00_00:00:00是置空位(留坑),表示詢問(wèn)者不知道,等待接收方回應(yīng)(填坑)。ARP回應(yīng)數(shù)據(jù)包的內(nèi)容如下。上面請(qǐng)求數(shù)據(jù)的含義是,我是主機(jī)B,IP地址為09,MAC地址為44:6d:57:xx:xx:xx。你好,主機(jī)A,IP地址為4,MAC地址為00:08:ca:xx:xx:xx。下表是ARP中每個(gè)屬性的具體含義詳解。1.2.4傳輸層現(xiàn)在我們已經(jīng)知道,網(wǎng)絡(luò)層的IP地址幫我們區(qū)分子網(wǎng),以太網(wǎng)層的MAC地址幫我們找到主機(jī)。大家使用的都是應(yīng)用程序,你的計(jì)算機(jī)上可能同時(shí)開(kāi)啟QQ、微信等多個(gè)應(yīng)用程序,那么我們通過(guò)IP地址和MAC地址找到了一臺(tái)特定的主機(jī),如何標(biāo)識(shí)這臺(tái)主機(jī)上的應(yīng)用程序?答案就是端口,端口就是應(yīng)用程序與網(wǎng)卡關(guān)聯(lián)的編號(hào)。那么傳輸層就是用來(lái)建立端口到端口的通信機(jī)制的。補(bǔ)充:主機(jī)端口的取值范圍為0~65535,其中0~1023為系統(tǒng)保留端口的取值范圍,也叫作BSD保留端口。用戶可注冊(cè)端口的取值范圍為1024~49152,還有隨機(jī)動(dòng)態(tài)端口的取值范圍為49152~65535。為什么取值范圍只能為0~65535,多一個(gè)都不行?從協(xié)議來(lái)講,在TCP頭部留給存儲(chǔ)端口的空間只有2字節(jié),最大值就是65535。1.TCPTCP(TransmissionControlProtocol,傳輸控制協(xié)議)是一種可靠傳輸協(xié)議,TCP數(shù)據(jù)包沒(méi)有長(zhǎng)度限制,理論上可以無(wú)限長(zhǎng),但是為了保證網(wǎng)絡(luò)的效率,通常TCP數(shù)據(jù)包的長(zhǎng)度不會(huì)超過(guò)IP數(shù)據(jù)包的長(zhǎng)度,以確保單個(gè)TCP數(shù)據(jù)包不必再分割,其數(shù)據(jù)結(jié)構(gòu)如下圖所示。2.UDPUDP(UserDatagramProtocol,用戶數(shù)據(jù)報(bào)協(xié)議)是一種不可靠傳輸協(xié)議,“報(bào)頭”部分總共有8字節(jié),總長(zhǎng)度不超過(guò)65535字節(jié),正好放進(jìn)一個(gè)IP數(shù)據(jù)包,其數(shù)據(jù)結(jié)構(gòu)如下圖所示。3.TCP報(bào)文結(jié)構(gòu)TCP報(bào)文是TCP層傳輸?shù)臄?shù)據(jù)單元,也叫作報(bào)文段。TCP報(bào)文結(jié)構(gòu)如下圖所示。下面對(duì)報(bào)文內(nèi)容做詳細(xì)介紹。(1)端口號(hào):用來(lái)標(biāo)識(shí)同一臺(tái)計(jì)算機(jī)的不同應(yīng)用進(jìn)程?!裨炊丝冢涸炊丝诤虸P地址的作用是標(biāo)識(shí)報(bào)文的返回地址?!衲康亩丝冢耗康亩丝谥该鹘邮辗接?jì)算機(jī)上的應(yīng)用程序接口。TCP報(bào)頭中的源端口號(hào)和目的端口號(hào)同IP數(shù)據(jù)包中的源IP地址和目的IP地址唯一確定一條TCP連接。(2)序號(hào)和確認(rèn)號(hào):TCP可靠傳輸?shù)年P(guān)鍵部分。序號(hào)是本報(bào)文段發(fā)送的數(shù)據(jù)組的第一個(gè)字節(jié)的序號(hào)。在TCP傳送的流中,每一個(gè)字節(jié)都有一個(gè)序號(hào)。例如:一個(gè)報(bào)文段的序號(hào)為300,此報(bào)文段數(shù)據(jù)部分共有100字節(jié),則下一個(gè)報(bào)文段的序號(hào)為400。所以序號(hào)確保了TCP傳輸?shù)挠行蛐?。確認(rèn)號(hào),即ACK,指明下一個(gè)期待收到的字節(jié)序號(hào),表明該序號(hào)之前的所有數(shù)據(jù)已經(jīng)正確無(wú)誤地收到。確認(rèn)號(hào)只有當(dāng)ACK標(biāo)志為1時(shí)才有效。比如建立連接時(shí),SYN報(bào)文的ACK標(biāo)志為0。(3)數(shù)據(jù)偏移/頭部長(zhǎng)度:4位。由于頭部可能含有可選項(xiàng)內(nèi)容,TCP報(bào)頭的長(zhǎng)度是不確定的,報(bào)頭不包含任何任選屬性則長(zhǎng)度為20字節(jié),4位頭部長(zhǎng)度屬性所能表示的最大值為1111,轉(zhuǎn)化成十進(jìn)制為15,15×32/8=60,故報(bào)頭最大長(zhǎng)度為60字節(jié)。頭部長(zhǎng)度也叫數(shù)據(jù)偏移,是因?yàn)轭^部長(zhǎng)度實(shí)際上指示了數(shù)據(jù)區(qū)在報(bào)文段中的起始偏移值。(4)保留:為將來(lái)定義新的用途保留,現(xiàn)在一般設(shè)置為0。(5)標(biāo)志位:URG、ACK、PSH、RST、SYN、FIN,共6個(gè),每一個(gè)標(biāo)志位都表示一個(gè)控制功能,具體含義如下表所示。(6)窗口:滑動(dòng)窗口大小,用來(lái)告知發(fā)送端接收端的緩存大小,以此控制發(fā)送端發(fā)送數(shù)據(jù)的速率,從而達(dá)到流量控制。窗口大小是一個(gè)16位屬性,因而窗口大小最大為65535。(7)校驗(yàn)和:奇偶校驗(yàn),此校驗(yàn)和針對(duì)整個(gè)TCP報(bào)文段,包括TCP頭部和TCP數(shù)據(jù),以16位屬性進(jìn)行計(jì)算所得。由發(fā)送端計(jì)算和存儲(chǔ),并由接收端進(jìn)行驗(yàn)證。(8)緊急指針:只有當(dāng)URG標(biāo)志為1時(shí)緊急指針才有效。緊急指針是一個(gè)正的偏移量,和順序號(hào)屬性中的值相加表示緊急數(shù)據(jù)最后一個(gè)字節(jié)的序號(hào)。TCP的緊急方式是發(fā)送端向另一端發(fā)送緊急數(shù)據(jù)的一種方式。(9)選項(xiàng)和填充:最常見(jiàn)的可選屬性是最長(zhǎng)報(bào)文大小,又稱為MSS(MaximumSegmentSize),每個(gè)連接方通常都在通信的第一個(gè)報(bào)文段(為建立連接而設(shè)置SYN標(biāo)志為1的那個(gè)段)中指明這個(gè)選項(xiàng),它表示本端所能接收的最大報(bào)文段的長(zhǎng)度。選項(xiàng)長(zhǎng)度不一定是32位的整數(shù)倍,所以要加填充位,即在這個(gè)屬性中加入額外的零,以保證TCP頭部長(zhǎng)度是32位的整數(shù)倍。(10)數(shù)據(jù)部分:TCP報(bào)文段中的數(shù)據(jù)部分是可選的。在一個(gè)連接建立和一個(gè)連接終止時(shí),雙方交換的報(bào)文段僅有TCP頭部。如果一方?jīng)]有數(shù)據(jù)要發(fā)送,也使用沒(méi)有任何數(shù)據(jù)的頭部來(lái)確認(rèn)收到的數(shù)據(jù)。在處理超時(shí)的許多情況中,也會(huì)發(fā)送不帶任何數(shù)據(jù)的報(bào)文段。4.TCP交互流程傳輸連接包括三個(gè)階段:連接建立、數(shù)據(jù)傳送和連接釋放。傳輸連接管理就是對(duì)連接建立和連接釋放過(guò)程的管控,使其能正常運(yùn)行,以達(dá)到這些目的:使通信雙方能夠確知對(duì)方的存在、可以允許通信雙方協(xié)商一些參數(shù)(最大報(bào)文段長(zhǎng)度、最大窗口大小等)、能夠?qū)\(yùn)輸實(shí)體資源進(jìn)行分配(緩存大小等)。TCP連接的建立采用客戶端-服務(wù)器模式:主動(dòng)發(fā)起連接建立的應(yīng)用進(jìn)程叫作客戶端,被動(dòng)等待連接建立的應(yīng)用進(jìn)程叫作服務(wù)器。接下來(lái),介紹TCP完成數(shù)據(jù)傳輸?shù)娜挝帐趾退拇螕]手的詳細(xì)過(guò)程。第一次握手:建立連接時(shí),客戶端發(fā)送SYN包(syn=1)到服務(wù)器,并進(jìn)入SYN_SENT狀態(tài),等待服務(wù)器確認(rèn)。第二次握手:服務(wù)器收到SYN包,必須確認(rèn)客戶端的SYN包(ack=x+1),同時(shí)自己也發(fā)送一個(gè)SYN包(syn=1),即SYN+ACK包,此時(shí)服務(wù)器進(jìn)入SYN_RECV狀態(tài)。第三次握手:客戶端收到服務(wù)器的SYN+ACK包,向服務(wù)器發(fā)送確認(rèn)包ACK(ack=y+1),此包發(fā)送完畢,客戶端和服務(wù)器進(jìn)入ESTABLISHED(TCP連接成功)狀態(tài),完成三次握手。至此,TCP連接就建立了,客戶端和服務(wù)器可以愉快地“玩?!绷?。只要通信雙方?jīng)]有一方發(fā)出連接釋放的請(qǐng)求,連接就將一直保持。如果有一方釋放連接,就會(huì)發(fā)起揮手操作。第一次揮手:客戶端進(jìn)程發(fā)出連接釋放報(bào)文,并且停止發(fā)送數(shù)據(jù)。釋放數(shù)據(jù)報(bào)文頭部,F(xiàn)IN=1,其序列號(hào)為seq=u(等于前面已經(jīng)傳送過(guò)來(lái)的數(shù)據(jù)的最后一個(gè)字節(jié)的序號(hào)加1),此時(shí),客戶端進(jìn)入FIN_WAIT_1(終止等待1)狀態(tài)。TCP規(guī)定,F(xiàn)IN報(bào)文段即使不攜帶數(shù)據(jù),也要消耗一個(gè)序號(hào)。第二次揮手:服務(wù)器收到連接釋放報(bào)文,發(fā)出確認(rèn)報(bào)文,ACK=1,ack=u+1,并且?guī)献约旱男蛄刑?hào)seq=v,此時(shí),服務(wù)器就進(jìn)入了CLOSE_WAIT(關(guān)閉等待)狀態(tài)。TCP服務(wù)器通知高層的應(yīng)用進(jìn)程,客戶端向服務(wù)器方向的連接就被釋放了,這時(shí)候處于半關(guān)閉狀態(tài),即客戶端已經(jīng)沒(méi)有數(shù)據(jù)要發(fā)送了,但是服務(wù)器若發(fā)送數(shù)據(jù),客戶端依然要接收。這個(gè)狀態(tài)還要持續(xù)一段時(shí)間,也就是整個(gè)CLOSE_WAIT狀態(tài)持續(xù)的時(shí)間??蛻舳耸盏椒?wù)器的確認(rèn)請(qǐng)求后,客戶端就進(jìn)入FIN_WAIT_2(終止等待2)狀態(tài),等待服務(wù)器發(fā)送連接釋放報(bào)文(在這之前還需要接收服務(wù)器發(fā)送的最后的數(shù)據(jù))。第三次揮手:服務(wù)器將最后的數(shù)據(jù)發(fā)送完畢后,就向客戶端發(fā)送連接釋放報(bào)文,F(xiàn)IN=1,ack=u+1,由于在半關(guān)閉狀態(tài),服務(wù)器很可能又發(fā)送了一些數(shù)據(jù),假定此時(shí)的序列號(hào)為seq=w,此時(shí),服務(wù)器就進(jìn)入了LAST_ACK(最后確認(rèn))狀態(tài),等待客戶端的確認(rèn)。第四次揮手:客戶端收到服務(wù)器的連接釋放報(bào)文后,必須發(fā)出確認(rèn),ACK=1,ack=w+1,而自己的序列號(hào)是seq=u+1,此時(shí),客戶端就進(jìn)入了TIME_WAIT(時(shí)間等待)狀態(tài)。注意此時(shí)TCP連接還沒(méi)有釋放,必須經(jīng)過(guò)2×MSL(最長(zhǎng)報(bào)文段壽命)的時(shí)間,當(dāng)客戶端撤銷相應(yīng)的TCB(TransmitControlBlock,傳輸控制模塊)后,才進(jìn)入CLOSED狀態(tài)。最后,服務(wù)器只要收到了客戶端發(fā)出的確認(rèn),就立即進(jìn)入CLOSED狀態(tài)。同樣,撤銷TCB后,就結(jié)束了這次TCP連接??梢钥吹剑?wù)器結(jié)束TCP連接的時(shí)間要比客戶端早一些。TCP交互的詳細(xì)過(guò)程如下圖所示。1.2.5應(yīng)用層在日常操作中,用戶使用的都是應(yīng)用程序,應(yīng)用程序都工作在應(yīng)用層?;ヂ?lián)網(wǎng)是開(kāi)放的,大家都可以開(kāi)發(fā)自己的應(yīng)用程序,數(shù)據(jù)多種多樣,必須規(guī)定好數(shù)據(jù)的組織形式。應(yīng)用層的功能就是規(guī)定應(yīng)用程序的數(shù)據(jù)格式。例如:TCP可以為各種各樣的程序傳遞數(shù)據(jù),比如SMTP、HTTP、FTP、POP3等,那么,必須有不同的協(xié)議規(guī)定電子郵件、網(wǎng)頁(yè)、FTP數(shù)據(jù)的格式,這些應(yīng)用程序協(xié)議就構(gòu)成了“應(yīng)用層”。如下圖所示是應(yīng)用層協(xié)議的基本組成結(jié)構(gòu)示意。用戶從應(yīng)用程序中發(fā)送數(shù)據(jù)是一個(gè)對(duì)數(shù)據(jù)封裝的過(guò)程,而接收數(shù)據(jù)則是一個(gè)解封裝的動(dòng)作。下面簡(jiǎn)單介紹一下常用的應(yīng)用層協(xié)議。1.DNS協(xié)議DNS是英文DomainNameSystem(域名系統(tǒng))的縮寫(xiě),用來(lái)把便于人們使用的機(jī)器名字轉(zhuǎn)換為IP地址?,F(xiàn)在頂級(jí)域名TLD(TotelLeadDomination)分為三大類:國(guó)家頂級(jí)域名nTLD、通用頂級(jí)域名gTLD和基礎(chǔ)結(jié)構(gòu)域名。域名服務(wù)器分為四種類型:根域名服務(wù)器、頂級(jí)域名服務(wù)器、本地域名服務(wù)器和權(quán)限域名服務(wù)器。DNS使用TCP和UDP端口53。當(dāng)前,對(duì)于每一級(jí)域名長(zhǎng)度的限制是63個(gè)字符,域名總長(zhǎng)度則不能超過(guò)253個(gè)字符。2.HTTPHTTP(HyperTextTransferProtocol,超文本傳輸協(xié)議)是面向事務(wù)的應(yīng)用層協(xié)議。它是互聯(lián)網(wǎng)上能夠可靠地交換信息的重要基礎(chǔ)。HTTP使用面向連接的TCP作為運(yùn)輸層協(xié)議,保證了數(shù)據(jù)的可靠傳輸。3.FTPFTP(FileTransferProtocol,文件傳輸協(xié)議)是互聯(lián)網(wǎng)上使用最廣泛的文件傳送協(xié)議。FTP提供交互式的訪問(wèn),允許客戶指明文件類型與格式,并允許文件具有存取權(quán)限。FTP基于TCP工作。4.SMTPSMTP(SimpleMailTransferProtocol,簡(jiǎn)單郵件傳輸協(xié)議)規(guī)定了在兩個(gè)相互通信的SMTP進(jìn)程之間應(yīng)如何交換信息。SMTP通信的三個(gè)階段:建立連接、郵件傳送和連接釋放。5.POP3POP3(PostOfficeProtocol3,郵件讀取協(xié)議)通常被用來(lái)接收電子郵件。6.Telnet協(xié)議Telnet協(xié)議是一個(gè)簡(jiǎn)單的遠(yuǎn)程終端協(xié)議,也是互聯(lián)網(wǎng)的正式標(biāo)準(zhǔn),又稱為終端仿真協(xié)議。1.2.6小結(jié)總結(jié)一下OSI七層模型,它為開(kāi)放互聯(lián)信息系統(tǒng)提供了一種結(jié)構(gòu)框架。建立七層模型的主要目的是解決異種網(wǎng)絡(luò)互聯(lián)時(shí)所遇到的兼容性問(wèn)題。它的最大優(yōu)點(diǎn)是將服務(wù)、接口和協(xié)議這三個(gè)概念明確地區(qū)分開(kāi)來(lái):服務(wù)說(shuō)明某一層為上一層提供一些什么功能,接口說(shuō)明上一層如何使用下一層的服務(wù),而協(xié)議涉及如何實(shí)現(xiàn)本層的服務(wù);這樣各層之間具有很強(qiáng)的獨(dú)立性,互聯(lián)網(wǎng)絡(luò)中各實(shí)體采用什么樣的協(xié)議是沒(méi)有限制的,只要向上提供相同的服務(wù)并且不改變相鄰層的接口就可以。其詳細(xì)結(jié)構(gòu)如下圖所示。1.3網(wǎng)絡(luò)通信實(shí)現(xiàn)原理要想實(shí)現(xiàn)網(wǎng)絡(luò)通信,每臺(tái)主機(jī)需具備四要素:本機(jī)的IP地址、子網(wǎng)掩碼、網(wǎng)關(guān)的IP地址和DNS的IP地址。獲取這四要素有兩種方式:一是靜態(tài)獲取,即手動(dòng)配置;二是動(dòng)態(tài)獲取,即通過(guò)DHCP(DynamicHostConfigurationProtocol,動(dòng)態(tài)主機(jī)配置協(xié)議)獲取。下圖是網(wǎng)絡(luò)通信的數(shù)據(jù)結(jié)構(gòu)示意圖。我們來(lái)詳細(xì)分析一下網(wǎng)絡(luò)通信的交互過(guò)程。(1)最前面的“以太網(wǎng)頭部”,設(shè)置發(fā)出方(本機(jī))的MAC地址和接收方(DHCP服務(wù)器)的MAC地址。前者就是本機(jī)網(wǎng)卡的MAC地址,后者這時(shí)不知道,就填入一個(gè)廣播地址:FF-FF-FF-FF-FF-FF。(2)后面的“IP頭部”,設(shè)置發(fā)出方的IP地址和接收方的IP地址。這時(shí),對(duì)于這兩者,本機(jī)都不知道。于是,發(fā)出方的IP地址就設(shè)為,接收方的IP地址設(shè)為55。(3)最后的“UDP頭部”,設(shè)置發(fā)出方的端口和接收方的端口。這一部分是DHCP規(guī)定好的,發(fā)出方是68端口,接收方是67端口。這個(gè)數(shù)據(jù)包構(gòu)造完成后,就可以發(fā)出了。以太網(wǎng)是廣播發(fā)送的,同一個(gè)子網(wǎng)的每臺(tái)計(jì)算機(jī)都收到了這個(gè)數(shù)據(jù)包。因?yàn)榻邮辗降腗AC地址是FF-FF-FF-FF-FF-FF,看不出是發(fā)給誰(shuí)的,所以每臺(tái)收到這個(gè)數(shù)據(jù)包的計(jì)算機(jī),還必須分析這個(gè)數(shù)據(jù)包的IP地址,才能確定是不是發(fā)給自己的??吹桨l(fā)出方IP地址是,接收方IP地址是55,于是DHCP服務(wù)器知道“這個(gè)數(shù)據(jù)包是發(fā)給我的”,而其他計(jì)算機(jī)就可以丟棄這個(gè)數(shù)據(jù)包。接下來(lái),DHCP服務(wù)器讀出這個(gè)數(shù)據(jù)包的數(shù)據(jù)內(nèi)容,分配好IP地址,發(fā)送回去一個(gè)“DHCP響應(yīng)”數(shù)據(jù)包。這個(gè)響應(yīng)包的結(jié)構(gòu)也是類似的,以太網(wǎng)頭部的MAC地址是雙方的網(wǎng)卡地址,IP頭部的IP地址是DHCP服務(wù)器的IP地址(發(fā)出方)和55(接收方),UDP頭部的端口是67(發(fā)出方)和68(接收方),分配給請(qǐng)求端的IP地址和本網(wǎng)絡(luò)的具體參數(shù)則包含在Data部分。新加入的計(jì)算機(jī)收到這個(gè)響應(yīng)包,于是就知道了自己的IP地址、子網(wǎng)掩碼、網(wǎng)關(guān)地址、DNS服務(wù)器等參數(shù)。1.4向?yàn)g覽器輸入U(xiǎn)RL后發(fā)生了什么當(dāng)在瀏覽器地址欄中輸入網(wǎng)址后,瀏覽器是怎么把最終的頁(yè)面呈現(xiàn)出來(lái)的呢?這個(gè)過(guò)程大致可以分為兩個(gè)部分:網(wǎng)絡(luò)通信和頁(yè)面渲染。下面詳細(xì)分析完整的通信過(guò)程。第一步,本機(jī)設(shè)置以下信息。第二步,打開(kāi)瀏覽器,想要訪問(wèn)咕泡官網(wǎng),在地址欄中輸入網(wǎng)址。第三步,通過(guò)訪問(wèn)DNS域名系統(tǒng)服務(wù)器(基于UDP)獲得IP地址。下圖完整地說(shuō)明了一次網(wǎng)絡(luò)請(qǐng)求如何獲取目標(biāo)服務(wù)器IP地址的全過(guò)程。圖中13臺(tái)國(guó)際DNS根服務(wù)器IP地址具體如下。通過(guò)域名尋找到目標(biāo)機(jī)器所在位置。下面簡(jiǎn)單科普一下域名知識(shí),域名有頂級(jí)域名和二級(jí)域名。頂級(jí)域名如.com、.net、.org、.cn等屬于國(guó)際頂級(jí)域名。根據(jù)目前的國(guó)際互聯(lián)網(wǎng)域名體系,國(guó)際頂級(jí)域名分為兩類:類別頂級(jí)域名(gTLD)和地理頂級(jí)域名(ccTLD)。類別頂級(jí)域名是以“com”“net”“org”“biz”“info”等結(jié)尾的域名,均由國(guó)外公司負(fù)責(zé)管理。地理頂級(jí)域名是以國(guó)家或地區(qū)代碼為結(jié)尾的域名,如“cn”代表中國(guó),“uk”代表英國(guó)。地理頂級(jí)域名一般由各個(gè)國(guó)家或地區(qū)負(fù)責(zé)管理。在不同的地域還會(huì)使用二級(jí)域名,二級(jí)域名是以頂級(jí)域名為基礎(chǔ)的地理域名,例如中國(guó)的二級(jí)域有.、.、.、.等。在實(shí)際的網(wǎng)站應(yīng)用中,通常會(huì)使用子域名。比如父域名是,子域名就是或者*.。一般來(lái)說(shuō),子域名是域名的一條記錄,比如是一個(gè)域名,是其中比較常用的記錄,一般默認(rèn)類似*.的域名全部被稱作的子域名。第四步,向目標(biāo)機(jī)器發(fā)起HTTP請(qǐng)求,獲得如下格式的數(shù)據(jù)內(nèi)容。我們假定這個(gè)部分的長(zhǎng)度為4960字節(jié),它會(huì)被嵌在TCP數(shù)據(jù)包中。第五步,TCP。TCP數(shù)據(jù)包需要設(shè)置端口,接收方(咕泡官網(wǎng))的HTTP端口默認(rèn)是80,發(fā)送方(本機(jī))的端口是一個(gè)隨機(jī)生成的1024~65535之間的整數(shù),假定為51775。TCP數(shù)據(jù)包的頭部長(zhǎng)度為20字節(jié),加上嵌入HTTP的數(shù)據(jù)包,總長(zhǎng)度變?yōu)?980字節(jié)。第六步,IP。TCP數(shù)據(jù)包再嵌入IP數(shù)據(jù)包。IP數(shù)據(jù)包需要設(shè)置雙方的IP地址,這是已知的。IP數(shù)據(jù)包的頭部長(zhǎng)度為20字節(jié),加上嵌入的TCP數(shù)據(jù)包,總長(zhǎng)度變?yōu)?000字節(jié)。第七步,以太網(wǎng)協(xié)議。IP數(shù)據(jù)包嵌入以太網(wǎng)數(shù)據(jù)包。以太網(wǎng)數(shù)據(jù)包需要設(shè)置雙方的MAC地址,發(fā)送方為本機(jī)的網(wǎng)卡MAC地址,接收方為網(wǎng)關(guān)的MAC地址(通過(guò)ARP得到)。以太網(wǎng)數(shù)據(jù)包的數(shù)據(jù)部分最大長(zhǎng)度為1500字節(jié),而現(xiàn)在的IP數(shù)據(jù)包長(zhǎng)度為5000字節(jié)。因此,IP數(shù)據(jù)包必須分割成四個(gè)包。因?yàn)槊總€(gè)包都有自己的IP頭部(20字節(jié)),所以四個(gè)包的長(zhǎng)度分別為1500字節(jié)、1500字節(jié)、1500字節(jié)、560字節(jié)。如下圖所示是以太網(wǎng)數(shù)據(jù)包示意圖。第八步,服務(wù)器響應(yīng)。經(jīng)過(guò)多個(gè)網(wǎng)關(guān)的轉(zhuǎn)發(fā),咕泡官網(wǎng)的服務(wù)器收到了這四個(gè)以太網(wǎng)數(shù)據(jù)包。根據(jù)IP頭部的序號(hào),咕泡官網(wǎng)將四個(gè)包拼起來(lái),取出完整的TCP數(shù)據(jù)包,然后讀出里面的“HTTP請(qǐng)求”,接著做出“HTTP響應(yīng)”,再用TCP發(fā)回來(lái)。本機(jī)收到HTTP響應(yīng)以后,就可以將網(wǎng)頁(yè)顯示出來(lái),完成一次網(wǎng)絡(luò)通信。1.5網(wǎng)絡(luò)通信之“魂”——Socket我們知道兩個(gè)進(jìn)程進(jìn)行通信一個(gè)最基本的前提是能夠唯一地標(biāo)識(shí)一個(gè)進(jìn)程。在本地進(jìn)程通信中,我們可以使用PID來(lái)唯一標(biāo)識(shí)一個(gè)進(jìn)程,但PID只在本地唯一,網(wǎng)絡(luò)中的兩個(gè)進(jìn)程PID沖突概率很大,這時(shí)候就需要另辟蹊徑了。我們知道IP層的IP地址可以唯一標(biāo)識(shí)主機(jī),而TCP層的協(xié)議和端口號(hào)可以唯一標(biāo)識(shí)主機(jī)的一個(gè)進(jìn)程,可以用IP地址+協(xié)議+端口號(hào)唯一標(biāo)識(shí)網(wǎng)絡(luò)中的一個(gè)進(jìn)程。能夠唯一標(biāo)識(shí)網(wǎng)絡(luò)中的進(jìn)程后,它們就可以利用Socket進(jìn)行通信了。那么,什么是Socket呢?我們經(jīng)常把Socket翻譯為套接字,Socket是在應(yīng)用層和傳輸層之間的一個(gè)抽象層,它把TCP/IP層復(fù)雜的操作抽象為幾個(gè)簡(jiǎn)單的接口供應(yīng)用層調(diào)用,以實(shí)現(xiàn)進(jìn)程在網(wǎng)絡(luò)中的通信,具體結(jié)構(gòu)如下圖所示。Socket起源于UNIX,在UNIX“一切皆文件”的哲學(xué)思想下,Socket是一種從打開(kāi),到完成讀、寫(xiě)操作,最后關(guān)閉的模式,服務(wù)器和客戶端各自維護(hù)一個(gè)“文件”,在建立連接打開(kāi)文件后,可以向自己的文件寫(xiě)入內(nèi)容供對(duì)方讀取或者讀取對(duì)方的內(nèi)容,通信結(jié)束時(shí)關(guān)閉文件。第2章JavaI/O演進(jìn)之路2.1I/O的問(wèn)世2.1.1什么是I/O我們都知道在UNIX世界里一切皆文件,而文件是什么呢?文件就是一串二進(jìn)制流而已,其實(shí)不管是Socket,還是FIFO(FirstInputFirstOutput,先進(jìn)先出隊(duì)列))、管道、終端。對(duì)計(jì)算機(jī)來(lái)說(shuō),一切都是文件,一切都是流。在信息交換的過(guò)程中,計(jì)算機(jī)都是對(duì)這些流進(jìn)行數(shù)據(jù)的收發(fā)操作,簡(jiǎn)稱為I/O操作(InputandOutput),包括往流中讀出數(shù)據(jù)、系統(tǒng)調(diào)用Read、寫(xiě)入數(shù)據(jù)、系統(tǒng)調(diào)用Write。不過(guò)計(jì)算機(jī)里有那么多流,怎么知道要操作哪個(gè)流呢?實(shí)際上是由操作系統(tǒng)內(nèi)核創(chuàng)建文件描述符(FileDescriptor,F(xiàn)D)來(lái)標(biāo)識(shí)的,一個(gè)FD就是一個(gè)非負(fù)整數(shù),所以對(duì)這個(gè)整數(shù)的操作就是對(duì)這個(gè)文件(流)的操作。我們創(chuàng)建一個(gè)Socket,通過(guò)系統(tǒng)調(diào)用會(huì)返回一個(gè)FD,那么剩下的對(duì)Socket的操作就會(huì)轉(zhuǎn)化為對(duì)這個(gè)描述符的操作,這又是一種分層和抽象的思想。2.1.2I/O交互流程通常用戶進(jìn)程中的一次完整I/O交互流程分為兩階段,首先是經(jīng)過(guò)內(nèi)核空間,也就是由操作系統(tǒng)處理;緊接著就是到用戶空間,也就是交由應(yīng)用程序。具體交互流程如下圖所示。內(nèi)核空間中存放的是內(nèi)核代碼和數(shù)據(jù),而進(jìn)程的用戶空間中存放的是用戶程序的代碼和數(shù)據(jù)。不管是內(nèi)核空間還是用戶空間,它們都處于虛擬空間中,Linux使用兩級(jí)保護(hù)機(jī)制:0級(jí)供內(nèi)核(Kernel)使用,3級(jí)供用戶程序使用。每個(gè)進(jìn)程都有各自的私有用戶空間(0~3G),這個(gè)空間對(duì)系統(tǒng)中的其他進(jìn)程是不可見(jiàn)的。最高的1G字節(jié)虛擬內(nèi)核空間則為所有進(jìn)程及內(nèi)核共享。操作系統(tǒng)和驅(qū)動(dòng)程序運(yùn)行在內(nèi)核空間,應(yīng)用程序運(yùn)行在用戶空間,兩者不能簡(jiǎn)單地使用指針傳遞數(shù)據(jù)。因?yàn)長(zhǎng)inux使用的虛擬內(nèi)存機(jī)制,必須通過(guò)系統(tǒng)調(diào)用請(qǐng)求Kernel來(lái)協(xié)助完成I/O操作,內(nèi)核會(huì)為每個(gè)I/O設(shè)備維護(hù)一個(gè)緩沖區(qū),用戶空間的數(shù)據(jù)可能被換出,所以當(dāng)內(nèi)核空間使用用戶空間的指針時(shí),對(duì)應(yīng)的數(shù)據(jù)可能不在內(nèi)存中。對(duì)于一個(gè)輸入操作來(lái)說(shuō),進(jìn)程I/O系統(tǒng)調(diào)用后,內(nèi)核會(huì)先看緩沖區(qū)中有沒(méi)有相應(yīng)的緩存數(shù)據(jù),如果沒(méi)有再到設(shè)備中讀取。因?yàn)樵O(shè)備I/O一般速度較慢,需要等待,內(nèi)核緩沖區(qū)有數(shù)據(jù)則直接復(fù)制到進(jìn)程空間。所以,一個(gè)網(wǎng)絡(luò)輸入操作通常包括兩個(gè)不同階段。(1)等待網(wǎng)絡(luò)數(shù)據(jù)到達(dá)網(wǎng)卡,然后將數(shù)據(jù)讀取到內(nèi)核緩沖區(qū)。(2)從內(nèi)核緩沖區(qū)復(fù)制數(shù)據(jù),然后拷貝到用戶空間。I/O有內(nèi)存I/O、網(wǎng)絡(luò)I/O和磁盤(pán)I/O三種,通常我們說(shuō)的I/O指的是后兩者。如下圖所示是I/O通信過(guò)程的調(diào)度示意。2.2五種I/O通信模型在網(wǎng)絡(luò)環(huán)境下,通俗地講,將I/O分為兩步:第一步是等待;第二步是數(shù)據(jù)搬遷。如果想要提高I/O效率,需要將等待時(shí)間降低。因此發(fā)展出來(lái)五種I/O模型,分別是:阻塞I/O模型、非阻塞I/O模型、多路復(fù)用I/O模型、信號(hào)驅(qū)動(dòng)I/O模型、異步I/O模型。其中,前四種被稱為同步I/O,下面對(duì)每一種I/O模型進(jìn)行詳細(xì)分析。2.2.1阻塞I/O模型阻塞I/O模型的通信過(guò)程示意如下圖所示。當(dāng)用戶進(jìn)程調(diào)用了recvfrom這個(gè)系統(tǒng)調(diào)用,內(nèi)核就開(kāi)始了I/O的第一個(gè)階段:準(zhǔn)備數(shù)據(jù)。對(duì)于網(wǎng)絡(luò)I/O來(lái)說(shuō),很多時(shí)候數(shù)據(jù)在一開(kāi)始還沒(méi)有到達(dá)(比如,還沒(méi)有收到一個(gè)完整的UDP包),這個(gè)時(shí)候內(nèi)核就要等待足夠的數(shù)據(jù)到來(lái)。而在用戶進(jìn)程這邊,整個(gè)進(jìn)程會(huì)被阻塞,當(dāng)數(shù)據(jù)準(zhǔn)備好時(shí),它就會(huì)將數(shù)據(jù)從內(nèi)核拷貝到用戶內(nèi)存,然后返回結(jié)果,用戶進(jìn)程才解除阻塞的狀態(tài),重新運(yùn)行起來(lái)。幾乎所有的開(kāi)發(fā)者第一次接觸到的網(wǎng)絡(luò)編程都是從listen()、send()、recv()等接口開(kāi)始的,這些接口都是阻塞型的。阻塞I/O模型的特性總結(jié)如下表所示。2.2.2非阻塞I/O模型非阻塞I/O模型的通信過(guò)程示意如下圖所示。當(dāng)用戶進(jìn)程發(fā)出read操作時(shí),如果內(nèi)核中的數(shù)據(jù)還沒(méi)有準(zhǔn)備好,那么它并不會(huì)阻塞用戶進(jìn)程,而是立刻返回一個(gè)error。從用戶進(jìn)程角度講,它發(fā)起一個(gè)read操作后,并不需要等待,而是馬上就得到了一個(gè)結(jié)果,用戶進(jìn)程判斷結(jié)果是一個(gè)error時(shí),它就知道數(shù)據(jù)還沒(méi)有準(zhǔn)備好。于是它可以再次發(fā)送read操作,一旦內(nèi)核中的數(shù)據(jù)準(zhǔn)備好了,并且再次收到了用戶進(jìn)程的系統(tǒng)調(diào)用,那么它會(huì)馬上將數(shù)據(jù)拷貝到用戶內(nèi)存,然后返回,非阻塞型接口相比于阻塞型接口的顯著差異在于,在被調(diào)用之后立即返回。非阻塞I/O模型的特性總結(jié)如下表所示。非阻塞模式套接字與阻塞模式套接字相比,不容易使用。使用非阻塞模式套接字,需要編寫(xiě)更多的代碼,但是,非阻塞模式套接字在控制建立多個(gè)連接、數(shù)據(jù)的收發(fā)量不均、時(shí)間不定時(shí),具有明顯優(yōu)勢(shì)。2.2.3多路復(fù)用I/O模型多路復(fù)用I/O模型的通信過(guò)程示意如下圖所示。多個(gè)進(jìn)程的I/O可以注冊(cè)到一個(gè)復(fù)用器(Selector)上,當(dāng)用戶進(jìn)程調(diào)用該Selector,Selector會(huì)監(jiān)聽(tīng)注冊(cè)進(jìn)來(lái)的所有I/O,如果Selector監(jiān)聽(tīng)的所有I/O在內(nèi)核緩沖區(qū)都沒(méi)有可讀數(shù)據(jù),select調(diào)用進(jìn)程會(huì)被阻塞,而當(dāng)任一I/O在內(nèi)核緩沖區(qū)中有可讀數(shù)據(jù)時(shí),select調(diào)用就會(huì)返回,而后select調(diào)用進(jìn)程可以自己或通知另外的進(jìn)程(注冊(cè)進(jìn)程)再次發(fā)起讀取I/O,讀取內(nèi)核中準(zhǔn)備好的數(shù)據(jù),多個(gè)進(jìn)程注冊(cè)I/O后,只有一個(gè)select調(diào)用進(jìn)程被阻塞。多路復(fù)用I/O相對(duì)阻塞和非阻塞更難簡(jiǎn)單說(shuō)明,所以額外解釋一段,其實(shí)多路復(fù)用I/O模型和阻塞I/O模型并沒(méi)有太大的不同,事實(shí)上,還更差一些,因?yàn)檫@里需要使用兩個(gè)系統(tǒng)調(diào)用(select和recvfrom),而阻塞I/O模型只有一次系統(tǒng)調(diào)用(recvfrom)。但是,用Selector的優(yōu)勢(shì)在于它可以同時(shí)處理多個(gè)連接,所以如果處理的連接數(shù)不是很多,使用select/epoll的WebServer不一定比使用多線程加阻塞I/O的WebServer性能更好,可能延遲還更大,select/epoll的優(yōu)勢(shì)并不是對(duì)于單個(gè)連接能處理得更快,而是能處理更多的連接。多路復(fù)用I/O模型的特性總結(jié)如下表所示。2.2.4信號(hào)驅(qū)動(dòng)I/O模型信號(hào)驅(qū)動(dòng)I/O模型的通信過(guò)程示意如下圖所示。信號(hào)驅(qū)動(dòng)I/O是指進(jìn)程預(yù)先告知內(nèi)核,向內(nèi)核注冊(cè)一個(gè)信號(hào)處理函數(shù),然后用戶進(jìn)程返回不阻塞,當(dāng)內(nèi)核數(shù)據(jù)就緒時(shí)會(huì)發(fā)送一個(gè)信號(hào)給進(jìn)程,用戶進(jìn)程便在信號(hào)處理函數(shù)中調(diào)用I/O讀取數(shù)據(jù)。從上圖可以看出,實(shí)際上I/O內(nèi)核拷貝到用戶進(jìn)程的過(guò)程還是阻塞的,信號(hào)驅(qū)動(dòng)I/O并沒(méi)有實(shí)現(xiàn)真正的異步,因?yàn)橥ㄖ竭M(jìn)程之后,依然由進(jìn)程來(lái)完成I/O操作。這和后面的異步I/O模型很容易混淆,需要理解I/O交互并結(jié)合五種I/O模型進(jìn)行比較閱讀。信號(hào)驅(qū)動(dòng)I/O模型的特性總結(jié)如下表所示。2.2.5異步I/O模型異步I/O模型的通信過(guò)程示意如下圖所示。用戶進(jìn)程發(fā)起aio_read操作后,給內(nèi)核傳遞與read相同的描述符、緩沖區(qū)指針、緩沖區(qū)大小三個(gè)參數(shù)及文件偏移,告訴內(nèi)核當(dāng)整個(gè)操作完成時(shí),如何通知我們立刻就可以開(kāi)始去做其他的事;而另一方面,從內(nèi)核的角度,當(dāng)它收到一個(gè)aio_read之后,首先它會(huì)立刻返回,所以不會(huì)對(duì)用戶進(jìn)程產(chǎn)生任何阻塞,內(nèi)核會(huì)等待數(shù)據(jù)準(zhǔn)備完成,然后將數(shù)據(jù)拷貝到用戶內(nèi)存,當(dāng)這一切都完成之后,內(nèi)核會(huì)給用戶進(jìn)程發(fā)送一個(gè)信號(hào),告訴它aio_read操作完成。異步I/O的工作機(jī)制是:告知內(nèi)核啟動(dòng)某個(gè)操作,并讓內(nèi)核在整個(gè)操作完成后通知我們,這種模型與信號(hào)驅(qū)動(dòng)I/O模型的區(qū)別在于,信號(hào)驅(qū)動(dòng)I/O模型是由內(nèi)核通知我們何時(shí)可以啟動(dòng)一個(gè)I/O操作,這個(gè)I/O操作由用戶自定義的信號(hào)函數(shù)來(lái)實(shí)現(xiàn),而異步I/O模型由內(nèi)核告知我們I/O操作何時(shí)完成。異步I/O模型的特性總結(jié)如下表所示。2.2.6易混淆的概念澄清在實(shí)際開(kāi)發(fā)中,我們經(jīng)常會(huì)聽(tīng)到同步、異步、阻塞、非阻塞這些概念,每次遇到的時(shí)候都會(huì)“蒙圈”,然后就查網(wǎng)上各種資料,結(jié)果越查越迷糊。大部分文章都千篇一律,沒(méi)有說(shuō)到本質(zhì)上的區(qū)別,所以下次再碰到這些概念,印象還是比較模糊,尤其是在一些場(chǎng)景下覺(jué)得同步與阻塞、異步與非阻塞沒(méi)什么區(qū)別,但其實(shí)這四個(gè)術(shù)語(yǔ)描述的還真不是一回事。下面我們來(lái)慢慢探討它們之間的區(qū)別與聯(lián)系,在這之前,我們還會(huì)經(jīng)??吹较旅娴慕M合術(shù)語(yǔ)。(1)同步阻塞。(2)同步非阻塞。(3)異步阻塞。(4)異步非阻塞。在什么是同步和異步、阻塞和非阻塞的概念還沒(méi)弄清楚之前,更別提上面這些組合術(shù)語(yǔ)了,只會(huì)讓你更加困惑。1.同步和異步同步和異步其實(shí)是指CPU時(shí)間片的利用,主要看請(qǐng)求發(fā)起方對(duì)消息結(jié)果的獲取是主動(dòng)發(fā)起的,還是被動(dòng)通知的,如下圖所示。如果是請(qǐng)求方主動(dòng)發(fā)起的,一直在等待應(yīng)答結(jié)果(同步阻塞),或者可以先去處理其他事情,但要不斷輪詢查看發(fā)起的請(qǐng)求是否有應(yīng)答結(jié)果(同步非阻塞),因?yàn)椴还苋绾味家l(fā)起方主動(dòng)獲取消息結(jié)果,所以形式上還是同步操作。如果是由服務(wù)方通知的,也就是請(qǐng)求方發(fā)出請(qǐng)求后,要么一直等待通知(異步阻塞),要么先去干自己的事(異步非阻塞)。當(dāng)事情處理完成后,服務(wù)方會(huì)主動(dòng)通知請(qǐng)求方,它的請(qǐng)求已經(jīng)完成,這就是異步。異步通知的方式一般通過(guò)狀態(tài)改變、消息通知或者回調(diào)函數(shù)來(lái)完成,大多數(shù)時(shí)候采用的都是回調(diào)函數(shù)。2.阻塞和非阻塞阻塞和非阻塞在計(jì)算機(jī)的世界里,通常指針對(duì)I/O的操作,如網(wǎng)絡(luò)I/O和磁盤(pán)I/O等。那么什么是阻塞和非阻塞呢?簡(jiǎn)單地說(shuō),就是我們調(diào)用了一個(gè)函數(shù)后,在等待這個(gè)函數(shù)返回結(jié)果之前,當(dāng)前的線程是處于掛起狀態(tài)還是運(yùn)行狀態(tài)。如果是掛起狀態(tài),就意味著當(dāng)前線程什么都不能干,就等著獲取結(jié)果,這就是同步阻塞;如果仍然是運(yùn)行狀態(tài),就意味著當(dāng)前線程是可以繼續(xù)處理其他任務(wù)的,但要時(shí)不時(shí)地看一下是否有結(jié)果了,這就是同步非阻塞。具體如下圖所示。3.實(shí)際生活場(chǎng)景同步、異步、阻塞和非阻塞可以組合成上面提到過(guò)的四種結(jié)果。舉個(gè)例子,比如我們?nèi)フ障囵^拍照,拍完照片之后,商家說(shuō)需要30min左右才能洗出來(lái)照片。(1)這個(gè)時(shí)候,如果我們一直在店里面什么都不干,一直等待直到洗完照片,這個(gè)過(guò)程就叫同步阻塞。(2)當(dāng)然,大部分人很少這么干,更多的是大家拿起手機(jī)開(kāi)始看電視,看一會(huì)兒就會(huì)問(wèn)老板洗完沒(méi),老板說(shuō)沒(méi)洗完,然后接著看,再過(guò)一會(huì)兒接著問(wèn),直到照片洗完,這個(gè)過(guò)程就叫同步非阻塞。(3)由于店里生意太好了,越來(lái)越多的人過(guò)來(lái)拍,店里面快沒(méi)地方坐了,老板說(shuō)你把手機(jī)號(hào)留下,我一會(huì)兒洗好了就打電話告訴你過(guò)來(lái)取,然后你去外面找了一個(gè)長(zhǎng)凳開(kāi)始躺著睡覺(jué)等待老板打電話,什么都不干,這個(gè)過(guò)程就叫異步阻塞(實(shí)際不應(yīng)用)。(4)當(dāng)然實(shí)際情況是,大家可能會(huì)先去逛街或者吃飯,或者做其他活動(dòng),這樣一來(lái),兩不耽誤,這個(gè)過(guò)程就叫異步非阻塞(效率最高)。4.小結(jié)從上面的描述中,我們能夠看到阻塞和非阻塞通常是指在客戶端發(fā)出請(qǐng)求后,在服務(wù)端處理這個(gè)請(qǐng)求的過(guò)程中,客戶端本身是直接掛起等待結(jié)果,還是繼續(xù)做其他的任務(wù)。而異步和同步則是對(duì)于請(qǐng)求結(jié)果的獲取是客戶端主動(dòng)獲取結(jié)果,還是由服務(wù)端來(lái)通知結(jié)果。從這一點(diǎn)來(lái)看,同步和阻塞其實(shí)描述的是兩個(gè)不同角度的事情,阻塞和非阻塞指的是客戶端等待消息處理時(shí)本身的狀態(tài),是掛起還是繼續(xù)干別的。同步和異步指的是對(duì)于消息結(jié)果是客戶端主動(dòng)獲取的,還是由服務(wù)端間接推送的。記住這兩點(diǎn)關(guān)鍵的區(qū)別將有助于我們更好地區(qū)分和理解它們。2.2.7各I/O模型的對(duì)比與總結(jié)其實(shí)前四種I/O模型都是同步I/O操作,它們的區(qū)別在于第一階段,而第二階段是一樣的:在數(shù)據(jù)從內(nèi)核拷貝到應(yīng)用緩沖區(qū)期間(用戶空間),進(jìn)程阻塞于recvfrom調(diào)用。有人可能會(huì)說(shuō),NIO(Non-BlockingI/O)并沒(méi)有被阻塞。這里有個(gè)非?!敖苹钡牡胤剑x中所指的“I/OOperation”是指真實(shí)的I/O操作。NIO在執(zhí)行recvfrom的時(shí)候,如果內(nèi)核(Kernel)的數(shù)據(jù)沒(méi)有準(zhǔn)備好,這時(shí)候不會(huì)阻塞進(jìn)程。但是,當(dāng)內(nèi)核(Kernel)中數(shù)據(jù)準(zhǔn)備好的時(shí)候,recvfrom會(huì)將數(shù)據(jù)從內(nèi)核(Kernel)拷貝到用戶內(nèi)存中,這個(gè)時(shí)候進(jìn)程就被阻塞了。在這段時(shí)間內(nèi),進(jìn)程是被阻塞的。下圖是各I/O模型的阻塞狀態(tài)對(duì)比。從上圖可以看出,阻塞程度:阻塞I/O>非阻塞I/O>多路復(fù)用I/O>信號(hào)驅(qū)動(dòng)I/O>異步I/O,效率是由低到高的。最后,再看一下下表,從多維度總結(jié)了各I/O模型之間的差異,可以加深理解。2.3從BIO到NIO的演進(jìn)下表總結(jié)了JavaBIO(BlockingI/O)和NIO(Non-BlockingI/O)之間的主要差異。2.3.1面向流與面向緩沖JavaNIO和BIO之間第一個(gè)最大的區(qū)別是,BIO是面向流的,NIO是面向緩沖區(qū)的。JavaBIO面向流意味著每次從流中讀一個(gè)或多個(gè)字節(jié),直至讀取所有字節(jié),它們沒(méi)有被緩存在任何地方。此外,不能前后移動(dòng)流中的數(shù)據(jù)。如果需要前后移動(dòng)從流中讀取的數(shù)據(jù),需要先將它緩存到一個(gè)緩沖區(qū)。JavaNIO的緩沖導(dǎo)向方法略有不同。數(shù)據(jù)讀取到一個(gè)它稍后處理的緩沖區(qū),需要時(shí)可在緩沖區(qū)中前后移動(dòng)。這就增加了處理過(guò)程的靈活性。但是,還需要檢查該緩沖區(qū)是否包含所有需要處理的數(shù)據(jù)。而且,要確保當(dāng)更多的數(shù)據(jù)讀入緩沖區(qū)時(shí),不能覆蓋緩沖區(qū)里尚未處理的數(shù)據(jù)。2.3.2阻塞與非阻塞JavaBIO的各種流是阻塞的。這意味著,當(dāng)一個(gè)線程調(diào)用read()或write()時(shí),該線程被阻塞,直到有一些數(shù)據(jù)被讀取,或數(shù)據(jù)完全寫(xiě)入。該線程在此期間不能再干任何事情。JavaNIO的非阻塞模式,是一個(gè)線程從某通道(Channel)發(fā)送請(qǐng)求讀取數(shù)據(jù),但是它僅能得到目前可用的數(shù)據(jù),如果目前沒(méi)有數(shù)據(jù)可用,就什么都不會(huì)獲取,而不是保持線程阻塞,所以直到數(shù)據(jù)變成可以讀取之前,該線程可以繼續(xù)做其他的事情。非阻塞寫(xiě)也是如此。一個(gè)線程請(qǐng)求寫(xiě)入某通道一些數(shù)據(jù),但不需要等待它完全寫(xiě)入,這個(gè)線程同時(shí)可以去做別的事情。線程通常將非阻塞I/O的空閑時(shí)間用于在其他通道上執(zhí)行I/O操作,所以一個(gè)單獨(dú)的線程現(xiàn)在可以管理多個(gè)I/O通道。2.3.3選擇器在I/O中的應(yīng)用JavaNIO的選擇器(Selector)允許一個(gè)單獨(dú)的線程監(jiān)視多個(gè)輸入通道,可以注冊(cè)多個(gè)通道使用一個(gè)選擇器,然后使用一個(gè)單獨(dú)的線程來(lái)“選擇”通道:這些通道里已經(jīng)有可以處理的輸入,或者選擇已準(zhǔn)備寫(xiě)入的通道。這種選擇機(jī)制使一個(gè)單獨(dú)的線程很容易管理多個(gè)通道。2.3.4NIO和BIO如何影響應(yīng)用程序的設(shè)計(jì)無(wú)論選擇BIO還是NIO工具箱,都可能會(huì)影響應(yīng)用程序設(shè)計(jì)的以下幾個(gè)方面。(1)對(duì)NIO或BIO類的API調(diào)用。(2)數(shù)據(jù)處理邏輯。(3)用來(lái)處理數(shù)據(jù)的線程數(shù)。1.API調(diào)用當(dāng)然,使用NIO的API調(diào)用看起來(lái)與使用BIO時(shí)有所不同,但這并不意外,因?yàn)椴⒉皇莾H從一個(gè)InputStream逐字節(jié)讀取,而是數(shù)據(jù)必須先讀入緩沖區(qū)再處理。2.?dāng)?shù)據(jù)處理使用純粹的NIO設(shè)計(jì)相較BIO設(shè)計(jì),數(shù)據(jù)處理也會(huì)受到影響。在BIO設(shè)計(jì)中,我們從InputStream或Reader逐字節(jié)讀取數(shù)據(jù)。假設(shè)你正在處理一個(gè)基于行的文本數(shù)據(jù)流,有如下一段文本。該文本行的流可以這樣處理。請(qǐng)注意處理狀態(tài)由程序執(zhí)行多久決定。換句話說(shuō),一旦reader.readLine()方法返回,你就知道文本行肯定已讀完,readline()阻塞直到整行讀完,這就是原因。你也知道此行包含名稱;同樣,第二個(gè)readline()調(diào)用返回的時(shí)候,你知道這行包含年齡。正如你可以看到,該處理程序僅在有新數(shù)據(jù)讀入時(shí)運(yùn)行,并知道每步的數(shù)據(jù)是什么。一旦正在運(yùn)行的線程已處理過(guò)讀入的某些數(shù)據(jù),該線程不會(huì)再回退數(shù)據(jù)(大多如此)。下圖也說(shuō)明了這條原則。JavaBIO從一個(gè)阻塞的流中讀數(shù)據(jù),而一個(gè)NIO的實(shí)現(xiàn)會(huì)有所不同,下面是一個(gè)簡(jiǎn)單的例子。注意第二行,從通道讀取字節(jié)到ByteBuffer。當(dāng)這個(gè)方法調(diào)用返回時(shí),你不知道你所需的所有數(shù)據(jù)是否在緩沖區(qū)內(nèi)。你所知道的是,該緩沖區(qū)包含一些字節(jié),這使得處理有點(diǎn)困難。假設(shè)第一次read(buffer)調(diào)用后,讀入緩沖區(qū)的數(shù)據(jù)只有半行,例如,“Name:An”,你能處理數(shù)據(jù)嗎?顯然不能,需要等待,直到整行數(shù)據(jù)讀入緩存。在此之前,對(duì)數(shù)據(jù)的任何處理都毫無(wú)意義。所以,你怎么知道是否該緩沖區(qū)包含足夠的數(shù)據(jù)可以處理呢?好了,你不知道。發(fā)現(xiàn)的方法只能查看緩沖區(qū)中的數(shù)據(jù)。其結(jié)果是,在你知道所有數(shù)據(jù)都在緩沖區(qū)里之前,你必須檢查幾次緩沖區(qū)的數(shù)據(jù)。這不僅效率低下,而且會(huì)使程序設(shè)計(jì)方案雜亂不堪。例如:bufferFull()方法必須跟蹤有多少數(shù)據(jù)讀入緩沖區(qū),并返回真或假,這取決于緩沖區(qū)是否已滿。換句話說(shuō),如果緩沖區(qū)準(zhǔn)備好被處理,那么表示緩沖區(qū)已滿。bufferFull()方法掃描緩沖區(qū),但必須保持與bufferFull()方法被調(diào)用之前狀態(tài)相同。如果沒(méi)有,下一個(gè)讀入緩沖區(qū)的數(shù)據(jù)可能無(wú)法讀到正確的位置。雖然這是不可能的,但卻是需要注意的又一個(gè)問(wèn)題。如果緩沖區(qū)已滿,它可以被處理。如果它不滿,并且在實(shí)際案例中有意義,或許能處理其中的部分?jǐn)?shù)據(jù),但是許多情況下并非如此。下圖展示了“緩沖區(qū)數(shù)據(jù)循環(huán)就緒”。3.設(shè)置處理線程數(shù)NIO可以只使用一個(gè)(或幾個(gè))單線程管理多個(gè)通道(網(wǎng)絡(luò)連接或文件),但付出的代價(jià)是解析數(shù)據(jù)可能會(huì)比從一個(gè)阻塞流中讀取數(shù)據(jù)更復(fù)雜。如果需要管理同時(shí)打開(kāi)的成千上萬(wàn)個(gè)連接,這些連接每次只是發(fā)送少量的數(shù)據(jù),例如聊天服務(wù)器,實(shí)現(xiàn)NIO的服務(wù)器可能是一個(gè)優(yōu)勢(shì)。同樣,如果需要維持許多打開(kāi)的連接,如P2P網(wǎng)絡(luò)中,使用一個(gè)單獨(dú)的線程來(lái)管理所有出站連接,可能是一個(gè)優(yōu)勢(shì)。一個(gè)線程有多個(gè)連接的設(shè)計(jì)方案如下圖所示。(1)JavaNIO:?jiǎn)尉€程管理多個(gè)連接。如果有少量的連接使用非常高的帶寬,一次發(fā)送大量的數(shù)據(jù),也許用典型的I/O服務(wù)器實(shí)現(xiàn)可能非常契合。下圖說(shuō)明了一個(gè)典型的I/O服務(wù)器設(shè)計(jì)。(2)JavaBIO:一個(gè)典型的I/O服務(wù)器設(shè)計(jì)。一個(gè)連接只用一個(gè)線程來(lái)處理。2.4JavaAIO詳解JDK1.7(NIO2)才是實(shí)現(xiàn)真正的異步AIO(AsynchronousI/O)、把I/O讀寫(xiě)操作完全交給操作系統(tǒng),學(xué)習(xí)了LinuxEpoll模式,下面我們來(lái)做一些演示。2.4.1AIO基本原理JavaAIO處理API中,重要的三個(gè)類分別是:AsynchronousServerSocketChannel(服務(wù)端)、AsynchronousSocketChannel(客戶端)及CompletionHandler(用戶處理器)。CompletionHandler接口實(shí)現(xiàn)應(yīng)用程序向操作系統(tǒng)發(fā)起I/O請(qǐng)求,當(dāng)完成后處理具體邏輯,否則做自己該做的事情,“真正”的異步I/O需要操作系統(tǒng)更強(qiáng)的支持。在多路復(fù)用I/O模型中,事件循環(huán)將文件句柄的狀態(tài)事件通知給用戶線程,由用戶線程自行讀取數(shù)據(jù)、處理數(shù)據(jù)。而在異步I/O模型中,當(dāng)用戶線程收到通知時(shí),數(shù)據(jù)已經(jīng)被內(nèi)核讀取完畢,并放在了用戶線程指定的緩沖區(qū)內(nèi),內(nèi)核在I/O完成后通知用戶線程直接使用即可。異步I/O模型使用Proactor設(shè)計(jì)模式實(shí)現(xiàn)這一機(jī)制,如下圖所示。2.4.2AIO初體驗(yàn)我們基于AIO先寫(xiě)一段簡(jiǎn)單的代碼,來(lái)感受一下服務(wù)端和客戶端的交互過(guò)程,同時(shí)也體驗(yàn)一下API的使用。先來(lái)看服務(wù)端代碼。上述代碼的主要功能就是開(kāi)啟一個(gè)監(jiān)聽(tīng)端口,然后在CompletionHandler中處理接收到消息以后的邏輯,將接收到的信息再輸出到客戶端。下面來(lái)看客戶端的代碼。客戶端的代碼的主要功能是發(fā)送一串字符到服務(wù)端。同時(shí),在CompletionHandler接口處理服務(wù)端發(fā)送過(guò)來(lái)的結(jié)果。服務(wù)端執(zhí)行結(jié)果如下圖所示??蛻舳藞?zhí)行結(jié)果如下圖所示。運(yùn)行代碼后,我們會(huì)發(fā)現(xiàn)不管是客戶端還是服務(wù)端,其處理接收消息的邏輯都是異步操作,和BIO、NIO的API使用有根本上的區(qū)別。第2篇Netty初體驗(yàn)第3章Netty與NIO之前世今生第4章基于Netty手寫(xiě)Tomcat第5章基于Netty重構(gòu)RPC框架第3章Netty與NIO之前世今生3.1JavaNIO三件套在NIO中有三個(gè)核心對(duì)象需要掌握:緩沖區(qū)(Buffer)、選擇器(Selector)和通道(Channel)。3.1.1緩沖區(qū)1.Buffer操作基本API緩沖區(qū)實(shí)際上是一個(gè)容器對(duì)象,更直接地說(shuō),其實(shí)就是一個(gè)數(shù)組,在NIO庫(kù)中,所有數(shù)據(jù)都是用緩沖區(qū)處理的。在讀取數(shù)據(jù)時(shí),它是直接讀到緩沖區(qū)中的;在寫(xiě)入數(shù)據(jù)時(shí),它也是寫(xiě)入緩沖區(qū)的;任何時(shí)候訪問(wèn)NIO中的數(shù)據(jù),都是將它放到緩沖區(qū)中。而在面向流I/O系統(tǒng)中,所有數(shù)據(jù)都是直接寫(xiě)入或者直接將數(shù)據(jù)讀取到Stream對(duì)象中。在NIO中,所有的緩沖區(qū)類型都繼承于抽象類Buffer,最常用的就是ByteBuffer,對(duì)于Java中的基本類型,基本都有一個(gè)具體Buffer類型與之相對(duì)應(yīng),它們之間的繼承關(guān)系如下圖所示。下面是一個(gè)簡(jiǎn)單的使用IntBuffer的例子。運(yùn)行后可以看到如下圖所示的結(jié)果。2.Buffer的基本原理在談到緩沖區(qū)時(shí),我們說(shuō)緩沖區(qū)對(duì)象本質(zhì)上是一個(gè)數(shù)組,但它其實(shí)是一個(gè)特殊的數(shù)組,緩沖區(qū)對(duì)象內(nèi)置了一些機(jī)制,能夠跟蹤和記錄緩沖區(qū)的狀態(tài)變化情況,如果我們使用get()方法從緩沖區(qū)獲取數(shù)據(jù)或者使用put()方法把數(shù)據(jù)寫(xiě)入緩沖區(qū),都會(huì)引起緩沖區(qū)狀態(tài)的變化。在緩沖區(qū)中,最重要的屬性有下面三個(gè),它們一起合作完成對(duì)緩沖區(qū)內(nèi)部狀態(tài)的變化跟蹤?!駊osition:指定下一個(gè)將要被寫(xiě)入或者讀取的元素索引,它的值由get()/put()方法自動(dòng)更新,在新創(chuàng)建一個(gè)Buffer對(duì)象時(shí),position被初始化為0?!駆imit:指定還有多少數(shù)據(jù)需要取出(在從緩沖區(qū)寫(xiě)入通道時(shí)),或者還有多少空間可以放入數(shù)據(jù)(在從通道讀入緩沖區(qū)時(shí))?!馽apacity:指定了可以存儲(chǔ)在緩沖區(qū)中的最大數(shù)據(jù)容量,實(shí)際上,它指定了底層數(shù)組的大小,或者至少是指定了準(zhǔn)許我們使用的底層數(shù)組的容量。以上三個(gè)屬性值之間有一些相對(duì)大小的關(guān)系:0<=position<=limit<=capacity。如果我們創(chuàng)建一個(gè)新的容量大小為10的ByteBuffer對(duì)象,在初始化的時(shí)候,position設(shè)置為0,limit和capacity設(shè)置為10,在以后使用ByteBuffer對(duì)象過(guò)程中,capacity的值不會(huì)再發(fā)生變化,而其他兩個(gè)將會(huì)隨著使用而變化。準(zhǔn)備一個(gè)txt文檔,存放在E盤(pán),輸入以下內(nèi)容。我們用一段代碼來(lái)驗(yàn)證position、limit和capacity這三個(gè)值的變化過(guò)程,代碼如下。完成后的輸出結(jié)果如下圖所示。我們已經(jīng)看到運(yùn)行結(jié)果,下面對(duì)以上結(jié)果進(jìn)行圖解,三個(gè)屬性值分別如下圖所示。我們可以從通道中讀取一些數(shù)據(jù)到緩沖區(qū)中,注意從通道讀取數(shù)據(jù),相當(dāng)于往緩沖區(qū)寫(xiě)入數(shù)據(jù)。如果讀取4個(gè)自己的數(shù)據(jù),則此時(shí)position的值為4,即下一個(gè)將要被寫(xiě)入的字節(jié)索引為4,而limit仍然是10,如下圖所示。下一步把讀取的數(shù)據(jù)寫(xiě)入輸出通道,相當(dāng)于從緩沖區(qū)中讀取數(shù)據(jù),在此之前,必須調(diào)用flip()方法。該方法將會(huì)完成以下兩件事情:一是把limit設(shè)置為當(dāng)前的position值。二是把position設(shè)置為0。由于position被設(shè)置為0,所以可以保證在下一步輸出時(shí)讀取的是緩沖區(qū)的第一個(gè)字節(jié),而limit被設(shè)置為當(dāng)前的position,可以保證讀取的數(shù)據(jù)正好是之前寫(xiě)入緩沖區(qū)的數(shù)據(jù),如下圖所示?,F(xiàn)在調(diào)用get()方法從緩沖區(qū)中讀取數(shù)據(jù)寫(xiě)入輸出通道,這會(huì)導(dǎo)致position的增加而limit保持不變,但position不會(huì)超過(guò)limit的值,所以在讀取之前寫(xiě)入緩沖區(qū)的4字節(jié)之后,position和limit的值都為4,如下圖所示。在從緩沖區(qū)中讀取數(shù)據(jù)完畢后,limit的值仍然保持在調(diào)用flip()方法時(shí)的值,調(diào)用clear()方法能夠把所有的狀態(tài)變化設(shè)置為初始化時(shí)的值,如下圖所示。3.緩沖區(qū)的分配在前面的幾個(gè)例子中,我們已經(jīng)看到,在創(chuàng)建一個(gè)緩沖區(qū)對(duì)象時(shí),會(huì)調(diào)用靜態(tài)方法allocate()來(lái)指定緩沖區(qū)的容量,其實(shí)調(diào)用allocate()方法相當(dāng)于創(chuàng)建了一個(gè)指定大小的數(shù)組,并把它包裝為緩沖區(qū)對(duì)象?;蛘呶覀円部梢灾苯訉⒁粋€(gè)現(xiàn)有的數(shù)組包裝為緩沖區(qū)對(duì)象,示例代碼如下。4.緩沖區(qū)分片在NIO中,除了可以分配或者包裝一個(gè)緩沖區(qū)對(duì)象,還可以根據(jù)現(xiàn)有的緩沖區(qū)對(duì)象創(chuàng)建一個(gè)子緩沖區(qū),即在現(xiàn)有緩沖區(qū)上切出一片作為一個(gè)新的緩沖區(qū),但現(xiàn)有的緩沖區(qū)與創(chuàng)建的子緩沖區(qū)在底層數(shù)組層面上是數(shù)據(jù)共享的,也就是說(shuō),子緩沖區(qū)相當(dāng)于現(xiàn)有緩沖區(qū)的一個(gè)視圖窗口。調(diào)用slice()方法可以創(chuàng)建一個(gè)子緩沖區(qū),下面我們通過(guò)例子來(lái)看一下。在該示例中,分配了一個(gè)容量大小為10的緩沖區(qū),并在其中放入了數(shù)據(jù)0~9,而在該緩沖區(qū)基礎(chǔ)上又創(chuàng)建了一個(gè)子緩沖區(qū),并改變子緩沖區(qū)中的內(nèi)容,從最后輸出的結(jié)果來(lái)看,只有子緩沖區(qū)“可見(jiàn)的”那部分?jǐn)?shù)據(jù)發(fā)生了變化,并且說(shuō)明子緩沖區(qū)與原緩沖區(qū)是數(shù)據(jù)共享的,輸出結(jié)果如下圖所示。5.只讀緩沖區(qū)只讀緩沖區(qū)非常簡(jiǎn)單,可以讀取它們,但是不能向它們寫(xiě)入數(shù)據(jù)??梢酝ㄟ^(guò)調(diào)用緩沖區(qū)的asReadOnlyBuffer()方法,將任何常規(guī)緩沖區(qū)轉(zhuǎn)換為只讀緩沖區(qū),這個(gè)方法返回一個(gè)與原緩沖區(qū)完全相同的緩沖區(qū),并與原緩沖區(qū)共享數(shù)據(jù),只不過(guò)它是只讀的。如果原緩沖區(qū)的內(nèi)容發(fā)生了變化,只讀緩沖區(qū)的內(nèi)容也隨之發(fā)生變化。具體代碼如下。如果嘗試修改只讀緩沖區(qū)的內(nèi)容,則會(huì)報(bào)ReadOnlyBufferException異常。只讀緩沖區(qū)對(duì)于保護(hù)數(shù)據(jù)很有用。在將緩沖區(qū)傳遞給某個(gè)對(duì)象的方法時(shí),無(wú)法知道這個(gè)方法是否會(huì)修改緩沖區(qū)中的數(shù)據(jù)。創(chuàng)建一個(gè)只讀緩沖區(qū)可以保證該緩沖區(qū)不會(huì)被修改。只可以把常規(guī)緩沖區(qū)轉(zhuǎn)換為只讀緩沖區(qū),而不能將只讀緩沖區(qū)轉(zhuǎn)換為可寫(xiě)的緩沖區(qū)。6.直接緩沖區(qū)直接緩沖區(qū)是為加快I/O速度,使用一種特殊方式為其分配內(nèi)存的緩沖區(qū),JDK文檔中的描述為:給定一個(gè)直接字節(jié)緩沖區(qū),Java虛擬機(jī)將盡最大努力直接對(duì)它執(zhí)行本機(jī)I/O操作。也就是說(shuō),它會(huì)在每一次調(diào)用底層操作系統(tǒng)的本機(jī)I/O操作之前(或之后),嘗試避免將緩沖區(qū)的內(nèi)容拷貝到一個(gè)中間緩沖區(qū)或者從一個(gè)中間緩沖區(qū)拷貝數(shù)據(jù)。要分配直接緩沖區(qū),需要調(diào)用allocateDirect()方法,而不是allocate()方法,使用方式與普通緩沖區(qū)并無(wú)區(qū)別,如下面的文件所示。7.內(nèi)存映射內(nèi)存映射是一種讀和寫(xiě)文件數(shù)據(jù)的方法,可以比常規(guī)的基于流或者基于通道的I/O快得多。內(nèi)存映射文件I/O通過(guò)使文件中的數(shù)據(jù)表現(xiàn)為內(nèi)存數(shù)組的內(nèi)容來(lái)完成,這初聽(tīng)起來(lái)似乎不過(guò)就是將整個(gè)文件讀到內(nèi)存中,但事實(shí)上并不是這樣的。一般來(lái)說(shuō),只有文件中實(shí)際讀取或?qū)懭氲牟糠植艜?huì)映射到內(nèi)存中。來(lái)看下面的示例代碼。3.1.2選擇器傳統(tǒng)的Client/Server模式會(huì)基于TPR(ThreadperRequest),服務(wù)器會(huì)為每個(gè)客戶端請(qǐng)求建立一個(gè)線程,由該線程單獨(dú)負(fù)責(zé)處理一個(gè)客戶請(qǐng)求。這種模式帶來(lái)的一個(gè)問(wèn)題就是線程數(shù)量的劇增,大量的線程會(huì)增大服務(wù)器的開(kāi)銷。大多數(shù)的實(shí)現(xiàn)為了避免這個(gè)問(wèn)題,都采用了線程池模型,并設(shè)置線程池中線程的最大數(shù)量,這又帶來(lái)了新的問(wèn)題,如果線程池中有200個(gè)線程,而有200個(gè)用戶都在進(jìn)行大文件下載,會(huì)導(dǎo)致第201個(gè)用戶的請(qǐng)求無(wú)法及時(shí)處理,即便第201個(gè)用戶只想請(qǐng)求一個(gè)幾KB大小的頁(yè)面。傳統(tǒng)的Client/Server模式如下圖所示。NIO中非阻塞I/O采用了基于Reactor模式的工作方式,I/O調(diào)用不會(huì)被阻塞,而是注冊(cè)感興趣的特定I/O事件,如可讀數(shù)據(jù)到達(dá)、新的套接字連接等,在發(fā)生特定事件時(shí),系統(tǒng)再通知我們。NIO中實(shí)現(xiàn)非阻塞I/O的核心對(duì)象是Selector,Selector是注冊(cè)各種I/O事件的地方,而且當(dāng)那些事件發(fā)生時(shí),就是Seleetor告訴我們所發(fā)生的事件,如下圖所示。從圖中可以看出,當(dāng)有讀或?qū)懙热魏巫?cè)的事件發(fā)生時(shí),可以從Selector中獲得相應(yīng)的SelectionKey,同時(shí)從SelectionKey中可以找到發(fā)生的事件和該事件所發(fā)生的具體的SelectableChannel,以獲得客戶端發(fā)送過(guò)來(lái)的數(shù)據(jù)。使用NIO中非阻塞I/O編寫(xiě)服務(wù)器處理程序,大體上可以分為下面三個(gè)步驟。(1)向Selector對(duì)象注冊(cè)感興趣的事件。(2)從Selector中獲取感興趣的事件。(3)根據(jù)不同的事件進(jìn)行相應(yīng)的處理。下面我們用一個(gè)簡(jiǎn)單的示例來(lái)說(shuō)明整個(gè)過(guò)程。首先是向Selector對(duì)象注冊(cè)感興趣的事件。上述代碼中先創(chuàng)建了ServerSocketChannel對(duì)象,并調(diào)用configureBlocking()方法,配置為非阻塞模式。接下來(lái)的三行代碼把該通道綁定到指定端口,最后向Selector注冊(cè)事件。此處指定的參數(shù)是OP_ACCEPT,即指定想要監(jiān)聽(tīng)accept事件,也就是新的連接發(fā)生時(shí)所產(chǎn)生的事件。對(duì)于ServerSocketChannel通道來(lái)說(shuō),我們唯一可以指定的參數(shù)就是OP_ACCEPT。從Selector中獲取感興趣的事件,即開(kāi)始監(jiān)聽(tīng),進(jìn)入內(nèi)部循環(huán)。在非阻塞I/O中,內(nèi)部循環(huán)模式基本都遵循這種方式。首先調(diào)用select()方法,該方法會(huì)阻塞,直到至少有一個(gè)事件發(fā)生,然后使用selectedKeys()方法獲取發(fā)生事件的SelectionKey,再使用迭代器進(jìn)行循環(huán)。最后一步就是根據(jù)不同的事件,編寫(xiě)相應(yīng)的處理代碼。此處判斷是接受請(qǐng)求、讀數(shù)據(jù)還是寫(xiě)事件,分別做不同的處理。在Java1.4之前的I/O系統(tǒng)中,提供的都是面向流的I/O系統(tǒng),系統(tǒng)一次一個(gè)字節(jié)地處理數(shù)據(jù),一個(gè)輸入流產(chǎn)生一個(gè)字節(jié)的數(shù)據(jù),一個(gè)輸出流消費(fèi)一個(gè)字節(jié)的數(shù)據(jù),面向流的I/O速度非常慢;而在Java1.4中推出了NIO,這是一個(gè)面向塊的I/O系統(tǒng),系統(tǒng)以塊的方式處理數(shù)據(jù),每一個(gè)操作在一步中都產(chǎn)生或者消費(fèi)一個(gè)數(shù)據(jù)庫(kù),按塊處理數(shù)據(jù)要比按字節(jié)處理數(shù)據(jù)快得多。3.1.3通道通道是一個(gè)對(duì)象,通過(guò)它可以讀取和寫(xiě)入數(shù)據(jù),當(dāng)然所有數(shù)據(jù)都通過(guò)Buffer對(duì)象來(lái)處理。我們永遠(yuǎn)不會(huì)將字節(jié)直接寫(xiě)入通道,而是將數(shù)據(jù)寫(xiě)入包含一個(gè)或者多個(gè)字節(jié)的緩沖區(qū)。同樣也不會(huì)直接從通道中讀取字節(jié),而是將數(shù)據(jù)從通道讀入緩沖區(qū),再?gòu)木彌_區(qū)獲取這個(gè)字節(jié)。NIO提供了多種通道對(duì)象,所有的通道對(duì)象都實(shí)現(xiàn)了Channel接口。它們之間的繼承關(guān)系如下圖所示。1.使用NIO讀取數(shù)據(jù)前面我們說(shuō)過(guò),任何時(shí)候讀取數(shù)據(jù),都不是直接從通道讀取的,而是從通道讀取到緩沖區(qū)的。所以使用NIO讀取數(shù)據(jù)可以分為下面三個(gè)步驟。(1)從FileInputStream獲取Channel。(2)創(chuàng)建Buffer。(3)將數(shù)據(jù)從Channel讀取到Buffer中。下面是一個(gè)簡(jiǎn)單的使用NIO從文件中讀取數(shù)據(jù)的例子。2.使用NIO寫(xiě)入數(shù)據(jù)使用NIO寫(xiě)入數(shù)據(jù)與讀取數(shù)據(jù)的過(guò)程類似,數(shù)據(jù)同樣不是直接寫(xiě)入通道的,而是寫(xiě)入緩沖區(qū),可以分為下面三個(gè)步驟。(1)從FileInputStream獲取Channel。(2)創(chuàng)建Buffer。(3)將數(shù)據(jù)從Channel寫(xiě)入Buffer。下面是一個(gè)簡(jiǎn)單的使用NIO向文件中寫(xiě)入數(shù)據(jù)的例子。3.多路復(fù)用I/O我們?cè)囅胍幌逻@樣的現(xiàn)實(shí)場(chǎng)景。一個(gè)餐廳同時(shí)有100位客人到店,到店后要做的第一件事情就是點(diǎn)菜。但是問(wèn)題來(lái)了,餐廳老板為了節(jié)約人力成本,目前只有一名大堂服務(wù)員拿著唯一的一本菜單給客人提供服務(wù)。那么最笨(但是最簡(jiǎn)單)的方法(方法A)是,無(wú)論有多少客人等待點(diǎn)餐,服務(wù)員都把僅有的一份菜單遞給其中一位客人,然后站在客人身旁等待這位客人完成點(diǎn)菜過(guò)程。在記錄客人的點(diǎn)菜內(nèi)容后,把點(diǎn)菜記錄交給后堂廚師。然后是第二位客人、第三位客人……很明顯,沒(méi)有老板會(huì)這樣設(shè)置服務(wù)流程。因?yàn)殡S后的80位客人,在等待超時(shí)后就會(huì)離店(還會(huì)給差評(píng))。于是還有一種辦法(方法B),老板馬上新雇傭99名服務(wù)員,同時(shí)印制99本新的菜單。每一名服務(wù)員手持一本菜單負(fù)責(zé)一位客人(關(guān)鍵不只在于服務(wù)員,還在于菜單。因?yàn)闆](méi)有菜單,客人也無(wú)法點(diǎn)菜)。在客人點(diǎn)完菜后,服務(wù)員記錄點(diǎn)菜內(nèi)容交給后堂廚師(當(dāng)然為了更高效,后堂廚師最好也有100名),如下圖所示。這樣每一位客人享受的都是VIP服務(wù),當(dāng)然客人也不會(huì)走,但是人力成本卻很高(必虧無(wú)疑)。另外一種辦法(方法C),就是改進(jìn)點(diǎn)菜的方式,當(dāng)客人到店后,自己申請(qǐng)一本菜單。想好自己要點(diǎn)的菜后,就呼叫服務(wù)員。服務(wù)員站在自己身邊記錄客人的菜單內(nèi)容。將菜單遞給廚師的過(guò)程也要進(jìn)行改進(jìn),并不是每一份菜單記錄好以后,都要交給后堂廚師。服務(wù)員可以記錄好多份菜單后,同時(shí)交給廚師就行了,如下圖所示。那么這種方式,對(duì)于老板來(lái)說(shuō)人力成本是最低的;對(duì)于客人來(lái)說(shuō),雖然不再享受VIP服務(wù),并且要進(jìn)行一段時(shí)間的等待,但是這些都是可以接受的;對(duì)于服務(wù)員來(lái)說(shuō),基本上她的時(shí)間都沒(méi)有浪費(fèi),最大程度地提高了時(shí)間利用率。如果你是老板,你會(huì)采用哪種方式呢?到店情況:并發(fā)量。到店情況不理想時(shí),一個(gè)服務(wù)員一本菜單,當(dāng)然是足夠的。所以不同的老板在不同的場(chǎng)合下,將會(huì)靈活選擇服務(wù)員和菜單的配置??腿耍嚎蛻舳苏?qǐng)求。點(diǎn)餐內(nèi)容:客戶端發(fā)送的實(shí)際數(shù)據(jù)。老板:操作系統(tǒng)。人力成本:系統(tǒng)資源。菜單:文件描述符(FD)。操作系統(tǒng)對(duì)于一個(gè)進(jìn)程能夠同時(shí)持有的文件描述符的個(gè)數(shù)是有限制的,在Linux系統(tǒng)中用$ulimit-n查看這個(gè)限制值,當(dāng)然也是可以(并且應(yīng)該)進(jìn)行內(nèi)核參數(shù)調(diào)整的。服務(wù)員:操作系統(tǒng)內(nèi)核用于I/O操作的線程(內(nèi)核線程)。廚師:應(yīng)用程序線程(當(dāng)然廚房就是應(yīng)用程序進(jìn)程)。方法A:同步I/O。方法B:同步I/O。方法C:多路復(fù)用I/O。目前流行的多路復(fù)用I/O的實(shí)現(xiàn)主要包括四種:select、poll、epoll、kqueue。如下表所示是它們的一些重要特性的比較。多路復(fù)用I/O技術(shù)最適用的是“高并發(fā)”場(chǎng)景,所謂“高并發(fā)”是指1ms內(nèi)至少同時(shí)有上千個(gè)連接請(qǐng)求準(zhǔn)備好。其他情況下多路復(fù)用I/O技術(shù)發(fā)揮不出它的優(yōu)勢(shì)。另外,使用JavaNIO進(jìn)行功能實(shí)現(xiàn),相對(duì)于傳統(tǒng)的套接字實(shí)現(xiàn)要復(fù)雜一些,所以實(shí)際應(yīng)用中,需要根據(jù)自己的業(yè)務(wù)需求進(jìn)行技術(shù)選擇。3.2NIO源碼初探說(shuō)到源碼,先得從Selector的open方法開(kāi)始看,我們看java.nio.channels.Selector類的源碼??碨electorPvider()的具體代碼。其中provider=sun.nio.ch.DefaultSelectorProvider.create()會(huì)根據(jù)操作系統(tǒng)來(lái)返回不同的實(shí)現(xiàn)類,Windows平臺(tái)返回WindowsSelectorProvider;而if(provider!=null)returnprovider保證了整個(gè)Server程序中只有一個(gè)WindowsSelectorProvider對(duì)象;看WindowsSelectorProvider.openSelector()代碼。newWindowsSelectorImpl(SelectorProvider)的代碼如下。其中Pipe.open()是關(guān)鍵,這個(gè)方法的調(diào)用過(guò)程如下。在SelectorProvider中,代碼如下。再看一下PipeImpl()的代碼。其中IOUtil.makePipe(true)是一個(gè)本地方法。具體實(shí)現(xiàn)代碼如下。正如下面這段注釋所描述的內(nèi)容。高位存放的是通道read端的文件描述符,低32位存放的是write端的文件描述符。所以取得makepipe()返回值后要做移位處理。這行代碼把返回的pipe的write端的FD放在pollWrapper中(后面會(huì)發(fā)現(xiàn)這么做是為了實(shí)現(xiàn)Selector的wakeup())。ServerSocketChannel.open()的實(shí)現(xiàn)代碼如下。SelectorProvider的實(shí)現(xiàn)代碼如下??梢?jiàn)ServerSocketChannelImpl也有WindowsSelectorImpl的引用。然后通過(guò)serverChannel1.register(selector,SelectionKey.OP_ACCEPT);把Selector和Channel綁定在一起,也就是把新建ServerSocketChannel時(shí)創(chuàng)建的FD與Selector綁定在一起。到此,Server端已啟動(dòng)完成,主要?jiǎng)?chuàng)建了以下對(duì)象。(1)WindowsSelectorProvider:為單例對(duì)象,實(shí)際上是調(diào)用操作系統(tǒng)的API。。(2)WindowsSelectorImpl中包含如下內(nèi)容?!駊ollWrapper:保存Selector上注冊(cè)的FD,包括pipe的write端FD和ServerSocketChannel所用的FD?!駑akeupPipe:通道(其實(shí)就是兩個(gè)FD,一個(gè)是read端的,一個(gè)是write端的)。下面來(lái)看Selector中的select()方法,selector.select()主要調(diào)用了WindowsSelectorImpl中的doSelect()方法。其中subSelector.poll()是核心,也就是輪詢pollWrapper中保存的FD;具體實(shí)現(xiàn)是調(diào)用native方法poll0()。poll0.()會(huì)監(jiān)聽(tīng)pollWrapper中的FD有沒(méi)有數(shù)據(jù)進(jìn)出,這會(huì)造成I/O阻塞,直到有數(shù)據(jù)讀寫(xiě)事件發(fā)生。比如,由于pollWrapper中保存的也有ServerSocketChannel的FD,所以只要ClientSocket發(fā)一份數(shù)據(jù)到ServerSocket,那么poll0()就會(huì)返回;又由于pollWrapper中保存的也有pipe的write端的FD,所以只要pipe的write端向FD發(fā)一份數(shù)據(jù),也會(huì)造成poll0()返回;如果這兩種情況都沒(méi)有發(fā)生,那么poll0()就會(huì)一直阻塞,也就是selector.select()會(huì)一直阻塞;如果有任何一種情況發(fā)生,那么selector.select()就會(huì)返回,所以在OperationServer的run()里要用while(true),這樣可以保證在Selector接收數(shù)據(jù)并處理完后繼續(xù)監(jiān)聽(tīng)poll()。再來(lái)看WindowsSelectorImpl.Wakeup()??梢?jiàn)wakeup()是通過(guò)pipe的write端send(scoutFd,&byte,1,0)發(fā)送一個(gè)字節(jié)1來(lái)喚醒poll()的,所以在需要的時(shí)候就可以調(diào)用selector.wakeup()來(lái)喚醒Selector。3.3反應(yīng)堆現(xiàn)在我們已經(jīng)對(duì)阻塞I/O有了一定了解,知道阻塞I/O在調(diào)用InputStream.read()方法時(shí)是阻塞的,它會(huì)一直等到數(shù)據(jù)到來(lái)(或超時(shí))時(shí)才會(huì)返回;同樣,在調(diào)用ServerSocket.accept()方法時(shí),也會(huì)一直阻塞到有客戶端連接才會(huì)返回,每個(gè)客戶端連接成功后,服務(wù)端都會(huì)啟動(dòng)一個(gè)線程去處理該客戶端的請(qǐng)求。阻塞I/O的通信模型示意如下圖所示。如果仔細(xì)分析,一定會(huì)發(fā)現(xiàn)阻塞I/O存在一些缺點(diǎn)。根據(jù)阻塞I/O通信模型,總結(jié)了它的兩個(gè)缺點(diǎn)。(1)當(dāng)客戶端多時(shí),會(huì)創(chuàng)建大量的處理線程。且每個(gè)線程都要占用??臻g和一些CPU時(shí)間。(2)阻塞可能帶來(lái)頻繁的上下文切換,且大部分上下文切換可能是無(wú)意義的。在這種情況下非阻塞I/O就有了它的應(yīng)用前景。JavaNIO是從JDK1.4開(kāi)始使用的,它既可以說(shuō)成是“新I/O”,也可以說(shuō)成是“非阻塞I/O”。下面是Java
溫馨提示
- 1. 本站所有資源如無(wú)特殊說(shuō)明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請(qǐng)下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請(qǐng)聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁(yè)內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒(méi)有圖紙預(yù)覽就沒(méi)有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫(kù)網(wǎng)僅提供信息存儲(chǔ)空間,僅對(duì)用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對(duì)用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對(duì)任何下載內(nèi)容負(fù)責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請(qǐng)與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對(duì)自己和他人造成任何形式的傷害或損失。
最新文檔
- 2025裝飾裝修工程程施工合同范
- 2025年度地下綜合交通樞紐車(chē)位使用權(quán)出讓合同4篇
- 2025年度大蒜精油產(chǎn)品包裝與設(shè)計(jì)合作合同樣本4篇
- 2025版塔吊操作員培訓(xùn)與考核服務(wù)勞務(wù)分包合同協(xié)議書(shū)6篇
- 碎石購(gòu)買(mǎi)與合同履行監(jiān)督2025年度合同
- 2025年度車(chē)輛轉(zhuǎn)讓及二手車(chē)市場(chǎng)推廣服務(wù)合同4篇
- 2025年車(chē)輛掛靠售后服務(wù)及技術(shù)支持合同范本4篇
- 2025版協(xié)議離婚操作流程及子女撫養(yǎng)權(quán)分配合同3篇
- 2025年度瓷磚行業(yè)電子商務(wù)培訓(xùn)合同4篇
- 2025年度廠房裝修設(shè)計(jì)與施工環(huán)境風(fēng)險(xiǎn)評(píng)估合同書(shū)4篇
- 我的家鄉(xiāng)瓊海
- (2025)專業(yè)技術(shù)人員繼續(xù)教育公需課題庫(kù)(附含答案)
- 《互聯(lián)網(wǎng)現(xiàn)狀和發(fā)展》課件
- 【MOOC】計(jì)算機(jī)組成原理-電子科技大學(xué) 中國(guó)大學(xué)慕課MOOC答案
- 2024年上海健康醫(yī)學(xué)院?jiǎn)握新殬I(yè)適應(yīng)性測(cè)試題庫(kù)及答案解析
- 2024年湖北省武漢市中考語(yǔ)文適應(yīng)性試卷
- 非新生兒破傷風(fēng)診療規(guī)范(2024年版)解讀
- EDIFIER漫步者S880使用說(shuō)明書(shū)
- 皮膚惡性黑色素瘤-疾病研究白皮書(shū)
- 從心理學(xué)看現(xiàn)代家庭教育課件
- C語(yǔ)言程序設(shè)計(jì)PPT(第7版)高職完整全套教學(xué)課件
評(píng)論
0/150
提交評(píng)論