《TCP/IP協議 詳解》思考總結 · 一

前言

從去年年底開始大約花了半年時間去啃《TCP/IP協議 詳解》這本書。雖然整體過了一遍,也給了我一些基礎能夠參與到網絡相關話題下的討論,但對於這樣一本全面詳細的指導書,我並沒有掌握的很好。拋開沒有記住的內容,很多問題在第一次閱讀的時候會以理所當然的態度給跳過。

最近重新拿起這本書,審視一些第一遍沒有考慮到的問題。我選擇將一些思考的問題記錄下來,分節整理。當然,要捋完整本書需要一些時間,一篇文章的篇幅也有限,這篇文章主要講述了五個問題字數已經過萬,所以後續我會分篇一一來總結。更新的內容目錄會添加到《TCP/IP協議 詳解》思考總結目錄裏,有興趣的朋友可以在這裏搜索找到對應的文章。

因爲本人水平有限,只是一名普通的軟件開發而非專業的網工人員,文章難免有所缺漏。如果讀者有任何疑問,望不吝賜教。

有任何問題歡迎評論留言,我會盡力給予回答。


網絡分層

計算機網絡採用了分層結構,但是不同地方關於劃分的標準並不是完全一致。主流的模型分別是TCP/IP五層模型(在TCP/IP協議詳解中分爲四層,硬件相關的數據鏈路層和物理層被合併在一起)和OSI的七層模型。兩者的差異就在於會話層和表示層的缺失上。

會話層的功能是會話控制和同步,表示層是解決兩個系統間交換信息的語法和語義的問題,以及數據表示轉化,加解密和壓縮解壓縮的功能。在OSI七層模型中將這兩層從廣泛意義上的應用層裏獨立出來,主要的目的還是讓這兩層的邏輯代碼可以爲所有的應用程序共享,同時瘦身應用程序。

但是現實的情況是這兩層在各種應用程序當中很難設定一個統一的標準,每一個不同的程序關於表示和會話的需求各不相同。這意味着相關的邏輯和代碼是無法複用的,那麼獨立出來也就無從談起了。這部分的邏輯最終都交給應用開發者在應用層決策實現了。

關於網絡分層的原因,實際大部分軟件系統都是分層架構,這一切都是爲了工程上實現/調試/維護的方便。之前在一個關於爲什麼Linux爲什麼還要堅持使用宏內核的問題下我看到一段話非常的有意思。

因爲Linus可以把這些亂七八糟的東西全都一個人寫了,一遍寫對了,還能穩定跑起來無bug,而我們這些渣渣做不到,只能依靠保護模式來防止幾百個工程師寫出來的那一坨垃圾動不動藍屏,自己弱卻去質疑天才的做法,和明知自己弱還要模仿天才的做法,都是認不清現實的表現。
==============================================================================
工程這個東西是很有意思的,我們說科學是掌握規律,技術是利用規律克服大自然的限制,而工程,卻是利用技術來克服人自身的限制。技術會告訴你,造個金字塔,把石頭壘成四棱錐就行了,如果你是個力大無窮的巨人,或者是個能意念移物的魔法師,你就啪啪啪把石頭搬過來堆起來就完事了。但我們是凡人,我們力量很小,我們很弱,所以我們需要滾木,需要滑輪,需要繩索來幫忙,做了許多額外的麻煩事情,只爲了克服我們肉體的自身限制。體力上有限制,智力上同樣有限制。軟件工程很大程度上就是解決我們人類智力上限制的問題,軟件工程師在面對不知所謂的kernel dump的時候會無助,會哭泣,在面對無休止的接口變動的時候會歇斯底里,面對改一行代碼系統就全掛的窘境束手無策,所以我們需要微內核、微服務這樣的框架來約束系統,降低系統的複雜性,讓我們所有犯的錯誤都能保持在可控的範圍內,讓因爲我們的愚笨而寫出的有bug的代碼也能勉勉強強運行起來而不是分分鐘crash,哪怕這些方法額外增加了許多工作量、還降低了效率。但是,總是有超人存在的,我們人要造一個紀念碑,設計一堆方案,superman會說,哈?這個事情,不是隻要我去把那個石頭舉起來,然後飛過來,放在這裏不就好了嘛?體力上差距這麼大的超人也許不存在,但智力上差距這麼大的超人卻是存在的,所以要記住工程方法只是爲了拯救我們這些凡人,對於超人來說,他們是不需要這些的,他們要做的僅僅是“搬起來,放下去”而已。
可以多人開發這件事情在軟件工程上是至關重要的,如果一個工程必須每個程序員都完全理解整個工程的架構才能着手開發,那這個工程一定做不成,不說完全理解一個龐大項目本身不可能這個問題,大家理解也會有偏差,寫出來代碼合不到一起,這就是爲什麼需要有嚴格的模塊拆分,而且接口必須控制在解耦的、可以理解的、數量儘量少的範圍內,而且通過框架來避免錯誤相互擴散。

