談一談網絡編程學習經驗
本文談一談我在學習網絡編程方面的一些個人經驗。“網絡編程”這個術語的范圍很廣,本文指用Sockets API 開發基于TCP/IP 的網絡應用程序,具體定義見§A.1.5 “網絡編程的各種任務角色”。
受限于本人的經歷和經驗,本附錄的適應范圍是:
x86-64 Linux 服務端網絡編程,直接或間接使用Sockets API。
公司內網。不一定是局域網,但總體位于公司防火墻之內,環境可控。
本文可能不適合:
PC 客戶端網絡編程,程序運行在客戶的PC 上,環境多變且不可控。
Windows 網絡編程。
面向公網的服務程序。
高性能網絡服務器。
本文分兩個部分:
1.網絡編程的一些“胡思亂想”,以自問自答的形式談談我對這一領域的認識。
2.幾本必看的書,基本上還是W.Richard Stevents 的那幾本。
另外,本文沒有特別說明時均暗指TCP 協議,“連接”是“TCP 連接”,“服務端”是“TCP 服務端”。
A.1 網絡編程的一些“胡思亂想”
以下大致列出我對網絡編程的一些想法,前后無關聯。
A.1.1 網絡編程是什么
網絡編程是什么?是熟練使用Sockets API嗎?說實話,在實際項目里我只用過兩次Sockets API,其他時候都是使用封裝好的網絡庫。
第一次是2005年在學校做一個羽毛球賽場計分系統:我用C#編寫運行在PC上的軟件,負責比分的顯示;再用C#寫了運行在PDA上的計分界面,記分員拿著PDA記錄比分;這兩部分程序通過TCP協議相互通信。這其實是個簡單的分布式系統,體育館有幾片場地,每個場地都有一名拿PDA的記分員,每個場地都有兩臺顯示比分的PC(顯示器是42寸平板電視,放在場地的對角,這樣兩邊看臺的觀眾都能看到比分)。這兩臺PC的功能不完全一樣,一臺只負責顯示當前比分,另一臺還要負責與PDA通信,并更新數據庫里的比分信息。此外,還有一臺PC負責周期性地從數據庫讀出全部7片場地的比分,顯示在體育館墻上的大屏幕上。這臺PC上還運行著一個程序,負責生成比分數據的靜態頁面,通過FTP上傳發布到某門戶網站的體育頻道。系統中還有一個錄入賽程(參賽隊、運動員、出場順序等)數據庫的程序,運行在數據庫服務器上。算下來整個系統有十來個程序,運行在二十多臺設備(PC 和PDA)上,還要考慮可靠性,避免single point of failure。
這是我第一次寫實際項目中的網絡程序,當時寫下來的感覺是像寫命令行與用戶交互的程序:程序在命令行輸出一句提示語,等待客戶輸入一句話,然后處理客戶輸入,再輸出下一句提示語,如此循環。只不過這里的“客戶”不是人,而是另一個程序。在建立好TCP 連接之后,雙方的程序都是read/write 循環(為求簡單,我用的是blocking 讀寫),直到有一方斷開連接。
第二次是2010 年編寫muduo 網絡庫,我再次拿起了Sockets API,寫了一個基于Reactor 模式的C++ 網絡庫。寫這個庫的目的之一就是想讓日常的網絡編程從Sockets API 的瑣碎細節中解脫出來,讓程序員專注于業務邏輯,把時間用在刀刃上。muduo 網絡庫的示例代碼包含了幾十個網絡程序,這些示例程序都沒有直接使用Sockets API。
在此之外,無論是實習還是工作,雖然我寫的程序都會通過TCP 協議與其他程序打交道,但我沒有直接使用過Sockets API。對于TCP 網絡編程,我認為核心是處理“三個半事件”,見§6.4.1“TCP 網絡編程本質論”。程序員的主要工作是在事件處理函數中實現業務邏輯,而不是和Sockets API“較勁”。
這里還是沒有說清楚“網絡編程”是什么,請繼續閱讀后文§A.1.5“網絡編程的各種任務角色”。
A.1.2 學習網絡編程有用嗎
以上說的是比較底層的網絡編程,程序代碼直接面對從TCP 或UDP 收到的數據以及構造數據包發出去。在實際工作中,另一種常見的情況是通過各種client library來與服務端打交道,或者在現成的框架中填空來實現server,或者采用更上層的通信方式。比如用libmemcached 與memcached 打交道,使用libpq 來與PostgreSQL 打交道,編寫Servlet 來響應HTTP 請求,使用某種RPC 與其他進程通信,等等。這些情況都會發生網絡通信,但不一定算作“網絡編程”。如果你的工作是前面列舉的這些,學習TCP/IP 網絡編程還有用嗎?
我認為還是有必要學一學,至少在troubleshooting 的時候有用。無論如何,這些library 或framework 都會調用底層的Sockets API 來實現網絡功能。當你的程序遇到一個線上問題時,如果你熟悉Sockets API,那么從strace 不難發現程序卡在哪里,盡管可能你沒有直接調用這些Sockets API。另外,熟悉TCP/IP 協議、會用tcpdump 也非常有助于分析解決線上網絡服務問題。
A.1.3 在什么平臺上學習網絡編程
對于服務端網絡編程,我建議在Linux 上學習。
如果在10年前,這個問題的答案或許是FreeBSD,因為FreeBSD“根正苗紅”,在2000年那一次互聯網浪潮中扮演了重要角色,是很多公司首選的免費服務器操作系統。2000 年那會兒Linux 還遠未成熟,連epoll 都還沒有實現。(FreeBSD 在2001年發布4.1 版,加入了kqueue,從此C10k 不是問題。)
10年后的今天,事情起了一些變化,Linux成為市場份額最大的服務器操作系統。在Linux這種大眾系統上學網絡編程,遇到什么問題會比較容易解決。因為用的人多,你遇到的問題別人多半也遇到過;同樣因為用的人多,如果真的有什么內核bug,很快就會得到修復,至少有work around 的辦法。如果用別的系統,可能一個問題發到論壇上半個月都不會有人理。從內核源碼的風格看,FreeBSD 更干凈整潔,注釋到位,但是無奈它的市場份額遠不如Linux,學習Linux 是更好的技術投資。
A.1.4 可移植性重要嗎
寫網絡程序要不要考慮移植性?要不要跨平臺?這取決于項目需要,如果貴公司做的程序要賣給其他公司,而對方可能使用Windows、Linux、FreeBSD、Solaris、AIX、HP-UX 等等操作系統,這時候當然要考慮移植性。如果編寫公司內部的服務器上用的網絡程序,那么大可只關注一個平臺,比如Linux。因為編寫和維護可移植的網絡程序的代價相當高,平臺間的差異可能遠比想象中大,即便是POSIX 系統之間也有不小的差異(比如Linux 沒有SO_NOSIGPIPE 選項,Linux 的pipe(2) 是單向的,而FreeBSD 是雙向的),錯誤的返回碼也大不一樣。
我就不打算把muduo 往Windows 或其他操作系統移植。如果需要編寫可移植的網絡程序,我寧愿用libevent、libuv、Java Netty 這樣現成的庫,把“臟活、累活”留給別人。
A.1.5 網絡編程的各種任務角色
計算機網絡是個big topic,涉及很多人物和角色,既有開發人員,也有運維人員。比方說:公司內部兩臺機器之間ping 不通,通常由網絡運維人員解決,看看是布線有問題還是路由器設置不對;兩臺機器能ping 通,但是程序連不上,經檢查是本機防火墻設置有問題,通常由系統管理員解決;兩臺機器能連上,但是丟包很嚴重,發現是網卡或者交換機的網口故障,由硬件維修人員解決;兩臺機器的程序能連上,但是偶爾發過去的請求得不到響應,通常是程序bug,應該由開發人員解決。
本文主要關心開發人員這一角色。下面簡單列出一些我能想到的跟網絡打交道的編程任務,其中前三項是面向網絡本身,后面幾項是在計算機網絡之上構建信息系統。
1.開發網絡設備,編寫防火墻、交換機、路由器的固件(firmware)。
2.開發或移植網卡的驅動。
3.移植或維護TCP/IP 協議棧(特別是在嵌入式系統上)。
4.開發或維護標準的網絡協議程序,HTTP、FTP、DNS、SMTP、POP3、NFS。
5.開發標準網絡協議的“附加品”,比如HAProxy、squid、varnish 等Web loadbalancer。
6.開發標準或非標準網絡服務的客戶端庫,比如ZooKeeper 客戶端庫、memcached客戶端庫。
7.開發與公司業務直接相關的網絡服務程序,比如即時聊天軟件的后臺服務器、網游服務器、金融交易系統、互聯網企業用的分布式海量存儲、微博發帖的內部廣播通知等等。
8.客戶端程序中涉及網絡的部分,比如郵件客戶端中與POP3、SMTP 通信的部分,以及網游的客戶端程序中與服務器通信的部分。
本文所指的“網絡編程”專指第7 項,即在TCP/IP 協議之上開發業務軟件。換句話說,不是用Sockets API 開發muduo 這樣的網絡庫,而是用libevent、muduo、Netty、gevent 這樣現成的庫開發業務軟件,muduo 自帶的十幾個示例程序是業務軟件的代表。
A.1.6 面向業務的網絡編程的特點
與通用的網絡服務器不同,面向公司業務的專用網絡程序有其自身的特點。
業務邏輯比較復雜,而且時常變化
如果寫一個HTTP 服務器,在大致實現HTTP 1.1 標準之后,程序的主體功能一般不會有太大的變化,程序員會把時間放在性能調優和bug 修復上。而開發針對公司業務的專用程序時,功能說明書(spec)很可能不如HTTP 1.1 標準那么細致明確。更重要的是,程序是快速演化的。以即時聊天工具的后臺服務器為例,可能第一版只支持在線聊天;幾個月之后發布第二版,支持離線消息;又過了幾個月,第三版支持隱身聊天;隨后,第四版支持上傳頭像;如此等等。這要求程序員能快速響應新的業務需求,公司才能保持競爭力。由于業務時常變化(假設每月一次版本升級),也會降低服務程序連續運行時間的要求。相反,我們要設計一套流程,通過輪流重啟服務器來完成平滑升級(§9.2.2)。
不一定需要遵循公認的通信協議標準
比方說網游服務器就沒什么協議標準,反正客戶端和服務端都是本公司開發的,如果發現目前的協議設計有問題,兩邊一起改就行了。由于可以自己設計協議,因此我們可以繞開一些性能難點,簡化程序結構。比方說,對于多線程的服務程序,如果用短連接TCP 協議,為了優化性能通常要精心設計accept 新連接的機制2,避免驚群并減少上下文切換。但是如果改用長連接,用最簡單的單線程accept 就行了。
程序結構沒有定論
對于高并發大吞吐的標準網絡服務,一般采用單線程事件驅動的方式開發,比如HAProxy、lighttpd 等都是這個模式。但是對于專用的業務系統,其業務邏輯比較復雜,占用較多的CPU 資源,這種單線程事件驅動方式不見得能發揮現在多核處理器的優勢。這留給程序員比較大的自由發揮空間,做好了“橫掃千軍”,做爛了一敗涂地。我認為目前one loop per thread 是通用性較高的一種程序結構,能發揮多核的優勢,見§3.3 和§6.6。
性能評判的標準不同
如果開發httpd 這樣的通用服務,必然會和開源的Nginx、lighttpd 等高性能服務器比較,程序員要投入相當的精力去優化程序,才能在市場上占有一席之地。而面向業務的專用網絡程序不一定是IO bound,也不一定有開源的實現以供對比性能,優化方向也可能不同。程序員通常更加注重功能的穩定性與開發的便捷性。性能只要一代比一代強即可。
網絡編程起到支撐作用,但不處于主導地位
程序員的主要工作是實現業務邏輯,而不只是實現網絡通信協議。這要求程序員深入理解業務。程序的性能瓶頸不一定在網絡上,瓶頸有可能是CPU、Disk IO、數據庫等,這時優化網絡方面的代碼并不能提高整體性能。只有對所在的領域有深入的了解,明白各種因素的權衡(trade-off),才能做出一些有針對性的優化。現在的機器上,簡單的并發長連接echo服務程序不用特別優化就做到十多萬qps,但是如果每個業務請求需要1ms 密集計算,在8 核機器上充其量能達到8 000 qps,優化IO 不如去優化業務計算(如果投入產出合算的話)。
A.1.7 幾個術語
互聯網上的很多“口水戰”是由對同一術語的不同理解引起的,比如我寫的《多線程服務器的適用場合》3,就曾經被人說是“掛羊頭賣狗肉”,因為這篇文章中舉的master 例子“根本就算不上是個網絡服務器。因為它的瓶頸根本就跟網絡無關。”
網絡服務器
“網絡服務器”這個術語確實含義模糊,到底指硬件還是軟件?到底是服務于網絡本身的機器(交換機、路由器、防火墻、NAT),還是利用網絡為其他人或程序提供服務的機器(打印服務器、文件服務器、郵件服務器)?每個人根據自己熟悉的領域,可能會有不同的解讀。比方說,或許有人認為只有支持高并發、高吞吐量的才算是網絡服務器。
為了避免無謂的爭執,我只用“網絡服務程序”或者“網絡應用程序”這種含義明確的術語。“開發網絡服務程序”通常不會造成誤解。
客戶端?服務端?在TCP 網絡編程中,客戶端和服務端很容易區分,主動發起連接的是客戶端,被動接受連接的是服務端。當然,這個“客戶端”本身也可能是個后臺服務程序,HTTP proxy 對HTTP server 來說就是個客戶端。
客戶端編程?服務端編程?但是“服務端編程”和“客戶端編程”就不那么好區分了。比如Web crawler,它會主動發起大量連接,扮演的是HTTP客戶端的角色,但似乎應該歸入“服務端編程”。又比如寫一個HTTP proxy,它既會扮演服務端——被動接受Web browser 發起的連接,也會扮演客戶端——主動向HTTP server發起連接,它究竟算服務端還是客戶端?我猜大多數人會把它歸入服務端編程。
那么究竟如何定義“服務端編程”?
服務端編程需要處理大量并發連接?也許是,也許不是。比如云風在一篇介紹網游服務器的博客4中就談到,網游中用到的“連接服務器”需要處理大量連接,而“邏輯服務器”只有一個外部連接。那么開發這種網游“邏輯服務器”算服務端編程還是客戶端編程呢?又比如機房的服務進程監控軟件,并發數跟機器數成正比,至多也就是兩三千的并發連接。(再大規模就超出本書的范圍了。)
我認為,“服務端網絡編程”指的是編寫沒有用戶界面的長期運行的網絡程序,程序默默地運行在一臺服務器上,通過網絡與其他程序打交道,而不必和人打交道。與之對應的是客戶端網絡程序,要么是短時間運行,比如wget;要么是有用戶界面(無論是字符界面還是圖形界面)。本文主要談服務端網絡編程。