默認
打賞 發表評論 0
想開發IM:買成品怕坑?租第3方怕貴?找開源自已擼?盡量別走彎路了... 找站長給點建議
技術大牛陳碩的分享:由淺入深,網絡編程學習經驗干貨總結
閱讀(7153) | 評論(0 收藏5 淘帖1 1

1、引言


本文談一談我在學習網絡編程方面的一些個人經驗。“網絡編程”這個術語的范圍很廣,本文指用Sockets API開發基于TCP/IP的網絡應用程序,具體定義見“網絡編程的各種任務角色”一節。

受限于本人的經歷和經驗,這篇文章的適應范圍是:

  • 1)x86-64 Linux服務端網絡編程,直接或間接使用 Sockets API;
  • 2)公司內網:不一定是局域網,但總體位于公司防火墻之內,環境可控。

本文可能不適合:

  • 1)PC客戶端網絡編程,程序運行在客戶的PC上,環境多變且不可控;
  • 2)Windows網絡編程;
  • 3)面向公網的服務程序;
  • 4)高性能網絡服務器。

本文分兩個部分:

  • 1)網絡編程的一些胡思亂想,談談我對這一領域的認識;
  • 2)幾本必看的書,基本上還是W. Richard Stevents那幾本。

另外,本文沒有特別說明時均暗指TCP協議,“連接”是“TCP連接”,“服務端”是“TCP服務端”。

2、關于陳碩


技術大牛陳碩的分享:由淺入深,網絡編程學習經驗干貨總結_231074.png
陳碩:北京師范大學碩士,擅長 C++ 多線程網絡編程和實時分布式系統架構。編寫了開源 C++ 網絡庫 muduo; 參與翻譯了《代碼大全(第二版)》和《C++ 編程規范(繁體版)》,整理了《C++ Primer 第4版評注版》;曾多次在各地技術大會演講,同時也是《Linux 多線程服務端編程》的作者。


2、網絡編程是什么?


網絡編程是什么?是熟練使用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較勁。

這里還是沒有說清楚“網絡編程”是什么,請繼續閱讀后文“網絡編程的各種任務角色”。

3、學習網絡編程有用嗎?


以上說的是比較底層的網絡編程,程序代碼直接面對從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也大大有助于分析解決線上網絡服務問題。

4、在什么平臺上學習網絡編程?


對于服務端網絡編程,我建議在Linux上學習。

如果在10年前,這個問題的答案或許是FreeBSD,因為FreeBSD根正苗紅,在2000年那一次互聯網浪潮中扮演了重要角色,是很多公司首選的免費服務器操作系統。2000年那會兒Linux還遠未成熟,連epoll都還沒有實現。(FreeBSD在2001年發布4.1版,加入了kqueue,從此C10k不是問題)。

有關C10K問題的相關文章請見:


10年后的今天,事情起了變化,Linux成為了市場份額最大的服務器操作系統(見:http://en.wikipedia.org/wiki/Usage_share_of_operating_systems)。

在Linux這種大眾系統上學網絡編程,遇到什么問題會比較容易解決:

  • 1)因為用的人多,你遇到的問題別人多半也遇到過;
  • 2)同樣因為用的人多,如果真的有什么內核bug,很快就會得到修復,至少有work around的辦法。

如果用別的系統,可能一個問題發到論壇上半個月都不會有人理。從內核源碼的風格看,FreeBSD更干凈整潔,注釋到位,但是無奈它的市場份額遠不如Linux,學習Linux是更好的技術投資。

5、可移植性重要嗎?


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

我就不打算把muduo往Windows或其他操作系統移植。如果需要編寫可移植的網絡程序,我寧愿用libevent或者Java Netty這樣現成的庫,把臟活累活留給別人。

6、網絡編程的各種任務角色


計算機網絡是個 big topic,涉及很多人物和角色,既有開發人員,也有運維人員。

