引言
從 T 跳槽到 A 之后,我的編程語言也從 C++ 轉為 了 Java。在 T 做的偏服務器端開發,而在 A 更偏向於業務開發。上周在 A 公司組內做了一個《服務器端高性能網絡編程》的分享,我訝異於組內的十個人竟然沒有一個人做過直接基於 TCP/IP 協議的開發,更多的是 Web 后台的業務開發。連 Java 最強大的網絡庫 Netty,用過的人也只有一個。但也不難理解---A 公司的中間件平台,將業務與底層進行了隔離,讓程序員可以專心於業務開發。
孰優孰劣?不能一概而論,還記得跳槽 A 公司之前面試過 N 公司,面試官問我 HTTP 協議、RPC 框架等知識,我也是一知半解。要什么 HTTP 協議、要什么 RPC ,我們直接 TCP/IP。其實這也是 T 公司基礎設施不夠完善的一種表現,這些在我的另一篇文章《我在騰訊和阿里的見聞》中談過,感興趣的可以移步。
作為 Java Web 程序員,你有想過 Web 服務器(Nginx,Tomcat,Jetty等)是如何接受你的 HTTP 請求的嗎?你知道 其實 HTTP 也是基於 TCP/IP 的文本協議嗎?之所以取這樣的標題,希望 Java Web 程序員對服務器端的某些工作原理有一些簡單的了解,也不至於在面試的時候一問三不知。
概念
在編寫服務器端網絡程序時,我們最常見到阻塞、非阻塞、同步和異步這四個詞。它們的解釋分別如下:
阻塞: 阻塞調用是指調用返回之前,當前線程會被掛起,只有當調用得到結果后才返回。
非阻塞:與阻塞相反,非阻塞調用是指在不能立即得到結果之前,該函數不會將當前線程阻塞,而是立即返回。
同步:所謂同步,就是在發出一個功能調用時,在沒有得到結果之前,該調用就不返回。等前一件做完了才能做下一件事。
異步:異步的概念和同步相對。當一個異步過程調用發出后,調用者不能立刻得到結果。實際處理這個調用的部件在完成后,通過狀態、通知和回調來通知調用者。
常常有人弄不清阻塞/非阻塞與同步/異步之間的關系,容易將他們混為一談。阻塞/非阻塞更多的用來形容某次調用的屬性(比如 read(),write() 是否是阻塞/非阻塞 )所以應用范圍比較窄;而同步/異步則更上層,通常指各個功能/線程之間的關系(比如 Thread1 和 Thread2 是同步執行還是異步執行)。
IO 模型
服務器端 IO 主要分為兩種:磁盤 IO 和網絡 IO,在講服務器端高性能網絡編程時更多時候我們講的是網絡 IO 模型。一次完整的服務器端處理網絡請求流程圖如下(簡化版,以 Web 服務器為例):
這張圖比較簡單,但是很多人在沒看到這張圖之前肯定都以為每次網絡讀(recvfrom())或者寫(sendto())都是在網卡與用戶進程之間進行操作,其實不是。從上圖可以看出,數據無論從網卡到用戶空間還是從用戶空間到網卡都需要經過內核。從磁盤上讀寫數據也是如此。所以就有了 mmap 技術,感興趣的可以自行百度。應用進程(Web 服務器也屬於應用進程,這里需要再統一幾個概念:用戶進程、應用程序、Web 服務器程序,它們相對於內核來說都是應用進程,所以后面文章中統一成應用進程)需要通過系統調用(recvfrom/sendto)向內核讀寫數據,內核再進一步操作網卡。
根據應用進程系統調用方式的阻塞、非阻塞,操作系統在處理應用程序請求時處理方式的同步、異步處理的不同,參考《UNIX 網絡編程卷 I》可以分為 5 種 IO 模型:
1、阻塞 IO 模型(blocking IO)
描述:應用程序進行 recvfrom 系統調用時將阻塞在此調用,直到該套接字上有數據並且復制到用戶空間緩沖區。該模式一般配合多線程使用,應用進程每接收一個連接,為此連接創建一個線程來處理該連接上的讀寫以及業務處理。
優點:編程簡單,適合教學。《UNIX網絡編程卷I》上很多例子都是基於這種模式。
缺點:如果套接字上沒有數據,進程將一直阻塞。這時其他套接字上有數據也不能進行及時處理。如果是多線程方式,除非連接關閉否則線程會一直存在,而線程的創建、維護和銷毀非常消耗資源,所以能建立的連接數量非常有限。
2、非阻塞 IO 模型(nonblocking IO)
描述:應用進程每次調用 recvfrom 即使沒有數據准備好也不會阻塞,會繼續往下執行,避免了進程阻塞在某個連接上的弊端。
優點:代碼編寫相對簡單,進程不會阻塞,可以在同一線程中處理所有連接。
缺點:需要頻繁的輪詢,比較耗 CPU,在並發量很大的時候將花費大量時間在沒有任何數據的連接上輪詢。所以該模型只在專門提供某種功能的系統中才會出現。
3、IO 復用模型(IO multiplexing)
描述:應用進程阻塞於 select/poll/epoll 等系統函數等待某個連接變成可讀(有數據過來),再調用 recvfrom 從連接上讀取數據。雖然此模式也會阻塞在 select/poll/epoll 上,但與阻塞IO 模型不同它阻塞在等待多個連接上有讀(寫)事件的發生,明顯提高了效率且增加了單線程/單進程中並行處理多連接的可能。
優點:統一管理連接,不一定采用多線程的方式,同時也不需要輪詢。只需要阻塞於 select 即可,可以同時管理多個連接。
缺點:當 select/poll/epoll 管理的連接數過少時,這種模型將退化成阻塞 IO 模型。並且還多了一次系統調用:一次 select/poll/epoll 一次 recvfrom。
4、信號驅動 IO 模型(signal-driven IO)
描述:應用進程創建 SIGIO 信號處理程序,此程序可處理連接上數據的讀寫和業務處理。並向操作系統安裝此信號,進程可以往下執行。當內核數據准備好會向應用進程發送信號,觸發信號處理程序的執行。再在信號處理程序中進行 recvfrom 和業務處理。
優點:非阻塞
缺點:在前一個通知信號沒被處理的情況下,后一個信號來了也不能被處理。所以在信號量大的時候會導致后面的信號不能被及時感知。
5、異步 IO 模型(asynchronous IO)
描述:應用進程通過 aio_read 告知內核啟動某個操作,並且在整個操作完成之后再通知應用進程,包括把數據從內核空間拷貝到用戶空間。信號驅動 IO 是內核通知我們何時可以啟動一個 IO 操作,而一部 IO 模型是由內核通知我們 IO 操作何時完成。
注:前 4 種模型都是帶有阻塞部分的,有的阻塞在等待數據准備好,有的阻塞在從內核空間拷貝數據到用戶空間。而這種模型應用進程從調用 aio_read 到數據被拷貝到用戶空間,不用任何阻塞,所以該種模式叫異步 IO 模型。這五種模型的取名和並列方式我是保留意見的,感覺容易迷惑讀者。
優點:沒有任何阻塞,充分利用系統內核將 IO 操作與計算邏輯並行。
缺點:編程復雜、操作系統支持不好。目前只有 windows 下的 iocp 實現了真正的 AIO。linux 下在 2.6 版本中才引入,目前並不完善,所以 Linux 下一般采用多路復用模型。
對比
前四種模型的主要區別於第一階段,因為他們的第二階段都是一樣的:在數據從內核拷貝到應用進程的緩沖區期間,進程阻塞於 recvfrom 調用。相反,異步 IO 模型在這兩個階段都需要處理,從而不同於其他四種模型。
以上圖片所有原型都來自於《UNIX網絡編程卷 I》,里面有很多跟網絡編程有關的知識點和例子,是程序員必備書籍,即使你是業務程序員也應該購買一本,知其然,知其所以然!
總結
JDK 的網絡編程相關的類、接口雖然不像 C++ 是直接依賴於操作系統的,但它的 IO 模型是離不開以上五種模型的。畢竟這是模型,與語言、操作系統無關。 IO 模型只是高性能網絡編程中的基礎部分,光有好的 IO 模型還不行,我們還需要好的架構(線程模型)。線程模型是高性能網絡編程的核心部分,在后面的文章中應該還會分析。