TCP\IP協(xié)議詳解
本文中用到的圖片轉自: 頭條文章
TCP與UDP協(xié)議有什么區(qū)別?
TCP是一個面向連接的、可靠的、基于字節(jié)流的傳輸層協(xié)議。
UDP是一個面向無連接的傳輸層協(xié)議。
與UDP相比,TCP有以下特性:
面向連接。所謂的連接,指的是客戶端和服務器的連接,在雙方互相通信之前,TCP 需要三次握手建立連接,而 UDP 沒有相應建立連接的過程。
可靠性。TCP 使用了非常多機制的保證連接的可靠,這個可靠性體現(xiàn)在哪些方面呢?一個是有狀態(tài),另一個是可控制。
面向字節(jié)流。UDP 的數據傳輸是基于數據報的,這是因為僅僅只是繼承了 IP 層的特性,而 TCP 為了維護狀態(tài),將一個個 IP 包變成了字節(jié)流。
TCP 會精準記錄哪些數據發(fā)送了,哪些數據被對方接收了,哪些沒有被接收到,而且保證數據包按序到達,不允許半點差錯。這是有狀態(tài)。
當意識到丟包了或者網絡環(huán)境不佳,TCP 會根據具體情況調整自己的行為,控制自己的發(fā)送速度或者重發(fā)。這是可控制。
相應的,UDP 就是無狀態(tài), 不可控的。
三次握手
在介紹三次握手時我們先來看看TCP的報文頭格式:

端口號:用來標識同一臺計算機的不同的應用進程。
源端口:源端口和IP地址的作用是標識報文的返回地址。
目的端口:端口指明接收方計算機上的應用程序接口。(TCP報頭中的源端口號和目的端口號同IP數據報中的源IP與目的IP唯一確定一條TCP連接。)
序號和確認號:是TCP可靠傳輸的關鍵部分。序號是本報文段發(fā)送的數據組的第一個字節(jié)的序號。在TCP傳送的流中,每一個字節(jié)一個序號。e.g.一個報文段的序號為300,此報文段數據部分共有100字節(jié),則下一個報文段的序號為400。所以序號確保了TCP傳輸的有序性。確認號,即ACK,指明下一個期待收到的字節(jié)序號,表明該序號之前的所有數據已經正確無誤的收到。確認號只有當ACK標志為1時才有效。比如建立連接時,SYN報文的ACK標志位為0。
數據偏移/首部長度:4bits。由于首部可能含有可選項內容,因此TCP報頭的長度是不確定的,報頭不包含任何可選字段則長度為20字節(jié),4位首部長度字段所能表示的最大值為1111,轉化為10進制為15,15*32/8 = 60,故報頭最大長度為60字節(jié)。首部長度也叫數據偏移,是因為首部長度實際上指示了數據區(qū)在報文段中的起始偏移值。
保留:為將來定義新的用途保留,現(xiàn)在一般置0。
-
控制位:URG ACK PSH RST SYN FIN,共6個,每一個標志位表示一個控制功能。
URG:緊急指針標志,為1時表示緊急指針有效,為0則忽略緊急指針。
ACK:確認序號標志,為1時表示確認號有效,為0表示報文中不含確認信息,忽略確認號字段。
PSH:push標志,為1表示是帶有push標志的數據,指示接收方在接收到該報文段以后,應盡快將這個報文段交給應用程序,而不是在緩沖區(qū)排隊。
RST:重置連接標志,用于重置由于主機崩潰或其他原因而出現(xiàn)錯誤的連接?;蛘哂糜诰芙^非法的報文段和拒絕連接請求。
SYN:同步序號,用于建立連接過程,在連接請求中,SYN=1和ACK=0表示該數據段沒有使用捎帶的確認域,而連接應答捎帶一個確認,即SYN=1和ACK=1。
FIN:finish標志,用于釋放連接,為1時表示發(fā)送方已經沒有數據發(fā)送了,即關閉本方數據流。
窗口:滑動窗口大小,用來告知發(fā)送端接受端的緩存大小,以此控制發(fā)送端發(fā)送數據的速率,從而達到流量控制。窗口大小16bit,因此窗口大小最大為65535。
校驗和:奇偶校驗,此校驗和是對整個的 TCP 報文段,包括 TCP 頭部和 TCP 數據,以 16 位字進行計算所得。由發(fā)送端計算和存儲,并由接收端進行驗證。
緊急指針:只有當 URG 標志置 1 時緊急指針才有效。緊急指針是一個正的偏移量,和順序號字段中的值相加表示緊急數據最后一個字節(jié)的序號。 TCP 的緊急方式是發(fā)送端向另一端發(fā)送緊急數據的一種方式。
選項和填充:最常見的可選字段是最長報文大小,又稱為MSS(Maximum Segment Size),每個連接方通常都在通信的第一個報文段(為建立連接而設置SYN標志為1的那個段)中指明這個選項,它表示本端所能接受的最大報文段的長度。選項長度不一定是32位的整數倍,所以要加填充位,即在這個字段中加入額外的零,以保證TCP頭是32的整數倍。
數據部分: TCP 報文段中的數據部分是可選的。在一個連接建立和一個連接終止時,雙方交換的報文段僅有 TCP 首部。如果一方沒有數據要發(fā)送,也使用沒有任何數據的首部來確認收到的數據。在處理超時的許多情況中,也會發(fā)送不帶任何數據的報文段。
報頭格式介紹完了,接下來我們看看TCP的三次握手過程:

過程描述:
客戶端發(fā)起連接請求,報頭中的SYN=1,ACK=0,TCP規(guī)定SYN=1時不能攜帶數據,但要消耗一個序號,因此聲明自己的序號是 seq=x。此時客戶端狀態(tài)CLOSED->SYN-SENT;
服務器接收到連接請求,然后進行回復確認,發(fā)送SYN=1 ACK=1 seq=y ack=x+1。此時服務端狀態(tài)LISTEN->SYN-RCVD
客戶端收到對SYN的確認包之后再次確認,SYN=0 ACK=1 seq=x+1 ack=y+1,此時客戶端狀態(tài)SYN-SENT->ESTABLISHED
-
服務端接收到客戶端的確認包后狀態(tài)變?yōu)?strong>ESTABLISHED
為什么要進行三次握手?
保證通信是“全雙工”的,即客戶端和服務器都具備發(fā)送和接收能力,假如沒有客戶端的第二次確認,那么服務端無法確??蛻舳艘呀浗邮盏阶约喊l(fā)出去的SYN確認包。而且假如只有兩次握手,那么會存在資源浪費的情況:
客戶端發(fā)出SYN請求后,由于網絡復雜情況,這個請求一直沒有發(fā)送到服務器端,這時候客戶端超時重發(fā)第二個請求,然后第二個請求服務端正常接收并建立連接,數據傳輸完畢后雙方斷開了連接,但這時候第一次發(fā)送的那個SYN包終于到達服務端了,由于是二次握手,所以服務端發(fā)送確認包,并建立連接,但是客戶端實際上已經斷開連接了,而服務端建立了一條“沒有”客戶端的連接,造成了資源浪費。
重傳機制
三次握手結束之后就可以開始收發(fā)數據了,那要怎么保證我們發(fā)送出去的消息確實被接收到了呢?以寄快遞為例,雖然我們寄快遞前確認收件人信息無誤,但是快遞寄出去的時候,如果我們沒有收到反饋(比如收件人告知你快遞我已經收到了或者是快遞公司的收件通知)。那么我們是無法知道快遞是否準確送達的。
在TCP中,是通過序列號與確認應答來保證的(回想下上面的報頭格式)。正常的傳輸過程如下:

但是實際情況是非常復雜的,假如網絡出現(xiàn)丟包的情況要怎么辦呢?這就涉及到TCP的重傳機制:
- 超時重傳
- 快速重傳
- SACK
- D-SACK
超時重傳
在發(fā)送數據時,設定一個定時器,當超過指定的時間后,沒有收到對方的 ACK 確認應答報文,就會重發(fā)該數據。
在數據包丟失或者確認應答丟失時會發(fā)生超時重傳:

問題來了,這個“特定的時間間隔”應該設置成多少合適呢?這就引入了RTT(Round-Trip Time 往返時延)的概念:

RTT簡單來說就是數據一次往返的時間,超時重傳時間是以 RTO (Retransmission Timeout 超時重傳時間)表示。這個重傳時間的設置比較玄學,設置高了或者低了都會有問題:

