Java網絡編程和NIO詳解3:IO模型與Java網絡編程模型

微信公衆號【Java技術江湖】一位阿里 Java 工程師的技術小站。(關注公衆號後回覆」Java「便可領取 Java基礎、進階、項目和架構師等免費學習資料,更有數據庫、分佈式、微服務等熱門技術學習視頻,內容豐富,兼顧原理和實踐,另外也將贈送做者原創的Java學習指南、Java程序員面試指南等乾貨資源)

基本概念說明

用戶空間與內核空間java

如今操做系統都是採用虛擬存儲器,那麼對32位操做系統而言,它的尋址空間(虛擬存儲空間)爲4G(2的32次方)。操做系統的核心是內核,獨立於普通的應用程序,能夠訪問受保護的內存空間,也有訪問底層硬件設備的全部權限。爲了保證用戶進程不能直接操做內核(kernel),保證內核的安全,操做系統將虛擬空間劃分爲兩部分,一部分爲內核空間,一部分爲用戶空間。針對linux操做系統而言,將最高的1G字節(從虛擬地址0xC0000000到0xFFFFFFFF),供內核使用,稱爲內核空間,而將較低的3G字節(從虛擬地址0x00000000到0xBFFFFFFF),供各個進程使用,稱爲用戶空間。linux

進程切換程序員

爲了控制進程的執行,內核必須有能力掛起正在CPU上運行的進程,並恢復之前掛起的某個進程的執行。這種行爲被稱爲進程切換。所以能夠說,任何進程都是在操做系統內核的支持下運行的,是與內核緊密相關的。web

從一個進程的運行轉到另外一個進程上運行,這個過程當中通過下面這些變化:面試

  • 保存處理機上下文,包括程序計數器和其餘寄存器。
  • 更新PCB信息。
  • 把進程的PCB移入相應的隊列,如就緒、在某事件阻塞等隊列。 選擇另外一個進程執行,並更新其PCB。
  • 更新內存管理的數據結構。
  • 恢復處理機上下文。

進程的阻塞數據庫

正在執行的進程,因爲期待的某些事件未發生,如請求系統資源失敗、等待某種操做的完成、新數據還沒有到達或無新工做作等,則由系統自動執行阻塞原語(Block),使本身由運行狀態變爲阻塞狀態。可見,進程的阻塞是進程自身的一種主動行爲,也所以只有處於運行態的進程(得到CPU),纔可能將其轉爲阻塞狀態。當進程進入阻塞狀態,是不佔用CPU資源的。編程

文件描述符windows

文件描述符(File descriptor)是計算機科學中的一個術語,是一個用於表述指向文件的引用的抽象化概念。後端

文件描述符在形式上是一個非負整數。實際上,它是一個索引值,指向內核爲每個進程所維護的該進程打開文件的記錄表。當程序打開一個現有文件或者建立一個新文件時,內核向進程返回一個文件描述符。在程序設計中,一些涉及底層的程序編寫每每會圍繞着文件描述符展開。可是文件描述符這一律念每每只適用於UNIX、Linux這樣的操做系統。數組

緩存 IO

緩存 IO 又被稱做標準 IO,大多數文件系統的默認 IO 操做都是緩存 IO。在 Linux 的緩存 IO 機制中,操做系統會將 IO 的數據緩存在文件系統的頁緩存( page cache )中,也就是說,數據會先被拷貝到操做系統內核的緩衝區中,而後纔會從操做系統內核的緩衝區拷貝到應用程序的地址空間。

緩存 IO 的缺點:

數據在傳輸過程當中須要在應用程序地址空間和內核進行屢次數據拷貝操做,這些數據拷貝操做所帶來的 CPU 以及內存開銷是很是大的。

IO模型介紹

做者:cooffeelis
連接:https://www.jianshu.com/p/511...
來源:簡書
著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。

經常使用的5種IO模型:
blocking IO
nonblocking IO
IO multiplexing
signal driven IO
asynchronous IO

再說一下IO發生時涉及的對象和步驟:

對於一個network IO (這裏咱們以read舉例),它會涉及到兩個系統對象:

  • 一個是調用這個IO的process (or thread)
  • 一個就是系統內核(kernel)

當一個read操做發生時,它會經歷兩個階段:

  • 等待數據準備,好比accept(), recv()等待數據 (Waiting for the data to be ready)
  • 將數據從內核拷貝到進程中, 好比 accept()接受到請求,recv()接收鏈接發送的數據後須要複製到內核,再從內核複製到進程用戶空間(Copying the data from the kernel to the process)

