談一談網(wǎng)絡(luò)編程學(xué)習(xí)經(jīng)驗(yàn)
陳碩
giantchen@gmail.com
blog.csdn.net/Solstice
2011-06-08
PDF 版下載:https://github.com/downloads/chenshuo/documents/LearningNetworkProgramming.pdf
本文談一談我在學(xué)習(xí)網(wǎng)絡(luò)編程方面的一些個(gè)人經(jīng)驗(yàn)。“網(wǎng)絡(luò)編程”這個(gè)術(shù)語(yǔ)的范圍很廣,本文指用Sockets API開(kāi)發(fā)基于TCP/IP的網(wǎng)絡(luò)應(yīng)用程序,具體定義見(jiàn)“網(wǎng)絡(luò)編程的各種任務(wù)角色”一節(jié)。
受限于本人的經(jīng)歷和經(jīng)驗(yàn),這篇文章的適應(yīng)范圍是:
· x86-64 Linux服務(wù)端網(wǎng)絡(luò)編程,直接或間接使用 Sockets API
· 公司內(nèi)網(wǎng)。不一定是局域網(wǎng),但總體位于公司防火墻之內(nèi),環(huán)境可控
本文可能不適合:
· PC客戶端網(wǎng)絡(luò)編程,程序運(yùn)行在客戶的PC上,環(huán)境多變且不可控
· Windows網(wǎng)絡(luò)編程
· 面向公網(wǎng)的服務(wù)程序
· 高性能網(wǎng)絡(luò)服務(wù)器
本文分兩個(gè)部分:
1. 網(wǎng)絡(luò)編程的一些胡思亂想,談?wù)勎覍?duì)這一領(lǐng)域的認(rèn)識(shí)
2. 幾本必看的書(shū),基本上還是W. Richard Stevents那幾本
另外,本文沒(méi)有特別說(shuō)明時(shí)均暗指TCP協(xié)議,“連接”是“TCP連接”,“服務(wù)端”是“TCP服務(wù)端”。
網(wǎng)絡(luò)編程的一些胡思亂想
以下胡亂列出我對(duì)網(wǎng)絡(luò)編程的一些想法,前后無(wú)關(guān)聯(lián)。
網(wǎng)絡(luò)編程是什么?
網(wǎng)絡(luò)編程是什么?是熟練使用Sockets API嗎?說(shuō)實(shí)話,在實(shí)際項(xiàng)目里我只用過(guò)兩次Sockets API,其他時(shí)候都是使用封裝好的網(wǎng)絡(luò)庫(kù)。
第一次是2005年在學(xué)校做一個(gè)羽毛球賽場(chǎng)計(jì)分系統(tǒng):我用C# 編寫(xiě)運(yùn)行在PC機(jī)上的軟件,負(fù)責(zé)比分的顯示;再用C# 寫(xiě)了運(yùn)行在PDA上的計(jì)分界面,記分員拿著PDA記錄比分;這兩部分程序通過(guò) TCP協(xié)議相互通信。這其實(shí)是個(gè)簡(jiǎn)單的分布式系統(tǒng),體育館有不止一片場(chǎng)地,每個(gè)場(chǎng)地都有一名拿PDA的記分員,每個(gè)場(chǎng)地都有兩臺(tái)顯示比分的PC機(jī)(顯示器是42吋平板電視,放在場(chǎng)地的對(duì)角,這樣兩邊看臺(tái)的觀眾都能看到比分)。這兩臺(tái)PC機(jī)功能不完全一樣,一臺(tái)只負(fù)責(zé)顯示當(dāng)前比分,另一臺(tái)還要負(fù)責(zé)與PDA通信,并更新數(shù)據(jù)庫(kù)里的比分信息。此外,還有一臺(tái)PC機(jī)負(fù)責(zé)周期性地從數(shù)據(jù)庫(kù)讀出全部7片場(chǎng)地的比分,顯示在體育館墻上的大屏幕上。這臺(tái)PC上還運(yùn)行著一個(gè)程序,負(fù)責(zé)生成比分?jǐn)?shù)據(jù)的靜態(tài)頁(yè)面,通過(guò)FTP上傳發(fā)布到某門(mén)戶網(wǎng)站的體育頻道。系統(tǒng)中還有一個(gè)錄入賽程(參賽隊(duì),運(yùn)動(dòng)員,出場(chǎng)順序等)數(shù)據(jù)庫(kù)的程序,運(yùn)行在數(shù)據(jù)庫(kù)服務(wù)器上。算下來(lái)整個(gè)系統(tǒng)有十來(lái)個(gè)程序,運(yùn)行在二十多臺(tái)設(shè)備(PC和PDA)上,還要考慮可靠性。將來(lái)有機(jī)會(huì)把這個(gè)小系統(tǒng)仔細(xì)講一講,挺有意思的。
這是我第一次寫(xiě)實(shí)際項(xiàng)目中的網(wǎng)絡(luò)程序,當(dāng)時(shí)寫(xiě)下來(lái)的感覺(jué)是像寫(xiě)命令行與用戶交互的程序:程序在命令行輸出一句提示語(yǔ),等待客戶輸入一句話,然后處理客戶輸入,再輸出下一句提示語(yǔ),如此循環(huán)。只不過(guò)這里的“客戶”不是人,而是另一個(gè)程序。在建立好TCP連接之后,雙方的程序都是read/write循環(huán)(為求簡(jiǎn)單,我用的是blocking讀寫(xiě)),直到有一方斷開(kāi)連接。
第二次是2010年編寫(xiě)muduo網(wǎng)絡(luò)庫(kù),我再次拿起了Sockets API,寫(xiě)了一個(gè)基于Reactor模式的C++ 網(wǎng)絡(luò)庫(kù)。寫(xiě)這個(gè)庫(kù)的目的之一就是想讓日常的網(wǎng)絡(luò)編程從Sockets API的瑣碎細(xì)節(jié)中解脫出來(lái),讓程序員專注于業(yè)務(wù)邏輯,把時(shí)間用在刀刃上。Muduo 網(wǎng)絡(luò)庫(kù)的示例代碼包含了幾十個(gè)網(wǎng)絡(luò)程序,這些示例程序都沒(méi)有直接使用Sockets API。
在此之外,無(wú)論是實(shí)習(xí)還是工作,雖然我寫(xiě)的程序都會(huì)通過(guò)TCP協(xié)議與其他程序打交道,但我沒(méi)有直接使用過(guò)Sockets API。對(duì)于TCP網(wǎng)絡(luò)編程,我認(rèn)為核心是處理“三個(gè)半事件”,見(jiàn)《Muduo 網(wǎng)絡(luò)編程示例之零:前言》中的“TCP 網(wǎng)絡(luò)編程本質(zhì)論”。程序員的主要工作是在事件處理函數(shù)中實(shí)現(xiàn)業(yè)務(wù)邏輯,而不是和Sockets API較勁。
這里還是沒(méi)有說(shuō)清楚“網(wǎng)絡(luò)編程”是什么,請(qǐng)繼續(xù)閱讀后文“網(wǎng)絡(luò)編程的各種任務(wù)角色”。
學(xué)習(xí)網(wǎng)絡(luò)編程有用嗎?
以上說(shuō)的是比較底層的網(wǎng)絡(luò)編程,程序代碼直接面對(duì)從TCP或UDP收到的數(shù)據(jù)以及構(gòu)造數(shù)據(jù)包發(fā)出去。在實(shí)際工作中,另一種常見(jiàn) 的情況是通過(guò)各種 client library 來(lái)與服務(wù)端打交道,或者在現(xiàn)成的框架中填空來(lái)實(shí)現(xiàn)server,或者采用更上層的通信方式。比如用libmemcached與memcached打交道,使用libpq來(lái)與PostgreSQL 打交道,編寫(xiě)Servlet來(lái)響應(yīng)http請(qǐng)求,使用某種RPC與其他進(jìn)程通信,等等。這些情況都會(huì)發(fā)生網(wǎng)絡(luò)通信,但不一定算作“網(wǎng)絡(luò)編程”。如果你的工作是前面列舉的這些,學(xué)習(xí)TCP/IP網(wǎng)絡(luò)編程還有用嗎?
我認(rèn)為還是有必要學(xué)一學(xué),至少在troubleshooting 的時(shí)候有用。無(wú)論如何,這些library或framework都會(huì)調(diào)用底層的Sockets API來(lái)實(shí)現(xiàn)網(wǎng)絡(luò)功能。當(dāng)你的程序遇到一個(gè)線上問(wèn)題,如果你熟悉Sockets API,那么從strace不難發(fā)現(xiàn)程序卡在哪里,盡管可能你沒(méi)有直接調(diào)用這些Sockets API。另外,熟悉TCP/IP協(xié)議、會(huì)用tcpdump也大大有助于分析解決線上網(wǎng)絡(luò)服務(wù)問(wèn)題。
在什么平臺(tái)上學(xué)習(xí)網(wǎng)絡(luò)編程?
對(duì)于服務(wù)端網(wǎng)絡(luò)編程,我建議在Linux上學(xué)習(xí)。
如果在10年前,這個(gè)問(wèn)題的答案或許是FreeBSD,因?yàn)镕reeBSD根正苗紅,在2000年那一次互聯(lián)網(wǎng)浪潮中扮演了重要角色,是很多公司首選的免費(fèi)服務(wù)器操作系統(tǒng)。2000年那會(huì)兒Linux還遠(yuǎn)未成熟,連epoll都還沒(méi)有實(shí)現(xiàn)。(FreeBSD在2001年發(fā)布4.1版,加入了kqueue,從此C10k不是問(wèn)題。)
10年后的今天,事情起了變化,Linux成為了市場(chǎng)份額最大的服務(wù)器操作系統(tǒng)(http://en.wikipedia.org/wiki/Usage_share_of_operating_systems)。在Linux這種大眾系統(tǒng)上學(xué)網(wǎng)絡(luò)編程,遇到什么問(wèn)題會(huì)比較容易解決。因?yàn)橛玫娜硕啵阌龅降膯?wèn)題別人多半也遇到過(guò);同樣因?yàn)橛玫娜硕啵绻娴挠惺裁磧?nèi)核bug,很快就會(huì)得到修復(fù),至少有work around的辦法。如果用別的系統(tǒng),可能一個(gè)問(wèn)題發(fā)到論壇上半個(gè)月都不會(huì)有人理。從內(nèi)核源碼的風(fēng)格看,F(xiàn)reeBSD更干凈整潔,注釋到位,但是無(wú)奈它的市場(chǎng)份額遠(yuǎn)不如Linux,學(xué)習(xí)Linux是更好的技術(shù)投資。
可移植性重要嗎?
寫(xiě)網(wǎng)絡(luò)程序要不要考慮移植性?這取決于項(xiàng)目需要,如果貴公司做的程序要賣(mài)給其他公司,而對(duì)方可能使用Windows、Linux、FreeBSD、Solaris、AIX、HP-UX等等操作系統(tǒng),這時(shí)候考慮移植性。如果編寫(xiě)公司內(nèi)部的服務(wù)器上用的網(wǎng)絡(luò)程序,那么大可只關(guān)注一個(gè)平臺(tái),比如Linux。因?yàn)榫帉?xiě)和維護(hù)可移植的網(wǎng)絡(luò)程序的代價(jià)相當(dāng)高,平臺(tái)間的差異可能遠(yuǎn)比想象中大,即便是POSIX系統(tǒng)之間也有不小的差異(比如Linux沒(méi)有SO_NOSIGPIPE選項(xiàng)),錯(cuò)誤的返回碼也大不一樣。
我就不打算把muduo往Windows或其他操作系統(tǒng)移植。如果需要編寫(xiě)可移植的網(wǎng)絡(luò)程序,我寧愿用libevent或者Java Netty這樣現(xiàn)成的庫(kù),把臟活累活留給別人。
網(wǎng)絡(luò)編程的各種任務(wù)角色
計(jì)算機(jī)網(wǎng)絡(luò)是個(gè) big topic,涉及很多人物和角色,既有開(kāi)發(fā)人員,也有運(yùn)維人員。比方說(shuō):公司內(nèi)部?jī)膳_(tái)機(jī)器之間 ping 不通,通常由網(wǎng)絡(luò)運(yùn)維人員解決,看看是布線有問(wèn)題還是路由器設(shè)置不對(duì);兩臺(tái)機(jī)器能ping通,但是程序連不上,經(jīng)檢查是本機(jī)防火墻設(shè)置有問(wèn)題,通常由系統(tǒng)管理員解決;兩臺(tái)機(jī)器能連上,但是丟包很?chē)?yán)重,發(fā)現(xiàn)是網(wǎng)卡或者交換機(jī)的網(wǎng)口故障,由硬件維修人員解決;兩臺(tái)機(jī)器的程序能連上,但是偶爾發(fā)過(guò)去的請(qǐng)求得不到響應(yīng),通常是程序bug,應(yīng)該由開(kāi)發(fā)人員解決。
本文主要關(guān)心開(kāi)發(fā)人員這一角色。下面簡(jiǎn)單列出一些我能想到的跟網(wǎng)絡(luò)打交道的編程任務(wù),其中前三項(xiàng)是面向網(wǎng)絡(luò)本身,后面幾項(xiàng)是在計(jì)算機(jī)網(wǎng)絡(luò)之上構(gòu)建信息系統(tǒng)。
1. 開(kāi)發(fā)網(wǎng)絡(luò)設(shè)備,編寫(xiě)防火墻、交換機(jī)、路由器的固件 firmware
2. 開(kāi)發(fā)或移植網(wǎng)卡的驅(qū)動(dòng)
3. 移植或維護(hù)TCP/IP協(xié)議棧(特別是在嵌入式系統(tǒng)上)
4. 開(kāi)發(fā)或維護(hù)標(biāo)準(zhǔn)的網(wǎng)絡(luò)協(xié)議程序,HTTP、FTP、DNS、SMTP、POP3、NFS
5. 開(kāi)發(fā)標(biāo)準(zhǔn)網(wǎng)絡(luò)協(xié)議的“附加品”,比如HAProxy、squid、varnish等web load balancer
6. 開(kāi)發(fā)標(biāo)準(zhǔn)或非標(biāo)準(zhǔn)網(wǎng)絡(luò)服務(wù)的客戶端庫(kù),比如ZooKeeper客戶端庫(kù),memcached客戶端庫(kù)
7. 開(kāi)發(fā)與公司業(yè)務(wù)直接相關(guān)的網(wǎng)絡(luò)服務(wù)程序,比如即時(shí)聊天軟件的后臺(tái)服務(wù)器,網(wǎng)游服務(wù)器,金融交易系統(tǒng),互聯(lián)網(wǎng)企業(yè)用的分布式海量存儲(chǔ),微博發(fā)帖的內(nèi)部廣播通知,等等
8. 客戶端程序中涉及網(wǎng)絡(luò)的部分,比如郵件客戶端中與 POP3、SMTP通信的部分,以及網(wǎng)游的客戶端程序中與服務(wù)器通信的部分
本文所指的“網(wǎng)絡(luò)編程”專指第7項(xiàng),即在TCP/IP協(xié)議之上開(kāi)發(fā)業(yè)務(wù)軟件。
面向業(yè)務(wù)的網(wǎng)絡(luò)編程的特點(diǎn)
跟開(kāi)發(fā)通用的網(wǎng)絡(luò)程序不同,開(kāi)發(fā)面向公司業(yè)務(wù)的專用網(wǎng)絡(luò)程序有其特點(diǎn):
· 業(yè)務(wù)邏輯比較復(fù)雜,而且時(shí)常變化
如果寫(xiě)一個(gè)HTTP服務(wù)器,在大致實(shí)現(xiàn)HTTP /1.1標(biāo)準(zhǔn)之后,程序的主體功能一般不會(huì)有太大的變化,程序員會(huì)把時(shí)間放在性能調(diào)優(yōu)和bug修復(fù)上。而開(kāi)發(fā)針對(duì)公司業(yè)務(wù)的專用程序時(shí),功能說(shuō)明書(shū)(spec)很可能不如HTTP/1.1標(biāo)準(zhǔn)那么細(xì)致明確。更重要的是,程序是快速演化的。以即時(shí)聊天工具的后臺(tái)服務(wù)器為例,可能第一版只支持在線聊天;幾個(gè)月之后發(fā)布第二版,支持離線消息;又過(guò)了幾個(gè)月,第三版支持隱身聊天;隨后,第四版支持上傳頭像;如此等等。這要求程序員能快速響應(yīng)新的業(yè)務(wù)需求,公司才能保持競(jìng)爭(zhēng)力。
· 不一定需要遵循公認(rèn)的通信協(xié)議標(biāo)準(zhǔn)
比方說(shuō)網(wǎng)游服務(wù)器就沒(méi)什么協(xié)議標(biāo)準(zhǔn),反正客戶端和服務(wù)端都是本公司開(kāi)發(fā),如果發(fā)現(xiàn)目前的協(xié)議設(shè)計(jì)有問(wèn)題,兩邊一起改了就是了。
· 程序結(jié)構(gòu)沒(méi)有定論
對(duì)于高并發(fā)大吞吐的標(biāo)準(zhǔn)網(wǎng)絡(luò)服務(wù),一般采用單線程事件驅(qū)動(dòng)的方式開(kāi)發(fā),比如HAProxy、lighttpd等都是這個(gè)模式。但是對(duì)于專用的業(yè)務(wù)系統(tǒng),其業(yè)務(wù)邏輯比較復(fù)雜,占用較多的CPU資源,這種單線程事件驅(qū)動(dòng)方式不見(jiàn)得能發(fā)揮現(xiàn)在多核處理器的優(yōu)勢(shì)。這留給程序員比較大的自由發(fā)揮空間,做好了橫掃千軍,做爛了一敗涂地。
· 性能評(píng)判的標(biāo)準(zhǔn)不同
如果開(kāi)發(fā)httpd這樣的通用服務(wù),必然會(huì)和開(kāi)源的Nginx、lighttpd等高性能服務(wù)器比較,程序員要投入相當(dāng)?shù)木θ?yōu)化程序,才能在市場(chǎng)上占有一席之地。而面向業(yè)務(wù)的專用網(wǎng)絡(luò)程序不一定有開(kāi)源的實(shí)現(xiàn)以供對(duì)比性能,程序員通常更加注重功能的穩(wěn)定性與開(kāi)發(fā)的便捷性。性能只要一代比一代強(qiáng)即可。
· 網(wǎng)絡(luò)編程起到支撐作用,但不處于主導(dǎo)地位
程序員的主要工作是實(shí)現(xiàn)業(yè)務(wù)邏輯,而不只是實(shí)現(xiàn)網(wǎng)絡(luò)通信協(xié)議。這要求程序員深入理解業(yè)務(wù)。程序的性能瓶頸不一定在網(wǎng)絡(luò)上,瓶頸有可能是CPU、Disk IO、數(shù)據(jù)庫(kù)等等,這時(shí)優(yōu)化網(wǎng)絡(luò)方面的代碼并不能提高整體性能。只有對(duì)所在的領(lǐng)域有深入的了解,明白各種因素的權(quán)衡(trade-off),才能做出一些有針對(duì)性的優(yōu)化。
幾個(gè)術(shù)語(yǔ)
互聯(lián)網(wǎng)上的很多口水戰(zhàn)是由對(duì)同一術(shù)語(yǔ)的不同理解引起的,比我寫(xiě)的《多線程服務(wù)器的適用場(chǎng)合》就曾經(jīng)人被說(shuō)是“掛羊頭賣(mài)狗肉”,因?yàn)檫@篇文章中舉的 master例子“根本就算不上是個(gè)網(wǎng)絡(luò)服務(wù)器。因?yàn)樗钠款i根本就跟網(wǎng)絡(luò)無(wú)關(guān)。”
· 網(wǎng)絡(luò)服務(wù)器
“網(wǎng)絡(luò)服務(wù)器”這個(gè)術(shù)語(yǔ)確實(shí)含義模糊,到底指硬件還是軟件?到底是服務(wù)于網(wǎng)絡(luò)本身的機(jī)器(交換機(jī)、路由器、防火墻、NAT),還是利用網(wǎng)絡(luò)為其他人或程序提供服務(wù)的機(jī)器(打印服務(wù)器、文件服務(wù)器、郵件服務(wù)器)。每個(gè)人根據(jù)自己熟悉的領(lǐng)域,可能會(huì)有不同的解讀。比方說(shuō)或許有人認(rèn)為只有支持高并發(fā)高吞吐的才算是網(wǎng)絡(luò)服務(wù)器。
為了避免無(wú)謂的爭(zhēng)執(zhí),我只用“網(wǎng)絡(luò)服務(wù)程序”或者“網(wǎng)絡(luò)應(yīng)用程序”這種含義明確的術(shù)語(yǔ)。“開(kāi)發(fā)網(wǎng)絡(luò)服務(wù)程序”通常不會(huì)造成誤解。
· 客戶端?服務(wù)端?
在TCP網(wǎng)絡(luò)編程里邊,客戶端和服務(wù)端很容易區(qū)分,主動(dòng)發(fā)起連接的是客戶端,被動(dòng)接受連接的是服務(wù)端。當(dāng)然,這個(gè)“客戶端”本身也可能是個(gè)后臺(tái)服務(wù)程序,HTTP Proxy對(duì)HTTP Server來(lái)說(shuō)就是個(gè)客戶端。
· 客戶端編程?服務(wù)端編程?
但是“服務(wù)端編程”和“客戶端編程”就不那么好區(qū)分。比如 Web crawler,它會(huì)主動(dòng)發(fā)起大量連接,扮演的是HTTP客戶端的角色,但似乎應(yīng)該歸入“服務(wù)端編程”。又比如寫(xiě)一個(gè) HTTP proxy,它既會(huì)扮演服務(wù)端——被動(dòng)接受 web browser 發(fā)起的連接,也會(huì)扮演客戶端——主動(dòng)向 HTTP server 發(fā)起連接,它究竟算服務(wù)端還是客戶端?我猜大多數(shù)人會(huì)把它歸入服務(wù)端編程。
那么究竟如何定義“服務(wù)端編程”?
服務(wù)端編程需要處理大量并發(fā)連接?也許是,也許不是。比如云風(fēng)在一篇介紹網(wǎng)游服務(wù)器的博客http://blog.codingnow.com/2006/04/iocp_kqueue_epoll.html中就談到,網(wǎng)游中用到的“連接服務(wù)器”需要處理大量連接,而“邏輯服務(wù)器”只有一個(gè)外部連接。那么開(kāi)發(fā)這種網(wǎng)游“邏輯服務(wù)器”算服務(wù)端編程還是客戶端編程呢?
我認(rèn)為,“服務(wù)端網(wǎng)絡(luò)編程”指的是編寫(xiě)沒(méi)有用戶界面的長(zhǎng)期運(yùn)行的網(wǎng)絡(luò)程序,程序默默地運(yùn)行在一臺(tái)服務(wù)器上,通過(guò)網(wǎng)絡(luò)與其他程序打交道,而不必和人打交道。與之對(duì)應(yīng)的是客戶端網(wǎng)絡(luò)程序,要么是短時(shí)間運(yùn)行,比如wget;要么是有用戶界面(無(wú)論是字符界面還是圖形界面)。本文主要談服務(wù)端網(wǎng)絡(luò)編程。
7x24重要嗎??jī)?nèi)存碎片可怕嗎?
一談到服務(wù)端網(wǎng)絡(luò)編程,有人立刻會(huì)提出7x24運(yùn)行的要求。對(duì)于某些網(wǎng)絡(luò)設(shè)備而言,這是合理的需求,比如交換機(jī)、路由器。對(duì)于開(kāi)發(fā)商業(yè)系統(tǒng),我認(rèn)為要求程序7x24運(yùn)行通常是系統(tǒng)設(shè)計(jì)上考慮不周。具體見(jiàn)《分布式系統(tǒng)的工程化開(kāi)發(fā)方法》第20頁(yè)起。重要的不是7x24,而是在程序不必做到7x24的情況下也能達(dá)到足夠高的可用性。一個(gè)考慮周到的系統(tǒng)應(yīng)該允許每個(gè)進(jìn)程都能隨時(shí)重啟,這樣才能在廉價(jià)的服務(wù)器硬件上做到高可用性。
既然不要求7x24,那么也不必害怕內(nèi)存碎片,理由如下:
· 64-bit系統(tǒng)的地址空間足夠大,不會(huì)出現(xiàn)沒(méi)有足夠的連續(xù)空間這種情況。
· 現(xiàn)在的內(nèi)存分配器(malloc及其第三方實(shí)現(xiàn))今非昔比,除了memcached這種純以內(nèi)存為賣(mài)點(diǎn)的程序需要自己設(shè)計(jì)分配器之外,其他網(wǎng)絡(luò)程序大可使用系統(tǒng)自帶的malloc或者某個(gè)第三方實(shí)現(xiàn)。
· Linux Kernel也大量用到了動(dòng)態(tài)內(nèi)存分配。既然操作系統(tǒng)內(nèi)核都不怕動(dòng)態(tài)分配內(nèi)存造成碎片,應(yīng)用程序?yàn)槭裁匆ε拢?
· 內(nèi)存碎片如何度量?有沒(méi)有什么工具能為當(dāng)前進(jìn)程的內(nèi)存碎片狀況評(píng)個(gè)分?如果不能比較兩種方案的內(nèi)存碎片程度,談何優(yōu)化?
有人為了避免內(nèi)存碎片,不使用STL容器,也不敢new/delete,這算是premature optimization還是因噎廢食呢?
協(xié)議設(shè)計(jì)是網(wǎng)絡(luò)編程的核心
對(duì)于專用的業(yè)務(wù)系統(tǒng),協(xié)議設(shè)計(jì)是核心任務(wù),決定了系統(tǒng)的開(kāi)發(fā)難度與可靠性,但是這個(gè)領(lǐng)域還沒(méi)有形成大家公認(rèn)的設(shè)計(jì)流程。
系統(tǒng)中哪個(gè)程序發(fā)起連接,哪個(gè)程序接受連接?如果寫(xiě)標(biāo)準(zhǔn)的網(wǎng)絡(luò)服務(wù),那么這不是問(wèn)題,按RFC來(lái)就行了。自己設(shè)計(jì)業(yè)務(wù)系統(tǒng),有沒(méi)有章法可循?以網(wǎng)游為例,到底是連接服務(wù)器主動(dòng)連接邏輯服務(wù)器,還是邏輯服務(wù)器主動(dòng)連接“連接服務(wù)器”?似乎沒(méi)有定論,兩種做法都行。一般可以按照“依賴->被依賴”的關(guān)系來(lái)設(shè)計(jì)發(fā)起連接的方向。
比新建連接難的是關(guān)閉連接。在傳統(tǒng)的網(wǎng)絡(luò)服務(wù)中(特別是短連接服務(wù)),不少是服務(wù)端主動(dòng)關(guān)閉連接,比如daytime、HTTP/1.0。也有少部分是客戶端主動(dòng)關(guān)閉連接,通常是些長(zhǎng)連接服務(wù),比如 echo、chargen等。我們自己的業(yè)務(wù)系統(tǒng)該如何設(shè)計(jì)連接關(guān)閉協(xié)議呢?
服務(wù)端主動(dòng)關(guān)閉連接的缺點(diǎn)之一是會(huì)多占用服務(wù)器資源。服務(wù)端主動(dòng)關(guān)閉連接之后會(huì)進(jìn)入TIME_WAIT狀態(tài),在一段時(shí)間之內(nèi)hold住一些內(nèi)核資源。如果并發(fā)訪問(wèn)量很高,這會(huì)影響服務(wù)端的處理能力。這似乎暗示我們應(yīng)該把協(xié)議設(shè)計(jì)為客戶端主動(dòng)關(guān)閉,讓TIME_WAIT狀態(tài)分散到多臺(tái)客戶機(jī)器上,化整為零。
這又有另外的問(wèn)題:客戶端賴著不走怎么辦?會(huì)不會(huì)造成拒絕服務(wù)攻擊?或許有一個(gè)二者結(jié)合的方案:客戶端在收到響應(yīng)之后就應(yīng)該主動(dòng)關(guān)閉,這樣把 TIME_WAIT 留在客戶端。服務(wù)端有一個(gè)定時(shí)器,如果客戶端若干秒鐘之內(nèi)沒(méi)有主動(dòng)斷開(kāi),就踢掉它。這樣善意的客戶端會(huì)把TIME_WAIT留給自己,buggy的客戶端會(huì)把 TIME_WAIT留給服務(wù)端。或者干脆使用長(zhǎng)連接協(xié)議,這樣避免頻繁創(chuàng)建銷(xiāo)毀連接。
比連接的建立與斷開(kāi)更重要的是設(shè)計(jì)消息協(xié)議。消息格式很好辦,XML、JSON、Protobuf都是很好的選擇;難的是消息內(nèi)容。一個(gè)消息應(yīng)該包含哪些內(nèi)容?多個(gè)程序相互通信如何避免race condition(見(jiàn)《分布式系統(tǒng)的工程化開(kāi)發(fā)方法》p.16的例子)?系統(tǒng)的全局狀態(tài)該如何躍遷?可惜這方面可供參考的例子不多,也沒(méi)有太多通用的指導(dǎo)原則,我知道的只有30年前提出的end-to-end principle和happens-before relationship。只能從實(shí)踐中慢慢積累了。
網(wǎng)絡(luò)編程的三個(gè)層次
侯捷先生在《漫談程序員與編程》中講到 STL 運(yùn)用的三個(gè)檔次:“會(huì)用STL,是一種檔次。對(duì)STL原理有所了解,又是一個(gè)檔次。追蹤過(guò)STL源碼,又是一個(gè)檔次。第三種檔次的人用起 STL 來(lái),虎虎生風(fēng)之勢(shì)絕非第一檔次的人能夠望其項(xiàng)背。”
我認(rèn)為網(wǎng)絡(luò)編程也可以分為三個(gè)層次:
1. 讀過(guò)教程和文檔
2. 熟悉本系統(tǒng)TCP/IP協(xié)議棧的脾氣
3. 自己寫(xiě)過(guò)一個(gè)簡(jiǎn)單的TCP/IP stack
第一個(gè)層次是基本要求,讀過(guò)《Unix網(wǎng)絡(luò)編程》這樣的編程教材,讀過(guò)《TCP/IP詳解》基本理解TCP/IP協(xié)議,讀過(guò)本系統(tǒng)的manpage。這個(gè)層次可以編寫(xiě)一些基本的網(wǎng)絡(luò)程序,完成常見(jiàn)的任務(wù)。但網(wǎng)絡(luò)編程不是照貓畫(huà)虎這么簡(jiǎn)單,若是按照manpage的功能描述就能編寫(xiě)產(chǎn)品級(jí)的網(wǎng)絡(luò)程序,那人生就太幸福了。
第二個(gè)層次,熟悉本系統(tǒng)的TCP/IP協(xié)議棧參數(shù)設(shè)置與優(yōu)化是開(kāi)發(fā)高性能網(wǎng)絡(luò)程序的必備條件。摸透協(xié)議棧的脾氣還能解決工作中遇到的比較復(fù)雜的網(wǎng)絡(luò)問(wèn)題。拿Linux的TCP/IP協(xié)議棧來(lái)說(shuō):
· 有可能出現(xiàn)自連接(見(jiàn)《學(xué)之者生,用之者死——ACE歷史與簡(jiǎn)評(píng)》舉的三個(gè)硬傷),程序應(yīng)該有所準(zhǔn)備。
· Linux的內(nèi)核會(huì)有bug,比如某種TCP擁塞控制算法曾經(jīng)出現(xiàn)TCP window clamping(窗口箝位)bug,導(dǎo)致吞吐量暴跌,可以選用其他擁塞控制算法來(lái)繞開(kāi)(work around)這個(gè)問(wèn)題。
這些陰暗角落在manpage里沒(méi)有描述,要通過(guò)其他渠道了解。
編寫(xiě)可靠的網(wǎng)絡(luò)程序的關(guān)鍵是熟悉各種場(chǎng)景下的error code(文件描述符用完了如何?本地ephemeral port暫時(shí)用完,不能發(fā)起新連接怎么辦?服務(wù)端新建并發(fā)連接太快,backlog用完了,客戶端connect會(huì)返回什么錯(cuò)誤?),有的在manpage里有描述,有的要通過(guò)實(shí)踐或閱讀源碼獲得。
第三個(gè)層次,通過(guò)自己寫(xiě)一個(gè)簡(jiǎn)單的TCP/IP協(xié)議棧,能大大加深對(duì)TCP/IP的理解,更能明白TCP為什么要這么設(shè)計(jì),有哪些因素制約,每一步操作的代價(jià)是什么,寫(xiě)起網(wǎng)絡(luò)程序來(lái)更是成竹在胸。
其實(shí)實(shí)現(xiàn)TCP/IP只需要操作系統(tǒng)提供三個(gè)接口函數(shù):一個(gè)函數(shù),兩個(gè)回調(diào)函數(shù)。分別是:send_packet()、on_receive_packet()、on_timer()。多年前有一篇文章《使用libnet與libpcap構(gòu)造TCP/IP協(xié)議軟件》介紹了在用戶態(tài)實(shí)現(xiàn)TCP/IP的方法。lwIP也是很好的借鑒對(duì)象。
如果有時(shí)間,我打算自己寫(xiě)一個(gè)Mini/Tiny/Toy/Trivial/Yet-Another TCP/IP。我準(zhǔn)備換一個(gè)思路,用TUN/TAP設(shè)備在用戶態(tài)實(shí)現(xiàn)一個(gè)能與本機(jī)點(diǎn)對(duì)點(diǎn)通信的TCP/IP協(xié)議棧,這樣那三個(gè)接口函數(shù)就表現(xiàn)為我最熟悉的文件讀寫(xiě)。在用戶態(tài)實(shí)現(xiàn)的好處是便于調(diào)試,協(xié)議棧做成靜態(tài)庫(kù),與應(yīng)用程序鏈接到一起(庫(kù)的接口不必是標(biāo)準(zhǔn)的Sockets API)。做完這一版,還可以繼續(xù)發(fā)揮,用FTDI的USB-SPI接口芯片連接ENC28J60適配器,做一個(gè)真正獨(dú)立于操作系統(tǒng)的TCP/IP stack。如果只實(shí)現(xiàn)最基本的IP、ICMP Echo、TCP的話,代碼應(yīng)能控制在3000行以內(nèi);也可以實(shí)現(xiàn)UDP,如果應(yīng)用程序需要用到DNS的話。
最主要的三個(gè)例子
我認(rèn)為T(mén)CP網(wǎng)絡(luò)編程有三個(gè)例子最值得學(xué)習(xí)研究,分別是echo、chat、proxy,都是長(zhǎng)連接協(xié)議。
Echo的作用:熟悉服務(wù)端被動(dòng)接受新連接、收發(fā)數(shù)據(jù)、被動(dòng)處理連接斷開(kāi)。每個(gè)連接是獨(dú)立服務(wù)的,連接之間沒(méi)有關(guān)聯(lián)。在消息內(nèi)容方面Echo有一些變種:比如做成一問(wèn)一答的方式,收到的請(qǐng)求和發(fā)送響應(yīng)的內(nèi)容不一樣,這時(shí)候要考慮打包與拆包格式的設(shè)計(jì),進(jìn)一步還可以寫(xiě)簡(jiǎn)單的HTTP服務(wù)。
Chat的作用:連接之間的數(shù)據(jù)有交流,從a收到的數(shù)據(jù)要發(fā)給b。這樣對(duì)連接管理提出的更高的要求:如何用一個(gè)程序同時(shí)處理多個(gè)連接?fork() per connection似乎是不行的。如何防止串話?b有可能隨時(shí)斷開(kāi)連接,而新建立的連接c可能恰好復(fù)用了b的文件描述符,那么a會(huì)不會(huì)錯(cuò)誤地把消息發(fā)給c?
Proxy的作用:連接的管理更加復(fù)雜:既要被動(dòng)接受連接,也要主動(dòng)發(fā)起連接,既要主動(dòng)關(guān)閉連接,也要被動(dòng)關(guān)閉連接。還要考慮兩邊速度不匹配,見(jiàn)《Muduo 網(wǎng)絡(luò)編程示例之十:socks4a 代理服務(wù)器》。
這三個(gè)例子功能簡(jiǎn)單,突出了TCP網(wǎng)絡(luò)編程中的重點(diǎn)問(wèn)題,挨著做一遍基本就能達(dá)到層次一的要求。
TCP的可靠性有多高?
TCP是“面向連接的、可靠的、字節(jié)流傳輸協(xié)議”,這里的“可靠”究竟是什么意思?《Effective TCP/IP Programming》第9條說(shuō):Realize That TCP Is a Reliable Protocol, Not an Infallible Protocol,那么TCP在哪種情況下會(huì)出錯(cuò)?這里說(shuō)的“出錯(cuò)”指的是收到的數(shù)據(jù)與發(fā)送的數(shù)據(jù)不一致,而不是數(shù)據(jù)不可達(dá)。
我在《一種自動(dòng)反射消息類(lèi)型的 Google Protobuf 網(wǎng)絡(luò)傳輸方案》中設(shè)計(jì)了帶check sum的消息格式,很多人表示不理解,認(rèn)為是多余的。IP header里邊有check sum,TCP header也有check sum,鏈路層以太網(wǎng)還有CRC32校驗(yàn),那么為什么還需要在應(yīng)用層做校驗(yàn)?什么情況下TCP傳送的數(shù)據(jù)會(huì)出錯(cuò)?
IP header和TCP header的check sum是一種非常弱的16-bit check sum算法,把數(shù)據(jù)當(dāng)成反碼表示的16-bit integers,再加到一起。這種checksum算法能檢出一些簡(jiǎn)單的錯(cuò)誤,而對(duì)某些錯(cuò)誤無(wú)能為力,由于是簡(jiǎn)單的加法,遇到“和”不變的情況就無(wú)法檢查出錯(cuò)誤(比如交換兩個(gè)16-bit整數(shù),加法滿足交換律,結(jié)果不變)。以太網(wǎng)的CRC32只能保證同一個(gè)網(wǎng)段上的通信不會(huì)出錯(cuò)(兩臺(tái)機(jī)器的網(wǎng)線插到同一個(gè)交換機(jī)上,這時(shí)候以太網(wǎng)的CRC是有用的)。但是,如果兩臺(tái)機(jī)器之間經(jīng)過(guò)了多級(jí)路由器呢?