即使是一次看似非常簡單的數據傳輸,背後實際需要做的工作也非常多。我經常會感嘆網絡的強大,無論需要傳輸的是一段文字,一張圖片,一段視頻,又或是無論我和通信的對方相距多遠,數據都可以穩當的送達給對方。如何打包處理大小不同用戶的數據;如何去和對方建立連接;如何在不可靠的信道上提供可靠的數據傳輸?處理這裏面繁雜的邏輯,是一個大問題。前輩們給出的解決辦法是將大問題分割成若干個小問題,交由不同的層去解決。每一層相互獨立,互不干擾,只關心自身的任務;處理結束之後將結果交由下一層繼續處理。

這樣做的好處非常明顯:
  • 每一層只需要專注自己本身,而不必關心整個網絡的結構,這讓問題簡化了不少,每一層的自由度也很高;
  • 其他層面的操作對於本層而言可以視作一個黑盒,它隱藏了具體的實現細節,只提供了一套可供調用的API,這樣只要外部接口不變,內部做修改對其他地方毫無影響;
  • 相關邏輯的代碼可以重複被使用,提高了開發的效率;
  • 同時也給調試提供了便捷,允許逐層排查確認問題。
那麼劃分層級的標準又是如何呢?
單個網絡

上圖我們可以看出應用層的程序是用戶的一個進程,而下層則是交由操作系統(內核)處理的,儘管這不是絕對的,但大多數情況都是這樣的,我們可以簡單的這樣去認爲。應用層的程序更多的是關心業務邏輯的處理,而不是數據在網絡中的傳輸活動;而下層對應用程序毫不知情,但它們需要處理所有的通信細節

鏈路層(或者說數據鏈路層和物理層)是和硬件設備以及相關的驅動打交道的這一部分

如果只看這張圖,運輸層和網絡層之間的區別並不是那麼的明顯。百度百科上給出的答案是:

  • 網絡層:使用權數據路由經過大型網絡;
  • 運輸層:提供終端到終端的可靠連接。

這樣的說法是完全錯誤的!因爲這是基於一個大的前提:網絡層的協議是IP協議,而運輸層的協議是TCP協議。但實際情況是在網絡發展初期,各種協議層出不窮,TCP/IP也只是其中非常普通的一個部分,至少在OSI模型(五層模型)落地的時候誰也不會料到今天TCP/IP會佔據主流地位。所以這並不是劃分運輸層和網絡層的理由。

要理解這兩層的含義,我們需要把視野從單個網絡放大到一組網絡

組網絡

上圖中我們可以劃分出一個端系統(兩邊的主機)和中間系統(路由器)。在這張圖中可以明確看出應用層和運輸層的協議都是端對端協議,也就是隻有端系統會使用這兩層協議的內容;而網絡層的協議是逐跳協議,兩個端系統和每一箇中間系統都需要使用到它。

不同就在於網絡層服務的對象不僅僅是端系統,也包括中間節點。提供服務的目的是將分組儘可能快的從源節點傳輸到目的節點,但不爲此提供任何可靠性保證。如果我們略去運輸層直接將數據交給應用層,是否可以?當然可以!這也是爲什麼我們會覺得運輸層和網絡層區分不明顯的原因。但這樣的做法是非常不值得推薦的,因爲數據經過不可靠信道傳輸之後狀態是未知的,這一部分的數據在交由應用程序之前我們還需要進行處理,包括但不限於數據校驗,安全性檢查,可靠性傳輸服務的提供。所以我們要在網絡層和應用層之間建立一個端到端的連接,允許我們在這之間對數據能夠有操作的空間和餘地。而運輸層就是負責建立,管理和維護這一部分的連接的