比方說:

  • 1)公司內部兩臺機器之間 ping 不通,通常由網絡運維人員解決,看看是布線有問題還是路由器設置不對;
  • 2)兩臺機器能ping通,但是程序連不上,經檢查是本機防火墻設置有問題,通常由系統管理員解決;
  • 3)兩臺機器能連上,但是丟包很嚴重,發現是網卡或者交換機的網口故障,由硬件維修人員解決;
  • 4)兩臺機器的程序能連上,但是偶爾發過去的請求得不到響應,通常是程序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協議之上開發業務軟件。

7、面向業務的網絡編程的特點


跟開發通用的網絡程序不同,開發面向公司業務的專用網絡程序有其特點,比如。。。

1)業務邏輯比較復雜,而且時常變化:

如果寫一個HTTP服務器,在大致實現HTTP /1.1標準之后,程序的主體功能一般不會有太大的變化,程序員會把時間放在性能調優和bug修復上。而開發針對公司業務的專用程序時,功能說明書(spec)很可能不如HTTP/1.1標準那么細致明確。更重要的是,程序是快速演化的。

以即時聊天工具的后臺服務器為例,可能第一版只支持在線聊天;幾個月之后發布第二版,支持離線消息;又過了幾個月,第三版支持隱身聊天;隨后,第四版支持上傳頭像;如此等等。這要求程序員能快速響應新的業務需求,公司才能保持競爭力。

2)不一定需要遵循公認的通信協議標準:

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

3)程序結構沒有定論:

對于高并發大吞吐的標準網絡服務,一般采用單線程事件驅動的方式開發,比如HAProxy、lighttpd等都是這個模式。但是對于專用的業務系統,其業務邏輯比較復雜,占用較多的CPU資源,這種單線程事件驅動方式不見得能發揮現在多核處理器的優勢。這留給程序員比較大的自由發揮空間,做好了橫掃千軍,做爛了一敗涂地。

4)性能評判的標準不同:

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

5)網絡編程起到支撐作用,但不處于主導地位:

程序員的主要工作是實現業務邏輯,而不只是實現網絡通信協議。這要求程序員深入理解業務。程序的性能瓶頸不一定在網絡上,瓶頸有可能是CPU、Disk IO、數據庫等等,這時優化網絡方面的代碼并不能提高整體性能。只有對所在的領域有深入的了解,明白各種因素的權衡(trade-off),才能做出一些有針對性的優化。

8、幾個術語


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

1)網絡服務器:

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

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

2)客戶端?服務端?

在TCP網絡編程里邊,客戶端和服務端很容易區分,主動發起連接的是客戶端,被動接受連接的是服務端。當然,這個“客戶端”本身也可能是個后臺服務程序,HTTP Proxy對HTTP Server來說就是個客戶端。

3)客戶端編程?服務端編程?

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

那么究竟如何定義“服務端編程”?

服務端編程需要處理大量并發連接?也許是,也許不是。比如云風在一篇介紹網游服務器的博客《IOCP , kqueue , epoll ... 有多重要?》中就談到,網游中用到的“連接服務器”需要處理大量連接,而“邏輯服務器”只有一個外部連接。那么開發這種網游“邏輯服務器”算服務端編程還是客戶端編程呢?

我認為:“服務端網絡編程”指的是編寫沒有用戶界面的長期運行的網絡程序,程序默默地運行在一臺服務器上,通過網絡與其他程序打交道,而不必和人打交道。與之對應的是客戶端網絡程序,要么是短時間運行,比如wget;要么是有用戶界面(無論是字符界面還是圖形界面)。本文主要談服務端網絡編程

9、7x24重要嗎?內存碎片可怕嗎?


一談到服務端網絡編程,有人立刻會提出7x24運行的要求。

對于某些網絡設備而言,這是合理的需求,比如交換機、路由器。對于開發商業系統,我認為要求程序7x24運行通常是系統設計上考慮不周(具體見《分布式系統的工程化開發方法》第20頁起)。

重要的不是7x24,而是在程序不必做到7x24的情況下也能達到足夠高的可用性。一個考慮周到的系統應該允許每個進程都能隨時重啟,這樣才能在廉價的服務器硬件上做到高可用性。

