談一談網絡編程學習經驗(06-08更新)

談一談網絡編程學習經驗

陳碩

[email protected]

blog.csdn.net/Solstice

2011-06-06

PDF 版下載:https://github.com/downloads/chenshuo/documents/LearningNetworkProgramming.pdf

本文談一談我在學習網絡編程方面的一些個人經驗。“網絡編程”這個術語的範圍很廣,本文指用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 協議,程序輸出會與原書有些不同。

一個好消息:TCPv1將於今年10月(2011年)推出第二版,Amazon 的預定頁面是: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.

搞網絡編程,一定要熟悉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二人合作的兩本書是最好的,近年來嶄露頭角的《深入理解計算機系統》也非常好;當然,側重點不同。

(完)

發佈了145 篇原創文章 · 獲贊 114 · 訪問量 363萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章