1. 程式人生 > >【轉載】【計算機網路】【TCP】當我們說"TCP是可靠協議"時,我們真正表達的是什麼?

【轉載】【計算機網路】【TCP】當我們說"TCP是可靠協議"時,我們真正表達的是什麼?

很明確地說,從通訊意義上推敲,TCP一點都不可靠。一個抽象的協議,怎麼可能左右介質來保證可靠,不存在的。但凡是經由某種介質的通訊行為均不可能是絕對可靠的!

正好比我們現實生活中的保險,其實它什麼都不能阻止,什麼風險也保證不了它的不發生,它保證不了飛機不會掉下來,也無法阻止人生病…事實上,TCP就是通訊中的保險業。

TCP是如何設計出來的?推而廣之這類通訊協議是如何設計出來的?如果說讓你在一個不可靠的介質上執行一個可靠的協議,你該怎麼做?本文將介紹內中的些許因果。

可靠的通訊協議如何構建

這要從經典的兩軍問題說起。

首先介紹一下兩軍問題,來自Wiki的解說是最好的: Two Generals’ Problem:

https://en.wikipedia.org/wiki/Two_Generals'_Problem 兩軍問題本質上一個一致性確認問題,也就是說通訊雙方而不是一方(這對理解TCP非常重要)都要確保資訊的一致性。即假設通訊雙方為A和B,那麼A傳送一則訊息M給B,所謂的可靠性則是要同時滿足下面的條件:

通道是不可靠的,任何訊息均可能以任何概率丟失 如果AAA或者BBB不能確保訊息到達對方時,不能重發訊息 這一點非常重要,在經典的兩軍問題中,訊息是由信使傳遞的,而信使是人,人是軍隊作戰的最重要資源也是最不可靠的資源,比如會叛變…因此每條訊息或者確認相互只能派遣一個信使去遞送訊息,在通訊上講,就是訊息不能重發! 對於A而言,要確保A知道B已經收到了M 對於B而言,要確保自己收到M這件事已經被A知道

數學上很容易用反證法證明上述的兩軍問題是根本無解,即一致性通訊的完全可靠性是一種奢望。下面我來試著推導一下。

假設在時間點TnTnT_n,AAA和BBB兩端達到了資訊的完全一致性,所有互動的資訊包按照時間順序分別為M0M0M_0,M1M1M_1,M2M2M_2,…MnMnM_n,為了達到一致性,這些資料包是缺一不可的。現在來觀察最後的一個數據包MnMnM_n的傳輸,我們知道,通道是不可靠的,所以它可能會丟失,而它一旦丟失,整個互動過程便失去了一致性,這與假設是矛盾的,所以,一致性是不可能的。

這個問題貌似徹底拆了通訊技術的根基,那麼通訊技術還有什麼意義呢?

事實上,

首先,通訊協議從來都不是為了滿足完全的一致性需求

通訊的意義是,在時間序列上滿足訊息傳遞的單向完成需求即可!通訊的本質問題是確保訊息傳遞,而不是維護一致性,一致性應該由業務自身來負責,通訊僅僅提供訊息傳遞的基礎設施而已。

其次,通訊傳輸的是位元組電脈衝,訊息可以重發

這便大大削弱了兩軍問題的強約束。基於上述的假設,我們來一步步地推匯出TCP協議為什麼要這麼設計。

如果仔細推敲的話,你會發現,即便是訊息傳遞,在數學上也是無法確保在不可靠的通道上確保訊息傳遞的,然而,我們換個思路,即自問“通道到底不可靠到什麼程度?”。

是100%不可靠嗎?如果是的話,意味著斷路,即雙方是不可達的,無論我們傳送多少次資料包,均會丟失,這樣我們馬上可以結束這個沒有意義的討論,因此,所謂的不可靠只是說通道會出現概率性丟包,丟包概率ppp一定是介於開區間(0,1)(0,1)(0,1)之間的!

這個意義十分重大,這意味著,只要我們重試特定訊息MnMnM_n的次數足夠多,就一定能收到來自對端針對訊息MnMnM_n的確認!,這是完全確定的一個結論,沒人反對吧。

這邊自然而然匯出了可靠通訊的第一個原則:

1.超時重傳

該原則可以確保訊息一定能有機會到達對端。每當發出一個數據包,在預期的時間內沒有確認到達,就重傳它。關於超時重傳的細節,本文稍後會淺談一下,但是現在,我們來看另外一個問題。

如何確保訊息單向傳遞的完成?

換句話說,所謂訊息單向傳遞的完成,即需要一種標誌性的訊號*,該訊號揭示了訊息已經被對端接收這個事實,很顯然,對端傳送針對特定訊息的確認並且本端收到即可。