既然不要求7x24,那么也不必害怕內存碎片,理由如下:

  • 1)64-bit系統的地址空間足夠大,不會出現沒有足夠的連續空間這種情況;
  • 2)現在的內存分配器(malloc及其第三方實現)今非昔比,除了memcached這種純以內存為賣點的程序需要自己設計分配器之外,其他網絡程序大可使用系統自帶的malloc或者某個第三方實現;
  • 3)Linux Kernel也大量用到了動態內存分配。既然操作系統內核都不怕動態分配內存造成碎片,應用程序為什么要害怕?
  • 4)內存碎片如何度量?有沒有什么工具能為當前進程的內存碎片狀況評個分?如果不能比較兩種方案的內存碎片程度,談何優化?

有人為了避免內存碎片,不使用STL容器,也不敢new/delete,這算是premature optimization還是因噎廢食呢?

10、協議設計是網絡編程的核心


對于專用的業務系統,協議設計是核心任務,決定了系統的開發難度與可靠性,但是這個領域還沒有形成大家公認的設計流程。

系統中哪個程序發起連接,哪個程序接受連接?如果寫標準的網絡服務,那么這不是問題,按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。只能從實踐中慢慢積累了。

有關消息格式方面的資料詳見:


11、網絡編程的三個層次


侯捷先生在《漫談程序員與編程()》中講到 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協議棧來說:

  • 1)有可能出現自連接(見《學之者生,用之者死——ACE歷史與簡評》舉的三個硬傷),程序應該有所準備;
  • 2)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的話。

12、最主要的三個例子


我認為TCP網絡編程有三個例子最值得學習研究:分別是echo、chat、proxy,都是長連接協議。

Echo的作用:熟悉服務端被動接受新連接、收發數據、被動處理連接斷開。每個連接是獨立服務的,連接之間沒有關聯。在消息內容方面Echo有一些變種:比如做成一問一答的方式,收到的請求和發送響應的內容不一樣,這時候要考慮打包與拆包格式的設計,進一步還可以寫簡單的HTTP服務。

Chat的作用:連接之間的數據有交流,從a收到的數據要發給b。這樣對連接管理提出的更高的要求:如何用一個程序同時處理多個連接?fork() per connection似乎是不行的。如何防止串話?b有可能隨時斷開連接,而新建立的連接c可能恰好復用了b的文件描述符,那么a會不會錯誤地把消息發給c?

Proxy的作用:連接的管理更加復雜:既要被動接受連接,也要主動發起連接,既要主動關閉連接,也要被動關閉連接。還要考慮兩邊速度不匹配,見《Muduo 網絡編程示例之十:socks4a 代理服務器》。

這三個例子功能簡單,突出了TCP網絡編程中的重點問題,挨著做一遍基本就能達到層次一的要求。

13、學習 Sockets API 的利器:IPython


我在編寫 muduo 網絡庫的時候,寫了一個命令行交互式的調試工具,方便我試驗各個 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

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++網絡庫,采用非阻塞 IO,支持高并發和多線程,核心代碼 量不大(3000 多行),示例豐富,可供網絡編程的學習者參考。

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

技術大牛陳碩的分享:由淺入深,網絡編程學習經驗干貨總結_1.png

上圖中:

  • 1)Client向Server發了一個TCP segment,這個segment先被封裝成一個IP packet,再被封裝成ethernet frame,發送到路由器(圖中消息a);
  • 2)Router收到ethernet frame (b),轉發到另一個網段(c);
  • 3)最后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》也值得一讀。

這個情況真的會發生嗎?會的,Amazon S3 在2008年7月就遇到過,單bit反轉導致了一次嚴重線上事故,所以他們吸取教訓加了 check sum,見:http://status.aws.amazon.com/s3-20080720.html

另外一個例證:下載大文件的時候一般都會附上MD5,這除了有安全方面的考慮(防止篡改),也說明應用層應該自己設法校驗數據的正確性。這是end-to-end principle的一個例證。

15、三本必看的書


談到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。

技術大牛陳碩的分享:由淺入深,網絡編程學習經驗干貨總結_2.jpg

TCPv1 是一本奇書(《TCP/IP詳解 卷1:協議》網頁在線閱讀版)。