對於socket流而言,數據的流向經歷兩個階段:

  • 第一步一般涉及等待網絡上的數據分組到達,而後被複制到內核的某個緩衝區。
  • 第二步把數據從內核緩衝區複製到應用進程緩衝區。

記住這兩點很重要,由於這些IO Model的區別就是在兩個階段上各有不一樣的狀況。

阻塞 I/O(blocking IO)

    • *

在linux中,默認狀況下全部的socket都是blocking,一個典型的讀操做流程大概是這樣:

阻塞IO流程

當用戶進程調用了recvfrom這個系統調用,kernel就開始了IO的第一個階段:準備數據(對於網絡IO來講,不少時候數據在一開始尚未到達。好比,尚未收到一個完整的UDP包。這個時候kernel就要等待足夠的數據到來)。這個過程須要等待,也就是說數據被拷貝到操做系統內核的緩衝區中是須要一個過程的。而在用戶進程這邊,整個進程會被阻塞(固然,是進程本身選擇的阻塞)。當kernel一直等到數據準備好了,它就會將數據從kernel中拷貝到用戶內存,而後kernel返回結果,用戶進程才解除block的狀態,從新運行起來。

因此,blocking IO的特色就是在IO執行的兩個階段都被block了。

非阻塞 I/O(nonblocking IO)

    • *

linux下,能夠經過設置socket使其變爲non-blocking。當對一個non-blocking socket執行讀操做時,流程是這個樣子:

非阻塞 I/O 流程

當用戶進程發出read操做時,若是kernel中的數據尚未準備好,那麼它並不會block用戶進程,而是馬上返回一個error。從用戶進程角度講 ,它發起一個read操做後,並不須要等待,而是立刻就獲得了一個結果。用戶進程判斷結果是一個error時,它就知道數據尚未準備好,因而它能夠再次發送read操做。一旦kernel中的數據準備好了,而且又再次收到了用戶進程的system call,那麼它立刻就將數據拷貝到了用戶內存,而後返回。

因此,nonblocking IO的特色是用戶進程須要不斷的主動詢問kernel數據好了沒有。

值得注意的是,此時的非阻塞IO只是應用到等待數據上,當真正有數據到達執行recvfrom的時候,仍是同步阻塞IO來的, 從圖中的copy data from kernel to user能夠看出

I/O 多路複用( IO multiplexing)

    • *

IO multiplexing就是咱們說的select,poll,epoll,有些地方也稱這種IO方式爲event driven IO。select/epoll的好處就在於單個process就能夠同時處理多個網絡鏈接的IO。它的基本原理就是select,poll,epoll這個function會不斷的輪詢所負責的全部socket,當某個socket有數據到達了,就通知用戶進程。

I/O 多路複用流程

這個圖和blocking IO的圖其實並無太大的不一樣,事實上,還更差一些。由於這裏須要使用兩個system call (select 和 recvfrom),而blocking IO只調用了一個system call (recvfrom)。可是,用select的優點在於它能夠同時處理多個connection。

因此,若是處理的鏈接數不是很高的話,使用select/epoll的web server不必定比使用multi-threading + blocking IO的web server性能更好,可能延遲還更大。select/epoll的優點並非對於單個鏈接能處理得更快,而是在於能處理更多的鏈接。)

IO複用的實現方式目前主要有select、poll和epoll。

select和poll的原理基本相同:

  • 註冊待偵聽的fd(這裏的fd建立時最好使用非阻塞)
  • 每次調用都去檢查這些fd的狀態,當有一個或者多個fd就緒的時候返回
  • 返回結果中包括已就緒和未就緒的fd

相比select,poll解決了單個進程可以打開的文件描述符數量有限制這個問題:select受限於FD_SIZE的限制,若是修改則須要修改這個宏從新編譯內核;而poll經過一個pollfd數組向內核傳遞須要關注的事件,避開了文件描述符數量限制。

此外,select和poll共同具備的一個很大的缺點就是包含大量fd的數組被總體複製於用戶態和內核態地址空間之間,開銷會隨着fd數量增多而線性增大。

