建議大家去看原文:http://cloud.github.com/downloads/chenshuo/documents/LearningNetworkProgramming.pdf
1
談一談網絡編程學習經驗
陳碩
giantchen@gmail.com
blog.csdn.net/Solstice
weibo.com/giantchen
2012-02-13
本文談一談我在學習網絡編程方面的一些個人經驗。“網絡編程”這個術語的范圍很廣,本文指用
Sockets API 開發基於 TCP/IP 的網絡應用程序,具體定義見“網絡編程的各種任務角色”一節。
受限於本人的經歷和經驗,這篇文章的適應范圍是:
x86-64 Linux 服務端網絡編程,直接或間接使用 Sockets API
公司內網。不一定是局域網,但總體位於公司防火牆之內,環境可控
本文可能不適合:
PC 客戶端網絡編程,程序運行在客戶的 PC 上,環境多變且不可控
Windows 網絡編程
面向公網的服務程序
高性能網絡服務器
本文分兩個部分:
1. 網絡編程的一些胡思亂想,談談我對這一領域的認識
2. 幾本必看的書,基本上還是 W. Richard Stevents 那幾本
另外,本文沒有特別說明時均暗指 TCP 協議,“連接”是“TCP 連接”,“服務端”是“TCP 服務端”。
網絡編程的一些胡思亂想
以下胡亂列出我對網絡編程的一些想法,前后無關聯。
網絡編程是什么?
網絡編程是什么?是熟練使用 Sockets API 嗎?說實話,在實際項目里我只用過兩次 Sockets API,其
他時候都是使用封裝好的網絡庫。
第一次是 2005 年在學校做一個羽毛球賽場計分系統:我用 C# 編寫運行在 PC 機上的軟件,負責比
分的顯示;再用 C# 寫了運行在 PDA 上的計分界面,記分員拿着 PDA 記錄比分;這兩部分程序通過 TCP
協議相互通信。這其實是個簡單的分布式系統,體育館有不止一片場地,每個場地都有一名拿 PDA 的
記分員,每個場地都有兩台顯示比分的 PC 機(顯示器是 42 吋平板電視,放在場地的對角,這樣兩邊看
台的觀眾都能看到比分)。這兩台 PC 機功能不完全一樣,一台只負責顯示當前比分,另一台還要負責
與 PDA 通信,並更新數據庫里的比分信息。此外,還有一台 PC 機負責周期性地從數據庫讀出全部 7 片
場地的比分,顯示在體育館牆上的大屏幕上。這台 PC 上還運行着一個程序,負責生成比分數據的靜態
頁面,通過 FTP 上傳發布到某門戶網站的體育頻道。系統中還有一個錄入賽程(參賽隊,運動員,出場2
順序等)數據庫的程序,運行在數據庫服務器上。算下來整個系統有十來個程序,運行在二十多台設備
(PC 和 PDA)上,還要考慮可靠性。將來有機會把這個小系統仔細講一講,挺有意思的。
這是我第一次寫實際項目中的網絡程序,當時寫下來的感覺是像寫命令行與用戶交互的程序:程序
在命令行輸出一句提示語,等待客戶輸入一句話,然后處理客戶輸入,再輸出下一句提示語,如此循環。
只不過這里的“客戶”不是人,而是另一個程序。在建立好 TCP 連接之后,雙方的程序都是 read/write
循環(為求簡單,我用的是 blocking 讀寫),直到有一方斷開連接。
第二次是 2010 年編寫 muduo 網絡庫,我再次拿起了 Sockets API,寫了一個基於 Reactor 模式的
C++ 網絡庫。寫這個庫的目的之一就是想讓日常的網絡編程從 Sockets API 的瑣碎細節中解脫出來,讓程
序員專注於業務邏輯,把時間用在刀刃上。Muduo 網絡庫的示例代碼包含了十幾個網絡程序,這些程
序都沒有直接使用 Sockets API。
在此之外,無論是實習還是工作,雖然我寫的程序都會通過 TCP 協議與其他程序打交道,但我沒有
直接使用過 Sockets API。對於 TCP 網絡編程,我認為核心是處理“三個半事件”,見《Muduo 網絡編
程示例之零:前言》中的“TCP 網絡編程本質論”。程序員的主要工作是在事件處理函數中實現業務邏
輯,而不是和 Sockets API 較勁。
這里還是沒有說清楚“網絡編程”是什么,請繼續閱讀后文“網絡編程的各種任務角色”。
學習網絡編程有用嗎?
以上說的是比較底層的網絡編程,程序代碼直接面對從 TCP 或 UDP 收到的數據以及構造數據包發
出去。在實際工作中,另一種常見 的情況是通過各種 client library 來與服務端打交道,或者在現成的框
架中填空來實現 server,或者采用更上層的通信方式。比如用 libmemcached 與 memcached 打交道,使
用 libpq 來與 PostgreSQL 打交道,編寫 Servlet 來響應 http 請求,使用某種 RPC 與其他進程通信,等等。
這些情況都會發生網絡通信,但不一定算作“網絡編程”。如果你的工作是前面列舉的這些,學習
TCP/IP 網絡編程還有用嗎?
我認為還是有必要學一學,至少在 troubleshooting 的時候有用。無論你是用 libevent/netty/gevent
來寫網絡程序,還是用前述更高層庫來通信,這些 library 或 framework 都會調用底層的 Sockets API 來
實現網絡功能。當你的程序遇到一個線上問題,如果你熟悉 Sockets API,那么從 strace 不難發現程序卡
在哪里,盡管可能你沒有直接調用這些 Sockets API。另外,熟悉 TCP/IP 協議、會用 tcpdump 也大大有
助於分析解決線上網絡服務問題。
在什么平台上學習網絡編程?
對於服務端網絡編程,我建議在 Linux 上學習。
如果在 10 年前,這個問題的答案或許是 FreeBSD,因為 FreeBSD 根正苗紅,在 2000 年那一次互聯
網浪潮中扮演了重要角色,是很多公司首選的免費服務器操作系統。2000 年那會兒 Linux 還遠未成熟,
連 epoll 都還沒有實現。(FreeBSD 在 2001 年發布 4.1 版,加入了 kqueue,從此 C10k 不是問題。)
10 年后的今天,事情起了變化,Linux 成為了市場份額最大的服務器操作系統
1
。在 Linux 這種大眾
系統上學網絡編程,遇到什么問題會比較容易解決。因為用的人多,你遇到的問題別人多半也遇到過;
1
http://en.wikipedia.org/wiki/Usage_share_of_operating_systems3
同樣因為用的人多,如果真的有什么內核 bug,很快就會得到修復,至少有 work around 的辦法。如果
用別的系統,可能一個問題發到論壇上半個月都不會有人理。從內核源碼的風格看,FreeBSD 更干凈整
潔,注釋到位,但是無奈它的市場份額遠不如 Linux,學習 Linux 是更好的技術投資。
可移植性重要嗎?
寫網絡程序要不要考慮移植性?這取決於項目需要,如果貴公司做的程序要賣給其他公司,而對方
可能使用 Windows、Linux、FreeBSD、Solaris、AIX、HP-UX 等等操作系統,這時候考慮移植性。如果編
寫公司內部的服務器上用的網絡程序,那么大可只關注一個平台,比如 Linux。因為編寫和維護可移植
的網絡程序的代價相當高,平台間的差異可能遠比想象中大,即便是 POSIX 系統之間也有不小的差異
(比如 Linux 沒有 SO_NOSIGPIPE 選項),錯誤的返回碼也大不一樣。
我就不打算把 muduo 往 Windows 或其他操作系統移植。如果需要編寫可移植的網絡程序,我寧願
用 libevent、libuv、Java Netty 這樣現成的庫,把臟活累活留給別人。
網絡編程的各種任務角色
計算機網絡是個 big topic,涉及很多人物和角色,既有開發人員,也有運維人員。比方說:公司內
部兩台機器之間 ping 不通,通常由網絡運維人員解決,看看是布線有問題還是路由器設置不對;兩台
機器能 ping 通,但是程序連不上,經檢查是本機防火牆設置有問題,通常由系統管理員解決;兩台機
器能連上,但是丟包很嚴重,發現是網卡或者交換機的網口故障,由硬件維修人員解決;兩台機器的程
序能連上,但是偶爾發過去的請求得不到響應,通常是程序 bug,應該由開發人員解決。
本文主要關心開發人員這一角色。下面簡單列出一些我能想到的跟網絡打交道的編程任務,其中前
三項是面向網絡本身,后面幾項是在計算機網絡之上構建信息系統。
1. 開發網絡設備,編寫防火牆、交換機、路由器的固件 firmware
2. 開發或移植網卡的驅動
3. 移植或維護 TCP/IP 協議棧(特別是在嵌入式系統上)
4. 開發或維護標准的網絡協議程序,HTTP、FTP、DNS、SMTP、POP3、NFS
5. 開發標准網絡協議的“附加品”,比如 HAProxy、squid、varnish 等 web load balancer
6. 開發標准或非標准網絡服務的客戶端庫,比如 ZooKeeper 客戶端庫,memcached 客戶端庫
7. 開發與公司業務直接相關的網絡服務程序,比如即時聊天軟件的后台服務器,網游服務器,
金融交易系統,互聯網企業用的分布式海量存儲,微博發帖的內部廣播通知,等等
8. 客戶端程序中涉及網絡的部分,比如郵件客戶端中與 POP3、SMTP 通信的部分,以及網游
的客戶端程序中與服務器通信的部分
本文所指的“網絡編程”專指第 7 項,即在 TCP/IP 協議之上開發業務軟件。換句話說,不是用
Sockets API 開發 muduo 這樣的網絡庫,而是用 libevent/muduo/netty/gevent 這樣現成的庫開發業務軟件,
muduo 自帶的十幾個示例程序是業務軟件的代表。
面向業務的網絡編程的特點
跟開發通用的網絡程序不同,開發面向公司業務的專用網絡程序有其特點:4
業務邏輯比較復雜,而且時常變化
如果寫一個 HTTP 服務器,在大致實現 HTTP /1.1 標准之后,程序的主體功能一般不會有太大的變
化,程序員會把時間放在性能調優和 bug 修復上。而開發針對公司業務的專用程序時,功能說明書
(spec)很可能不如 HTTP/1.1 標准那么細致明確。更重要的是,程序是快速演化的。以即時聊天工具的
后台服務器為例,可能第一版只支持在線聊天;幾個月之后發布第二版,支持離線消息;又過了幾個月,
第三版支持隱身聊天;隨后,第四版支持上傳頭像;如此等等。這要求程序員能快速響應新的業務需求,
公司才能保持競爭力。由於業務時常變化(假設每月一次版本升級),也會降低服務程序連續運行時間
的要求,相反,我們要設計一套流程,通過輪流重啟服務器來完成平滑升級。
不一定需要遵循公認的通信協議標准
比方說網游服務器就沒什么協議標准,反正客戶端和服務端都是本公司開發,如果發現目前的協議
設計有問題,兩邊一起改了就是了。由於可以自己設計協議,我們可以繞開一些性能難點,簡化程序結
構。比方說,對於多線程的服務程序,如果用短連接 TCP 協議,為了優化性能通常要精心設計 accept
新連接的機制,避免驚群並減少上下文切換。但是如果改用長連接,用最簡單的單線程 accept 就行了。
程序結構沒有定論
對於高並發大吞吐的標准網絡服務,一般采用單線程事件驅動的方式開發,比如 HAProxy、lighttpd
等都是這個模式。但是對於專用的業務系統,其業務邏輯比較復雜,占用較多的 CPU 資源,這種單線
程事件驅動方式不見得能發揮現在多核處理器的優勢。這留給程序員比較大的自由發揮空間,做好了橫
掃千軍,做爛了一敗塗地。我認為目前 one loop per thread 是通用性較高的一種程序結構,能發揮多核
的優勢,見《多線程服務器的常用編程模型》
2
和《Muduo 多線程模型:一個 Sudoku 服務器演變》
3
。
性能評判的標准不同
如果開發 httpd 這樣的通用網絡服務,必然會和開源的 Nginx、lighttpd 等高性能服務器比較,程序
員要投入相當的精力去優化 IO,才能在市場上占有一席之地。而面向業務的專用網絡程序不一定是 IO
bound,也不一定有開源的實現以供對比性能,優化方向也可能不同。程序員通常更加注重功能的穩定
性與開發的便捷性。性能只要一代比一代強即可。
網絡編程起到支撐作用,但不處於主導地位
程序員的主要工作是實現業務邏輯,而不只是實現網絡通信協議。這要求程序員深入理解業務。程
序的性能瓶頸不一定在網絡上,瓶頸有可能是 CPU、Disk IO、數據庫等等,這時優化網絡方面的代碼並
不能提高整體性能。只有對所在的領域有深入的了解,明白各種因素的權衡(trade-off),才能做出一些
有針對性的優化。現在的機器上,簡單的並發長連接 echo 服務程序不用特別優化就做到十多萬 QPS
4
,
但是如果每個業務請求需要 1ms 密集計算,在 8 核機器上充其量能達到 8k QPS,優化 IO 不如去優化業
務計算(如果投入產出划得來的話)。
2
https://github.com/downloads/chenshuo/documents/multithreaded_server.pdf
3
http://www.cnblogs.com/Solstice/archive/2011/06/16/2082590.html
4
我寫的基於 Netty 和 Protobuf 的簡易 RPC 經 bluedavy 測試是 14.9 萬 QPS,他自己的更高
http://blog.bluedavy.com/?p=334 https://github.com/chenshuo/recipes/tree/master/protorpc5
幾個術語
互聯網上的很多口水戰是由對同一術語的不同理解引起的,比我寫的《多線程服務器的適用場合》
就曾經人被說是“掛羊頭賣狗肉”,因為這篇文章中舉的 master 例子“根本就算不上是個網絡服務器。
因為它的瓶頸根本就跟網絡無關。”
網絡服務器
“網絡服務器”這個術語確實含義模糊,到底指硬件還是軟件?到底是服務於網絡本身的機器(交
換機、路由器、防火牆、NAT),還是利用網絡為其他人或程序提供服務的機器(打印服務器、文件服
務器、郵件服務器)。每個人根據自己熟悉的領域,可能會有不同的解讀。比方說或許有人認為只有支
持高並發高吞吐的才算是網絡服務器。
為了避免無謂的爭執,我只用“網絡服務程序”或者“網絡應用程序”這種含義明確的術語。“開
發網絡服務程序”通常不會造成誤解。
客戶端?服務端?
在 TCP 網絡編程里邊,客戶端和服務端很容易區分,主動發起連接的是客戶端,被動接受連接的是
服務端。當然,這個“客戶端”本身也可能是個后台服務程序,HTTP Proxy 對 HTTP Server 來說就是個
客戶端。
客戶端編程?服務端編程?
但是“服務端編程”和“客戶端編程”就不那么好區分。比如 Web crawler,它會主動發起大量連
接,扮演的是 HTTP 客戶端的角色,但似乎應該歸入“服務端編程”。又比如寫一個 HTTP proxy,它既
會扮演服務端——被動接受 web browser 發起的連接,也會扮演客戶端——主動向 HTTP server 發起連接,
它究竟算服務端還是客戶端?我猜大多數人會把它歸入服務端編程。
那么究竟如何定義“服務端編程”?
服務端編程需要處理幾萬、幾十萬、上百萬並發連接?也許是,也許不是。比如雲風在一篇介紹網
游服務器的博客
5
中就談到,網游中用到的“連接服務器”需要處理大量連接,而“邏輯服務器”只有
一個外部連接。那么開發這種網游“邏輯服務器”算服務端編程還是客戶端編程呢?又比如機房的服務
監控軟件,並發數跟機器數成正比,至多也就是兩三千的並發連接。
我認為,“服務端網絡編程”指的是編寫沒有用戶界面的長期運行的網絡程序,程序默默地運行在
一台服務器上,通過網絡與其他程序打交道,而不必和人打交道。與之對應的是客戶端網絡程序,要么
是短時間運行,比如 wget;要么是有用戶界面(無論是字符界面還是圖形界面)。本文主要談服務端
網絡編程。服務端網絡編程有一些通用的模式,可參考前面提到的兩篇文章。
7x24 重要嗎?內存碎片可怕嗎?
一談到服務端網絡編程,有人立刻會提出 7x24 運行的要求。對於某些網絡設備而言,這是合理的
需求,比如交換機、路由器。對於開發商業系統,我認為要求程序 7x24 運行通常是系統設計上考慮不
5
http://blog.codingnow.com/2006/04/iocp_kqueue_epoll.html6
周。具體見《分布式系統的工程化開發方法》第 20 頁起。重要的不是 7x24,而是在程序不必做到 7x24
的情況下也能達到足夠高的可用性。一個考慮周到的系統應該允許每個進程都能隨時重啟,這樣才能在
廉價的服務器硬件上做到高可用性。
既然不要求 7x24,那么也不必害怕內存碎片,理由如下:
64-bit 系統的地址空間足夠大,不會出現沒有足夠的連續空間這種情況。有沒有誰能故意
制造內存碎片(不是內存泄露)使得服務程序失去響應?
現代的內存分配器(malloc 及其第三方實現)今非昔比,除了 memcached 這種純以內存為
賣點的程序需要自己設計分配器之外,其他網絡程序大可使用系統自帶的 malloc 或者某個
第三方實現。(重新發明 memory pool 似乎已經不流行了。)
Linux Kernel 也大量用到了動態內存分配。既然操作系統內核都不怕動態分配內存造成碎片,
應用程序為什么要害怕?應用程序的可靠性只要不低於硬件和操作系統的可靠性就行,普
通 PC 服務器的年故障率約為 3%,算一算你的服務程序一年要被意外重啟多少次。
內存碎片如何度量?有沒有什么工具能為當前進程的內存碎片狀況評個分?如果不能比較
兩種方案的內存碎片程度,談何優化?
有人為了避免所謂的內存碎片,害怕使用 STL 容器,也不敢 new/delete,這算是 premature
optimization 還是因噎廢食呢?
協議設計是網絡編程的核心
對於專用的業務系統,協議設計是核心任務,決定了系統的開發難度與可靠性,但是這個領域還沒
有形成大家公認的設計流程。
系統中哪個程序發起連接,哪個程序接受連接?如果寫標准的網絡服務,那么這不是問題,按 RFC
來就行了。自己設計業務系統,有沒有章法可循?以網游為例,到底是連接服務器主動連接邏輯服務器,
還是邏輯服務器主動連接“連接服務器”?似乎沒有定論,兩種做法都行。一般可以按照“依賴->被依
賴”的關系來設計發起連接的方向。
比新建連接難的是關閉連接。在傳統的網絡服務中(特別是短連接服務),不少是服務端主動關閉
連接,比如 daytime、HTTP/1.0。也有少部分是客戶端主動關閉連接,通常是些長連接服務,比如 echo、
chargen 等。我們自己的業務系統該如何設計連接關閉協議呢?
服務端主動關閉連接的缺點之一是會多占用服務器資源。服務端主動關閉連接之后會進入
TIME_WAIT 狀態,在一段時間之內 hold 住一些內核資源。如果並發訪問量很高,這會影響服務端的處
理能力。這似乎暗示我們應該把協議設計為客戶端主動關閉,讓 TIME_WAIT 狀態分散到多台客戶機器
上,化整為零。
這又有另外的問題:客戶端賴着不走怎么辦?會不會造成拒絕服務攻擊?或許有一個二者結合的方
案:客戶端在收到響應之后就應該主動關閉,這樣把 TIME_WAIT 留在客戶端。服務端有一個定時器,
如果客戶端若干秒鍾之內沒有主動斷開,就踢掉它。這樣善意的客戶端會把 TIME_WAIT 留給自己,
buggy 的客戶端會把 TIME_WAIT 留給服務端。或者干脆使用長連接協議,這樣避免頻繁創建銷毀連接。
比連接的建立與斷開更重要的是設計消息協議。消息格式很好辦,XML、JSON、Protobuf 都是很好
的選擇;難的是消息內容。一個消息應該包含哪些內容?多個程序相互通信如何避免 race condition(見
《分布式系統的工程化開發方法》p.16 的例子)?系統的全局狀態該如何躍遷?可惜這方面可供參考的7
例子不多,也沒有太多通用的指導原則,我知道的只有 30 年前提出的 end-to-end principle 和 happensbefore relationship。只能從實踐中慢慢積累了。
網絡編程的三個層次
侯捷先生在《漫談程序員與編程》中講到 STL 運用的三個檔次:“會用 STL,是一種檔次。對 STL
原理有所了解,又是一個檔次。追蹤過 STL 源碼,又是一個檔次。第三種檔次的人用起 STL 來,虎虎生
風之勢絕非第一檔次的人能夠望其項背。”
我認為網絡編程也可以分為三個層次:
1. 讀過教程和文檔
2. 熟悉本系統 TCP/IP 協議棧的脾氣
3. 自己寫過一個簡單的 TCP/IP stack
第一個層次是基本要求,讀過《Unix 網絡編程》這樣的編程教材,讀過《TCP/IP 詳解》基本理解
TCP/IP 協議,讀過本系統的 manpage。這個層次可以編寫一些基本的網絡程序,完成常見的任務。但網
絡編程不是照貓畫虎這么簡單,若是按照 manpage 的功能描述就能編寫產品級的網絡程序,那人生就
太幸福了。
第二個層次,熟悉本系統的 TCP/IP 協議棧參數設置與優化是開發高性能網絡程序的必備條件。摸
透協議棧的脾氣還能解決工作中遇到的比較復雜的網絡問題。拿 Linux 的 TCP/IP 協議棧來說:
有可能出現自連接(見《學之者生,用之者死——ACE 歷史與簡評》舉的三個硬傷),程
序應該有所准備。
Linux 的內核會有 bug,比如某種 TCP 擁塞控制算法曾經出現 TCP window clamping(窗口箝
位)bug,導致吞吐量暴跌,可以選用其他擁塞控制算法來繞開(work around)這個問題。
這些陰暗角落在 manpage 里沒有描述,要通過其他渠道了解。
編寫可靠的網絡程序的關鍵是熟悉各種場景下的 error code(文件描述符用完了如何?本地
ephemeral port 暫時用完,不能發起新連接怎么辦?服務端新建並發連接太快,backlog 用完了,客戶端
connect 會返回什么錯誤?),有的在 manpage 里有描述,有的要通過實踐或閱讀源碼獲得。
第三個層次,通過自己寫一個簡單的 TCP/IP 協議棧,能大大加深對 TCP/IP 的理解,更能明白 TCP
為什么要這么設計,有哪些因素制約,每一步操作的代價是什么,寫起網絡程序來更是成竹在胸。
其實實現 TCP/IP 只需要操作系統提供三個接口函數:一個函數,兩個回調函數。分別是:
send_packet()、on_receive_packet()、on_timer()。多年前有一篇文章《使用 libnet 與 libpcap 構造 TCP/IP
協議軟件》介紹了在用戶態實現 TCP/IP 的方法。lwIP 也是很好的借鑒對象。
如果有時間,我打算自己寫一個 Mini/Tiny/Toy/Trivial/Yet-Another TCP/IP。我准備換一個思路,用
TUN/TAP 設備在用戶態實現一個能與本機點對點通信的 TCP/IP 協議棧
6
,這樣那三個接口函數就表現為
我最熟悉的文件讀寫。在用戶態實現的好處是便於調試,協議棧做成靜態庫,與應用程序鏈接到一起
(庫的接口不必是標准的 Sockets API)。做完這一版,還可以繼續發揮,用 FTDI 的 USB-SPI 接口芯片連
6
《關於 TCP 並發連接的幾個思考題與試驗》 http://blog.csdn.net/solstice/article/details/65792328
接 ENC28J60 適配器,做一個真正獨立於操作系統的 TCP/IP stack。如果只實現最基本的 IP、ICMP Echo、
TCP 的話,代碼應能控制在 3000 行以內;也可以實現 UDP,如果應用程序需要用到 DNS 的話。
最主要的三個例子
我認為 TCP 網絡編程有三個例子最值得學習研究,分別是 echo、chat、proxy,都是長連接協議。
Echo 的作用:熟悉服務端被動接受新連接、收發數據、被動處理連接斷開。每個連接是獨立服務
的,連接之間沒有關聯。在消息內容方面 Echo 有一些變種:比如做成一問一答的方式,收到的請求和
發送響應的內容不一樣,這時候要考慮打包與拆包格式的設計,進一步還可以寫簡單的 HTTP 服務。
Chat 的作用:連接之間的數據有交流,從 a 收到的數據要發給 b。這樣對連接管理提出的更高的要
求:如何用一個程序同時處理多個連接?fork() per connection 似乎是不行的。如何防止串話?b 有可能
隨時斷開連接,而新建立的連接 c 可能恰好復用了 b 的文件描述符,那么 a 會不會錯誤地把消息發給 c?
Proxy 的作用:連接的管理更加復雜:既要被動接受連接,也要主動發起連接,既要主動關閉連接,
也要被動關閉連接。還要考慮兩邊速度不匹配,見《Muduo 網絡編程示例之十:socks4a 代理服務器》。
這三個例子功能簡單,突出了 TCP 網絡編程中的重點問題,挨着做一遍基本就能達到層次一的要求。
學習 Sockets API 的利器:IPython
我在編寫 muduo 網絡庫的時候,寫了一個命令行交互式的調試工具
7
,方便我試驗各個 Sockets API
的返回時機和返回值。后來發現其實可以用 IPython 達到相同的效果,不必自己編程。用交互式工具很
快就能摸清各種 IO 事件的發生條件,比反復編譯 C 代碼高效得多。比方說想簡單試驗一下 TCP 服務器
和 epoll,可以這么寫:
$ ipython
In [1]: import socket, select
In [2]: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
In [3]: s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
In [4]: s.bind(('', 5000))
In [5]: s.listen(5)
In [6]: client, address = s.accept() # client.fileno() == 4
In [7]: client.recv(1024) # 此處會阻塞
Out[7]: 'Hello\n'
In [8]: epoll = select.epoll()
In [9]: epoll.register(client.fileno(), select.EPOLLIN) # 試試省略第二個參數
In [10]: epoll.poll(60) # 此處會阻塞
Out[10]: [(4, 1)] # 表示第 4 號文件可讀(select.EPOLLIN == 1)
In [11]: client.recv(1024) # 已經有數據可讀,不會阻塞了
Out[11]: 'World\n'
In [12]: client.setblocking(0) # 改為非阻塞方式
In [13]: client.recv(1024) # 沒有數據可讀,立刻返回,錯誤碼 EAGAIN == 11
error: [Errno 11] Resource temporarily unavailable
7
http://blog.csdn.net/Solstice/article/details/54978149
In [14]: epoll.poll(60) # epoll_wait() 一下
Out[14]: [(4, 1)]
In [15]: client.recv(1024) # 再去讀數據,有了
Out[15]: 'Bye!\n'
In [16]: client.close()
同時在另一個命令行窗口用 nc 發送數據。
$ nc localhost 5000
Hello
World
Bye!
在編寫 muduo 的時候,我一般會開四個命令行窗口,其一看 log,其二看 strace,其三用 netcat/
tempest/ipython 充作通信對方,其四看 tcpdump。各個工具的輸出相互驗證,很快就摸清了門道。
muduo 是一個基於 Reactor 模式的 Linux C++網絡庫
8
,采用非阻塞 IO,支持高並發和多線程,核心代碼
量不大(3000 多行),示例豐富,可供網絡編程的學習者參考。
TCP 的可靠性有多高?
TCP 是“面向連接的、可靠的、字節流傳輸協議”,這里的“可靠”究竟是什么意思?
《Effective TCP/IP Programming》第 9 條說:Realize That TCP Is a Reliable Protocol, Not
an Infallible Protocol,那么 TCP 在哪種情況下會出錯?這里說的“出錯”指的是收到的數據與發送
的數據不一致,而不是數據不可達。
我在《一種自動反射消息類型的 Google Protobuf 網絡傳輸方案》中設計了帶 check sum 的消
息格式,很多人表示不理解,認為是多余的。IP header 里邊有 check sum,TCP header 也有
check sum,鏈路層以太網還有 CRC32 校驗,那么為什么還需要在應用層做校驗?什么情況下 TCP
傳送的數據會出錯?
IP header 和 TCP header 的 check sum 是一種非常弱的 16-bit check sum 算法,把數據當
成反碼表示的 16-bit integers,再加到一起。這種 checksum 算法能檢出一些簡單的錯誤,而對某些
錯誤無能為力,由於是簡單的加法,遇到“和”不變的情況就無法檢查出錯誤(比如交換兩個 16-bit 整
數,加法滿足交換律,結果不變)。以太網的 CRC32 比較強,但它只能保證同一個網段上的通信不會
出錯(兩台機器的網線插到同一個交換機上,這時候以太網的 CRC 是有用的)。但是,如果兩台機器
之間經過了多級路由器呢?
8
http://blog.csdn.net/solstice/article/category/77964610
上圖中 Client 向 Server 發了一個 TCP segment,這個 segment 先被封裝成一個 IP packet,再被封裝
成 ethernet frame,發送到路由器(圖中消息 a)。Router 收到 ethernet frame (b),轉發到另一個網段(c),
最后 Server 收到 d,通知應用程序。Ethernet CRC 能保證 a 和 b 相同,c 和 d 相同;TCP header check
sum 的強度不足以保證收發 payload 的內容一樣。另外,如果把 Router 換成 NAT,那么 NAT 自己會構
造 c(替換掉源地址),這時候 a 和 d 的 payload 不能用 tcp header checksum 校驗。
路由器可能出現硬件故障,比方說它的內存故障(或偶然錯誤)導致收發 IP 報文出現多 bit 的反
轉或雙字節交換,這個反轉如果發生在 payload 區,那么無法用鏈路層、網絡層、傳輸層的 check
sum 查出來,只能通過應用層的 check sum 來檢測。這個現象在開發的時候不會遇到,因為開發用
的幾台機器很可能都連到同一個交換機,ethernet CRC 能防止錯誤。開發和測試的時候數據量不大,錯
誤很難發生。之后大規模部署到生產環境,網絡環境復雜,這時候出個錯就讓人措手不及。有一篇論文
《When the CRC and TCP checksum disagree》分析了這個問題。另外《The Limitations of the
Ethernet CRC and TCP/IP checksums for error detection》
9
也值得一讀。
這個情況真的會發生嗎?會的,Amazon S3 在 2008 年 7 月就遇到過,單 bit 反轉導致了一次嚴
重線上事故,所以他們吸取教訓加了 check sum。見 http://status.aws.amazon.com/s3-20080720.html
另外一個例證:下載大文件的時候一般都會附上 MD5,這除了有安全方面的考慮(防止篡改),
也說明應用層應該自己設法校驗數據的正確性。這是 end-to-end principle 的一個例證。
三本必看的書
談到 Unix 編程和網絡編程,W. Richard Stevens 是個繞不開的人物,他生前寫了 6 本書,APUE、兩
卷 UNP、三卷 TCP/IP。有四本與網絡編程直接相關。UNP 第二卷其實跟網絡編程關系不大,是 APUE 在
多線程和進程間通信(IPC)方面的補充。很多人把 TCP/IP 一二三卷作為整體推薦,其實這三本書用處不同,
應該區別對待。
這里談到的幾本書都沒有超出孟岩在《TCP/IP 網絡編程之四書五經》中的推薦,說明網絡編程這一
領域已經相對成熟穩定。
《TCP/IP Illustrated, Vol. 1: The Protocols》中文名《TCP/IP 詳解》,以下簡稱 TCPv1。
TCPv1 是一本奇書。
9
http://noahdavids.org/self_published/CRC_and_checksum.html11
這本書迄今至少被三百多篇學術論文引用過 http://portal.acm.org/citation.cfm?id=161724。一本學術
專著被論文引用算不上出奇,難得的是一本寫給程序員看的技術書能被學術論文引用幾百次,我不知道
還有哪本技術書能做到這一點。
TCPv1 堪稱 TCP/IP 領域的聖經。作者 W. Richard Stevens 不是 TCP/IP 協議的發明人,他從使用者
(程序員)的角度,以 tcpdump 為工具,對 TCP 協議抽絲剝繭娓娓道來(第 17~24 章),讓人嘆服。
恐怕 TCP 協議的設計者也難以講解得如此出色,至少不會像他這么耐心細致地畫幾百幅收發 package 的
時序圖。
TCP 作為一個可靠的傳輸層協議,其核心有三點:
1. Positive acknowledgement with retransmission
2. Flow control using sliding window(包括 Nagle 算法等)
3. Congestion control(包括 slow start、congestion avoidance、fast retransmit 等)
第一點已經足以滿足“可靠性”要求(為什么?);第二點是為了提高吞吐量,充分利用鏈路層帶
寬;第三點是防止過載造成丟包。換言之,第二點是避免發得太慢,第三點是避免發得太快,二者相互
制約。從反饋控制的角度看,TCP 像是一個自適應的節流閥,根據管道的擁堵情況自動調整閥門的流量。
TCP 的 flow control 有一個問題,每個 TCP connection 是彼此獨立的,保存有自己的狀態變量;一個
程序如果同時開啟多個連接,或者操作系統中運行多個網絡程序,這些連接似乎不知道他人的存在,缺
少對網卡帶寬的統籌安排。(或許現代的操作系統已經解決了這個問題?)
TCPv1 唯一的不足是它出版太早了,1993 年至今網絡技術發展了幾代。鏈路層方面,當年主流的
10Mbit 網卡和集線器早已經被淘汰;100Mbit 以太網也沒什么企業在用了,交換機(switch)也已經全面取
代了集線器(hub);服務器機房以 1Gbit 網絡為主,有些場合甚至用上了 10Gbit 以太網。另外,無線網的
普及也讓 TCP flow control 面臨新挑戰;原來設計 TCP 的時候,人們認為丟包通常是擁塞造成的,這時
應該放慢發送速度,減輕擁塞;而在無線網中,丟包可能是信號太弱造成的,這時反而應該快速重試,
以保證性能。網絡層方面變化不大,IPv6 雷聲大雨點小。傳輸層方面,由於鏈路層帶寬大增,TCP
window scale option 被普遍使用,另外 TCP timestamps option 和 TCP selective ack option 也很常用。由於
這些因素,在現在的 Linux 機器上運行 tcpdump 觀察 TCP 協議,程序輸出會與原書有些不同。
一個好消息:TCPv1 已於 2011 年 10 月由人續寫了第二版,不知能否延續輝煌?
http://www.amazon.com/gp/product/0321336313
《Unix Network Programming, Vol. 1: Networking API》第二版或第三版(這兩版的副標題稍有不同,
第三版去掉了 XTI),以下統稱 UNP,如果需要會以 UNP2e、UNP3e 細分。
UNP 是 Sockets API 的權威指南,但是網絡編程遠不是使用那十幾個 Sockets API 那么簡單,作者
W. Richard Stevens 深刻地認識到這一點,他在 UNP2e 的前言中寫到:
http://www.kohala.com/start/preface.unpv12e.html
I have found when teaching network programming that about 80% of all network programming
problems have nothing to do with network programming, per se. That is, the problems are not
with the API functions such as accept and select, but the problems arise from a lack of
understanding of the underlying network protocols. For example, I have found that once a
student understands TCP's three-way handshake and four-packet connection termination, many
network programming problems are immediately understood.12
搞網絡編程,一定要熟悉 TCP/IP 協議及其外在表現(比如打開和關閉 Nagle 算法對收發包的影響),
不然出點意料之外的情況就摸不着頭腦了。我不知道為什么 UNP3e 在前言中去掉了這段至關重要的話。
另外值得一提的是,UNP 中文版翻譯得相當好,譯者楊繼張先生是真懂網絡編程的。
UNP 很詳細,面面俱到,UDP、TCP、IPv4、IPv6 都講到了。要說有什么缺點的話,就是太詳細了,
重點不夠突出。我十分贊同孟岩說的
“(孟岩)我主張,在具備基礎之后,學習任何新東西,都要抓住主線,突出重點。對
於關鍵理論的學習,要集中精力,速戰速決。而旁枝末節和非本質性的知識內容,完全可
以留給實踐去零敲碎打。
“原因是這樣的,任何一個高級的知識內容,其中都只有一小部分是有思想創新、有重
大影響的,而其它很多東西都是瑣碎的、非本質的。因此,集中學習時必須把握住真正重
要那部分,把其它東西留給實踐。對於重點知識,只有集中學習其理論,才能確保體系性、
連貫性、正確性,而對於那些旁枝末節,只有邊干邊學能夠讓你了解它們的真實價值是大
是小,才能讓你留下更生動的印象。如果你把精力用錯了地方,比如用集中大塊的時間來
學習那些本來只需要查查手冊就可以明白的小技巧,而對於真正重要的、思想性東西放在
平時零敲碎打,那么肯定是事倍功半,甚至適得其反。
“因此我對於市面上絕大部分開發類圖書都不滿——它們基本上都是面向知識體系本身
的,而不是面向讀者的。總是把相關的所有知識細節都放在一堆,然后一堆一堆攢起來變
成一本書。反映在內容上,就是毫無重點地平鋪直敘,不分輕重地陳述細節,往往在第三
章以前就用無聊的細節謀殺了讀者的熱情。為什么當年侯捷先生的《深入淺出 MFC》和
Scott Meyers 的 Effective C++ 能夠成為經典?就在於這兩本書抓住了各自領域中的主干,提
綱挈領,綱舉目張,一下子打通讀者的任督二脈。可惜這樣的書太少,就算是已故 Richard
Stevens 和當今 Jeffrey Richter 的書,也只是在體系性和深入性上高人一頭,並不是面向讀者
的書。”
什么是旁枝末節呢?拿以太網來說,CRC32 如何計算就是“旁枝末節”。網絡程序員要明白 check
sum 的作用,知道為什么需要 check sum,至於具體怎么算 CRC 就不需要程序員操心。這部分通常是由
網卡硬件完成的,在發包的時候由硬件填充 CRC,在收包的時候網卡自動丟棄 CRC 不合格的包。如果代
碼里邊確實要用到 CRC 計算,調用通用的 zlib 就行,也不用自己實現。
UNP 就像給了你一堆做菜的原料(各種 Sockets 函數的用法),常用和不常用的都給了(Out-ofBand Data、Signal-Driven IO 等等),要靠讀者自己設法取舍組合,做出一盤大菜來。在第一遍讀的時候,
我建議只讀那些基本且重要的章節;另外那些次要的內容可略作了解,即便跳過不讀也無妨。UNP 是一
本操作性很強的書,讀這本這本書一定要上機練習。13
另外,UNP 舉的兩個例子(菜譜)太簡單,daytime 和 echo 一個是短連接協議,一個是長連接無
格式協議,不足以覆蓋基本的網絡開發場景(比如 TCP 封包與拆包、多連接之間交換數據)。我估計
W. Richard Stevens 原打算在 UNP 第三卷中講解一些實際的例子,只可惜他英年早逝,我等無福閱讀。
UNP 是一本偏重 Unix 傳統的書,這本書寫作的時候服務端還不需要處理成千上萬的連接,也沒有
現在那么多網絡攻擊。書中重點介紹的以 accept()+fork()來處理並發連接的方式在現在看來已經有點吃
力,這本書的代碼也沒有特別防范惡意攻擊。如果工作涉及這些方面,需要再進一步學習專門的知識
(C10k 問題,安全編程)。
TCPv1 和 UNP 應該先看哪本?我不知道。我自己是先看的 TCPv1,花了大約半學期時間,然后再讀
UNP2e 和 APUE。
《Effective TCP/IP Programming》
第三本書我猶豫了很久,不知道該推薦哪本,還有哪本書能與 W. Richard Stevens 的這兩本比肩嗎?
W. Richard Stevens 為技術書籍的寫作樹立了難以逾越的標桿,他是一位偉大的技術作家。沒能看到他寫
完 UNP 第三卷實在是人生的遺憾。
《Effective TCP/IP Programming》這本書屬於專家經驗總結類,初看時覺得收獲很大,工作一段時
間再看也能有新的發現。比如第 6 條“TCP 是一個字節流協議”,看過這一條就不會去研究所謂的
“TCP 粘包問題”。我手頭這本電力社 2001 年的中文版翻譯尚可,但是很狗血的是把參考文獻去掉了,
正文中引用的文章資料根本查不到名字。人郵 2011 年重新翻譯出版的版本有參考文獻。
其他值得一看的書
以下兩本都不易讀,需要相當的基礎。
《TCP/IP Illustrated, Vol. 2: The Implementation》以下簡稱 TCPv2
1200 頁的大部頭,詳細講解了 4.4BSD 的完整 TCP/IP 協議棧,注釋了 15,000 行 C 源碼。這本書啃
下來不容易,如果時間不充裕,我認為沒必要啃完,應用層的網絡程序員選其中與工作相關的部分來閱
讀即可。
這本書第一作者是 Gary Wright,從敘述風格和內容組織上是典型的“面向知識體系本身”,先講
mbuf,再從鏈路層一路往上、以太網、IP 網絡層、ICMP、IP 多播、IGMP、IP 路由、多播路由、Sockets
系統調用、ARP 等等。到了正文內容 3/4 的地方才開始講 TCP。面面俱到、主次不明。
對於主要使用 TCP 的程序員,我認為 TCPv2 一大半內容可以跳過不看,比如路由表、IGMP 等等
(開發網絡設備的人可能更關心這些內容)。在工作中大可以把 IP 視為 host-to-host 的協議,把“IP
packet 如何送達對方機器”的細節視為黑盒子,這不會影響對 TCP 的理解和運用,因為網絡協議是分層
的。這樣精簡下來,需要看的只有三四百頁,四五千行代碼,大大減輕了負擔。
這本書直接呈現高質量的工業級操作系統源碼,讀起來有難度,讀懂它甚至要有“不求甚解的能
力”。其一,代碼只能看,不能上機運行,也不能改動試驗。其二,與操作系統其他部分緊密關聯。比
如 TCP/IP stack 下接網卡驅動、軟中斷;上承 inode 轉發來的系統調用操作;中間還要與平級的進程文
件描述符管理子系統打交道;如果要把每一部分都弄清楚,把持不住就迷失主題了。其三,一些歷史包
袱讓代碼變復雜晦澀。比如 BSD 在 80 年代初需要在只有 4M 內存的 VAX 上實現 TCP/IP,內存方面捉襟
見肘,這才發明了 mbuf 結構,代碼也增加了不少偶發復雜度(buffer 不連續的處理)。14
讀這套 TCP/IP 書切忌膠柱鼓瑟,這套書以 4.4BSD 為底,其描述的行為(特別是與 timer 相關的行
為)與現在的 Linux TCP/IP 有不小的出入,用書本上的知識直接套用到生產環境的 Linux 系統可能會造
成不小的誤解和困擾。(TCPv3 不重要,可以成套買來收藏,不讀亦可。)
《Pattern-Oriented Software Architecture Volume 2: Patterns for Concurrent and Networked Objects》以
下簡稱 POSA2
這本書總結了開發並發網絡服務程序的模式,是對 UNP 很好的補充。UNP 中的代碼往往把業務邏
輯和 Sockets API 調用混在一起,代碼固然短小精悍,但是這種編碼風格恐怕不適合開發大型的網絡程
序。POSA2 強調模塊化,網絡通信交給 library/framework 去做,程序員寫代碼只關注業務邏輯,這是非
常重要的思想。閱讀這本書對於深入理解常用的 event-driven 網絡庫(libevent、Java Netty、Java Mina、
Perl POE、Python Twisted 等等)也很有幫助,因為這些庫都是依照這本書的思想編寫的。
POSA2 的代碼是示意性的,思想很好,細節不佳。其 C++ 代碼沒有充分考慮資源的自動化管理
(RAII),如果直接按照書中介紹的方式去實現網絡庫,那么會給使用者造成不小的負擔與陷阱。換言之,
照他說的做,而不是照他做的學。
不值一看的書
Douglas Comer 教授名氣很大,著作等身,但是他寫的網絡方面的書不值一讀,味同嚼蠟。網絡編
程與 TCP/IP 方面,有 W. Richard Stevens 的書扛鼎;計算機網絡原理方面,有 Kurose 的“自頂向下”和
Peterson 的“系統”打旗,沒其他人什么事兒。順便一提,Tanenbaum 的操作系統教材是最好的之一
(嗯,之二,因為他寫了兩本:“現代”和“設計與實現”),不過他的計算機網絡和體系結構教材的
地位比不上他的操作系統書的地位。體系結構方面,Patterson 和 Hennessy 二人合作的兩本書是最好的,
近年來嶄露頭角的《深入理解計算機系統》也非常好;當然,側重點不同。
(完)
2011-06-06: 初版。
2011-06-08: 修正 TCP checksum 表述錯誤。
2012-02-13: 增加 ipython 小節。
