運輸層
因特網的運輸層在應用程序端點之間傳送應用層報文,運輸層分組稱爲報文段(segment)。運輸層協議運行在端系統中,爲運行在不同主機上的應用進程之間提供了邏輯通信功能。
3.2 多路複用與多路分解
一個進程有一個或多個套接字(socket)。將運輸層報文段中的數據交付到正確的套接字的工作稱爲多路分解。在源主機從不同套接字中收集數據塊,並未每個數據塊封裝上首部信息(這將在以後用於分解)從而生成報文段,然後將報文段傳遞到網絡層,所有這些工作稱爲多路複用。
要實施正確的多路複用與多路分解,每個套接字都需要唯一的標識符以區分彼此,而 UDP 與 TCP 的套接字標識符又有所不同。兩者共有的標識部分,是運輸層報文段的首部信息中的端口號字段(包括源端口號字段和目的端口號字段)。端口號是一個 16 比特的數,其大小在 0 ~ 65535 之間。0 ~ 1023 範圍的端口號稱爲周知端口號,保留給周知應用層協議使用。其餘的端口號用於客戶端進程,通常由運輸層自動爲客戶端從中選取。
- UDP
一個 UDP 套接字由一個二元組 (目的 IP 地址,目的端口號) 標識的。因此,如果兩個 UDP 報文段具有不同的源 IP 地址/源端口號,而有相同的目的 IP 地址和目的端口號,那麼這兩個報文將被指向同一個套接字。
UDP 套接字標識並未使用到首部信息中的源端口號字段,但該字段仍有存在的必要:用作返回地址的一部分。
- TCP
一個 TCP 套接字由一個四元組 **(源 IP 地址,源端口號,目的 IP 地址,目的端口號)**標識的。因此,如果兩個 TCP 報文段具有不同的源 IP 地址/源端口號,即使有相同的目的 IP 地址和目的端口號,也將被指向不同的套接字(例外狀態是,服務器準備了一個特殊套接字用於初始創建連接,所以爲創建連接的、來源不同的 TCP 報文段,將被指向同一個套接字。當連接建立完成後,來源不同的客戶將具有各自不同的套接字。此時,來源不同的 TCP 報文段就會被指向不同的套接字了)。
3.3 無連接運輸:UDP
UDP 只做了運輸層協議能做的最少的工作:複用/分解和少量的差錯檢測。
選擇 UDP 的應用,主要考慮到
- 能夠更自由地發送數據,不必受限於一些擁塞控制機制;
- 無需建立連接,也就不會有建立連接的時延;
- 無連接狀態,也就不會有維護連接的資源消耗;
- 分組首部開銷小,僅 8 字節(相比 TCP 的 20 字節)。
UDP 報文段結構
UDP 首部只有 4 個字段,每個字段由兩個字節組成。源端口號用於返信,目的端口號用於分解,長度指示 UDP 報文段的字節數(首部 + 數據),檢驗和用於差錯檢測。
UDP 檢驗和
發送方將 UDP 報文段(事實上,還要加上 IP 數據報的首部,稱爲僞首部)看作若干個 16 比特字,對這些字的和取反碼(溢出時回捲,即加回到最低位),得到的結果放到 UDP 報文段的檢驗和字段。
接收方重複發送方的計算過程,並將結果與報文段中的檢驗和字段值相加,若無差錯,結果將爲 1111111111111111。
許多鏈路層協議都提供了差錯檢測,那麼 UDP 爲什麼還要提供檢驗和呢?原因是,不能保證源和目的之間的所有鏈路都提供差錯檢測,也不能保證報文段在路由器內存中時不引入比特差錯。指望較低層次提供差錯檢測(或其他功能)是徒勞的,因此,UDP 就必須在端與端之間實現差錯檢測(或其他功能)。這被稱作 端到端原則(end2end principle)。
雖然 UDP 提供差錯檢測,但它無法恢復差錯。UDP 只能丟棄受損的報文段。
3.4 可靠數據傳輸原理
經完全可靠信道的可靠數據傳輸:rdt 1.0
既然下層信道完全可靠,那就沒什麼好說的了。
經具有比特差錯信道的可靠數據傳輸:rdt 2.0
在分組中的比特可能受損的情況下,可以採用自動重傳請求協議(ARQ)來確保可靠傳輸。ARQ 由三個重要機制構成:
- 差錯檢測。接收方需要能夠檢測出分組是否出錯,如 UDP 的檢驗和。
- 接收方反饋。接收方通過回送肯定確認(ACK/1)與否定確認(NAK/0)反饋接收信息。
- 重傳。接收方受到由差錯的分組時,發送方將重傳該分組。
rdt 2.0 是個標準的 ARQ 協議。
rdt 2.0 的發送方初始時等待上層的調用。當調用發生時,發送方接收下傳的數據,將其封裝爲帶有檢驗和的分組,送往接收方。一旦發送完畢,發送方進入等待反饋狀態,根據接收方反饋的信息來決定是重傳(收到 NAK)還是進入等待調用狀態(收到 ACK)。
接收方則無腦接收分組。若分組未出差錯,接收方將分組信息抽出,傳給上層,並向接收方反饋 ACK,表明當前分組順利接收;若出錯,則反饋 NAK。
注意到,當發送方處於等待反饋狀態時,它無法接收上層的數據,直到接收到接收方反饋的 ACK。我們稱這樣的行爲爲停等,rdt 2.0 因此是個停等協議。
rdt 2.0 有一個致命缺陷:ACK/NAK 分組可能受損。要解決這個問題,有三種解決方案:
- 發送方請求重複確認。可惜,請求重複確認分組同樣可能受損:這將把我們引向哲學。
- 增強差錯檢測與差錯恢復。對於會產生差錯而不會丟包的信道,這很好。
- 當發送方收到含糊不清的 ACK/NAK 分組時,重傳當前分組。然而,這引入了冗餘分組,接收方無法得知接收到的分組是否是一次重傳。
解決方案三的簡單方法是,在分組中添加序號字段,接收方僅需檢查序號即可得知接收到的分組是否是一次重傳。此時,我們得到了 rdt 2.1 版本。
rdt 2.1
對於停等協議這種簡單情況,1 比特序號就足矣。發送方交替使用序號 0 和 1 來發送分組,而接收方並不需要使用序號來標註 ACK/NAK,因爲發送方知道所接收到的 ACK/NAK(無論是否受損)是爲響應其最近發送的數據分組而生成的。
要證實這點,想象當處於等待分組 0 的接收方,接收到了一個序號爲 1 的分組,這個序號爲 1 的分組必定是上一個分組,是接收方已經順利接收到了的分組(否則接收方就不會從等待分組 0 狀態轉移到等待分組 1 狀態)。此時,接收方就知道,自己的反饋受損了,發送方此時還處在等待反饋 1 狀態,需要接收到 ACK 才能發送序號爲 0 的分組,也就是接收方當前想要得到的分組。那麼,接收方就只要發送 ACK 就好了。既然接收方能夠從分組序號推斷出發送方的狀態,發送方只要無腦根據 ACK/NAK 來轉移狀態就好了。
rdt 2.2
如果不發送 NAK,而是對上次正確接收的分組發送一個 ACK,也能實現與 NAK 一樣的效果。發送方接收到對同一個分組的兩個 ACK (即冗餘 ACK)後,就知道接收方沒有正確接收到跟在被確認兩次的分組後面的分組。這時,接收方就需要在 ACK 中包括確認接收到的分組的序號了,發送方也需要檢查 ACK 中被確認的分組的序號。
經具有比特差錯的丟包信道的可靠數據傳輸:rdt 3.0
信道丟包在現實中很常見,這意味着發送方發送的分組與接收方反饋的 ACK 都有可能丟失。要解決這個問題,最簡單的方法,就是重傳。如果發送方發送的分組丟失,發送方重傳就再好不過了;如果接收方反饋的 ACK 丟失,這意味着接收方已收到完好的分組,而此時發送方重傳分組,就引入了冗餘數據分組。好在,rdt 2.2 協議利用序號就能解決冗餘分組的問題(接收到受損的分組或冗餘分組,直接反饋帶有接收到的上一個分組的序號的ACK)。
重傳能夠解決問題,但引入了新的問題:如何確定重傳需要等待的時間?可以知道的是,至少需要一個往返時延加上接收方處理一個分組所需的時間,但這仍然難以估計。
爲了實現基於時間的重傳機制,需要一個倒計數定時器,超出給定的時間後,可中斷髮送方。因此,發送方需要能做到:
- 每次發送一個分組(包括第一次分組和重傳分組)時,便啓動定時器;
- 響應定時器中斷(採取適當的動作,比如重傳);
- 終止定時器。
rdt 3.0 的確可靠,但面臨着低效的問題。定義發送方(或信道)的利用率爲:發送方實際忙於將發送比特送進信道的那部分時間與發送時間之比。那麼,定義 rdt 3.0 的發送方利用率 。這個公式很顯然。L/R 通常很小(微秒),RTT 相對來說大得多(毫秒),將量級帶入式子,我們發現利用率低得可憐。
要提高性能,一種技術是流水線:不採用停等,允許發送方發送多個分組而無需等待確認。採用流水線,協議需要做出以下改進:
- 增加序號範圍。流水線提高了同時在信道中傳播的分組。
- 發送方與接收方需要緩存多個分組。發送方至少要緩存已發送但未被確認的分組;接收方至少要緩存那些已正確接收的分組。
根據差錯恢復的不同處理方式,有兩種流水線協議:回退 N 步(GBN)和選擇重傳(SR)。
GBN
實踐中,分組的序號承載在分組首部的一個固定長度 k 的字段中,序號空間 [0, ] 被循環使用。定義以下變量
基序號 base:最早的未確認分組的序號
下一個序號 nextseqnum:最小的未使用的序號(即下一個待發分組的序號)
窗口長度 N:從基序號開始,已被髮送但還未被確認的分組的許可序號範圍
於是,整個序號空間被分割成 4 段:
-
[0, base-1]:已經發送並被確認的分組
-
[base, nextseqnum-1]:已經發送但未被確認的分組
-
[nextseqnum, base+N-1]:將要發送的分組能夠使用的序號
-
[base+N, MAX_SEQ_NUM-1]:暫時無法使用的序號,直到當前流水線中未被確認的分組(特別是序號爲 base 的分組)得到確認
窗口被限定長度,且隨着分組被確認而移動,這使得 GBN 協議也被稱爲滑動窗口協議。
接收方的動作很簡單:如果序號爲 n 的分組完好且按序(上一個接收到的分組序號爲 n-1)被接收到,則接收方發送一個序號爲 n 的 ACK,並將分組交付上層;在其他情況下,接收方丟棄該分組,併發送一個序號爲最近按序接收到的分組的 ACK。這種方式稱爲累積確認。
發送方需要響應三種事件:
- 上層的調用。上層希望發送數據時,首先檢查發送窗口是否已滿,只有窗口未滿時可以發送新的分組。當發送的分組之前所有發送的分組都已被確認時,開啓定時器。
- 收到 ACK。發送方接收到序號爲 n 的 ACK,意味着序號在 n 以前且包括 n 的分組都已被正確接收到了。如果已發送但未被確認的分組都已被確認,則關停定時器,否則,重置定時器(要注意,GBN 只有一個定時器,這個定時器爲最早發送的但未被確認的分組計時)。
- 超時。如果出現超時,發送方重傳所有已發送但未被確認的分組,即序號爲 [base, nextseqnum-1] 範圍內的分組。由於這個範圍最大長度爲 N,所以 “回退 N 步“ 的名稱也就不言而喻了。
SR
GBN 存在着性能問題,單個分組的差錯就能引起大量的重傳,而許多分組本不必重傳。選擇重傳協議通過讓發送方僅重傳那些它懷疑在接收方出錯(即丟失或受損)的分組而避免了不必要的重傳。這種個別的、按需的重傳要求接收方逐個地確認正確接收的分組,也就是說,SR 對失序的分組也會反饋 ACK。
發送方需要響應三種事件:
- 上層的調用。與 GBN 一樣,已發送但未被確認的分組數量不能超過窗口長度。
- 超時。GBN 只有一個定時器,SR 必須爲每個分組配置一個定時器,因爲超時發生後只能重傳一個分組。
- 收到 ACK。如果收到 ACK,若該分組序號在窗口內,則發送方將那個被確認的分組標記爲已接收。若該分組的序號等於 send_base,則窗口基序號向前移動到具有最小序號的未確認分組處。如果窗口移動了,並且有未發送分組的序號落在窗口內,則發送這些分組。
接收方也需要響應三種情況:
- 序號在 [rcv_base, rcv_base+N-1] 內的分組被正確接收。分組序號落在窗口內,接收方回送一個具有該分組序號的 ACK。若該分組以前沒有收到過,則緩存該分組。若該分組的序號等於接收窗口的基序號,則將該分組以及以前緩存的序號連續的(起始於 rcv_base 的)分組交付上層,然後移動窗口。
- 序號在 [rcv_base-N, rcv_base-1] 內的分組被正確接收。必須回送一個 ACK,即使該分組已接收過。原因在於,發送方與接收方看到的窗口在多數時間是不一樣的,對於哪些分組已經被正確接收,哪些沒有,發送方和接收方並不總是能看到相同的結果。發送方重傳分組,意味着其並未收到接收方對該分組的 ACK(ACK 分組丟失了,發送方以爲接收方沒收到),如果接收方因爲其已確認過該分組而選擇不反饋(接收方以爲發送方已收到),那麼,發送方的窗口就永遠無法向前滑動了。
- 其他情況。忽略該分組。
序號空間的大小與窗口長度的協調對 SR 來說是至關重要的。假定序號空間大小爲 4,窗口長度爲 3,那麼,在無知之幕下,以下兩種狀況對接收方而言是等價的,但真實的情況卻完全不同。
從上述情形,可以看到,序號空間對於窗口必須足夠大,否則會出現接收方無法判斷接收到的報文是超時重傳還是啓用了新一輪序號的分組。
假設當前接收方正在等待的、序號最小的分組序號爲 m,窗口大小爲 N,那麼接收方的窗口範圍爲 [m, m+N-1],這意味着接收方已發送了序號範圍爲 [m-N, m-1] 共 N 個分組,並且回送了 ACK。假設極端情況下(這也是 3-27 發生的情況),前 N 個分組的 ACK 均丟失(窗口範圍爲 [m-N, m-1]),或發送方接收到前 N 個分組的 ACK 併發送了接下來序號範圍爲 [m, m+N-1] 的分組(窗口範圍爲 [m, m+N-1])而前若干個分組丟失,若序號空間過小,那麼這兩種行爲接收方無法分辨。
在上述情況下,發送方可能發送的最小序號爲 m-N,最大序號爲 m+N-1,由於 m+N-1 可能超過序號空間 k,從而對 k 取模而大於等於 m-N,那麼就會出現範圍爲 [0, m-N] 的一段重疊空間,序號在其中的分組,接收方就無法分辨是重傳還是新分組。可以想到,只要阻止這個重疊空間出現即可,那麼就要滿足以下關係:
解得
即窗口的大小最大爲序號空間大小的一半。
演進歷程
—無損信道> rdt 1.0 無腦協議
—比特差錯> rdt 2.0 停等協議
—反饋受損> rdt 2.1 分組序號
—NAK多餘> rdt 2.2 冗餘 ACK
—信道丟包> rdt 3.0 超時重傳
—停等低效> GBN 流水線化
—冗餘重傳> SR 選擇重傳
3.5 面向連接的運輸:TCP
TCP 連接
TCP 是面向連接的,在兩個應用程序可以發送信息之前,必須握手以建立連接。這個連接並不是電路或虛電路,而是一個虛擬連接,其連接狀態完全保留在兩個端系統中。
TCP 連接在點對點之間提供全雙工服務,兩端都有各自的發送緩存和接收緩存。
TCP 報文段結構
TCP 報文段由首部字段和數據字段組成。
數據字段的長度是有限制的,最大報文長度 MSS 根據最大鏈路層幀長度(即最大傳輸單元 MTU)設置。MSS 要保證一個 TCP 數據段加上 TCP/IP 首部長度(通常 20+20 = 40 字節)不超過 MTU,而 MTU 的典型長度是 1500 字節,所以 MSS 的典型值是 1460 字節。注意,MSS 是數據字段的長度,而不是數據字段+首部的總長度。
首部包含許多字段:
- 源端口號和目的端口號:各 16 比特,與 UDP 一樣,用於複用/分解。
- 序號和確認號:各 32 比特,討論見後。
- 首部長度:4 比特,指示 TCP 報文段首部的長度,以 4 byte 爲單位。
- 標誌:6 比特,6 個標誌位各 1 比特。ACK 比特用於指示確認號字段中的值是有效的,即該報文段包括一個已被成功接收報文段的確認。RST、SYN 和 FIN 比特用於連接的建立和拆除。(PSH 比特用於指示接收方應立即將數據交付上層。URG 用於指示該報文爲緊急的,緊急數據的最後一個字節由 16 比特的緊急數據指針字段指出。括號內的這些都不常用。)
- 接收窗口:16 比特,用於流量控制。
- 檢驗和:16 比特,與 UDP 一樣,用於差錯檢測。
- 選項:可變長,實踐中不常用。
接下來澄清一下序號和確認號的作用。
TCP 把數據看成一個無結構的、有序的字節流,因爲序號建立在傳送的字節流之上,而不是傳送的報文段序列之上。一個 TCP 報文段的序號因此是該報文段首字節的字節流編號。假定數據流由一個 500000 字節的文件組成,其 MSS 爲 1000 字節,數據流的首字節編號是 0,那麼 TCP 將爲該數據流構建 500 各報文段,序號分別爲 0、1000、2000、…。當然,首字節編號也可以不爲 0。事實上,一條 TCP 連接的雙方均可隨機地選擇初始序號。這樣做可以減少將那些仍在網絡中存在的來自兩臺主機之間先前已終止的連接的報文段,誤認爲是後來這兩臺主機之間新建連接所產生的有效報文段的可能性(它碰巧於舊連接使用了相同的端口號)。
確認號則是報文段的發送方期望從對方收到的下一字節的序號。假定主機 A 已收到編號爲 0 ~ 535 的所有字節,那麼其發送的下一個報文段中的確認號字段爲 536。TCP 是累積確認的。假定主機 A 又收到編號爲 900 ~ 1000 的字節,那麼其發送的下一個報文段中的確認號字段仍爲 536,因爲主機此時期望收到的下一字節確實是 536。對於失序的報文段(如例子中的 900 ~ 1000),實踐中,接收方保留失序的字節,並等待缺少的字節以填補該間隔。這表現得類似於 SR 協議,但 TCP 絕不是一個完全的 SR 協議,後面我們將再來討論這個。
簡化模型
在序號與確認號的討論中,我們描述了 TCP 接收方的動作。現在,簡要(意指不受流量控制和擁塞控制的影響的情況下)描述一下發送方需要響應的事件:
- 上層的調用。序號方面與前面描述的相同。另外,當發送的分組之前所有發送的分組都已被確認時,開啓定時器(與 GBN 一樣,這個定時器是與最早的未被確認的報文段相關聯的)。
- 超時。重傳引起超時的報文段(與 SR 一樣),重置定時器(與 GBN 一樣,TCP 只有一個定時器)。
- 收到 ACK。累積確認,更新最早的未被確認的字節值。如果還有未被確認的報文段,則重置定時器。
可以看到,TCP 發送方的響應動作與 GBN 和 SR 都有重疊之處:公用定時器、累積確認等看起來像是 GBN,而選擇重傳等看起來又像是 SR,所以,TCP 是一個介於 GBN 與 SR 之間的協議。
往返時間的估計與超時間隔加倍
TCP 採用超時/重傳機制來處理報文段的丟失問題。前面我們看到,對超時間隔長度的設置是一個棘手的問題。
TCP 通過採樣並更新來應對。在任意時刻,TCP 僅爲一個已發送的但目前尚未被確認的報文段(這個報文段絕不會是已被重傳過的)來估計 SampleRTT 值。一旦獲得一個新 SampleRTT 時,TCP 就會根據下列公式來更新 RTT 估計值 EstimatedRTT:
其中 參考值爲 0.125。這是一個指數加權移動平均。
另外,TCP 還會測量 RTT 的偏差值:
其中 的參考值爲 0.25。這也是一個指數加權移動平均。
TCP 的超時間隔就依照上述兩個值來確定:
TimeoutInterval 的初始值參考值爲 1 秒。
實踐中,TCP 並不完全依靠採樣計算平均值來設定超時間隔,而是僅在“上層的調用” 和 ”收到 ACK“ 中重置定時器時,會通過 EstimatedRTT 和 DevRTT 計算 TimeoutInterval。其他時候(指重傳),都會將下一次的超時間隔設置爲先前值的兩倍。這樣,超時間隔就在每次重傳後呈指數型增長。這可以是一個形式受限的擁塞控制。
快速重傳
超時重傳存在的問題之一是超時間隔太長了,引入快速重傳可以提前發現丟包。
因爲發送方經常一個接一個地發送大量報文段,如果一個報文段丟失,就很可能引起許多一個接一個的冗餘 ACK。如果 TCP 發送方接收到對相同數據的 3 個冗餘 ACK,它把這當作一種指示,說明跟在這個已被確認過 3 此的報文段之後的報文段已經丟失。一旦收到 3 個冗餘 ACK,TCP 就執行快速重傳,即在該報文段的定時器過期之前重傳丟失的報文段。
爲什麼是 3 個冗餘 ACK 呢?只能說這是一個經驗值,出現三個冗餘 ACK 是丟包的概率較大。像逼乎的那個高贊回答,就是裝逼忽悠人的典型。
另外,RFC 爲產生 ACK 提出了建議:
流量控制
TCP 連接的兩端,都各自有一個接收緩存。放入接收緩存中的分組往往並不會立即被讀取,所以,若一方發送得太多、太快,就可能引起對方的接收緩存溢出。
爲了匹配發送方的發送速率與接收方應用程序的讀取速率,TCP 提供了流量控制。TCP 讓雙方各自維護一個稱爲接收窗口的變量,在發送數據時填入報文段的接收窗口字段中,用於提醒對方,自己的接收窗口所剩餘的空間。
對於接收方,定義以下變量:
LastByteRead: 應用進程從緩存讀出的數據流的最後一個字節的編號。
LastByteRcvd: 已緩存的數據流的最後一個字節的編號。
RsvBuffer: 接收緩存的大小。
rwnd: 接收窗口。
爲了防止溢出,很顯然,有
rwnd 就是需要填入接收窗口字段的值,它是動態變化的,初始時設置爲 RcvBuffer。
對於發送方,爲了控制流量,定義以下變量:
LastByteSent: 發送的最後一個字節的編號。
LastByteAcked: 確認的最後一個字節的編號。
發送方在連接的整個生命週期必須保證:
也就是,已發送但未被確認的字節數必須控制在 rwnd 以內。
特別的,當主機 A 向主機 B 通告 rwnd = 0 時,B 必須繼續發送只有一個字節數據報文段,A 在接收到這個報文段後開始清空緩存,並在確認報文中包含一個非 0 的 rwnd 值。這麼做是爲了防止 A 通告 rwnd = 0 後,又沒有任何數據要發送給 B,這時,B 就無法得知 A 的接收窗口大小,就陷入了阻塞。
TCP 連接建立:三次握手
要建立一個 TCP 連接,需要經過三次握手:
-
客戶端的 TCP 首先向服務器端的 TCP 發送一個 SYN 報文段。該報文段不包含應用層數據,但首部的 SYN 標誌位被置爲 1。另外,客戶會隨機選取一個初始序號 ISN,放入該報文的序號字段中。
-
服務器收到 SYN 報文段,爲該 TCP 連接分配緩存和變量,並向該客戶發送表示允許連接的 SYNACK 報文段。該報文段也不包含應用層數據,首部的 SYN 標誌位也被置爲 1。然後,服務器對客戶的 ISN 進行確認,即將該報文的確認號字段置爲接收到的 SYN 報文段中的 ISN + 1,這樣,客戶發送給服務器的數據就是可靠的了,而不用擔心會是前段時間在網絡中逗留過久的。然後,同客戶一樣,服務器隨機選取一個初始序號放入序號字段中。
-
客戶收到 SYNACK 報文段,爲該 TCP 連接分配緩存和變量,並向服務器發送對 SYNACK 報文段的確認報文段。該報文段的首部 SYN 標誌位被置爲 0,因爲連接已經建立了。然後,客戶需要對服務器的 ISN 進行確認,即將該報文段的確認號字段置爲接收到的 SYNACK 報文段中的 ISN + 1,這樣,服務器發送給客戶的數據就是可靠的了。另外,該報文可以捎帶應用層數據了。
爲什麼需要三次握手?
客戶和服務器都各自隨機選取一個初始序號值,是爲了避免將先前連接發送的、在網絡中逗留過久的報文段誤認爲是當前接收的,所以,互相確認對方選擇的初始序號值就很有必要。若只有兩次握手,只有服務器對客戶的 ISN 進行確認,而客戶並沒有對服務器的 ISN 進行確認,那麼,只有客戶到服務器的報文段是可靠的。舉個例子就是,當服務器接收到了逗留過久的、實際已經失效的 SYN 報文段,若返回一個 SYNACK 報文段,客戶並不會響應該 SYNACK 報文段,連接也就無法建立起來,而服務器又爲之配置了資源,造成了浪費。
至於四次握手,就是性能的問題了。三次握手已足夠。
TCP 連接拆除:四次揮手
要拆除一個 TCP 連接,需要經過四次揮手:
- 客戶向服務器發送一個 FIN 報文段,該報文段首部的 FIN 標誌位被置爲 1。
- 服務器收到 FIN 報文段,回送一個確認報文段。
- 服務器向客戶發送一個 FIN 報文段,該報文段首部的 FIN 標誌位被置爲 1。
- 客戶收到 FIN 報文段,回送一個確認報文段。
此時,兩臺主機上用於該連接的所有資源都被釋放了。
3.6 擁塞控制原理
擁塞的代價
- 當分組的到達速率接近鏈路容量時,分組經歷巨大的排隊時延。
- 發送方在遇到大時延時所進行的不必要重傳會引起路由器利用其鏈路帶寬來轉發不必要的分組副本。
- 當一個分組沿一條路徑被丟棄時,每個上游路由器用於轉發該分組到丟棄該分組而使用的傳輸容量最終被浪費掉了。
擁塞控制方法
- 端到端擁塞控制。TCP 的方案,後面提到。
- 網絡輔助的擁塞控制。路由器向發送方反饋擁塞狀態的信息,有兩種反饋方式:一是利用阻塞分組直接反饋給發送方,二是路由器標記或更新從發送方流向接收方的分組中的某個字段來指示擁塞的產生。
3.7 TCP 擁塞控制
概述
TCP 所採用的擁塞控制方法是,讓雙方根據所感知到的網絡擁塞程度來限制其能向連接發送流量的速率。關於這種方法,能夠提出三個問題:
- 一個 TCP 發送方如何限制它向其連接發送流量的速率呢?
與流量控制類似,採用一個擁塞窗口 cwnd,通過調節 cwnd 來限制其發送的速率,即有
- 一個 TCP 發送方如何感知從它到目的地之間的路徑上存在擁塞呢?
存在擁塞時,就會產生丟包。TCP 發送方將丟包事件定義爲超時或 3 個冗餘 ACK,也就是說,當發生兩者其一,就說明丟包,也就意味着出現了擁塞。
- 當發送方感知到端到端的擁塞時,採用何種算法來改變其發送速率呢?
TCP 通過以下指導性原則改變速率:
- 一個丟失的報文段意味着擁塞,因此當丟失報文段時應當降低 TCP 發送方的速率。
- 一個確認報文段指示該網絡正在向接收方交付發送方的報文段,因此,當對先前未確認報文段的確認到達時,能夠增加發送方的速率。
- 爲探測擁塞開始出現的速率,TCP 發送方增加它的發送速率,直到出現擁塞,降低速率,然後再度開始探測。
TCP 擁塞控制算法
TCP 擁塞控制算法包括三部分:慢啓動、擁塞避免和快速恢復。其中,快速恢復並非是必須的。
- 慢啓動
當一條 TCP 連接開始時,cwnd 的值通常設置爲 1 MSS,初始發送速率也就爲 1 MSS/RTT。
可用帶寬很可能必 1 MSS/RTT 大得多,所以 TCP 發送方希望迅速找到可用帶寬的值。因此,在慢啓動狀態,每當一個報文段首次被確認,就將 cwnd 增加 1 MSS。
想象這樣一個場景:第一個 RTT,cwnd = 1 MSS,發送方發送一個報文段,並收到一個確認,cwnd += 1 MSS;第二個 RTT,cwnd = 2 MSS,發送方發送兩個報文段,並收到兩個確認,cwnd += 2 MSS;第三個 RTT,cwnd = 4 MSS,發送方發送四個報文段,並受到四個確認,cwnd += 4 MSS;…。
就這樣,在慢啓動狀態,每過一個 RTT,發送速率就翻番,呈指數型增長。指數型增長很容易引發擁塞,因此需要適時停止。根據以下事件,慢啓動狀態發生轉移:
- 超時。TCP 發送方將 ssthresh(慢啓動閾值)置爲 cwnd/2,cwnd 置爲 1 MSS,然後重新開始慢啓動。
- cwnd ≥ ssthresh。進入擁塞避免狀態。
- 3 個冗餘 ACK。TCP 發送方將 ssthresh 置爲 cwnd/2,cwnd 置爲 ssthresh + 3 MSS(這 3 MSS 是 3 個冗餘 ACK 提供的速率增長),然後進入快速恢復狀態。
- 擁塞避免
一旦進入擁塞避免,cwnd 的值大約是上次遇到擁塞時的值的一半,距離擁塞可能並不遙遠。因此,在擁塞避免狀態,每個 RTT 只將 cwnd 的值增加 1 MSS。
一種方法是,每當一個報文段首次被確認,就將 cwnd 增加 (MSS/cwnd) MSS。由於一個 RTT 共發送 cwnd 字節,分爲 cwnd/MSS 個報文段發送,每個 ACK 增加 (MSS/cwnd) MSS,那麼,在該 RTT 結束後,cwnd 共增加 cwnd/MSS · (MSS/cwnd) MSS = 1 MSS。
就這樣,在擁塞避免狀態,cwnd 呈線性增長。根據以下事件,擁塞避免狀態發生轉移:
- 超時。TCP 發送方將 ssthresh(慢啓動閾值)置爲 cwnd/2,cwnd 置爲 1 MSS,然後進入慢啓動。
- 3 個冗餘 ACK。TCP 發送方將 ssthresh 置爲 cwnd/2,cwnd 置爲 ssthresh + 3 MSS(這 3 MSS 是 3 個冗餘 ACK 提供的速率增長),然後進入快速恢復狀態。
- 快速恢復
在快速恢復中,每個新收到的引起 TCP 進入快速恢復狀態的冗餘 ACK,都將 cwnd 的值增加 1 MSS。
根據以下事件,快速恢復狀態發生轉移:
- 超時。TCP 發送方將 ssthresh(慢啓動閾值)置爲 cwnd/2,cwnd 置爲 1 MSS,然後進入慢啓動。
- 對丟失報文段的一個 ACK 到達。TCP 發送方將 cwnd 置爲 ssthresh,然後進入擁塞避免狀態。
以上是 TCP Reno 版本的快速恢復動作。若是 TCP Tahoe,不管發生超時還是 3 個冗餘 ACK,都將 cwnd 減至 1 MSS,並進入慢啓動階段。TCP Reno 相對於 TCP Tahoe, cwnd 在一次慢啓動後,接下來整體處於一個較高的水平,在吞吐率得到保證的情況下減少了網絡波動。TCP Reno 相較於 TCP Tahoe 是更優的。
爲什麼 Reno 版本在進入快速恢復時選擇將 ssthresh 和 cwnd 都設爲出現數據包丟失時的 cwnd 值的 1/2,而不是其它值呢?比如 1/3, 2/3?
假設 A 與 B 建立了一個 TCP 連接,A 向 B 發送一個數據段,B 將以一個 ACK 數據段迴應,這段時間就是一個 RTT。由於光速的限制以及鏈路的長度,數據段在傳輸過程中是有容量的,假設這個容量爲 C,顯然有 C = r * RTT,r 爲最大發送速率。
試想,既然一個 A 發送一個數據段後 B 要以一個數據段迴應,而這兩個數據段是異步發送的,那麼當 cwnd 大於 1 時,一定有一個時刻,A 發送的尚未到達 B 的數據段與 B 發送的對已到達的數據段的 ACK 迴應同時在鏈路上傳播,並且兩者數量之和等於 A 此 RTT 中發送的數據段數量。由此,我們可將容量分爲兩份(或者說,出於 A、B 兩者的公平性,平分鏈路容量),一份是 A 發送給 B 的仍在傳播的數據段,一份是 B 迴應給 A 的仍在傳播的 ACK,假定 A、B 發送速率相同,那麼又可進一步得到 C = r * RTT = 2 * N,N 爲 A 發送給 B 和 B 發送給 A 的數據段剛好填滿各自的鏈路容量(C/2)時的數量。由於 B 每接收一個來自 A 的數據段,就會迴應一個 ACK 給 A,那麼,在 A 發送其剛剛最後一個數據段而 B 的最早一個 ACK 即將抵達 A 時,這樣一個 RTT 時間內,能使鏈路滿載的 A 發送的數據段數量顯然是 2 * N,這就是最大的 cwnd 值,一旦超過這個值,就會發生阻塞。
一旦發生阻塞,我們就能夠知道,當前可供 A、B 通信的最大網絡容量 C 即爲 cwnd。此時,需要將 ssthresh 減半,爲什麼是 1/2,而不是 1/3、2/3 呢?原因就在於 ssthresh 其實就相當於 N!N 是 A 發送至 B 的網絡容量能夠容納的數據段的數量,也就是說,對於 A 而言,此時網絡是滿載的,如果遭遇網絡波動,就會導致丟包。可以說,N 就是一個分水嶺,它是 A 發送給 B 的數據段數量相對能夠說是安全的最大值,低於 N,網絡尚未填滿,有一定的緩衝可以應對網絡波動;高於 N,網絡將會處於一段較長的持續滿載狀態,一旦有網絡波動,就會導致丟包。作爲慢啓動與擁塞避免的臨界值,ssthresh 的最佳選擇無疑是選擇一個分水嶺,也就是 N!
從以上推導可以得出,ssthresh= N,而 C = r * RTT = 2 * N = cwnd,有 cwnd = 2 * N,最終得到 N = ssthresh = cwnd/2,這也就是 1/2 的由來。
具體可參考:https://blog.csdn.net/dog250/article/details/51439747
公平性
TCP 趨於在競爭的多條 TCP 連接提供對一段瓶頸鍊路帶段的平等分享。