select和poll就相似於上面說的就餐方式。但當你每次都去詢問時,老闆會把全部你點的飯菜都輪詢一遍再告訴你狀況,當大量飯菜很長時間都不能準備好的狀況下是很低效的。因而,老闆有些不耐煩了,就讓廚師每作好一個菜就通知他。這樣每次你再去問的時候,他會直接把已經準備好的菜告訴你,你再去端。這就是事件驅動IO就緒通知的方式-epoll

epoll的出現,解決了select、poll的缺點:

  • 基於事件驅動的方式,避免了每次都要把全部fd都掃描一遍。
  • epoll_wait只返回就緒的fd。
  • epoll使用nmap內存映射技術避免了內存複製的開銷。
  • epoll的fd數量上限是操做系統的最大文件句柄數目,這個數目通常和內存有關,一般遠大於1024。

目前,epoll是Linux2.6下最高效的IO複用方式,也是Nginx、Node的IO實現方式。而在freeBSD下,kqueue是另外一種相似於epoll的IO複用方式。

此外,對於IO複用還有一個水平觸發和邊緣觸發的概念:

  • 水平觸發:當就緒的fd未被用戶進程處理後,下一次查詢依舊會返回,這是select和poll的觸發方式。
  • 邊緣觸發:不管就緒的fd是否被處理,下一次再也不返回。理論上性能更高,可是實現至關複雜,而且任何意外的丟失事件都會形成請求處理錯誤。epoll默認使用水平觸發,經過相應選項可使用邊緣觸發。

點評:
I/O 多路複用的特色是經過一種機制一個進程能同時等待多個文件描述符,而這些文件描述符(套接字描述符)其中的任意一個進入讀就緒狀態,select()函數就能夠返回。
因此, IO多路複用,本質上不會有併發的功能,由於任什麼時候候仍是隻有一個進程或線程進行工做,它之因此能提升效率是由於selectepoll 把進來的socket放到他們的 '監視' 列表裏面,當任何socket有可讀可寫數據立馬處理,那若是selectepoll 手裏同時檢測着不少socket, 一有動靜立刻返回給進程處理,總比一個一個socket過來,阻塞等待,處理高效率。
固然也能夠多線程/多進程方式,一個鏈接過來開一個進程/線程處理,這樣消耗的內存和進程切換頁會耗掉更多的系統資源。
因此咱們能夠結合IO多路複用和多進程/多線程 來高性能併發,IO複用負責提升接受socket的通知效率,收到請求後,交給進程池/線程池來處理邏輯。

信號驅動

上文的就餐方式仍是須要你每次都去問一下飯菜情況。因而,你再次不耐煩了,就跟老闆說,哪一個飯菜好了就通知我一聲吧。而後就本身坐在桌子那裏幹本身的事情。更甚者,你能夠把手機號留給老闆,本身出門,等飯菜好了直接發條短信給你。這就相似信號驅動的IO模型。

bio

流程以下:

  • 開啓套接字信號驅動IO功能
  • 系統調用sigaction執行信號處理函數(非阻塞,馬上返回)
  • 數據就緒,生成sigio信號,經過信號回調通知應用來讀取數據。

此種io方式存在的一個很大的問題:Linux中信號隊列是有限制的,若是超過這個數字問題就沒法讀取數據。

異步非阻塞

異步 I/O(asynchronous IO)

    • *

linux下的asynchronous IO其實用得不多。先看一下它的流程:

異步IO 流程

用戶進程發起read操做以後,馬上就能夠開始去作其它的事。而另外一方面,從kernel的角度,當它受到一個asynchronous read以後,首先它會馬上返回,因此不會對用戶進程產生任何block。而後,kernel會等待數據準備完成,而後將數據拷貝到用戶內存,當這一切都完成以後,kernel會給用戶進程發送一個signal,告訴它read操做完成了。

阻塞IO,非阻塞IO 與 同步IO, 異步IO的區別和聯繫

阻塞IO VS 非阻塞IO:

概念:
阻塞和非阻塞關注的是程序在等待調用結果(消息,返回值)時的狀態.
阻塞調用是指調用結果返回以前,當前線程會被掛起。調用線程只有在獲得結果以後纔會返回。非阻塞調用指在不能馬上獲得結果以前,該調用不會阻塞當前線程。

例子:你打電話問書店老闆有沒有《分佈式系統》這本書,你若是是阻塞式調用,你會一直把本身「掛起」,直到獲得這本書有沒有的結果,若是是非阻塞式調用,你無論老闆有沒有告訴你,你本身先一邊去玩了, 固然你也要偶爾過幾分鐘check一下老闆有沒有返回結果。在這裏阻塞與非阻塞與是否同步異步無關。跟老闆經過什麼方式回答你結果無關。

    • *