在TCP/IP協議簇當中,IP協議負責將分組從源節點傳輸到目的節點,而TCP在上層提供了可靠性服務。但這並不是說可靠性服務一定要由運輸層完成。和TCP對應的UDP是以不可靠傳輸聞名天下。我們可以將UDP看作一個最原始的TCP數據包,在去除了各項feature之後,UDP相比TCP有了輕便,簡潔的優勢。這讓它在一些網絡狀況不佳、及時性要求較高的業務場景中有了發揮的機會,TCP的三次握手以及擁塞控制在這些情況下反而變成了一種負擔。如果我們需要一些可靠傳輸的需求而又不想使用大而全的TCP,我們可以將這一部分的邏輯移交到應用層來實現。對於應用開發者而言,可以更加自由的控制數據報傳輸的邏輯。Ethernet Card + Driver + IP + UDP + TFTP就是一個很好的例子。


談一談可靠性是什麼

在談及計算機網絡的時候可靠性是一個無法迴避的概念。

我們經常會將運輸層上的TCP和udp協議進行比較:TCP是一個可靠傳輸的協議,而UDP則是不可靠的。在很長的一段時間裏,我簡單的把這個可靠性理解爲:UDP通信有一定的概率失敗,TCP通信是肯定會成功的。這是一個錯誤非常明顯的理解,非常容易舉出反例:如果你的電腦是脫機狀態,那麼無論選擇什麼樣的網絡協議你都無法和外界進行通信。

那麼TCP提供的可靠性傳輸究竟是什麼?

第一次意識到我的錯誤,是在開發一款藍牙產品的時候。多個藍牙設備自組網絡,我通過指定Mesh ID(類似IP地址,用於指明Mesh網絡中具體哪一個設備)發送消息給直連的藍牙設備,來控制任一一臺Mesh網絡中的藍牙設備。boss測試程序的時候給我提出了一個問題,爲什麼我明明點擊了這個按鈕發送了開的命令,應用內也顯示打開了這個設備,但實際這個設備並沒有打開?

問題其實很簡單:這個產品有一個缺陷,在同時發送多條消息的時候可能會造成信道擁塞,發送的消息可能會丟失沒有送達。在點擊打開按鈕的同時程序會發送一條消息,但是因爲當時底層可能正在頻繁通訊導致這條消息沒有送達,應用內又將設備狀態置成打開,造成了這種錯誤。最後解決的辦法是在底層限制了通訊的速率來避免這種情況的發生。那麼讀者可能會問:爲什麼你不能讓設備返回你一條打開成功的消息之後再改變設備在應用內的狀態呢?這是因爲Mesh網絡本身承載能力有限,爲此所有的消息是沒有返回的,所以我無法等收到消息然後再在回調裏修改設備的狀態。這就類似UDP的數據報一樣,發出以後,發送方就不會再去關心。

這個Bug讓我第一次感受到了TCP的美好:每一條消息發送成功以後都會返回一條Ack來告知發送方已送達。我重新定義了一遍可靠性:能夠明確告知發送方發送結果

就算沒有發送成功不會有Ack消息,我們也可以設置超時時間來認定發送失敗

但很顯然這並不全面,TCP做的遠不止這些

這一次幫助我意識到問題的是一個BugCocoaAsyncSocket UDP收發數據包大小限制,我開始重新審視TCP報文裏的參數。

TCP/UDP 首部

上圖是TCP和UDP報文的格式。TCP的報文中包含了非常多的選項可以設置,而UDP非常的簡單,除了目的地址和端口以外,只有Data Len和一個Checksum(如果爲了追求極致的速度你甚至可以關閉Checksum這一選項,這在TCP中是不可能的)。之前藍牙項目的消息有兩個特徵:精短;每一條消息獨立互不干擾。那麼數據報非常大(大到一條消息放置不下)的情況要如何處理?多個有序數據報在網絡中傳遞發生亂序,丟包該怎麼辦?

瞭解TCP的朋友應該知道TCP提供了擁塞避免快速重傳等機制來應對處理未知信道帶來的這些問題。當然,這其中每一部分拿出來都可以說上很多,不在這個問題下反覆糾結。但是我們應該要明確的一點是,在應用開發者看不見的地方,TCP已經爲我們解決了很多不可靠信道帶來的傳輸問題。關於可靠性我們應該爲它加上一點:能夠自主應對大部分數據傳輸過程中出現的問題,包括但不限於丟包,亂序等等