上圖中Client向Server發(fā)了一個(gè)TCP segment,這個(gè)segment先被封裝成一個(gè)IP packet,再被封裝成ethernet frame,發(fā)送到路由器(圖中消息a)。Router收到ethernet frame (b),轉(zhuǎn)發(fā)到另一個(gè)網(wǎng)段(c),最后Server收到d,通知應(yīng)用程序。Ethernet CRC能保證a和b相同,c和d相同;TCP header check sum的強(qiáng)度不足以保證收發(fā)payload的內(nèi)容一樣。另外,如果把Router換成NAT,那么NAT自己會(huì)構(gòu)造c(替換掉源地址),這時(shí)候a和d的payload不能用tcp header checksum校驗(yàn)。
路由器可能出現(xiàn)硬件故障,比方說(shuō)它的內(nèi)存故障(或偶然錯(cuò)誤)導(dǎo)致收發(fā)IP報(bào)文出現(xiàn)多bit的反轉(zhuǎn)或雙字節(jié)交換,這個(gè)反轉(zhuǎn)如果發(fā)生在payload區(qū),那么無(wú)法用鏈路層、網(wǎng)絡(luò)層、傳輸層的check sum查出來(lái),只能通過(guò)應(yīng)用層的check sum來(lái)檢測(cè)。這個(gè)現(xiàn)象在開(kāi)發(fā)的時(shí)候不會(huì)遇到,因?yàn)殚_(kāi)發(fā)用的幾臺(tái)機(jī)器很可能都連到同一個(gè)交換機(jī),ethernet CRC能防止錯(cuò)誤。開(kāi)發(fā)和測(cè)試的時(shí)候數(shù)據(jù)量不大,錯(cuò)誤很難發(fā)生。之后大規(guī)模部署到生產(chǎn)環(huán)境,網(wǎng)絡(luò)環(huán)境復(fù)雜,這時(shí)候出個(gè)錯(cuò)就讓人措手不及。有一篇論文《When the CRC and TCP checksum disagree》分析了這個(gè)問(wèn)題。另外《The Limitations of the Ethernet CRC and TCP/IP checksums for error detection》(http://noahdavids.org/self_published/CRC_and_checksum.html)也值得一讀。
這個(gè)情況真的會(huì)發(fā)生嗎?會(huì)的,Amazon S3 在2008年7月就遇到過(guò),單bit反轉(zhuǎn)導(dǎo)致了一次嚴(yán)重線上事故,所以他們吸取教訓(xùn)加了 check sum。見(jiàn)http://status.aws.amazon.com/s3-20080720.html
另外一個(gè)例證:下載大文件的時(shí)候一般都會(huì)附上MD5,這除了有安全方面的考慮(防止篡改),也說(shuō)明應(yīng)用層應(yīng)該自己設(shè)法校驗(yàn)數(shù)據(jù)的正確性。這是end-to-end principle的一個(gè)例證。
三本必看的書(shū)
談到Unix編程和網(wǎng)絡(luò)編程,W. Richard Stevens 是個(gè)繞不開(kāi)的人物,他生前寫(xiě)了6本書(shū),APUE、兩卷UNP、三卷TCP/IP。有四本與網(wǎng)絡(luò)編程直接相關(guān)。UNP第二卷其實(shí)跟網(wǎng)絡(luò)編程關(guān)系不大,是APUE在多線程和進(jìn)程間通信(IPC)方面的補(bǔ)充。很多人把TCP/IP一二三卷作為整體推薦,其實(shí)這三本書(shū)用處不同,應(yīng)該區(qū)別對(duì)待。
這里談到的幾本書(shū)都沒(méi)有超出孟巖在《TCP/IP 網(wǎng)絡(luò)編程之四書(shū)五經(jīng)》中的推薦,說(shuō)明網(wǎng)絡(luò)編程這一領(lǐng)域已經(jīng)相對(duì)成熟穩(wěn)定。
· 《TCP/IP Illustrated, Vol. 1: The Protocols》中文名《TCP/IP 詳解》,以下簡(jiǎn)稱 TCPv1。
TCPv1 是一本奇書(shū)。
這本書(shū)迄今至少被三百多篇學(xué)術(shù)論文引用過(guò)http://portal.acm.org/citation.cfm?id=161724。一本學(xué)術(shù)專著被論文引用算不上出奇,難得的是一本寫(xiě)給程序員看的技術(shù)書(shū)能被學(xué)術(shù)論文引用幾百次,我不知道還有哪本技術(shù)書(shū)能做到這一點(diǎn)。
TCPv1 堪稱 TCP/IP領(lǐng)域的圣經(jīng)。作者 W. Richard Stevens 不是 TCP/IP 協(xié)議的發(fā)明人,他從使用者(程序員)的角度,以 tcpdump 為工具,對(duì) TCP 協(xié)議抽絲剝繭娓娓道來(lái)(第17~24章),讓人嘆服。恐怕 TCP 協(xié)議的設(shè)計(jì)者也難以講解得如此出色,至少不會(huì)像他這么耐心細(xì)致地畫(huà)幾百幅收發(fā) package 的時(shí)序圖。
TCP作為一個(gè)可靠的傳輸層協(xié)議,其核心有三點(diǎn):
1. Positive acknowledgement with retransmission
2. Flow control using sliding window(包括Nagle 算法等)
3. Congestion control(包括slow start、congestion avoidance、fast retransmit等)
第一點(diǎn)已經(jīng)足以滿足“可靠性”要求(為什么?);第二點(diǎn)是為了提高吞吐量,充分利用鏈路層帶寬;第三點(diǎn)是防止過(guò)載造成丟包。換言之,第二點(diǎn)是避免發(fā)得太慢,第三點(diǎn)是避免發(fā)得太快,二者相互制約。從反饋控制的角度看,TCP像是一個(gè)自適應(yīng)的節(jié)流閥,根據(jù)管道的擁堵情況自動(dòng)調(diào)整閥門(mén)的流量。
TCP的 flow control 有一個(gè)問(wèn)題,每個(gè)TCP connection是彼此獨(dú)立的,保存有自己的狀態(tài)變量;一個(gè)程序如果同時(shí)開(kāi)啟多個(gè)連接,或者操作系統(tǒng)中運(yùn)行多個(gè)網(wǎng)絡(luò)程序,這些連接似乎不知道他人的存在,缺少對(duì)網(wǎng)卡帶寬的統(tǒng)籌安排。(或許現(xiàn)代的操作系統(tǒng)已經(jīng)解決了這個(gè)問(wèn)題?)
TCPv1 唯一的不足是它出版太早了,1993 年至今網(wǎng)絡(luò)技術(shù)發(fā)展了幾代。鏈路層方面,當(dāng)年主流的 10Mbit 網(wǎng)卡和集線器早已經(jīng)被淘汰;100Mbit 以太網(wǎng)也沒(méi)什么企業(yè)在用了,交換機(jī)(switch)也已經(jīng)全面取代了集線器(hub);服務(wù)器機(jī)房以 1Gbit 網(wǎng)絡(luò)為主,有些場(chǎng)合甚至用上了 10Gbit 以太網(wǎng)。另外,無(wú)線網(wǎng)的普及也讓TCP flow control面臨新挑戰(zhàn);原來(lái)設(shè)計(jì)TCP的時(shí)候,人們認(rèn)為丟包通常是擁塞造成的,這時(shí)應(yīng)該放慢發(fā)送速度,減輕擁塞;而在無(wú)線網(wǎng)中,丟包可能是信號(hào)太弱造成的,這時(shí)反而應(yīng)該快速重試,以保證性能。網(wǎng)絡(luò)層方面變化不大,IPv6 雷聲大雨點(diǎn)小。傳輸層方面,由于鏈路層帶寬大增,TCP window scale option 被普遍使用,另外 TCP timestamps option 和 TCP selective ack option 也很常用。由于這些因素,在現(xiàn)在的 Linux 機(jī)器上運(yùn)行 tcpdump 觀察 TCP 協(xié)議,程序輸出會(huì)與原書(shū)有些不同。
一個(gè)好消息:TCPv1將于今年10月(2011年)推出第二版,Amazon 的預(yù)定頁(yè)面是:http://www.amazon.com/gp/product/0321336313,讓我們拭目以待。
· 《Unix Network Programming, Vol. 1: Networking API》第二版或第三版(這兩版的副標(biāo)題稍有不同,第三版去掉了 XTI),以下統(tǒng)稱 UNP,如果需要會(huì)以 UNP2e、UNP3e 細(xì)分。
UNP是Sockets API的權(quán)威指南,但是網(wǎng)絡(luò)編程遠(yuǎn)不是使用那十幾個(gè)Sockets API那么簡(jiǎn)單,作者 W. Richard Stevens深刻地認(rèn)識(shí)到這一點(diǎn),他在UNP2e的前言中寫(xiě)到: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.
搞網(wǎng)絡(luò)編程,一定要熟悉TCP/IP協(xié)議及其外在表現(xiàn)(比如打開(kāi)和關(guān)閉Nagle算法對(duì)收發(fā)包的影響),不然出點(diǎn)意料之外的情況就摸不著頭腦了。我不知道為什么UNP3e在前言中去掉了這段至關(guān)重要的話。
另外值得一提的是,UNP中文版翻譯得相當(dāng)好,譯者楊繼張先生是真懂網(wǎng)絡(luò)編程的。
UNP很詳細(xì),面面俱到,UDP、TCP、IPv4、IPv6都講到了。要說(shuō)有什么缺點(diǎn)的話,就是太詳細(xì)了,重點(diǎn)不夠突出。我十分贊同孟巖說(shuō)的
“(孟巖)我主張,在具備基礎(chǔ)之后,學(xué)習(xí)任何新東西,都要抓住主線,突出重點(diǎn)。對(duì)于關(guān)鍵理論的學(xué)習(xí),要集中精力,速戰(zhàn)速?zèng)Q。而旁枝末節(jié)和非本質(zhì)性的知識(shí)內(nèi)容,完全可以留給實(shí)踐去零敲碎打。
“原因是這樣的,任何一個(gè)高級(jí)的知識(shí)內(nèi)容,其中都只有一小部分是有思想創(chuàng)新、有重大影響的,而其它很多東西都是瑣碎的、非本質(zhì)的。因此,集中學(xué)習(xí)時(shí)必須把握住真正重要那部分,把其它東西留給實(shí)踐。對(duì)于重點(diǎn)知識(shí),只有集中學(xué)習(xí)其理論,才能確保體系性、連貫性、正確性,而對(duì)于那些旁枝末節(jié),只有邊干邊學(xué)能夠讓你了解它們的真實(shí)價(jià)值是大是小,才能讓你留下更生動(dòng)的印象。如果你把精力用錯(cuò)了地方,比如用集中大塊的時(shí)間來(lái)學(xué)習(xí)那些本來(lái)只需要查查手冊(cè)就可以明白的小技巧,而對(duì)于真正重要的、思想性東西放在平時(shí)零敲碎打,那么肯定是事倍功半,甚至適得其反。
“因此我對(duì)于市面上絕大部分開(kāi)發(fā)類(lèi)圖書(shū)都不滿——它們基本上都是面向知識(shí)體系本身的,而不是面向讀者的。總是把相關(guān)的所有知識(shí)細(xì)節(jié)都放在一堆,然后一堆一堆攢起來(lái)變成一本書(shū)。反映在內(nèi)容上,就是毫無(wú)重點(diǎn)地平鋪直敘,不分輕重地陳述細(xì)節(jié),往往在第三章以前就用無(wú)聊的細(xì)節(jié)謀殺了讀者的熱情。為什么當(dāng)年侯捷先生的《深入淺出MFC》和 Scott Meyers 的 Effective C++ 能夠成為經(jīng)典?就在于這兩本書(shū)抓住了各自領(lǐng)域中的主干,提綱挈領(lǐng),綱舉目張,一下子打通讀者的任督二脈。可惜這樣的書(shū)太少,就算是已故 Richard Stevens 和當(dāng)今 Jeffrey Richter 的書(shū),也只是在體系性和深入性上高人一頭,并不是面向讀者的書(shū)。”
什么是旁枝末節(jié)呢?拿以太網(wǎng)來(lái)說(shuō),CRC32如何計(jì)算就是“旁枝末節(jié)”。網(wǎng)絡(luò)程序員要明白check sum的作用,知道為什么需要check sum,至于具體怎么算CRC就不需要程序員操心。這部分通常是由網(wǎng)卡硬件完成的,在發(fā)包的時(shí)候由硬件填充CRC,在收包的時(shí)候網(wǎng)卡自動(dòng)丟棄CRC不合格的包。如果代碼里邊確實(shí)要用到CRC計(jì)算,調(diào)用通用的zlib就行,也不用自己實(shí)現(xiàn)。
UNP就像給了你一堆做菜的原料(各種Sockets 函數(shù)的用法),常用和不常用的都給了(Out-of-Band Data、Signal-Driven IO 等等),要靠讀者自己設(shè)法取舍組合,做出一盤(pán)大菜來(lái)。在第一遍讀的時(shí)候,我建議只讀那些基本且重要的章節(jié);另外那些次要的內(nèi)容可略作了解,即便跳過(guò)不讀也無(wú)妨。UNP是一本操作性很強(qiáng)的書(shū),讀這本這本書(shū)一定要上機(jī)練習(xí)。
另外,UNP舉的兩個(gè)例子(菜譜)太簡(jiǎn)單,daytime和echo一個(gè)是短連接協(xié)議,一個(gè)是長(zhǎng)連接無(wú)格式協(xié)議,不足以覆蓋基本的網(wǎng)絡(luò)開(kāi)發(fā)場(chǎng)景(比如 TCP封包與拆包、多連接之間交換數(shù)據(jù))。我估計(jì) W. Richard Stevens 原打算在 UNP第三卷中講解一些實(shí)際的例子,只可惜他英年早逝,我等無(wú)福閱讀。
UNP是一本偏重Unix傳統(tǒng)的書(shū),這本書(shū)寫(xiě)作的時(shí)候服務(wù)端還不需要處理成千上萬(wàn)的連接,也沒(méi)有現(xiàn)在那么多網(wǎng)絡(luò)攻擊。書(shū)中重點(diǎn)介紹的以accept()+fork()來(lái)處理并發(fā)連接的方式在現(xiàn)在看來(lái)已經(jīng)有點(diǎn)吃力,這本書(shū)的代碼也沒(méi)有特別防范惡意攻擊。如果工作涉及這些方面,需要再進(jìn)一步學(xué)習(xí)專門(mén)的知識(shí)(C10k問(wèn)題,安全編程)。
TCPv1和UNP應(yīng)該先看哪本?我不知道。我自己是先看的TCPv1,花了大約半學(xué)期時(shí)間,然后再讀UNP2e和APUE。
· 《Effective TCP/IP Programming》
第三本書(shū)我猶豫了很久,不知道該推薦哪本,還有哪本書(shū)能與 W. Richard Stevens 的這兩本比肩嗎?W. Richard Stevens 為技術(shù)書(shū)籍的寫(xiě)作樹(shù)立了難以逾越的標(biāo)桿,他是一位偉大的技術(shù)作家。沒(méi)能看到他寫(xiě)完 UNP 第三卷實(shí)在是人生的遺憾。
《Effective TCP/IP Programming》這本書(shū)屬于專家經(jīng)驗(yàn)總結(jié)類(lèi),初看時(shí)覺(jué)得收獲很大,工作一段時(shí)間再看也能有新的發(fā)現(xiàn)。比如第6 條“TCP是一個(gè)字節(jié)流協(xié)議”,看過(guò)這一條就不會(huì)去研究所謂的“TCP粘包問(wèn)題”。我手頭這本電力社2001年的中文版翻譯尚可,但是很狗血的是把參考文獻(xiàn)去掉了,正文中引用的文章資料根本查不到名字。人郵2011年重新翻譯出版的版本有參考文獻(xiàn)。
其他值得一看的書(shū)
以下兩本都不易讀,需要相當(dāng)?shù)幕A(chǔ)。
· 《TCP/IP Illustrated, Vol. 2: The Implementation》以下簡(jiǎn)稱 TCPv2
1200頁(yè)的大部頭,詳細(xì)講解了4.4BSD的完整TCP/IP協(xié)議棧,注釋了15,000行C源碼。這本書(shū)啃下來(lái)不容易,如果時(shí)間不充裕,我認(rèn)為沒(méi)必要啃完,應(yīng)用層的網(wǎng)絡(luò)程序員選其中與工作相關(guān)的部分來(lái)閱讀即可。
這本書(shū)第一作者是Gary Wright,從敘述風(fēng)格和內(nèi)容組織上是典型的“面向知識(shí)體系本身”,先講mbuf,再?gòu)逆溌穼右宦吠稀⒁蕴W(wǎng)、IP網(wǎng)絡(luò)層、ICMP、IP多播、IGMP、IP路由、多播路由、Sockets系統(tǒng)調(diào)用、ARP等等。到了正文內(nèi)容3/4的地方才開(kāi)始講TCP。面面俱到、主次不明。
對(duì)于主要使用TCP的程序員,我認(rèn)為T(mén)CPv2一大半內(nèi)容可以跳過(guò)不看,比如路由表、IGMP等等(開(kāi)發(fā)網(wǎng)絡(luò)設(shè)備的人可能更關(guān)心這些內(nèi)容)。在工作中大可以把IP視為host-to-host的協(xié)議,把“IP packet如何送達(dá)對(duì)方機(jī)器”的細(xì)節(jié)視為黑盒子,這不會(huì)影響對(duì)TCP的理解和運(yùn)用,因?yàn)榫W(wǎng)絡(luò)協(xié)議是分層的。這樣精簡(jiǎn)下來(lái),需要看的只有三四百頁(yè),四五千行代碼,大大減輕了負(fù)擔(dān)。
這本書(shū)直接呈現(xiàn)高質(zhì)量的工業(yè)級(jí)操作系統(tǒng)源碼,讀起來(lái)有難度,讀懂它甚至要有“不求甚解的能力”。其一,代碼只能看,不能上機(jī)運(yùn)行,也不能改動(dòng)試驗(yàn)。其二,與操作系統(tǒng)其他部分緊密關(guān)聯(lián)。比如TCP/IP stack下接網(wǎng)卡驅(qū)動(dòng)、軟中斷;上承inode轉(zhuǎn)發(fā)來(lái)的系統(tǒng)調(diào)用操作;中間還要與平級(jí)的進(jìn)程文件描述符管理子系統(tǒng)打交道;如果要把每一部分都弄清楚,把持不住就迷失主題了。其三,一些歷史包袱讓代碼變復(fù)雜晦澀。比如BSD在80年代初需要在只有4M內(nèi)存的VAX上實(shí)現(xiàn)TCP/IP,內(nèi)存方面捉襟見(jiàn)肘,這才發(fā)明了mbuf結(jié)構(gòu),代碼也增加了不少偶發(fā)復(fù)雜度(buffer不連續(xù)的處理)。
讀這套TCP/IP書(shū)切忌膠柱鼓瑟,這套書(shū)以4.4BSD為底,其描述的行為(特別是與timer相關(guān)的行為)與現(xiàn)在的Linux TCP/IP有不小的出入,用書(shū)本上的知識(shí)直接套用到生產(chǎn)環(huán)境的Linux系統(tǒng)可能會(huì)造成不小的誤解和困擾。(TCPv3不重要,可以成套買(mǎi)來(lái)收藏,不讀亦可。)
· 《Pattern-Oriented Software Architecture Volume 2: Patterns for Concurrent and Networked Objects》以下簡(jiǎn)稱POSA2
這本書(shū)總結(jié)了開(kāi)發(fā)并發(fā)網(wǎng)絡(luò)服務(wù)程序的模式,是對(duì)UNP很好的補(bǔ)充。UNP中的代碼往往把業(yè)務(wù)邏輯和Sockets API調(diào)用混在一起,代碼固然短小精悍,但是這種編碼風(fēng)格恐怕不適合開(kāi)發(fā)大型的網(wǎng)絡(luò)程序。POSA2強(qiáng)調(diào)模塊化,網(wǎng)絡(luò)通信交給library/framework去做,程序員寫(xiě)代碼只關(guān)注業(yè)務(wù)邏輯,這是非常重要的思想。閱讀這本書(shū)對(duì)于深入理解常用的event-driven網(wǎng)絡(luò)庫(kù)(libevent、Java Netty、Java Mina、Perl POE、Python Twisted等等)也很有幫助,因?yàn)檫@些庫(kù)都是依照這本書(shū)的思想編寫(xiě)的。
POSA2的代碼是示意性的,思想很好,細(xì)節(jié)不佳。其C++ 代碼沒(méi)有充分考慮資源的自動(dòng)化管理(RAII),如果直接按照書(shū)中介紹的方式去實(shí)現(xiàn)網(wǎng)絡(luò)庫(kù),那么會(huì)給使用者造成不小的負(fù)擔(dān)與陷阱。換言之,照他說(shuō)的做,而不是照他做的學(xué)。
不值一看的書(shū)
Douglas Comer 教授名氣很大,著作等身,但是他寫(xiě)的網(wǎng)絡(luò)方面的書(shū)不值一讀,味同嚼蠟。網(wǎng)絡(luò)編程與 TCP/IP 方面,有W. Richard Stevens 的書(shū)扛鼎;計(jì)算機(jī)網(wǎng)絡(luò)原理方面,有Kurose的“自頂向下”和Peterson的“系統(tǒng)”打旗,沒(méi)其他人什么事兒。順便一提,Tanenbaum的操作系統(tǒng)教材是最好的之一(嗯,之二,因?yàn)樗麑?xiě)了兩本:“現(xiàn)代”和“設(shè)計(jì)與實(shí)現(xiàn)”),不過(guò)他的計(jì)算機(jī)網(wǎng)絡(luò)和體系結(jié)構(gòu)教材的地位比不上他的操作系統(tǒng)書(shū)的地位。體系結(jié)構(gòu)方面,Patterson 和 Hennessy二人合作的兩本書(shū)是最好的,近年來(lái)嶄露頭角的《深入理解計(jì)算機(jī)系統(tǒng)》也非常好;當(dāng)然,側(cè)重點(diǎn)不同。
(完)