本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內(nèi)容請到我的倉庫里查看
喜歡的話麻煩點(diǎn)下Star哈
文章將同步到我的個人博客:
本文是微信公眾號【Java技術(shù)江湖】的《不可輕視的Java網(wǎng)絡(luò)編程》其中一篇角雷,本文部分內(nèi)容來源于網(wǎng)絡(luò)堕阔,為了把本文主題講得清晰透徹溉贿,也整合了很多我認(rèn)為不錯的技術(shù)博客內(nèi)容,引用其中了一些比較好的博客文章,如有侵權(quán)瓮恭,請聯(lián)系作者。
該系列博文會告訴你如何從計算機(jī)網(wǎng)絡(luò)的基礎(chǔ)知識入手厘熟,一步步地學(xué)習(xí)Java網(wǎng)絡(luò)基礎(chǔ)屯蹦,從socket到nio、bio绳姨、aio和netty等網(wǎng)絡(luò)編程知識登澜,并且進(jìn)行實(shí)戰(zhàn),網(wǎng)絡(luò)編程是每一個Java后端工程師必須要學(xué)習(xí)和理解的知識點(diǎn)飘庄,進(jìn)一步來說脑蠕,你還需要掌握Linux中的網(wǎng)絡(luò)編程原理,包括IO模型跪削、網(wǎng)絡(luò)編程框架netty的進(jìn)階原理谴仙,才能更完整地了解整個Java網(wǎng)絡(luò)編程的知識體系,形成自己的知識框架碾盐。
為了更好地總結(jié)和檢驗?zāi)愕膶W(xué)習(xí)成果晃跺,本系列文章也會提供部分知識點(diǎn)對應(yīng)的面試題以及參考答案。
如果對本系列文章有什么建議毫玖,或者是有什么疑問的話掀虎,也可以關(guān)注公眾號【Java技術(shù)江湖】聯(lián)系作者,歡迎你參與本系列博文的創(chuàng)作和修訂付枫。
基本概念說明
用戶空間與內(nèi)核空間
現(xiàn)在操作系統(tǒng)都是采用虛擬存儲器烹玉,那么對32位操作系統(tǒng)而言,它的尋址空間(虛擬存儲空間)為4G(2的32次方)励背。操作系統(tǒng)的核心是內(nèi)核春霍,獨(dú)立于普通的應(yīng)用程序,可以訪問受保護(hù)的內(nèi)存空間叶眉,也有訪問底層硬件設(shè)備的所有權(quán)限址儒。為了保證用戶進(jìn)程不能直接操作內(nèi)核(kernel),保證內(nèi)核的安全衅疙,操作系統(tǒng)將虛擬空間劃分為兩部分莲趣,一部分為內(nèi)核空間,一部分為用戶空間饱溢。針對linux操作系統(tǒng)而言喧伞,將最高的1G字節(jié)(從虛擬地址0xC0000000到0xFFFFFFFF),供內(nèi)核使用,稱為內(nèi)核空間潘鲫,而將較低的3G字節(jié)(從虛擬地址0x00000000到0xBFFFFFFF)翁逞,供各個進(jìn)程使用,稱為用戶空間溉仑。
進(jìn)程切換
為了控制進(jìn)程的執(zhí)行挖函,內(nèi)核必須有能力掛起正在CPU上運(yùn)行的進(jìn)程,并恢復(fù)以前掛起的某個進(jìn)程的執(zhí)行浊竟。這種行為被稱為進(jìn)程切換怨喘。因此可以說,任何進(jìn)程都是在操作系統(tǒng)內(nèi)核的支持下運(yùn)行的振定,是與內(nèi)核緊密相關(guān)的必怜。
從一個進(jìn)程的運(yùn)行轉(zhuǎn)到另一個進(jìn)程上運(yùn)行,這個過程中經(jīng)過下面這些變化:
- 保存處理機(jī)上下文后频,包括程序計數(shù)器和其他寄存器梳庆。
- 更新PCB信息。
- 把進(jìn)程的PCB移入相應(yīng)的隊列卑惜,如就緒靠益、在某事件阻塞等隊列。 選擇另一個進(jìn)程執(zhí)行残揉,并更新其PCB胧后。
- 更新內(nèi)存管理的數(shù)據(jù)結(jié)構(gòu)。
- 恢復(fù)處理機(jī)上下文抱环。
進(jìn)程的阻塞
正在執(zhí)行的進(jìn)程壳快,由于期待的某些事件未發(fā)生,如請求系統(tǒng)資源失敗镇草、等待某種操作的完成眶痰、新數(shù)據(jù)尚未到達(dá)或無新工作做等,則由系統(tǒng)自動執(zhí)行阻塞原語(Block)梯啤,使自己由運(yùn)行狀態(tài)變?yōu)樽枞麪顟B(tài)竖伯。可見因宇,進(jìn)程的阻塞是進(jìn)程自身的一種主動行為七婴,也因此只有處于運(yùn)行態(tài)的進(jìn)程(獲得CPU),才可能將其轉(zhuǎn)為阻塞狀態(tài)察滑。當(dāng)進(jìn)程進(jìn)入阻塞狀態(tài)打厘,是不占用CPU資源的。
文件描述符
文件描述符(File descriptor)是計算機(jī)科學(xué)中的一個術(shù)語贺辰,是一個用于表述指向文件的引用的抽象化概念户盯。
文件描述符在形式上是一個非負(fù)整數(shù)嵌施。實(shí)際上,它是一個索引值莽鸭,指向內(nèi)核為每一個進(jìn)程所維護(hù)的該進(jìn)程打開文件的記錄表吗伤。當(dāng)程序打開一個現(xiàn)有文件或者創(chuàng)建一個新文件時,內(nèi)核向進(jìn)程返回一個文件描述符硫眨。在程序設(shè)計中牲芋,一些涉及底層的程序編寫往往會圍繞著文件描述符展開。但是文件描述符這一概念往往只適用于UNIX捺球、Linux這樣的操作系統(tǒng)。
緩存 IO
緩存 IO 又被稱作標(biāo)準(zhǔn) IO夕冲,大多數(shù)文件系統(tǒng)的默認(rèn) IO 操作都是緩存 IO氮兵。在 Linux 的緩存 IO 機(jī)制中,操作系統(tǒng)會將 IO 的數(shù)據(jù)緩存在文件系統(tǒng)的頁緩存( page cache )中歹鱼,也就是說泣栈,數(shù)據(jù)會先被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中,然后才會從操作系統(tǒng)內(nèi)核的緩沖區(qū)拷貝到應(yīng)用程序的地址空間弥姻。
緩存 IO 的缺點(diǎn):
數(shù)據(jù)在傳輸過程中需要在應(yīng)用程序地址空間和內(nèi)核進(jìn)行多次數(shù)據(jù)拷貝操作南片,這些數(shù)據(jù)拷貝操作所帶來的 CPU 以及內(nèi)存開銷是非常大的。
IO模型介紹
作者:cooffeelis
鏈接:http://www.reibang.com/p/511b9cffbdac
來源:簡書
著作權(quán)歸作者所有庭敦。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán)疼进,非商業(yè)轉(zhuǎn)載請注明出處。
常用的5種IO模型:
blocking IO
nonblocking IO
IO multiplexing
signal driven IO
asynchronous IO
再說一下IO發(fā)生時涉及的對象和步驟:
對于一個network IO (這里我們以read舉例)秧廉,它會涉及到兩個系統(tǒng)對象:
- 一個是調(diào)用這個IO的process (or thread)
- 一個就是系統(tǒng)內(nèi)核(kernel)
當(dāng)一個read操作發(fā)生時伞广,它會經(jīng)歷兩個階段:
- 等待數(shù)據(jù)準(zhǔn)備,比如accept(), recv()等待數(shù)據(jù)
(Waiting for the data to be ready)
- 將數(shù)據(jù)從內(nèi)核拷貝到進(jìn)程中, 比如 accept()接受到請求,recv()接收連接發(fā)送的數(shù)據(jù)后需要復(fù)制到內(nèi)核,再從內(nèi)核復(fù)制到進(jìn)程用戶空間
(Copying the data from the kernel to the process)
對于socket流而言,數(shù)據(jù)的流向經(jīng)歷兩個階段:
- 第一步通常涉及等待網(wǎng)絡(luò)上的數(shù)據(jù)分組到達(dá),然后被復(fù)制到內(nèi)核的某個緩沖區(qū)疼电。
- 第二步把數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到應(yīng)用進(jìn)程緩沖區(qū)嚼锄。
記住這兩點(diǎn)很重要,因為這些IO Model的區(qū)別就是在兩個階段上各有不同的情況蔽豺。
阻塞 I/O(blocking IO)
在linux中区丑,默認(rèn)情況下所有的socket都是blocking,一個典型的讀操作流程大概是這樣:
阻塞IO流程
當(dāng)用戶進(jìn)程調(diào)用了recvfrom這個系統(tǒng)調(diào)用修陡,kernel就開始了IO的第一個階段:準(zhǔn)備數(shù)據(jù)(對于網(wǎng)絡(luò)IO來說沧侥,很多時候數(shù)據(jù)在一開始還沒有到達(dá)。比如魄鸦,還沒有收到一個完整的UDP包正什。這個時候kernel就要等待足夠的數(shù)據(jù)到來)。這個過程需要等待号杏,也就是說數(shù)據(jù)被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中是需要一個過程的婴氮。而在用戶進(jìn)程這邊斯棒,整個進(jìn)程會被阻塞(當(dāng)然,是進(jìn)程自己選擇的阻塞)主经。當(dāng)kernel一直等到數(shù)據(jù)準(zhǔn)備好了荣暮,它就會將數(shù)據(jù)從kernel中拷貝到用戶內(nèi)存,然后kernel返回結(jié)果罩驻,用戶進(jìn)程才解除block的狀態(tài)穗酥,重新運(yùn)行起來。
所以惠遏,blocking IO的特點(diǎn)就是在IO執(zhí)行的兩個階段都被block了砾跃。
非阻塞 I/O(nonblocking IO)
linux下,可以通過設(shè)置socket使其變?yōu)閚on-blocking节吮。當(dāng)對一個non-blocking socket執(zhí)行讀操作時抽高,流程是這個樣子:
非阻塞 I/O 流程
當(dāng)用戶進(jìn)程發(fā)出read操作時,如果kernel中的數(shù)據(jù)還沒有準(zhǔn)備好透绩,那么它并不會block用戶進(jìn)程翘骂,而是立刻返回一個error。從用戶進(jìn)程角度講 帚豪,它發(fā)起一個read操作后碳竟,并不需要等待,而是馬上就得到了一個結(jié)果狸臣。用戶進(jìn)程判斷結(jié)果是一個error時莹桅,它就知道數(shù)據(jù)還沒有準(zhǔn)備好,于是它可以再次發(fā)送read操作烛亦。一旦kernel中的數(shù)據(jù)準(zhǔn)備好了统翩,并且又再次收到了用戶進(jìn)程的system call,那么它馬上就將數(shù)據(jù)拷貝到了用戶內(nèi)存此洲,然后返回厂汗。
所以,nonblocking IO的特點(diǎn)是用戶進(jìn)程需要不斷的主動詢問kernel數(shù)據(jù)好了沒有呜师。
***值得注意的是,此時的非阻塞IO只是應(yīng)用到等待數(shù)據(jù)上,當(dāng)真正有數(shù)據(jù)到達(dá)執(zhí)行recvfrom的時候,還是同步阻塞IO來的, 從圖中的copy data from kernel to user可以看出 ***
I/O 多路復(fù)用( IO multiplexing)
IO multiplexing就是我們說的select娶桦,poll,epoll汁汗,有些地方也稱這種IO方式為event driven IO衷畦。select/epoll的好處就在于單個process就可以同時處理多個網(wǎng)絡(luò)連接的IO。它的基本原理就是select知牌,poll祈争,epoll這個function會不斷的輪詢所負(fù)責(zé)的所有socket,當(dāng)某個socket有數(shù)據(jù)到達(dá)了角寸,就通知用戶進(jìn)程菩混。
I/O 多路復(fù)用流程
這個圖和blocking IO的圖其實(shí)并沒有太大的不同忿墅,事實(shí)上,還更差一些沮峡。因為這里需要使用兩個system call (select 和 recvfrom)疚脐,而blocking IO只調(diào)用了一個system call (recvfrom)。但是邢疙,用select的優(yōu)勢在于它可以同時處理多個connection棍弄。
所以,如果處理的連接數(shù)不是很高的話疟游,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好呼畸,可能延遲還更大。select/epoll的優(yōu)勢并不是對于單個連接能處理得更快颁虐,而是在于能處理更多的連接蛮原。)
IO復(fù)用的實(shí)現(xiàn)方式目前主要有select、poll和epoll聪廉。
select和poll的原理基本相同:
- 注冊待偵聽的fd(這里的fd創(chuàng)建時最好使用非阻塞)
- 每次調(diào)用都去檢查這些fd的狀態(tài),當(dāng)有一個或者多個fd就緒的時候返回
- 返回結(jié)果中包括已就緒和未就緒的fd
相比select故慈,poll解決了單個進(jìn)程能夠打開的文件描述符數(shù)量有限制這個問題:select受限于FD_SIZE的限制板熊,如果修改則需要修改這個宏重新編譯內(nèi)核;而poll通過一個pollfd數(shù)組向內(nèi)核傳遞需要關(guān)注的事件察绷,避開了文件描述符數(shù)量限制干签。
此外,select和poll共同具有的一個很大的缺點(diǎn)就是包含大量fd的數(shù)組被整體復(fù)制于用戶態(tài)和內(nèi)核態(tài)地址空間之間拆撼,開銷會隨著fd數(shù)量增多而線性增大容劳。
select和poll就類似于上面說的就餐方式。但當(dāng)你每次都去詢問時闸度,老板會把所有你點(diǎn)的飯菜都輪詢一遍再告訴你情況竭贩,當(dāng)大量飯菜很長時間都不能準(zhǔn)備好的情況下是很低效的。于是莺禁,老板有些不耐煩了留量,就讓廚師每做好一個菜就通知他。這樣每次你再去問的時候哟冬,他會直接把已經(jīng)準(zhǔn)備好的菜告訴你楼熄,你再去端。這就是事件驅(qū)動IO就緒通知的方式-epoll浩峡。
epoll的出現(xiàn)可岂,解決了select、poll的缺點(diǎn):
- 基于事件驅(qū)動的方式翰灾,避免了每次都要把所有fd都掃描一遍缕粹。
- epoll_wait只返回就緒的fd稚茅。
- epoll使用nmap內(nèi)存映射技術(shù)避免了內(nèi)存復(fù)制的開銷。
- epoll的fd數(shù)量上限是操作系統(tǒng)的最大文件句柄數(shù)目,這個數(shù)目一般和內(nèi)存有關(guān)致开,通常遠(yuǎn)大于1024峰锁。
目前,epoll是Linux2.6下最高效的IO復(fù)用方式双戳,也是Nginx虹蒋、Node的IO實(shí)現(xiàn)方式。而在freeBSD下飒货,kqueue是另一種類似于epoll的IO復(fù)用方式魄衅。
此外,對于IO復(fù)用還有一個水平觸發(fā)和邊緣觸發(fā)的概念:
- 水平觸發(fā):當(dāng)就緒的fd未被用戶進(jìn)程處理后塘辅,下一次查詢依舊會返回晃虫,這是select和poll的觸發(fā)方式。
- 邊緣觸發(fā):無論就緒的fd是否被處理扣墩,下一次不再返回哲银。理論上性能更高,但是實(shí)現(xiàn)相當(dāng)復(fù)雜呻惕,并且任何意外的丟失事件都會造成請求處理錯誤荆责。epoll默認(rèn)使用水平觸發(fā),通過相應(yīng)選項可以使用邊緣觸發(fā)亚脆。
點(diǎn)評:
I/O 多路復(fù)用的特點(diǎn)是通過一種機(jī)制一個進(jìn)程能同時等待多個文件描述符做院,而這些文件描述符(套接字描述符)其中的任意一個進(jìn)入讀就緒狀態(tài),select()函數(shù)就可以返回濒持。
所以, IO多路復(fù)用键耕,本質(zhì)上不會有并發(fā)的功能,因為任何時候還是只有一個進(jìn)程或線程進(jìn)行工作柑营,它之所以能提高效率是因為select\epoll 把進(jìn)來的socket放到他們的 '監(jiān)視' 列表里面屈雄,當(dāng)任何socket有可讀可寫數(shù)據(jù)立馬處理,那如果select\epoll 手里同時檢測著很多socket官套, 一有動靜馬上返回給進(jìn)程處理棚亩,總比一個一個socket過來,阻塞等待,處理高效率。
當(dāng)然也可以多線程/多進(jìn)程方式虏杰,一個連接過來開一個進(jìn)程/線程處理讥蟆,這樣消耗的內(nèi)存和進(jìn)程切換頁會耗掉更多的系統(tǒng)資源。
所以我們可以結(jié)合IO多路復(fù)用和多進(jìn)程/多線程 來高性能并發(fā)纺阔,IO復(fù)用負(fù)責(zé)提高接受socket的通知效率瘸彤,收到請求后,交給進(jìn)程池/線程池來處理邏輯笛钝。信號驅(qū)動
上文的就餐方式還是需要你每次都去問一下飯菜狀況质况。于是愕宋,你再次不耐煩了,就跟老板說结榄,哪個飯菜好了就通知我一聲吧中贝。然后就自己坐在桌子那里干自己的事情。更甚者臼朗,你可以把手機(jī)號留給老板邻寿,自己出門,等飯菜好了直接發(fā)條短信給你视哑。這就類似信號驅(qū)動的IO模型绣否。
流程如下:
- 開啟套接字信號驅(qū)動IO功能
- 系統(tǒng)調(diào)用sigaction執(zhí)行信號處理函數(shù)(非阻塞,立刻返回)
- 數(shù)據(jù)就緒挡毅,生成sigio信號蒜撮,通過信號回調(diào)通知應(yīng)用來讀取數(shù)據(jù)。
此種io方式存在的一個很大的問題:Linux中信號隊列是有限制的跪呈,如果超過這個數(shù)字問題就無法讀取數(shù)據(jù)段磨。
異步非阻塞
異步 I/O(asynchronous IO)
linux下的asynchronous IO其實(shí)用得很少。先看一下它的流程:
異步IO 流程
用戶進(jìn)程發(fā)起read操作之后耗绿,立刻就可以開始去做其它的事苹支。而另一方面,從kernel的角度缭乘,當(dāng)它受到一個asynchronous read之后沐序,首先它會立刻返回,所以不會對用戶進(jìn)程產(chǎn)生任何block。然后凤巨,kernel會等待數(shù)據(jù)準(zhǔn)備完成砚作,然后將數(shù)據(jù)拷貝到用戶內(nèi)存,當(dāng)這一切都完成之后懂牧,kernel會給用戶進(jìn)程發(fā)送一個signal,告訴它read操作完成了。
阻塞IO,非阻塞IO 與 同步IO, 異步IO的區(qū)別和聯(lián)系
阻塞IO VS 非阻塞IO:
概念:
阻塞和非阻塞關(guān)注的是程序在等待調(diào)用結(jié)果(消息黍氮,返回值)時的狀態(tài).
阻塞調(diào)用是指調(diào)用結(jié)果返回之前,當(dāng)前線程會被掛起浅浮。調(diào)用線程只有在得到結(jié)果之后才會返回沫浆。非阻塞調(diào)用指在不能立刻得到結(jié)果之前,該調(diào)用不會阻塞當(dāng)前線程滚秩。
例子:你打電話問書店老板有沒有《分布式系統(tǒng)》這本書专执,你如果是阻塞式調(diào)用,你會一直把自己“掛起”郁油,直到得到這本書有沒有的結(jié)果本股,如果是非阻塞式調(diào)用攀痊,你不管老板有沒有告訴你,你自己先一邊去玩了拄显, 當(dāng)然你也要偶爾過幾分鐘check一下老板有沒有返回結(jié)果苟径。在這里阻塞與非阻塞與是否同步異步無關(guān)。跟老板通過什么方式回答你結(jié)果無關(guān)躬审。
分析:
阻塞IO會一直block住對應(yīng)的進(jìn)程直到操作完成棘街,而非阻塞IO在kernel還準(zhǔn)備數(shù)據(jù)的情況下會立刻返回。
同步IO VS 異步IO:
概念:
同步與異步同步和異步關(guān)注的是消息通信機(jī)制 (synchronous communication/ asynchronous communication)所謂同步盒件,就是在發(fā)出一個調(diào)用時蹬碧,在沒有得到結(jié)果之前,該調(diào)用就不返回炒刁。但是一旦調(diào)用返回恩沽,就得到返回值了。換句話說翔始,就是由調(diào)用者主動等待這個調(diào)用的結(jié)果罗心。而異步則是相反,調(diào)用在發(fā)出之后城瞎,這個調(diào)用就直接返回了渤闷,所以沒有返回結(jié)果。換句話說脖镀,當(dāng)一個異步過程調(diào)用發(fā)出后飒箭,調(diào)用者不會立刻得到結(jié)果。而是在調(diào)用發(fā)出后蜒灰,被調(diào)用者通過狀態(tài)弦蹂、通知來通知調(diào)用者,或通過回調(diào)函數(shù)處理這個調(diào)用强窖。
典型的異步編程模型比如Node.js舉個通俗的例子:你打電話問書店老板有沒有《分布式系統(tǒng)》這本書凸椿,如果是同步通信機(jī)制,書店老板會說翅溺,你稍等脑漫,”我查一下",然后開始查啊查咙崎,等查好了(可能是5秒优幸,也可能是一天)告訴你結(jié)果(返回結(jié)果)。而異步通信機(jī)制褪猛,書店老板直接告訴你我查一下啊网杆,查好了打電話給你,然后直接掛電話了(不返回結(jié)果)。然后查好了跛璧,他會主動打電話給你严里。在這里老板通過“回電”這種方式來回調(diào)。
分析:
在說明同步IO和異步IO的區(qū)別之前追城,需要先給出兩者的定義刹碾。Stevens給出的定義(其實(shí)是POSIX的定義)是這樣子的:
A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
An asynchronous I/O operation does not cause the requesting process to be blocked;
兩者的區(qū)別就在于同步IO做”IO operation”的時候會將process阻塞。按照這個定義座柱,之前所述的阻塞IO,非阻塞IO 迷帜,IO復(fù)用都屬于同步IO。
有人可能會說色洞,非阻塞IO 并沒有被block啊戏锹。這里有個非常“狡猾”的地方火诸,定義中所指的”IO operation”是指真實(shí)的IO操作锦针,就是例子中的recvfrom這個system call。非阻塞IO在執(zhí)行recvfrom這個system call的時候置蜀,如果kernel的數(shù)據(jù)沒有準(zhǔn)備好奈搜,這時候不會block進(jìn)程。但是盯荤,當(dāng)kernel中數(shù)據(jù)準(zhǔn)備好的時候馋吗,recvfrom會將數(shù)據(jù)從kernel拷貝到用戶內(nèi)存中,這個時候進(jìn)程是被block了秋秤,在這段時間內(nèi)宏粤,進(jìn)程是被block的。
而異步IO則不一樣灼卢,當(dāng)進(jìn)程發(fā)起IO 操作之后绍哎,就直接返回再也不理睬了,直到kernel發(fā)送一個信號芥玉,告訴進(jìn)程說IO完成蛇摸。在這整個過程中备图,進(jìn)程完全沒有被block灿巧。
IO模型的形象舉例
最后,再舉幾個不是很恰當(dāng)?shù)睦觼碚f明這四個IO Model:
有A揽涮,B抠藕,C,D四個人在釣魚:
A用的是最老式的魚竿蒋困,所以呢盾似,得一直守著,等到魚上鉤了再拉桿;
B的魚竿有個功能零院,能夠顯示是否有魚上鉤溉跃,所以呢,B就和旁邊的MM聊天告抄,隔會再看看有沒有魚上鉤撰茎,有的話就迅速拉桿;
C用的魚竿和B差不多打洼,但他想了一個好辦法龄糊,就是同時放好幾根魚竿,然后守在旁邊募疮,一旦有顯示說魚上鉤了炫惩,它就將對應(yīng)的魚竿拉起來;
D是個有錢人阿浓,干脆雇了一個人幫他釣魚他嚷,一旦那個人把魚釣上來了,就給D發(fā)個短信芭毙。
Select/Poll/Epoll 輪詢機(jī)制
select爸舒,poll,epoll本質(zhì)上都是同步I/O稿蹲,因為他們都需要在讀寫事件就緒后自己負(fù)責(zé)進(jìn)行讀寫扭勉,也就是說這個讀寫過程是阻塞的
Select/Poll/Epoll 都是IO復(fù)用的實(shí)現(xiàn)方式, 上面說了使用IO復(fù)用苛聘,會把socket設(shè)置成non-blocking涂炎,然后放進(jìn)Select/Poll/Epoll 各自的監(jiān)視列表里面,那么设哗,他們的對socket是否有數(shù)據(jù)到達(dá)的監(jiān)視機(jī)制分別是怎樣的唱捣?效率又如何?我們應(yīng)該使用哪種方式實(shí)現(xiàn)IO復(fù)用比較好网梢?下面列出他們各自的實(shí)現(xiàn)方式震缭,效率,優(yōu)缺點(diǎn):
(1)select战虏,poll實(shí)現(xiàn)需要自己不斷輪詢所有fd集合拣宰,直到設(shè)備就緒,期間可能要睡眠和喚醒多次交替烦感。而epoll其實(shí)也需要調(diào)用epoll_wait不斷輪詢就緒鏈表巡社,期間也可能多次睡眠和喚醒交替,但是它是設(shè)備就緒時手趣,調(diào)用回調(diào)函數(shù)晌该,把就緒fd放入就緒鏈表中,并喚醒在epoll_wait中進(jìn)入睡眠的進(jìn)程。雖然都要睡眠和交替朝群,但是select和poll在“醒著”的時候要遍歷整個fd集合燕耿,而epoll在“醒著”的時候只要判斷一下就緒鏈表是否為空就行了,這節(jié)省了大量的CPU時間姜胖。這就是回調(diào)機(jī)制帶來的性能提升缸棵。
(2)select,poll每次調(diào)用都要把fd集合從用戶態(tài)往內(nèi)核態(tài)拷貝一次谭期,并且要把current往設(shè)備等待隊列中掛一次堵第,而epoll只要一次拷貝,而且把current往等待隊列上掛也只掛一次(在epoll_wait的開始隧出,注意這里的等待隊列并不是設(shè)備等待隊列踏志,只是一個epoll內(nèi)部定義的等待隊列)。這也能節(jié)省不少的開銷胀瞪。
Java網(wǎng)絡(luò)編程模型
上文講述了UNIX環(huán)境的五種IO模型针余。基于這五種模型凄诞,在Java中圆雁,隨著NIO和NIO2.0(AIO)的引入,一般具有以下幾種網(wǎng)絡(luò)編程模型:
- BIO
- NIO
- AIO
BIO
BIO是一個典型的網(wǎng)絡(luò)編程模型帆谍,是通常我們實(shí)現(xiàn)一個服務(wù)端程序的過程伪朽,步驟如下:
- 主線程accept請求阻塞
- 請求到達(dá),創(chuàng)建新的線程來處理這個套接字汛蝙,完成對客戶端的響應(yīng)烈涮。
- 主線程繼續(xù)accept下一個請求
這種模型有一個很大的問題是:當(dāng)客戶端連接增多時,服務(wù)端創(chuàng)建的線程也會暴漲窖剑,系統(tǒng)性能會急劇下降坚洽。因此,在此模型的基礎(chǔ)上西土,類似于 tomcat的bio connector讶舰,采用的是線程池來避免對于每一個客戶端都創(chuàng)建一個線程。有些地方把這種方式叫做偽異步IO(把請求拋到線程池中異步等待處理)需了。
NIO
JDK1.4開始引入了NIO類庫跳昼,這里的NIO指的是New IO,主要是使用Selector多路復(fù)用器來實(shí)現(xiàn)援所。Selector在Linux等主流操作系統(tǒng)上是通過epoll實(shí)現(xiàn)的庐舟。
NIO的實(shí)現(xiàn)流程欣除,類似于select:
- 創(chuàng)建ServerSocketChannel監(jiān)聽客戶端連接并綁定監(jiān)聽端口住拭,設(shè)置為非阻塞模式。
- 創(chuàng)建Reactor線程,創(chuàng)建多路復(fù)用器(Selector)并啟動線程滔岳。
- 將ServerSocketChannel注冊到Reactor線程的Selector上杠娱。監(jiān)聽accept事件。
- Selector在線程run方法中無線循環(huán)輪詢準(zhǔn)備就緒的Key谱煤。
- Selector監(jiān)聽到新的客戶端接入摊求,處理新的請求,完成tcp三次握手刘离,建立物理連接室叉。
- 將新的客戶端連接注冊到Selector上,監(jiān)聽讀操作硫惕。讀取客戶端發(fā)送的網(wǎng)絡(luò)消息茧痕。
- 客戶端發(fā)送的數(shù)據(jù)就緒則讀取客戶端請求,進(jìn)行處理恼除。
相比BIO踪旷,NIO的編程非常復(fù)雜。
AIO
JDK1.7引入NIO2.0豁辉,提供了異步文件通道和異步套接字通道的實(shí)現(xiàn)令野。其底層在windows上是通過IOCP,在Linux上是通過epoll來實(shí)現(xiàn)的(LinuxAsynchronousChannelProvider.java,UnixAsynchronousServerSocketChannelImpl.java)徽级。
- 創(chuàng)建AsynchronousServerSocketChannel气破,綁定監(jiān)聽端口
- 調(diào)用AsynchronousServerSocketChannel的accpet方法,傳入自己實(shí)現(xiàn)的CompletionHandler餐抢。包括上一步堵幽,都是非阻塞的
- 連接傳入,回調(diào)CompletionHandler的completed方法弹澎,在里面朴下,調(diào)用AsynchronousSocketChannel的read方法,傳入負(fù)責(zé)處理數(shù)據(jù)的CompletionHandler苦蒿。
- 數(shù)據(jù)就緒殴胧,觸發(fā)負(fù)責(zé)處理數(shù)據(jù)的CompletionHandler的completed方法。繼續(xù)做下一步處理即可佩迟。
- 寫入操作類似团滥,也需要傳入CompletionHandler。
其編程模型相比NIO有了不少的簡化报强。
對比
. | 同步阻塞IO | 偽異步IO | NIO | AIO |
---|---|---|---|---|
客戶端數(shù)目 :IO線程 | 1 : 1 | m : n | m : 1 | m : 0 |
IO模型 | 同步阻塞IO | 同步阻塞IO | 同步非阻塞IO | 異步非阻塞IO |
吞吐量 | 低 | 中 | 高 | 高 |
編程復(fù)雜度 | 簡單 | 簡單 | 非常復(fù)雜 | 復(fù)雜 |