其實不用看圖片大家應該也能猜到,超時時間過大,那么就會導致效率的降低(大部分時間都用來等待)。超時時間過低的話那就有可能造成不必要的重傳(重傳剛發(fā)出去結果就收到了應答)。
所以RTO的計算方法比較復雜,因為網絡狀態(tài)是時時刻刻變化的,而且也會存在波動較大的情況,所以RTO的值也是動態(tài)變化的,這里面的算法就不贅述了~~~有興趣的小伙伴可以網上查找相關資料。
快速重傳
我們一直強調網絡情況是非常復雜的,所以上面提到的超時重傳無法解決所有問題,設想這么一種場景:數據包只是因為某一個網絡節(jié)點的異常而丟失了,實際上網路是“暢通”的,假如還是使用超時重傳那么效率顯然太低了,因此,快速重傳機制誕生了。

- 發(fā)送端發(fā)送了5份數據;
- 接收方接收到seq1時返回ACK2,但是seq2由于網絡異常丟包沒有收到;
- 這時候接收端又接收到seq3,在應答的時候依然返回ACK2;
- 后續(xù)seq4和seq5也收到了,但是seq2還沒有收到,依然返回ACK2。
- 發(fā)送端連續(xù)收到三次同樣的ACK2,知道seq2沒有被接收到,就會在定時器任務觸發(fā)之前重傳seq2。
- 接收端接收到seq2,而且檢測到seq3、4、5都已經收到。所以返回ACK6
所以,快速重傳的工作方式是當收到三個相同的 ACK 報文時,會在定時器過期之前,重傳丟失的報文段。
但是快速重傳也有問題,發(fā)送端只知道seq2丟失了,但是不知道seq3、4、5有沒有被接收到。所以發(fā)送端并不清楚要不要把后續(xù)的seq3、4、5一并重傳。于是便引入了SACK
SACK( Selective Acknowledgment 選擇性確認)
這種方式需要在 TCP 頭部「選項」字段里加一個 SACK,他可以記錄已接收的數據,隨著報頭一起發(fā)送給發(fā)送方,這樣發(fā)送方就清楚什么數據被接收了,什么數據沒有接收,這樣就可以只重傳丟失的數據。

發(fā)送方收到了三次同樣的 ACK 確認報文,于是就會觸發(fā)快速重發(fā)機制,通過SACK 信息發(fā)現(xiàn)只有 200~299 這段數據丟失,重發(fā)時只需要重傳這段數據即可。
如果要支持 SACK,必須雙方都要支持。在 Linux 下,可以通過 net.ipv4.tcp_sack參數打開這個功能(Linux 2.4 后默認打開)。
Duplicate SACK
Duplicate SACK 又稱 D-SACK,其主要使用了 SACK 來告訴「發(fā)送方」有哪些數據被重復接收了。 主要解決下面兩種情況:

ACK丟包。

網絡延時。
在 Linux 下可以通過 net.ipv4.tcp_dsack 參數開啟/關閉這個功能(Linux 2.4 后默認打開)。
滑動窗口
細心的小伙伴可能發(fā)現(xiàn)了,上面的圖片里面并不是等到上一次數據的確認收到之后才發(fā)送下一批數據,因為串行發(fā)送數據效率太低,所以TCP引入了滑動窗口的概念。
有了窗口,就可以指定窗口大小,窗口大小就是指無需等待確認應答,而可以繼續(xù)發(fā)送數據的最大值。
窗口的實現(xiàn)實際上是操作系統(tǒng)開辟的一個緩存空間,發(fā)送方主機在等到確認應答返回之前,必須在緩沖區(qū)中保留已發(fā)送的數據。如果按期收到確認應答,此時數據就可以從緩存區(qū)清除。
假設窗口大小為 3 個 TCP 段,那么發(fā)送方就可以「連續(xù)發(fā)送」 3 個 TCP 段,并且中途若有 ACK 丟失,可以通過「下一個確認應答進行確認」。如下圖:

圖中的 ACK 600 確認應答報文丟失,也沒關系,因為可以通過下一個確認應答進行確認,只要發(fā)送方收到了 ACK 700 確認應答,就意味著 700 之前的所有數據「接收方」都收到了。這個模式就叫累計確認或者累計應答。
那么窗口大小應該怎么設置呢?
回想一下TCP報頭格式,是不是有一個“窗口”字段?這個“窗口”字段就是用來表示窗口大小的。這個字段是接收端告訴發(fā)送端自己還有多少緩沖區(qū)可以接收數據。于是發(fā)送端就可以根據接收端的處理能力來發(fā)送數據,而不會導致接收端處理不過來。
所以,通常窗口的大小是由接收方的決定的。
發(fā)送方發(fā)送的數據大小不能超過接收方的窗口大小,否則接收方就無法正常接收到數據。
下圖就是發(fā)送方緩存的數據,根據處理的情況分成四個部分,其中深藍色方框是發(fā)送窗口,紫色方框是可用窗口:

在下圖,當發(fā)送方把數據「全部」都一下發(fā)送出去后,可用窗口的大小就為 0 了,表明可用窗口耗盡,在沒收到 ACK 確認之前是無法繼續(xù)發(fā)送數據了。

在下圖,當收到之前發(fā)送的數據 32~36 字節(jié)的 ACK 確認應答后,如果發(fā)送窗口的大小沒有變化,則滑動窗口往右邊移動 5 個字節(jié),因為有 5 個字節(jié)的數據被應答確認,接下來 52~56 字節(jié)又變成了可用窗口,那么后續(xù)也就可以發(fā)送 52~56 這 5 個字節(jié)的數據了。

TCP 滑動窗口方案使用三個指針來跟蹤在四個傳輸類別中的每一個類別中的字節(jié)。其中兩個指針是絕對指針(指特定的序列號),一個是相對指針(需要做偏移)。

SND.WND:表示發(fā)送窗口的大小(大小是由接收方指定的);
SND.UNA:是一個絕對指針,它指向的是已發(fā)送但未收到確認的第一個字節(jié)的序列號,也就是 #2 的第一個字節(jié)。
SND.NXT:也是一個絕對指針,它指向未發(fā)送但可發(fā)送范圍的第一個字節(jié)的序列號,也就是 #3 的第一個字節(jié)。
指向 #4 的第一個字節(jié)是個相對指針,它需要 SND.NXT 指針加上 SND.WND大小的偏移量,就可以指向 #4 的第一個字節(jié)了。
可用窗口大小 = SND.WND -(SND.NXT - SND.UNA)
接下來是接收端的窗口

- RCV.WND:表示接收窗口的大小,它會通告給發(fā)送方。
- RCV.NXT:是一個指針,它指向期望從發(fā)送方發(fā)送來的下一個數據字節(jié)的序列號,也就是 #3 的第一個字節(jié)。
- 指向 #4 的第一個字節(jié)是個相對指針,它需要 RCV.NXT 指針加上 RCV.WND大小的偏移量,就可以指向 #4 的第一個字節(jié)了。
接收窗口≈發(fā)送窗口
因為滑動窗口是一直在變化的,假如接收方的應用讀取數據的速度很快,那么接收窗口就很快就可以騰出空間,但是在把新的接收窗口大小發(fā)給發(fā)送端的時候是存在時延的,所以發(fā)送窗口約等于接收窗口。
流量控制
從上面我們可以知道接收端是有接收窗口的,接收方的應用在處理數據時也需要一定的時間,假如發(fā)送方不加以控制,那么接收方將無法處理過多的數據,導致出發(fā)重傳機制,從而浪費網絡流量。
為了解決這種現(xiàn)象發(fā)生,TCP 提供一種機制可以讓「發(fā)送方」根據「接收方」的實際接收能力控制發(fā)送的數據量,這就是所謂的流量控制。
我們先看看假設接收窗口和發(fā)送窗口保持200不變的發(fā)送過程:

- 客戶端發(fā)送請求數據報文。
- 服務端接收到請求,發(fā)送確認以及80字節(jié)數據,這時發(fā)送窗口的SND.UNA = 241, SND.NXT = SND.WND + 80 = 321。可用發(fā)送窗口大小 = 200 - ( 321 - 241) = 120。
- 客戶端接收到數據,這時接收窗口RCV.NXT = RCV.NXT + 80 = 321,發(fā)送確認報文。
- 發(fā)送端繼續(xù)發(fā)送120個字節(jié),由于這時還沒收到客戶端的確認數據包,發(fā)送窗口SND.UNA = 241, SND.NXT = 321 + 120 = 441,可用發(fā)送窗口大小 = 200 - (441 - 241) = 0,可用窗口大小為0,這時無法再發(fā)送數據。
- 客戶端接收到120字節(jié)數據,RCV.NXT = 321 + 120 = 441,發(fā)送確認報文。
- 服務端收到客戶端的第一個確認報文,窗口往右”滑動“80字節(jié),SND.UNA = 241 + 80 = 321, 可用發(fā)送窗口大小 = 200 - (441 - 321) = 80。
- 服務端收到客戶端的第二個確認報文,窗口往右”滑動“120字節(jié),SND.UNA = 321 + 120 = 441, 可用發(fā)送窗口大小 = 200 - (441 - 441) = 200。
- 服務端繼續(xù)發(fā)送160個字節(jié)數據,SND.NXT = 441 + 160 = 601,可用發(fā)送窗口大小 = 200 - (601 - 441) = 40。
- 客戶端接收到數據,RCV.NXT = 441 + 160 = 601,發(fā)送確認報文。
- 服務端收到確認報文,窗口右移160個字節(jié),SND.UNA = 441 + 160 = 601,可用發(fā)送窗口大小 = 200 - (601 - 601) = 200。
上面的例子我們假定了發(fā)送窗口和接收窗口是不變的,但是實際上,發(fā)送窗口和接收窗口中所存放的字節(jié)數,都是放在操作系統(tǒng)內存緩沖區(qū)中的,而操作系統(tǒng)的緩沖區(qū),會被操作系統(tǒng)調整。當應用進程沒辦法及時讀取緩沖區(qū)的內容時,也會對我們的緩沖區(qū)造成影響。
我們來看下面這個例子:

- 客戶端發(fā)送 140 字節(jié)數據后,可用窗口變?yōu)?220 (360 - 140)。
- 服務端收到 140 字節(jié)數據,但是服務端非常繁忙,應用進程只讀取了 40 個字節(jié),還有 100 字節(jié)占用著緩沖區(qū),于是接收窗口收縮到了 260 (360 - 100),最后發(fā)送確認信息時,將窗口大小通過給客戶端。
- 客戶端收到確認和窗口通告報文后,發(fā)送窗口減少為 260。
- 客戶端發(fā)送 180 字節(jié)數據,此時可用窗口減少到 80。
- 服務端收到 180 字節(jié)數據,但是應用程序沒有讀取任何數據,這 180 字節(jié)直接就留在了緩沖區(qū),于是接收窗口收縮到了 80 (260 - 180),并在發(fā)送確認信息時,通過窗口大小給客戶端。
- 客戶端收到確認和窗口通告報文后,發(fā)送窗口減少為 80。
- 客戶端發(fā)送 80 字節(jié)數據后,可用窗口耗盡。
- 服務端收到 80 字節(jié)數據,但是應用程序依然沒有讀取任何數據,這 80 字節(jié)留在了緩沖區(qū),于是接收窗口收縮到了 0,并在發(fā)送確認信息時,通過窗口大小給客戶端。
- 客戶端收到確認和窗口通告報文后,發(fā)送窗口減少為 0。
可見最后窗口都收縮為 0 了,也就是發(fā)生了窗口關閉。我們再來看下面的例子:
當服務端系統(tǒng)資源非常緊張的時候,操心系統(tǒng)可能會直接減少了接收緩沖區(qū)大小,這時應用程序又無法及時讀取緩存數據,會出現(xiàn)數據包丟失的現(xiàn)象。

- 客戶端發(fā)送 140 字節(jié)的數據,于是可用窗口減少到了 220。
- 服務端因為現(xiàn)在非常的繁忙,操作系統(tǒng)于是就把接收緩存減少了 100 字節(jié),當收到 對 140 數據確認報文后,又因為應用程序沒有讀取任何數據,所以 140 字節(jié)留在了緩沖區(qū)中,于是接收窗口大小從 360 收縮成了 100,最后發(fā)送確認信息時,通告窗口大小給對方。
- 此時客戶端因為還沒有收到服務端的通告窗口報文,所以不知道此時接收窗口收縮成了 100,客戶端只會看自己的可用窗口還有 220,所以客戶端就發(fā)送了 180 字節(jié)數據,于是可用窗口減少到 40。
- 服務端收到了 180 字節(jié)數據時,發(fā)現(xiàn)數據大小超過了接收窗口的大小,于是就把數據包丟棄了。
- 客戶端收到服務端發(fā)送的確認報文和通告窗口報文,嘗試減少發(fā)送窗口到 100,把窗口的右端向左收縮了 80,此時可用窗口的大小就會出現(xiàn)詭異的負值。
所以,如果發(fā)生了先減少緩存,再收縮窗口,就會出現(xiàn)丟包的現(xiàn)象。
為了防止這種情況發(fā)生,TCP 規(guī)定是不允許同時減少緩存又收縮窗口的,而是采用先收縮窗口,過段時間再減少緩存,這樣就可以避免了丟包情況。
窗口關閉
如果窗口大小為 0 時,就會阻止發(fā)送方給接收方傳遞數據,直到窗口變?yōu)榉?0 為止,這就是窗口關閉。
窗口大小是通過ACK報文發(fā)送的,當發(fā)生窗口關閉時,接收方處理完數據后,會向發(fā)送方通告一個窗口非 0 的 ACK 報文,如果這個通告窗口的 ACK 報文在網絡中丟失了,那么就會產生“死鎖”:

發(fā)送方一直等待接收方的非 0 窗口通知,接收方也一直等待發(fā)送方的數據,如不不采取措施,這種相互等待的過程,會造成了死鎖的現(xiàn)象。
為了解決這個問題,TCP 為每個連接設有一個持續(xù)定時器,只要 TCP 連接一方收到對方的零窗口通知,就啟動持續(xù)計時器。
如果持續(xù)計時器超時,就會發(fā)送窗口探測 ( Window probe ) 報文,而對方在確認這個探測報文時,給出自己現(xiàn)在的接收窗口大小。窗口探測的次數一般為 3 次,每次大約 30-60 秒(不同的系統(tǒng)版本實現(xiàn)可能會不一樣)。如果探測 3 次過后接收窗口還是 0 的話,有的 TCP 實現(xiàn)就會發(fā) RST 報文來中斷連接。

糊涂窗口綜合征
如果接收方太忙了,來不及取走接收窗口里的數據,那么就會導致發(fā)送方的發(fā)送窗口越來越小。
到最后,如果接收方騰出幾個字節(jié)并告訴發(fā)送方現(xiàn)在有幾個字節(jié)的窗口,而發(fā)送方會義無反顧地發(fā)送這幾個字節(jié),這就是糊涂窗口綜合癥。
我們的 TCP + IP 頭至少有 40 個字節(jié),假如接收窗口只有幾字節(jié),那么就會出現(xiàn)大馬拉小車的情況,造成資源浪費。

參考上圖我們不難想象會有兩種場景導致“大馬拉小車”:
- 接收端發(fā)送了一個接收窗口很小的ACK包;
- 發(fā)送端任性的發(fā)送小數據包。
如何避免發(fā)送小窗口ACK包
當「窗口大小」小于 min( MSS,緩存空間/2 ) ,也就是小于 MSS 與 1/2 緩存大小中的最小值時,就會向發(fā)送方通告窗口為 0,也就阻止了發(fā)送方再發(fā)數據過來。
等到接收方處理了一些數據后,窗口大小 >= MSS,或者接收方緩存空間有一半可以使用,就可以把窗口打開讓發(fā)送方發(fā)送數據過來。
如何防止發(fā)送端發(fā)送小數據包
使用 Nagle 算法,該算法的思路是延時處理,它滿足以下兩個條件中的一條才可以發(fā)送數據
- 要等到窗口大小 >= MSS 或是 數據大小 >= MSS
- 收到之前發(fā)送數據的 ack 回包
只要沒滿足上面條件中的一條,發(fā)送方就一直囤積數據,直到滿足上面的發(fā)送條件。
另外,Nagle 算法默認是打開的,如果對于一些需要小數據包交互的場景的程序,比如,telnet 或 ssh 這樣的交互性比較強的程序,則需要關閉 Nagle 算法。
可以在 Socket 設置 TCP_NODELAY 選項來關閉這個算法(關閉 Nagle 算法沒有全局參數,需要根據每個應用自己的特點來關閉)
setsockopt(sock_fd, IPPROTO_TCP, TCP_NODELAY, (char *)&value, sizeof(int));
擁塞控制
可能有的小伙伴會有疑問,前面既然有了流量控制,為什么還需要一個擁塞控制呢?其實細想我們不難發(fā)現(xiàn),流量控制只是避免了發(fā)送方向接收方發(fā)送過多的數據,防止接收方無法及時響應。他們并不能感知到當前的網絡狀態(tài)是否擁堵。這就好比平時節(jié)假日出游時,我們總能遇到堵車的情況一樣,因為在出門的時候我們沒有查詢當前的交通情況,剛好大部分人都選擇在同一天出行,那顯然就容易出現(xiàn)交通擁堵的情況了。
要解決這個問題也不難,只要大家在出門前先通過APP查詢當前交通狀況,如果發(fā)現(xiàn)交通已經比較擁擠了,那就選擇改天再出門,選擇錯峰出行,那交通出現(xiàn)擁堵的概率就會降低。
TCP其實也是通過類似的方式去解決網絡擁塞的,當網絡發(fā)送擁塞時,TCP 會選擇降低發(fā)送的數據量。
擁塞窗口
在介紹擁塞控制算法前先介紹一個概念,擁塞窗口(CWND)。它是發(fā)送方維護的一個 的狀態(tài)變量,它會根據網絡的擁塞程度動態(tài)變化,前面提到過發(fā)送窗口 swnd 和接收窗口 rwnd 是約等于的關系,那么由于入了擁塞窗口的概念后,此時發(fā)送窗口的值是swnd = min(cwnd, rwnd),也就是擁塞窗口和接收窗口中的最小值。
- 只要網絡中沒有出現(xiàn)擁塞,cwnd 就會增大
- 網絡中出現(xiàn)了擁塞,cwnd 就減小
接下來我們先解決第一個問題,TCP怎么知道當前的網絡是否擁塞呢?
很簡單,只要發(fā)送端沒有在規(guī)定的時間內收到ACK應答報文(觸發(fā)了超時重傳),那么就認為網絡出現(xiàn)了擁塞。
判斷網絡出現(xiàn)擁塞后,TCP就會執(zhí)行下面四個算法:
- 慢啟動
- 擁塞避免
- 擁塞發(fā)生
- 快速恢復
慢啟動
TCP在連接建立之時,會通過慢啟動的方式逐漸增加發(fā)送數據量。慢啟動算法核心是:當發(fā)送方每收到一個 ACK,就擁塞窗口 cwnd 的大小就會加 1。假定擁塞窗口 cwnd 和發(fā)送窗口 swnd 相等。
- 連接建立完成后,一開始初始化 cwnd = 1,表示可以傳一個 MSS(MSS就是連接請求時在報頭設置的字段)大小的數據。
- 當收到一個 ACK 確認應答后,cwnd 增加 1,于是一次能夠發(fā)送 2 個
- 當收到 2 個的 ACK 確認應答后, cwnd 增加 2,于是就可以比之前多發(fā)2 個,所以這一次能夠發(fā)送 4 個
- 當這 4 個的 ACK 確認到來的時候,每個確認 cwnd 增加 1, 4 個確認 cwnd 增加 4,于是就可以比之前多發(fā) 4 個,所以這一次能夠發(fā)送 8 個。