一旦AAA收到了來自BBB針對MnMnM_n的確認,對於AAA而言,它知道BBB肯定收到了MnMnM_n,而對於BBB而言,它也確實收到了MnMnM_n,不然它也不會發送確認。但是由上文可知,這個確認在不可靠的通道上也可能丟失,不過這不必驚慌,因為我們已經有了推論,即針對任意訊息,只要我們重複傳輸的次數足夠多,該訊息就一定能到達對端,在該推論下,採用超時重傳原則即可。

現在看來,我們匯出的下列措施已經解決了幾乎所有問題:

  1. 針對訊息MnMnM_n的超時重傳機制
  2. 針對訊息MnMnM_n的確認AMnAMnA_{M_n}的超時重傳機制

但是這是最優解嗎?

非也!這只是一種可行的方案,但不是唯一的方案,更不是最有的方案。匯出最優解需要我們深入到通訊網路的本質,先看一篇文章: 馬太效應/冪律分佈的本質以及其數學表述:https://blog.csdn.net/dog250/article/details/79146511 注意,我們的通訊網路是一個網狀拓撲的連通圖,無論是單節點連線數屬性還是流量屬性均符合冪律規律,從雙對數座標曲線可以看出網路規模和節點的各屬性特徵之間的對數線性關係,而網路規模來自於某種指數級增長的複製,單節點的屬性特徵來自於該節點的行為,很顯然,在這個雙對數座標下線性的通訊網路中,如果想等比例地縮放其規模而不至於崩潰,就必須用指數來控制單節點的行為(把雙對數座標化為笛卡爾座標即可展現)。

實際上,我們把雙對數座標中的直線(求解微分方程的結果)展開到相應的笛卡爾座標系,就是一條指數規律的曲線了。

再看另一個抽象,即如果資料包在傳輸過程中丟失了,這件事跟什麼因素相關?誠然,在網路通訊中,這件事肯定有可能是和傳輸介質相關的,但是在節點數量,即網路規模這個因素下,介質的問題可以忽略不計。也就是說,節點越多,傳輸越容易發生衝突,資料也就越不容易到達對端。即丟包事件和網路規模相關,網路是一個線性系統,所以,丟包的重傳必須具備指數級的時間特徵。

介質的問題隨著網路規模的擴大是線性增長的,而傳輸衝突的問題隨著網路規模的擴大則是指數級的,孰重孰輕,立判!

如果你瞭解早期的乙太網,即匯流排式的CSMA/CD乙太網,你會發現同樣的事實。

因此,很明確,超時重傳的超時規則在線性系統的平衡通過指數特性的單獨節點行為來維持的原則下,則必須是:

2.超時重傳-指數退避

有了這個原則,我們再回過頭來看如何實現訊息以及訊息確認的超時重傳。直接說結論,即不對確認進行重傳,因為確認和訊息本身屬於同一個行為,針對訊息本身的超時重傳已經自動包含了一個確認,如果再針對確認進行重傳,就會破壞單點行為的指數特徵,因此我們匯出可靠通訊的第三個特徵:

3.不對確認進行超時重傳

由於我們僅僅想確保訊息單向傳遞的可靠,即確保對端收到了本端發出的訊息而無需讓對端知道這件事,第四個特徵也隨即匯出:

4.不對確認進行確認

基礎設施構建就此完畢,考慮到通訊往往是雙向的,我們需要在其上構建一個可靠的雙向通訊協議,怎麼辦?

簡單,在另一端BBB重新這麼來一遍即可!於是我們觀察到,兩軍問題如果在超時重傳的前提下將雙向的訊息傳遞和確認分解成兩個單向的訊息傳遞和確認,事情就會簡單得多。

原始的兩軍問題解法:

轉換後的解法:

嗯,轉換後的解法,即我們熟悉的協議,TCP協議的最基本形式。現在進入TCP時間!

TCP握手,揮手,一致性的問題

經常有人問,TCP為什麼是3次握手,而不是2次,也不是4次,5次。知乎上經常會有這種問題,但是答案几乎是千篇一律的錯誤或者答非所問,最常見的答案只是描述一下TCP握手的細節,然後匯出這麼做是OK的,其實不這麼做也是OK的這一點沒人提。

最常見的錯誤答案:

  1. 這是一種權衡,因為無數次握手也不可能完全可靠;
  2. 描述握手的協議細節;

看過了我上面的論述,這個問題應該非常好答了,所謂的TCP建立連線的握手,實質上就是建立一個雙向的可靠通訊連線,一邊一個來回,每一邊都自帶超時重傳來確保可靠性(而不是靠握手的次數)。TCP的3次握手是優化的結果,其實它應該是4次握手,由於是從零開始的建立連線,因此將SYN的ACK以及被動開啟的SYN合併成了一個SYN-ACK,僅此而已。