這本書迄今至少被三百多篇學術論文引用過: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 年至今網絡技術發展了幾代:

  • 1)鏈路層方面,當年主流的 10Mbit 網卡和集線器早已經被淘汰;
  • 2)100Mbit 以太網也沒什么企業在用了,交換機(switch)也已經全面取代了集線器(hub);
  • 3)服務器機房以 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年重新翻譯出版的版本有參考文獻。

16、其他值得一看的書


以下兩本都不易讀,需要相當的基礎。

· 《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),如果直接按照書中介紹的方式去實現網絡庫,那么會給使用者造成不小的負擔與陷阱。換言之,照他說的做,而不是照他做的學。

17、不值一看的書


Douglas Comer 教授名氣很大,著作等身,但是他寫的網絡方面的書不值一讀,味同嚼蠟。

網絡編程與 TCP/IP 方面,有W. Richard Stevens 的書扛鼎;計算機網絡原理方面,有Kurose的“自頂向下”和Peterson的“系統”打旗,沒其他人什么事兒。

順便一提,Tanenbaum的操作系統教材是最好的之一(嗯,之二,因為他寫了兩本:“現代”和“設計與實現”),不過他的計算機網絡和體系結構教材的地位比不上他的操作系統書的地位。

體系結構方面,Patterson 和 Hennessy二人合作的兩本書是最好的,近年來嶄露頭角的《深入理解計算機系統》也非常好;當然,側重點不同。