以上我們討論的內容其實都是圍繞消息是否送達,但實際傳輸過程中我們還需要關心送達的內容是否準確

這就是爲什麼我們需要校驗和的原因

在談論這一部分之前,我們需要認清網絡傳輸的本質是什麼。無論你需要計算機傳輸的是什麼樣的資源文件,最後都會變成一串01000101010101...的比特流,在互聯網的血管裏流淌。回想一下描述文件大小的單位,就算一個簡單的文件實際也是一串非常長的數據。要想保證它在層層傳遞的過程中不出現差錯,這幾乎是不可能實現的。

這也就是爲什麼多層協議上都引入了數據校驗這一部分。在數據鏈路層有FCS;網絡層IP協議和運輸層的TCP/UDP都提供了Checksum。目的就是爲了檢測出因爲網卡軟硬件Bug、電纜不可靠、信號干擾而造成信號失真造成的數據錯誤。

可是同樣一件事需要幾層同時去做嗎?

當然!第一個原因在於鏈路層CRC不能完全檢測出錯誤;第二,每一層校驗覆蓋的數據範圍是不一樣的:IP協議Checksum只覆蓋IP首部,而傳輸層只針對自己的數據包。那麼爲什麼設計成一次校驗完成避免多次校驗的浪費?IP協議Checknum只覆蓋IP首部的原因在於IP首部的信息傳輸過程中會被多次修改的,包括TTL以及某些情況下對源地址的修改。其次,我們需要理解分而治之是網絡中的一個重要概念。因爲你無法保證執行校驗的上層一定需要這個校驗的結果,即時性和可靠性是無法盡善盡美的,只能取一個平衡。最好的就是大家各自完成各自的任務,就像一個可以自由組合的積木,還是那句話:分而治之

有了校驗可以認爲數據更加安全了嗎

這其實是把安全性和可靠性混淆在了一起。所有的校驗操作是防君子而防不了小人,它可以檢測硬件故障、軟件bug、信號干擾、線路差、人爲誤操作這些非主觀原因造成的錯誤,但是卻無法防止別人惡意去篡改你的數據內容。道理很簡單,篡改了數據的同時,可以將重新校驗的結果覆蓋上去。可靠性提供的是:避免非主觀因素造成的數據錯誤

但是如何避免被別人攻擊篡改數據內容,這是安全性相關的內容,需要別的機制來提供相關的服務。以我們熟悉的TCP舉例說明,它提供的是一個可靠但不安全的傳輸服務。


談一談分片

鏈路層對於數據幀的長度都有一個限制,我們稱之爲MTU。如果需要傳輸的數據報長度大於鏈路層的MTU,那麼我們就需要將數據報分成若干長度小於MTU的數據報,這個過程叫做分片。因爲數據報在發送的過程中需要經過不同的網絡,鏈路層的MTU不盡相同,所以分片不僅發生在源主機端,也可能會發生在中間路由上。

上述所說的中間路由發生分片,是IPV4的環境下;在IPV6中只在源端分片重複,如果數據報長度大於中間路由的MTU,路由會直接返回一條ICMPv6 too big 給源主機端。這一部分我們後面會再說到

我們首先要明確一點爲什麼要把分片放在IP層

發展至今,IP協議幾乎和網絡層劃上了等號。雖然網絡層仍有其他協議的存在,但是可以說能走到最終用戶面前的協議,網絡層都是選擇IP協議。所以分片操作放在IP協議上,可以爲所有的上層協議服務,而不必再依次去運輸層的協議中實現,相關的邏輯代碼複用效率很高。在這裏爲什麼我們不再像討論Check num那樣去討論分而治之,原因在於分片的根源是MTU的限制,任何一種協議都無法避免大數據報傳輸過程中的分片。協議可以決策自己是否實現這部分邏輯,但必須保證這部分的邏輯一定要實現。而IP協議就是這一個必須實現的保障。

另外,IP協議比起上層協議還有一個很大的優勢就在於它更貼近鏈路層,這讓IP協議可以感知到底層的MTU。而上層協議既不關心,也無法直接關心到底層MTU。

