1. 程式人生 > >談一談網路程式設計學習經驗(06-08更新)

談一談網路程式設計學習經驗(06-08更新)

談一談網路程式設計學習經驗

陳碩

[email protected]

blog.csdn.net/Solstice

2011-06-08

本文談一談我在學習網路程式設計方面的一些個人經驗。“網路程式設計”這個術語的範圍很廣,本文指用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上傳發布到某入口網站的體育頻道。系統中還有一個錄入賽程(參賽隊,運動員,出場順序等)資料庫的程式,執行在資料庫伺服器上。算下來整個系統有十來個程式,執行在二十多臺裝置(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 的時候有用。無論如何,這些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成為了市場份額最大的伺服器作業系統(http://en.wikipedia.org/wiki/Usage_share_of_operating_systems)。在Linux這種大眾系統上學網路程式設計,遇到什麼問題會比較容易解決。因為用的人多,你遇到的問題別人多半也遇到過;同樣因為用的人多,如果真的有什麼核心bug,很快就會得到修復,至少有work around的辦法。如果用別的系統,可能一個問題發到論壇上半個月都不會有人理。從核心原始碼的風格看,FreeBSD更乾淨整潔,註釋到位,但是無奈它的市場份額遠不如Linux,學習Linux是更好的技術投資。

可移植性重要嗎?

寫網路程式要不要考慮移植性?這取決於專案需要,如果貴公司做的程式要賣給其他公司,而對方可能使用Windows、Linux、FreeBSD、Solaris、AIX、HP-UX等等作業系統,這時候考慮移植性。如果編寫公司內部的伺服器上用的網路程式,那麼大可只關注一個平臺,比如Linux。因為編寫和維護可移植的網路程式的代價相當高,平臺間的差異可能遠比想象中大,即便是POSIX系統之間也有不小的差異(比如Linux沒有SO_NOSIGPIPE選項),錯誤的返回碼也大不一樣。

我就不打算把muduo往Windows或其他作業系統移植。如果需要編寫可移植的網路程式,我寧願用libevent或者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協議之上開發業務軟體。

面向業務的網路程式設計的特點

跟開發通用的網路程式不同,開發面向公司業務的專用網路程式有其特點:

· 業務邏輯比較複雜,而且時常變化

如果寫一個HTTP伺服器,在大致實現HTTP /1.1標準之後,程式的主體功能一般不會有太大的變化,程式設計師會把時間放在效能調優和bug修復上。而開發針對公司業務的專用程式時,功能說明書(spec)很可能不如HTTP/1.1標準那麼細緻明確。更重要的是,程式是快速演化的。以即時聊天工具的後臺伺服器為例,可能第一版只支援線上聊天;幾個月之後釋出第二版,支援離線訊息;又過了幾個月,第三版支援隱身聊天;隨後,第四版支援上傳頭像;如此等等。這要求程式設計師能快速響應新的業務需求,公司才能保持競爭力。

· 不一定需要遵循公認的通訊協議標準

比方說網遊伺服器就沒什麼協議標準,反正客戶端和服務端都是本公司開發,如果發現目前的協議設計有問題,兩邊一起改了就是了。

· 程式結構沒有定論

對於高併發大吞吐的標準網路服務,一般採用單執行緒事件驅動的方式開發,比如HAProxy、lighttpd等都是這個模式。但是對於專用的業務系統,其業務邏輯比較複雜,佔用較多的CPU資源,這種單執行緒事件驅動方式不見得能發揮現在多核處理器的優勢。這留給程式設計師比較大的自由發揮空間,做好了橫掃千軍,做爛了一敗塗地。

· 效能評判的標準不同

如果開發httpd這樣的通用服務,必然會和開源的Nginx、lighttpd等高效能伺服器比較,程式設計師要投入相當的精力去優化程式,才能在市場上佔有一席之地。而面向業務的專用網路程式不一定有開源的實現以供對比效能,程式設計師通常更加註重功能的穩定性與開發的便捷性。效能只要一代比一代強即可。

· 網路程式設計起到支撐作用,但不處於主導地位

程式設計師的主要工作是實現業務邏輯,而不只是實現網路通訊協議。這要求程式設計師深入理解業務。程式的效能瓶頸不一定在網路上,瓶頸有可能是CPU、Disk IO、資料庫等等,這時優化網路方面的程式碼並不能提高整體效能。只有對所在的領域有深入的瞭解,明白各種因素的權衡(trade-off),才能做出一些有針對性的優化。

幾個術語

網際網路上的很多口水戰是由對同一術語的不同理解引起的,比我寫的《多執行緒伺服器的適用場合》就曾經人被說是“掛羊頭賣狗肉”,因為這篇文章中舉的 master例子“根本就算不上是個網路伺服器。因為它的瓶頸根本就跟網路無關。”

· 網路伺服器

“網路伺服器”這個術語確實含義模糊,到底指硬體還是軟體?到底是服務於網路本身的機器(交換機、路由器、防火牆、NAT),還是利用網路為其他人或程式提供服務的機器(列印伺服器、檔案伺服器、郵件伺服器)。每個人根據自己熟悉的領域,可能會有不同的解讀。比方說或許有人認為只有支援高併發高吞吐的才算是網路伺服器。

為了避免無謂的爭執,我只用“網路服務程式”或者“網路應用程式”這種含義明確的術語。“開發網路服務程式”通常不會造成誤解。

· 客戶端?服務端?

在TCP網路程式設計裡邊,客戶端和服務端很容易區分,主動發起連線的是客戶端,被動接受連線的是服務端。當然,這個“客戶端”本身也可能是個後臺服務程式,HTTP Proxy對HTTP Server來說就是個客戶端。

· 客戶端程式設計?服務端程式設計?

但是“服務端程式設計”和“客戶端程式設計”就不那麼好區分。比如 Web crawler,它會主動發起大量連線,扮演的是HTTP客戶端的角色,但似乎應該歸入“服務端程式設計”。又比如寫一個 HTTP proxy,它既會扮演服務端——被動接受 web browser 發起的連線,也會扮演客戶端——主動向 HTTP server 發起連線,它究竟算服務端還是客戶端?我猜大多數人會把它歸入服務端程式設計。

那麼究竟如何定義“服務端程式設計”?

服務端程式設計需要處理大量併發連線?也許是,也許不是。比如雲風在一篇介紹網遊伺服器的部落格http://blog.codingnow.com/2006/04/iocp_kqueue_epoll.html中就談到,網遊中用到的“連線伺服器”需要處理大量連線,而“邏輯伺服器”只有一個外部連線。那麼開發這種網遊“邏輯伺服器”算服務端程式設計還是客戶端程式設計呢?

我認為,“服務端網路程式設計”指的是編寫沒有使用者介面的長期執行的網路程式,程式默默地執行在一臺伺服器上,通過網路與其他程式打交道,而不必和人打交道。與之對應的是客戶端網路程式,要麼是短時間執行,比如wget;要麼是有使用者介面(無論是字元介面還是圖形介面)。本文主要談服務端網路程式設計。

7x24重要嗎?記憶體碎片可怕嗎?

一談到服務端網路程式設計,有人立刻會提出7x24執行的要求。對於某些網路裝置而言,這是合理的需求,比如交換機、路由器。對於開發商業系統,我認為要求程式7x24執行通常是系統設計上考慮不周。具體見《分散式系統的工程化開發方法》第20頁起。重要的不是7x24,而是在程式不必做到7x24的情況下也能達到足夠高的可用性。一個考慮周到的系統應該允許每個程序都能隨時重啟,這樣才能在廉價的伺服器硬體上做到高可用性。

既然不要求7x24,那麼也不必害怕記憶體碎片,理由如下:

· 64-bit系統的地址空間足夠大,不會出現沒有足夠的連續空間這種情況。

· 現在的記憶體分配器(malloc及其第三方實現)今非昔比,除了memcached這種純以記憶體為賣點的程式需要自己設計分配器之外,其他網路程式大可使用系統自帶的malloc或者某個第三方實現。

· Linux Kernel也大量用到了動態記憶體分配。既然作業系統核心都不怕動態分配記憶體造成碎片,應用程式為什麼要害怕?

· 記憶體碎片如何度量?有沒有什麼工具能為當前程序的記憶體碎片狀況評個分?如果不能比較兩種方案的記憶體碎片程度,談何優化?

有人為了避免記憶體碎片,不使用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的例子)?系統的全域性狀態該如何躍遷?可惜這方面可供參考的例子不多,也沒有太多通用的指導原則,我知道的只有30年前提出的end-to-end principle和happens-before 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協議棧,這樣那三個介面函式就表現為我最熟悉的檔案讀寫。在使用者態實現的好處是便於除錯,協議棧做成靜態庫,與應用程式連結到一起(庫的介面不必是標準的Sockets API)。做完這一版,還可以繼續發揮,用FTDI的USB-SPI介面晶片連線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網路程式設計中的重點問題,挨著做一遍基本就能達到層次一的要求。

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是有用的)。但是,如果兩臺機器之間經過了多級路由器呢?

router

上圖中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》( http://noahdavids.org/self_published/CRC_and_checksum.html )也值得一讀。

這個情況真的會發生嗎?會的,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 是一本奇書。

這本書迄今至少被三百多篇學術論文引用過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 協議,程式輸出會與原書有些不同。

· 《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.

搞網路程式設計,一定要熟悉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-of-Band Data、Signal-Driven IO 等等),要靠讀者自己設法取捨組合,做出一盤大菜來。在第一遍讀的時候,我建議只讀那些基本且重要的章節;另外那些次要的內容可略作了解,即便跳過不讀也無妨。UNP是一本操作性很強的書,讀這本這本書一定要上機練習。

另外,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不連續的處理)。

讀這套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二人合作的兩本書是最好的,近年來嶄露頭角的《深入理解計算機系統》也非常好;當然,側重點不同。

(完)