分析:
阻塞IO會一直block住對應的進程直到操做完成,而非阻塞IO在kernel還準備數據的狀況下會馬上返回。

同步IO VS 異步IO:

概念:
同步與異步同步和異步關注的是___消息通訊機制 ___(synchronous communication/ asynchronous communication)所謂同步,就是在發出一個_調用_時,在沒有獲得結果以前,該_調用_就不返回。可是一旦調用返回,就獲得返回值了。換句話說,就是由_調用者_主動等待這個_調用_的結果。而異步則是相反,_調用_在發出以後,這個調用就直接返回了,因此沒有返回結果。換句話說,當一個異步過程調用發出後,調用者不會馬上獲得結果。而是在_調用_發出後,_被調用者_經過狀態、通知來通知調用者,或經過回調函數處理這個調用。

典型的異步編程模型好比Node.js舉個通俗的例子:你打電話問書店老闆有沒有《分佈式系統》這本書,若是是同步通訊機制,書店老闆會說,你稍等,」我查一下",而後開始查啊查,等查好了(多是5秒,也多是一天)告訴你結果(返回結果)。而異步通訊機制,書店老闆直接告訴你我查一下啊,查好了打電話給你,而後直接掛電話了(不返回結果)。而後查好了,他會主動打電話給你。在這裏老闆經過「回電」這種方式來回調。

    • *