慢啟動窗口是呈指數增長的,如果任由它一直增長下去毫無疑問網絡會出現(xiàn)擁堵,所以TCP設置了一個閾值,它就是慢啟動門限 ssthresh (slow start threshold)狀態(tài)變量。
- 當 cwnd < ssthresh 時,使用慢啟動算法。
- 當 cwnd >= ssthresh 時,就會使用「擁塞避免算法」。
擁塞避免算法
當擁塞窗口 cwnd 「超過」慢啟動門限 ssthresh 就會進入擁塞避免算法。一般來說 ssthresh 的大小是 65535 字節(jié)。進入擁塞避免算法后,它的規(guī)則是:每當收到一個 ACK 時,cwnd 增加 1/cwnd。
接上前面的慢啟動的例子,假定 ssthresh 為 8:
當 8 個 ACK 應答確認到來時,每個確認增加 1/8,8 個 ACK 確認 cwnd 一共增加 1,于是這一次能夠發(fā)送 9 個 MSS 大小的數據,變成了線性增長。

擁塞避免算法就是將原本慢啟動算法的指數增長變成了線性增長。
隨著發(fā)送量增大,網絡慢慢就會出現(xiàn)擁塞,這時候就會出現(xiàn)丟包,進而觸發(fā)重傳。當觸發(fā)重傳機制,那么就會啟動擁塞發(fā)生算法。
擁塞發(fā)生
需要注意的是,上面我們介紹過兩種重傳機制:
- 超時重傳
- 快速重傳
這兩種重傳機制對應兩種不同的異常:超時重傳是網絡出現(xiàn)擁堵,無法快速處理所有數據包,所以導致數據包“堵”在網絡當中;而快速重傳是由于網絡異常從而導致某一個數據包“失蹤”了,而網絡實際是暢通的。
因此針對這兩種異常,TCP同樣選擇兩種算法處理:
擁塞發(fā)生<=>超時重傳
- ssthresh 設為 cwnd/2,
- cwnd 重置為 1