注意這裏說的是直接關心,也就是說上層協議如果想要關心MTU,可以去設計獲得,但這個信息並不是必要的。

總結來說,IP層實現分片是一個成本最低的選項。因爲物理層去做分片,需要和硬件打交道,如果處理的複雜或者不符合大衆標準很可能就是路越走越窄最後把自己走死了;而上層處理和我之前說的一樣,運輸層去各自實現成本很高,應用層自己去做對於應用開發者來說非常痛苦。

作爲一名應用開發者,我承認分片是一件簡單但是折磨人的工作。在實現藍牙OTA升級功能的時候,我需要將升級包.bin文件拆開依次發送給設備,中間需要處理分片,對齊,填充等等工作。這是一份不難但是非常考驗耐心的活。這只是簡單的點對點傳輸,如果情況複雜,需要處理的工作就會更多。要上層各自去實現分片,實在不是一個很好的選擇。

分包真的只有IP協議去實現了嗎

並不盡然。雖然我們說IP協議去做分片是一個成本較低的選擇,但實際還是有其他協議做了類似的操作。鏈路層中有類似Atm協議;在TCP協議中,MSS(Maximum Segment Size,最大報文長度)實際就是一個分片機制。

MSS

鏈路層協議作者並不熟悉,實際也並未接觸。我們後續討論以TCP爲對象。

TCP協議中的MSS和分片有一些簡單的不同:分片是因爲數據報長度大於MTU所以被動去分割數據報;而MSS是三次握手過程中雙方協商的結果,提前分割數據避免分片發送。TCP之所以提供這樣一個option來避免IP層分片,相信有部分原因在於IP分片並不如我們所假設的那麼完美。

我們之前提到IP協議去做分片確實複用率非常高,成本非常低,但實際情況中這些操作給負責分片和重組的主機和路由的cpu帶來了非常大的壓力。負責分組的終端的IP層需要去拆分數據報,用ID值相同的IP Fragmented Packet將數據發送出去;而重組終端的IP協議IP層根據ID,MF位,Fragment Offset信息進行重組,得到完整數據提交給上層。需要着重強調的是我們無法保證分片後的數據按照順序依次到達目的終端,並且IP層沒有類似超時重傳的可靠性支持,其中任一一個分片數據丟失都會引起整個數據報的丟失!!!!

亂序和丟包的問題確實非常的麻煩,這裏的麻煩更多的在於出現問題需要解決的成本過高。類似UDP只依靠IP協議分片處理大數據報的,實際是非常不推薦的。

如果上層提前分割了數據,IP層只要爲每一個數據報加上IP頭髮出即可。每一個數據相互獨立,由上層提供了可靠性支持。相比IP層分片的苦苦掙扎,這樣做確實簡單了很多。

但是這並不能完全的避免分片的發生。如RFC 879所說:TCP provides an option that may be used at the time a connection is established (only) to indicate the maximum size TCP segment that can be accepted on that connection.。這是一個在三次握手過程中協商的結果,不保證通訊過程中一定不會發生變化。

那麼是否還有其他的方式來避免分片

Path MTU Discovery(傳輸路徑MTU發現)就是爲此服務的。

在這一段的開始我們提到分片也可以發生在中間路由。舉一個簡單的例子:我們從源主機發生了一個IP包 = 1500,在互聯網上逐跳傳遞,在中間的某臺路由發送出去的時候,因爲該接口MTU只有1000 < 1500,那麼這個包就被分成兩個IP分片發出去了。但是作爲源主機對此毫不知情的,後續它仍然會按1500的大小發送IP包,那麼每一個大於1000的IP包都會被分片。

爲此我們可以在IP頭中設置DF(Don‘t Fragement)爲1來讓中間路由不要分片,如果IP包的大小大於中間路由的mtu,那麼直接丟棄並通過ICMP告知源主機。源主機再根據ICMP中提供的信息修改IP包的大小。

DF=1

如果後續還碰到更小的MTU怎麼辦?旁友,你聽過遞歸不咯?

在ICMP發送至源主機的過程中,可能會被攔截或者丟失,那麼這個IP包(包括後續大小超過中間路由MTU的IP包)就相當於靜悄悄的被丟棄了,TCP連接會被中斷。爲此要麼修改配置允許相關type的ICMP包的通過,要麼關閉PMTUD,畢竟允許分片的話雙方還是可以正常通信的。