(原文鏈接:https://blog.csdn.net/solstice/article/details/6527585

附錄:更多網絡編程相關文章


TCP/IP詳解 - 第11章·UDP:用戶數據報協議
TCP/IP詳解 - 第17章·TCP:傳輸控制協議
TCP/IP詳解 - 第18章·TCP連接的建立與終止
TCP/IP詳解 - 第21章·TCP的超時與重傳
技術往事:改變世界的TCP/IP協議(珍貴多圖、手機慎點)
通俗易懂-深入理解TCP協議(上):理論基礎
通俗易懂-深入理解TCP協議(下):RTT、滑動窗口、擁塞處理
理論經典:TCP協議的3次握手與4次揮手過程詳解
理論聯系實際:Wireshark抓包分析TCP 3次握手、4次揮手過程
計算機網絡通訊協議關系圖(中文珍藏版)
UDP中一個包的大小最大能多大?
P2P技術詳解(一):NAT詳解——詳細原理、P2P簡介
P2P技術詳解(二):P2P中的NAT穿越(打洞)方案詳解
P2P技術詳解(三):P2P技術之STUN、TURN、ICE詳解
通俗易懂:快速理解P2P技術中的NAT穿透原理
高性能網絡編程(一):單臺服務器并發TCP連接數到底可以有多少
高性能網絡編程(二):上一個10年,著名的C10K并發連接問題
高性能網絡編程(三):下一個10年,是時候考慮C10M并發問題了
高性能網絡編程(四):從C10K到C10M高性能網絡應用的理論探索
高性能網絡編程(五):一文讀懂高性能網絡編程中的I/O模型
高性能網絡編程(六):一文讀懂高性能網絡編程中的線程模型
不為人知的網絡編程(一):淺析TCP協議中的疑難雜癥(上篇)
不為人知的網絡編程(二):淺析TCP協議中的疑難雜癥(下篇)
不為人知的網絡編程(三):關閉TCP連接時為什么會TIME_WAIT、CLOSE_WAIT
不為人知的網絡編程(四):深入研究分析TCP的異常關閉
不為人知的網絡編程(五):UDP的連接性和負載均衡
不為人知的網絡編程(六):深入地理解UDP協議并用好它
不為人知的網絡編程(七):如何讓不可靠的UDP變的可靠?
不為人知的網絡編程(八):從數據傳輸層深度解密HTTP
網絡編程懶人入門(一):快速理解網絡通信協議(上篇)
網絡編程懶人入門(二):快速理解網絡通信協議(下篇)
網絡編程懶人入門(三):快速理解TCP協議一篇就夠
網絡編程懶人入門(四):快速理解TCP和UDP的差異
網絡編程懶人入門(五):快速理解為什么說UDP有時比TCP更有優勢
網絡編程懶人入門(六):史上最通俗的集線器、交換機、路由器功能原理入門
網絡編程懶人入門(七):深入淺出,全面理解HTTP協議
網絡編程懶人入門(八):手把手教你寫基于TCP的Socket長連接
網絡編程懶人入門(九):通俗講解,有了IP地址,為何還要用MAC地址?
技術掃盲:新一代基于UDP的低延時網絡傳輸層協議——QUIC詳解
讓互聯網更快:新一代QUIC協議在騰訊的技術實踐分享
現代移動端網絡短連接的優化手段總結:請求速度、弱網適應、安全保障
聊聊iOS中網絡編程長連接的那些事
移動端IM開發者必讀(一):通俗易懂,理解移動網絡的“弱”和“慢”
移動端IM開發者必讀(二):史上最全移動弱網絡優化方法總結
IPv6技術詳解:基本概念、應用現狀、技術實踐(上篇)
IPv6技術詳解:基本概念、應用現狀、技術實踐(下篇)
從HTTP/0.9到HTTP/2:一文讀懂HTTP協議的歷史演變和設計思路
腦殘式網絡編程入門(一):跟著動畫來學TCP三次握手和四次揮手
腦殘式網絡編程入門(二):我們在讀寫Socket時,究竟在讀寫什么?
腦殘式網絡編程入門(三):HTTP協議必知必會的一些知識
腦殘式網絡編程入門(四):快速理解HTTP/2的服務器推送(Server Push)
腦殘式網絡編程入門(五):每天都在用的Ping命令,它到底是什么?
腦殘式網絡編程入門(六):什么是公網IP和內網IP?NAT轉換又是什么鬼?
以網游服務端的網絡接入層設計為例,理解實時通信的技術挑戰
邁向高階:優秀Android程序員必知必會的網絡基礎
全面了解移動端DNS域名劫持等雜癥:技術原理、問題根源、解決方案等
美圖App的移動端DNS優化實踐:HTTPS請求耗時減小近半
Android程序員必知必會的網絡通信傳輸層協議——UDP和TCP
IM開發者的零基礎通信技術入門(一):通信交換技術的百年發展史(上)
IM開發者的零基礎通信技術入門(二):通信交換技術的百年發展史(下)
IM開發者的零基礎通信技術入門(三):國人通信方式的百年變遷
IM開發者的零基礎通信技術入門(四):手機的演進,史上最全移動終端發展史
IM開發者的零基礎通信技術入門(五):1G到5G,30年移動通信技術演進史
IM開發者的零基礎通信技術入門(六):移動終端的接頭人——“基站”技術
IM開發者的零基礎通信技術入門(七):移動終端的千里馬——“電磁波”
IM開發者的零基礎通信技術入門(八):零基礎,史上最強“天線”原理掃盲
IM開發者的零基礎通信技術入門(九):無線通信網絡的中樞——“核心網”
IM開發者的零基礎通信技術入門(十):零基礎,史上最強5G技術掃盲
IM開發者的零基礎通信技術入門(十一):為什么WiFi信號差?一文即懂!
IM開發者的零基礎通信技術入門(十二):上網卡頓?網絡掉線?一文即懂!
IM開發者的零基礎通信技術入門(十三):為什么手機信號差?一文即懂!
IM開發者的零基礎通信技術入門(十四):高鐵上無線上網有多難?一文即懂!
IM開發者的零基礎通信技術入門(十五):理解定位技術,一篇就夠
百度APP移動端網絡深度優化實踐分享(一):DNS優化篇
百度APP移動端網絡深度優化實踐分享(二):網絡連接優化篇
技術大牛陳碩的分享:由淺入深,網絡編程學習經驗干貨總結
>> 更多同類文章 ……

即時通訊網 - 即時通訊開發者社區! 來源: - 即時通訊開發者社區!

上一篇:請教大佬即時通訊網如何整理 <tcp/ip詳解> Web在線閱讀版的?下一篇:少啰嗦!一分鐘帶你讀懂Java的NIO和經典IO的區別

本帖已收錄至以下技術專輯

推薦方案
打賞樓主 ×
使用微信打賞! 使用支付寶打賞!

返回頂部
777彩票走势图表