握手的作用,旨在確定兩個雙向的初始序列號,TCP用序列號來編址傳輸的位元組,由於是兩個方向的連線,所以需要兩個序列號,握手過程不傳輸任何位元組,僅僅確定初始序列號:

說完了3次握手,那麼,其姊妹問題,為什麼TCP的斷鏈是4次揮手而不是3次?

換句話說,即是在問為什麼針對主動斷開方的FIN的ACK以及本端的FIN不能合併?

非常簡單,因為TCP是在一個單向可靠通訊系統基礎上構建而成的雙向傳輸控制協議,握手期間可以合併ACK和SYN,是因為在握手之前兩端沒有任何連線上的包袱,而在斷鏈揮手時,一端認為可以斷開了,另一端卻不一定,可能另一端還有資料要傳輸,所以便不能合併,被動關閉的一方只能單獨處理針對FIN的ACK以及自己的FIN,僅此而已。

再來一個問題,TCP能確保一致性嗎?換句話說,TCP協議是兩軍問題的一個解嗎?

遠遠不是!TCP並不確保一致性。

任何時間點,TCP都不能完全確認當前時刻連線雙方的狀態,此處所謂的狀態包括兩端傳輸的資料。一致性是基於訊息的,而不是基於連線的!也就是說,TCP只有收到下一個資料包時,才知道上一個資料包的接收情況,而無法實現隔空打人!TCP的好處僅在於,它在一個資訊流上實現了一個一致性確認的流水線方式。

我們在理解這個流水線方式的時候,不應該考慮滑動視窗,那樣會比較難以理解,我們應該僅僅考慮單位元組停等機制。事實上也確實是這樣,滑動視窗機制只是為流量控制而引入的,單位元組停等效率又太低,所以說這並無傷大雅,你把位元組換成視窗即可,即單視窗停等。

如果我們把一致性推廣到連線的層面,在連線層面,一致性就是靠4次揮手保證的。

我們可以看到,4次揮手那裡的狀態機非常之複雜,這是有原因的,即便是引入了TIMEWAIT狀態,也還是沒有辦法保證徹底的一致性,這是兩軍問題本質上不可解的一個結論,僅此而已。

1974年的TCP

現在你應該大致知道TCP如何保證可靠性了,進一步,如果你想知道TCP協議的頭部為什麼是那個樣子,這一切是如何安排的,你就不得不去讀一下一篇陳年的論文: 《A Protocol for Packet Network Intercommunication》:https://www.cs.princeton.edu/courses/archive/fall08/cos561/papers/cerf74.pdf 我來大致介紹一下這篇劃時代的論文。

毫不誇張地說,該論文奠定了以TCP/IP為核心的網際網路的基礎,我們今天能刷抖音,用微信聊天,能線上看片…這一切要不是這篇論文,不會是現在這個樣子。

該論文的重點不是TCP協議,而是TCP/IP作為一個整體如何發揮作用,早在1974年,分層模型還不算太成熟,所以當我們提起TCP/IP的時候,要明白,最初的時候,這兩個協議是牢牢切合在一起的,到了後來為了相容純IP轉發,才加入了UDP,這個時候,人們意識到分層模型的必要性。於是抽象而成的ISO/OSI模型。

該論文主要有兩個論題:

閘道器的概念和意義–最終的IP協議 程序間通訊的傳輸控制–最終的TCP協議

注意,我們看看TCP最初的形式,沒錯,它是作為一種程序間通訊的手段被提出的,當初TCP作為程序間通訊手段,側重於不同主機的程序間通訊,因此,我們可以清晰看到它的API和檔案IO的API是多麼相似,這也是socket可以作為檔案描述符的原因。

此外,還有值得注意的是,TCP的ACK號被定義為下一個索要位元組的序列號,這在當時實現了一種簡易且完備的位元組流水線,節省了協議頭空間,看到這個設計,簡直太帥!雖然它也帶來了很多問題,比如無法精確測準RTT,比如無法進行選擇確認,進而無法進行良好的擁塞控制,但不得不說,在空間重於時間的1970年代,這絕對是創舉,畢竟,擁塞控制在當時是沒有意義的,1988年才被引入。

1974年的網際網路

在1974年那篇論文之後,同樣的作者歸納總結出了RFC675: 《RFC675:SPECIFICATION OF INTERNET TRANSMISSION CONTROL PROGRAM》:https://tools.ietf.org/html/rfc675 這篇劃時代的RFC正式提出了網際網路這個概念,我們常說的Internet就是Internetworking的縮寫。

TCP/IP協議確實不是一個協議棧,最初它們只是一個協議,僅此而已,不多說。