IPv6中有關分片和IPv4有什麼區別嗎

IPv6中只會在源端和目的端分片重組,拒絕中間節點來進行分片。如果數據報大小小於中間節點的MTU,那麼中間節點會以ICMPv6 type=2的消息來告訴源端這個情況。這個操作看起來就像IPv4中DF=1一樣。據此我們可以認爲:避免分片是一種共識。

根據Wiki - IPv6 packet中有關IPv6的描述,端節點負責PMTUD來查詢允許發送數據報的最大長度,讓上層協議來限制Paylod的大小。如果上層協議不支持,那麼發送長度不大於1280的數據報來避免分片。

IPv6要求鏈路層的最小MTU是1280


TCP爲什麼需要三次握手

在思考爲什麼握手需要三次這個問題之前,我們應該先考慮的是

爲什麼TCP的建立需要握手。

TCP是一種可靠傳輸控制協議,它的主要任務是在可靠傳輸數據的基礎之上,儘可能的提高傳輸的效率。但是問題在於傳輸的信道並不可靠,我們面對的是一個未知的網絡環境,無法確定信道的可靠性。假如說信道百分百的可靠,那麼完全不需要握手的過程,我們只需要按照UDP的方式簡單的將消息發出即可,因爲我們知道無論何時何地我們只要發出消息對方一定能夠收到。但是在廣域網中這種完美的情況幾乎不存在,爲了解決在不可靠的信道上完成可靠傳輸這樣一個問題,那麼我們必須要在雙方傳輸數據之前,就某些問題達成一致。回到TCP中來解釋也就是通信雙方約定起始的Seq。

爲什麼需要的次數是三次,而不是兩次、四次或者更多?

首先我們先明確一點,在雙方通信的過程當中,一條消息單方面的確認需要兩次通信,也就是一次單向握手的過程。過程如下

單向握手

消息已收到傳遞到A之後,那麼作爲A這一方就可以確定消息已經被B接收到了。但是這個時候作爲B,它成功收到了A發來的消息但是它並不知道消息已送達這條消息是否成功抵達A處。

回到TCP中來看,建立連接的開始需要握手:我們需要驗證通信雙方之間的信道是否通暢,雖然中間只有一條信道,但是這條信道是雙向的(非常簡單的例子,一條馬路允許雙向的通行),我們需要同時驗證A->B以及B->A都是通暢的,雙方各有一個Seq需要和對方確認。

那麼也就是說兩次握手肯定是沒有辦法實現連接建立前雙方協商在某些方面達成一致的需求,因爲這隻能滿足一方確認消息,另一方無法確認。如果需要雙方都確認某一條消息,那麼必然需要兩次單向握手的過程。從TCP的角度來解釋,發送方SYN+ACK之後,響應方要初始化信道必須也要一次SYN+ACK。

那麼三次握手是如何來的呢。出於優化的目的,響應方將對發送方SYN的ACK和自身的SYN合併成了一條。所以說三次握手這個說法雖然貼切的描述了握手的過程,但並不準確。事實其實是雙方各一次的握手,各一次的確認,只不過其中一次握手和確認合併在一起。因爲這樣一個合併導致雙向握手+雙向確認的過程變成了三次握手。

四次揮手爲什麼不優化成三次揮手

理由也非常的簡單:通信是雙向的,如果響應方接受了發送方的連接請求,那麼必然也要發起一個單向握手來確認和發送方的連接;但是連接的關閉是允許半關閉的!任何一方都可以拆除自己發往對方數據的那個通道,而同時還要保證對方發往自己的數據能被正常處理。也就是說拆除階段的第二次單向握手並不一定是要在第一次單線握手之後立馬執行的,那麼中間兩次FIN+ACK的合併也就無從談起了。


爲什麼應用層還需要設計心跳

當TCP連接建立成功以後,它可以長時間處於空閒狀態沒有任何數據的交流。這個時間短則幾分鐘,長則幾個小時,幾天。這段時間裏只要雙方的主機沒有重啓進程依然存在,無論中間網絡發生什麼樣的情況連接都不會被中斷。