分析:
在說明同步IO和異步IO的區別以前,須要先給出二者的定義。Stevens給出的定義(實際上是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;

二者的區別就在於同步IO作」IO operation」的時候會將process阻塞。按照這個定義,以前所述的阻塞IO,非阻塞IO ,IO複用都屬於同步IO。
有人可能會說,非阻塞IO 並無被block啊。這裏有個很是「狡猾」的地方,定義中所指的」IO operation」是指真實的IO操做,就是例子中的recvfrom這個system call。非阻塞IO在執行recvfrom這個system call的時候,若是kernel的數據沒有準備好,這時候不會block進程。可是,當kernel中數據準備好的時候,recvfrom會將數據從kernel拷貝到用戶內存中,這個時候進程是被block了,在這段時間內,進程是被block的。

而異步IO則不同,當進程發起IO 操做以後,就直接返回不再理睬了,直到kernel發送一個信號,告訴進程說IO完成。在這整個過程當中,進程徹底沒有被block。

IO模型的形象舉例

最後,再舉幾個不是很恰當的例子來講明這四個IO Model:
有A,B,C,D四我的在釣魚:
A用的是最老式的魚竿,因此呢,得一直守着,等到魚上鉤了再拉桿;
B的魚竿有個功能,可以顯示是否有魚上鉤,因此呢,B就和旁邊的MM聊天,隔會再看看有沒有魚上鉤,有的話就迅速拉桿;
C用的魚竿和B差很少,但他想了一個好辦法,就是同時放好幾根魚竿,而後守在旁邊,一旦有顯示說魚上鉤了,它就將對應的魚竿拉起來;
D是個有錢人,乾脆僱了一我的幫他釣魚,一旦那我的把魚釣上來了,就給D發個短信。

Select/Poll/Epoll 輪詢機制

select,poll,epoll本質上都是同步I/O,由於他們都須要在讀寫事件就緒後本身負責進行讀寫,也就是說這個讀寫過程是阻塞的
Select/Poll/Epoll 都是IO複用的實現方式, 上面說了使用IO複用,會把socket設置成non-blocking,而後放進Select/Poll/Epoll 各自的監視列表裏面,那麼,他們的對socket是否有數據到達的監視機制分別是怎樣的?效率又如何?咱們應該使用哪一種方式實現IO複用比較好?下面列出他們各自的實現方式,效率,優缺點:

(1)select,poll實現須要本身不斷輪詢全部fd集合,直到設備就緒,期間可能要睡眠和喚醒屢次交替。而epoll其實也須要調用epoll_wait不斷輪詢就緒鏈表,期間也可能屢次睡眠和喚醒交替,可是它是設備就緒時,調用回調函數,把就緒fd放入就緒鏈表中,並喚醒在epoll_wait中進入睡眠的進程。雖然都要睡眠和交替,可是select和poll在「醒着」的時候要遍歷整個fd集合,而epoll在「醒着」的時候只要判斷一下就緒鏈表是否爲空就好了,這節省了大量的CPU時間。這就是回調機制帶來的性能提高。

(2)select,poll每次調用都要把fd集合從用戶態往內核態拷貝一次,而且要把current往設備等待隊列中掛一次,而epoll只要一次拷貝,並且把current往等待隊列上掛也只掛一次(在epoll_wait的開始,注意這裏的等待隊列並非設備等待隊列,只是一個epoll內部定義的等待隊列)。這也能節省很多的開銷。

Java網絡編程模型

上文講述了UNIX環境的五種IO模型。基於這五種模型,在Java中,隨着NIO和NIO2.0(AIO)的引入,通常具備如下幾種網絡編程模型:

  • BIO
  • NIO
  • AIO

BIO

BIO是一個典型的網絡編程模型,是一般咱們實現一個服務端程序的過程,步驟以下:

  • 主線程accept請求阻塞
  • 請求到達,建立新的線程來處理這個套接字,完成對客戶端的響應。
  • 主線程繼續accept下一個請求

這種模型有一個很大的問題是:當客戶端鏈接增多時,服務端建立的線程也會暴漲,系統性能會急劇降低。所以,在此模型的基礎上,相似於 tomcat的bio connector,採用的是線程池來避免對於每個客戶端都建立一個線程。有些地方把這種方式叫作僞異步IO(把請求拋到線程池中異步等待處理)。

NIO

JDK1.4開始引入了NIO類庫,這裏的NIO指的是New IO,主要是使用Selector多路複用器來實現。Selector在Linux等主流操做系統上是經過epoll實現的。

NIO的實現流程,相似於select:

  • 建立ServerSocketChannel監聽客戶端鏈接並綁定監聽端口,設置爲非阻塞模式。
  • 建立Reactor線程,建立多路複用器(Selector)並啓動線程。
  • 將ServerSocketChannel註冊到Reactor線程的Selector上。監聽accept事件。
  • Selector在線程run方法中無線循環輪詢準備就緒的Key。
  • Selector監聽到新的客戶端接入,處理新的請求,完成tcp三次握手,創建物理鏈接。
  • 將新的客戶端鏈接註冊到Selector上,監聽讀操做。讀取客戶端發送的網絡消息。
  • 客戶端發送的數據就緒則讀取客戶端請求,進行處理。

相比BIO,NIO的編程很是複雜。

AIO

JDK1.7引入NIO2.0,提供了異步文件通道和異步套接字通道的實現。其底層在windows上是經過IOCP,在Linux上是經過epoll來實現的(LinuxAsynchronousChannelProvider.java,UnixAsynchronousServerSocketChannelImpl.java)。

  • 建立AsynchronousServerSocketChannel,綁定監聽端口
  • 調用AsynchronousServerSocketChannel的accpet方法,傳入本身實現的CompletionHandler。包括上一步,都是非阻塞的
  • 鏈接傳入,回調CompletionHandler的completed方法,在裏面,調用AsynchronousSocketChannel的read方法,傳入負責處理數據的CompletionHandler。
  • 數據就緒,觸發負責處理數據的CompletionHandler的completed方法。繼續作下一步處理便可。
  • 寫入操做相似,也須要傳入CompletionHandler。

其編程模型相比NIO有了很多的簡化。

對比

. 同步阻塞IO 僞異步IO NIO AIO
客戶端數目 :IO線程 1 : 1 m : n m : 1 m : 0
IO模型 同步阻塞IO 同步阻塞IO 同步非阻塞IO 異步非阻塞IO
吞吐量
編程複雜度 簡單 簡單 很是複雜 複雜
微信公衆號【黃小斜】做者是螞蟻金服 JAVA 工程師,目前在螞蟻財富負責後端開發工做,專一於 JAVA 後端技術棧,同時也懂點投資理財,堅持學習和寫做,用大廠程序員的視角解讀技術與互聯網,個人世界裏不僅有 coding!關注公衆號後回覆」架構師「便可領取 Java基礎、進階、項目和架構師等免費學習資料,更有數據庫、分佈式、微服務等熱門技術學習視頻,內容豐富,兼顧原理和實踐,另外也將贈送做者原創的Java學習指南、Java程序員面試指南等乾貨資源

相關文章
相關標籤/搜索