從圖中不難看出,這種算法呈現(xiàn)的是“斷崖式下跌”(還好不是股市)。如果只采用這種擁塞發(fā)生算法的話,那么網絡的波動就會非常大,體現(xiàn)在打游戲時突然延遲很高~~
接下來就介紹針對快速重傳的擁塞發(fā)生算法——快速恢復,因為發(fā)送方還能連續(xù)收到3個ACK,說明網絡情況還比較樂觀。ssthresh 和cwnd 變化如下:
- cwnd = cwnd/2 ,也就是設置為原來的一半;
- ssthresh = cwnd;
快速恢復
- 擁塞窗口 cwnd = ssthresh + 3 ( 3 的意思是確認有 3 個數據包被收到了)
- 重傳丟失的數據包
- 如果再收到重復的 ACK,那么 cwnd 增加 1
- 如果收到新數據的 ACK 后,設置 cwnd 為 ssthresh,接著就進入了擁塞避免算法

四次揮手說再見

MSL是Maximum Segment Lifetime的英文縮寫,可譯為“最長報文段壽命”,它是任何報文在網絡上存在的最長的時間,超過這個時間報文將被丟棄。我們都知道IP頭部中有個TTL字段,TTL是time to live的縮寫,可譯為“生存時間”,這個生存時間是由源主機設置初始值但不是存在的具體時間,而是一個IP數據報可以經過的最大路由數,每經過一個路由器,它的值就減1,當此值為0則數據報被丟棄,同時發(fā)送ICMP報文通知源主機。RFC793中規(guī)定MSL為2分鐘,但這完全是從工程上來考慮,對于現(xiàn)在的網絡,MSL=2分鐘可能太長了一些。因此TCP允許不同的實現(xiàn)可根據具體情況使用更小的MSL值。TTL與MSL是有關系的但不是簡單的相等關系,MSL要大于TTL。
在TIME_WAIT狀態(tài)時兩端的端口不能使用,要等到2MSL時間結束才可繼續(xù)使用。當連接處于2MSL等待階段時任何遲到的報文段都將被丟棄。不過在實際應用中可以通過設置SO_REUSEADDR選項達到不必等待2MSL時間結束再使用此端口。
當客戶端沒有東西要發(fā)送時就要釋放連接,客戶端會發(fā)送一個報文(沒有數據),其中 FIN 設置為1, 服務端收到后會恢復確認,這時客戶端的連接已經關閉,即客戶端不再發(fā)送信息(但仍可接收信息)。 客戶端收到確認后進入等待狀態(tài),等待服務端請求釋放連接, 服務端數據發(fā)送完畢后就向客戶端請求連接釋放,同樣是用FIN=1 表示, 并且用 ack = u+1(如圖), 客戶端收到后同樣回復一個確認,并進入 TIME_WAIT 狀態(tài), 等待 2MSL 時間。
為什么需要等待?
因為服務端接收到釋放連接的請求后會發(fā)送確認信息,這個確認信息是有可能丟失的,所以客戶端就需要等待,如果超時還沒收到確認,那么就會再次發(fā)送釋放連接的請求。
為什么客戶端在FIN-WAIT-2階段發(fā)送ACK后還需要等待2MSL
1.為了保證客戶端(我們記為A端)發(fā)送的最后一個ACK報文段能夠到達服務器端。這個ACK報文段有可能丟失,因而使處在LASK—ACK端的服務器端(我們記為B端)收不到對已發(fā)送的FIN+ACK報文段。B會超時重傳這個FIN+ACK報文段,而A就能在2MSL時間內收到這個重傳的FIN+ACK報文段。接著A重傳一次確認,重新啟動2MSL計時器。最后,A和B都正常進入到CLOSED狀態(tài)。如果A在TIME_WAIT狀態(tài)不等待一段時間,而是在發(fā)送完ACK確認后立即釋放連接,那么就無法收到B重傳的FIN+ACK報文段,因而也不會再發(fā)送一次確認報文段,這樣,B就無法正常進入CLOSED狀態(tài)。
2.我們都知道,假如A發(fā)送的第一個請求連接報文段丟失而未收到確認,A就會重傳一次連接請求,后來B收到了確認,建立了連接。數據傳輸完畢后,就釋放了連接。A共發(fā)送了兩個連接請求報文段,其中第一個丟失,第二個到達了B。假如現(xiàn)在A發(fā)送的第一個連接請求報文段沒有丟失,而是在某些網絡節(jié)點長時間都留了,以至于延誤到連接釋放后的某個時間才到達B,這本來是已失效的報文段,但B并不知道,就會又建立一次連接。而等待的這2MSL就是為了解決這個問題的,A在發(fā)送完最后一個確認報后,在經過時間2MSL,就可以使本鏈接持續(xù)時間內所產生的所有報文段都從網絡中消失,這樣就可以使下一個新的連接中不會出現(xiàn)這種舊的連接請求報文段。