這樣一個說法似乎很難被理解,因爲這和我們生活接觸的實際例子不太一樣。比如水管斷開會漏水,電線斷開會停電。而TCP連接實質是通信雙方的主機保持的一個狀態,中間的連接是一個虛構的存在,這樣一個鏈接無論是發生波動抑或某一端異常斷開,通信的雙方是沒有辦法感知的。

上面所說的情況在實際情況中經常會發生。我們假設通信的雙方是服務器-客戶端提供服務的一端我們認爲是服務器,服務器通常會一直保持運行狀態並同時爲多個客戶端服務;發出請求的一方是客戶端。通常來說客戶端關閉程序或是手動關機的時候系統會爲我們主動斷開連接,發送一個FIN包給服務器。但是如果客戶端突然崩潰或客戶直接強制關閉了計算機,這個時候系統來不及發送FIN包,就會留下一個半開放連接在這裏。如果服務器再次向這個已經非正常關閉的客戶端發送消息,那麼它會收到一個RST的回覆。但是如果恰巧服務器正處在等待客戶端迴應的狀態,那麼它會一直等待下去

這樣的情況顯然是不可以接受的。因爲服務器打開一個鏈接的同時,會以一個客戶的身份佔用着某一部分的資源,這樣的一個半開放鏈接會導致這一部分的資源永遠得不到釋放。爲了應對解決這一個問題,TCP設計了保活功能,也就是SO_KEEPALIVE選項。

TCP的心跳包是一個有爭議的選項,這只是一個option,而不是一個必須實現的標準。通常情況下我們是在服務器打開這個選項,客戶端被動響應,默認間隔時間7200s。但這不是絕對的,如果有需求客戶端同樣可以打開。之所以在服務器設置這個選項是因爲它需要長時間保持在工作狀態,並且同時爲多個客戶端服務,而客戶端作爲個人使用會經常(異常)關閉。檢測出半開放的連接並刪除它,釋放資源對於服務器是非常必要的。

NFS雙方都打開了SO_KEEPALIVE,而Telnet和Rlogin則只有服務器打開了這個選項。

TCP保活機制的缺陷

保活機制是非常有必要的,但關於是否應該在TCP中提供一直爭論不休。在Host Requirement中提供了3個不使用保活定時器的理由

  • 在出現短暫差錯的情況下,可能會使一個運行正常的連接釋放掉。(比如中間路由崩潰並重新啓動時會發送一個保活探查,會讓TCP誤認客戶端已經崩潰)
  • 耗費不必要的帶寬
  • 在按分組計費的情況下會在互聯網下花費更多的金錢

拋開歷史的包袱來看,帶寬和計費相關的問題已經不再需要我們擔心,但TCP的保活機制仍然是一個不被推薦的選項。

首先我們需要明確的是,TCP的保活只能夠檢測連接是否存活,但是否可用是未知的。做一個簡單的比方,TCP的保活就像水道工,它只關心水管是否暢通能否通水,但水廠能否供水它無法保證。回到TCP上來解釋就是進程可能死鎖或者擁塞,操作系統是正常收發TCP消息的,但服務端繁忙無法提供服務。

其次,默認檢測的時間是7200s=120min=2h。這個間隔實際上是非常長的,這麼遲緩的響應能力在大部分的應用場景下是無法接受的。當然我們可以手動去修改SO_KEEPLIVE選項的參數,但這是系統級的變量,修改意味着會影響所有運行的程序。

有朋友提到這個參數在socket中可以爲pre socket單獨設置,類似Buffer

除此之外,還有一個比較棘手的問題是keep alive的數據包有可能會被運營商攔截。如果僅僅依賴TCP的保活機制,那麼在這種情況服務器可能會釋放掉一個運行正常的連接。

總結

考慮到以上的問題,很多上層的協議都提供了心跳機制來維持連接(比如MQTT中的PINGREQPINGRESP)。這並不是一種重複設計或者浪費!我們必須要明確的是TCP作爲運輸層的協議,它提供的是一個host級別的可靠傳輸服務,TCP所有任務實際的本質就是傳輸(就像我們提到的只是檢測連接是否存活)。如果在業務場景中有需要心跳機制來處理的邏輯,這部分的實現應該交由應用層來完成。作爲應用層的開發人員,應當把TCP簡單看成一個負責網絡傳輸的外部API,雖然它被廣泛應用但並不完美可靠,應用層關於自身差錯糾錯的邏輯是不應該被省略的


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章