這次,終于學(xué)會了 TCP
Hey guys ,我是 cxuan ,歡迎你閱讀我這一期的文章。
這是一篇詳細(xì)介紹 TCP 各種特點(diǎn)的文章,內(nèi)容主要包括 TCP 三次握手和四次揮手細(xì)節(jié)問題、TCP 狀態(tài)之間的轉(zhuǎn)換、TCP 超時(shí)和重傳、關(guān)于 TCP 包失序和重復(fù)問題、TCP 的數(shù)據(jù)流與窗口管理、TCP 的擁塞控制,思維導(dǎo)圖如下。

TCP 是一種面向連接的單播協(xié)議,在 TCP 中,并不存在多播、廣播的這種行為,因?yàn)?TCP 報(bào)文段中能明確發(fā)送方和接受方的 IP 地址。
在發(fā)送數(shù)據(jù)前,相互通信的雙方(即發(fā)送方和接受方)需要建立一條連接,在發(fā)送數(shù)據(jù)后,通信雙方需要斷開連接,這就是 TCP 連接的建立和終止。
TCP 連接的建立和終止
如果你看過我之前寫的關(guān)于網(wǎng)絡(luò)層的一篇文章,你應(yīng)該知道 TCP 的基本元素有四個(gè):即發(fā)送方的 IP 地址、發(fā)送方的端口號、接收方的 IP 地址、接收方的端口號。而每一方的 IP + 端口號都可以看作是一個(gè)套接字,套接字能夠被唯一標(biāo)示。套接字就相當(dāng)于是門,出了這個(gè)門,就要進(jìn)行數(shù)據(jù)傳輸了。
TCP 的連接建 立 -> 終止總共分為三個(gè)階段

下面我們所討論的重點(diǎn)也是集中在這三個(gè)層面。
下圖是一個(gè)非常典型的 TCP 連接的建立和關(guān)閉過程,其中不包括數(shù)據(jù)傳輸?shù)牟糠帧?/p>
TCP 建立連接 - 三次握手

服務(wù)端進(jìn)程準(zhǔn)備好接收來自外部的 TCP 連接,一般情況下是調(diào)用 bind、listen、socket 三個(gè)函數(shù)完成。這種打開方式被認(rèn)為是
被動(dòng)打開(passive open)。然后服務(wù)端進(jìn)程處于LISTEN狀態(tài),等待客戶端連接請求。客戶端通過
connect發(fā)起主動(dòng)打開(active open),向服務(wù)器發(fā)出連接請求,請求中首部同步位 SYN = 1,同時(shí)選擇一個(gè)初始序號 sequence ,簡寫 seq = x。SYN 報(bào)文段不允許攜帶數(shù)據(jù),只消耗一個(gè)序號。此時(shí),客戶端進(jìn)入SYN-SEND狀態(tài)。服務(wù)器收到客戶端連接后,,需要確認(rèn)客戶端的報(bào)文段。在確認(rèn)報(bào)文段中,把 SYN 和 ACK 位都置為 1 。確認(rèn)號是 ack = x + 1,同時(shí)也為自己選擇一個(gè)初始序號 seq = y。這個(gè)報(bào)文段也不能攜帶數(shù)據(jù),但同樣要消耗掉一個(gè)序號。此時(shí),TCP 服務(wù)器進(jìn)入
SYN-RECEIVED(同步收到)狀態(tài)。客戶端在收到服務(wù)器發(fā)出的響應(yīng)后,還需要給出確認(rèn)連接。確認(rèn)連接中的 ACK 置為 1 ,序號為 seq = x + 1,確認(rèn)號為 ack = y + 1。TCP 規(guī)定,這個(gè)報(bào)文段可以攜帶數(shù)據(jù)也可以不攜帶數(shù)據(jù),如果不攜帶數(shù)據(jù),那么下一個(gè)數(shù)據(jù)報(bào)文段的序號仍是 seq = x + 1。這時(shí),客戶端進(jìn)入
ESTABLISHED (已連接)狀態(tài)服務(wù)器收到客戶的確認(rèn)后,也進(jìn)入
ESTABLISHED狀態(tài)。
這是一個(gè)典型的三次握手過程,通過上面 3 個(gè)報(bào)文段就能夠完成一個(gè) TCP 連接的建立。三次握手的的目的不僅僅在于讓通信雙方知曉正在建立一個(gè)連接,也在于利用數(shù)據(jù)包中的選項(xiàng)字段來交換一些特殊信息,交換初始序列號。
一般首個(gè)發(fā)送 SYN 報(bào)文的一方被認(rèn)為是主動(dòng)打開一個(gè)連接,而這一方通常也被稱為
客戶端。而 SYN 的接收方通常被稱為服務(wù)端,它用于接收這個(gè) SYN,并發(fā)送下面的 SYN,因此這種打開方式是被動(dòng)打開。
TCP 建立一個(gè)連接需要三個(gè)報(bào)文段,釋放一個(gè)連接卻需要四個(gè)報(bào)文段。
TCP 斷開連接 - 四次揮手
數(shù)據(jù)傳輸結(jié)束后,通信的雙方可以釋放連接。數(shù)據(jù)傳輸結(jié)束后的客戶端主機(jī)和服務(wù)端主機(jī)都處于 ESTABLISHED 狀態(tài),然后進(jìn)入釋放連接的過程。

TCP 斷開連接需要?dú)v經(jīng)的過程如下
客戶端應(yīng)用程序發(fā)出釋放連接的報(bào)文段,并停止發(fā)送數(shù)據(jù),主動(dòng)關(guān)閉 TCP 連接??蛻舳酥鳈C(jī)發(fā)送釋放連接的報(bào)文段,報(bào)文段中首部 FIN 位置為 1 ,不包含數(shù)據(jù),序列號位 seq = u,此時(shí)客戶端主機(jī)進(jìn)入
FIN-WAIT-1(終止等待 1)階段。服務(wù)器主機(jī)接受到客戶端發(fā)出的報(bào)文段后,即發(fā)出確認(rèn)應(yīng)答報(bào)文,確認(rèn)應(yīng)答報(bào)文中 ACK = 1,生成自己的序號位 seq = v,ack = u + 1,然后服務(wù)器主機(jī)就進(jìn)入
CLOSE-WAIT(關(guān)閉等待)狀態(tài)。客戶端主機(jī)收到服務(wù)端主機(jī)的確認(rèn)應(yīng)答后,即進(jìn)入
FIN-WAIT-2(終止等待2)的狀態(tài)。等待客戶端發(fā)出連接釋放的報(bào)文段。這時(shí)服務(wù)端主機(jī)會發(fā)出斷開連接的報(bào)文段,報(bào)文段中 ACK = 1,序列號 seq = v,ack = u + 1,在發(fā)送完斷開請求的報(bào)文后,服務(wù)端主機(jī)就進(jìn)入了
LAST-ACK(最后確認(rèn))的階段。客戶端收到服務(wù)端的斷開連接請求后,客戶端需要作出響應(yīng),客戶端發(fā)出斷開連接的報(bào)文段,在報(bào)文段中,ACK = 1, 序列號 seq = u + 1,因?yàn)榭蛻舳藦倪B接開始斷開后就沒有再發(fā)送數(shù)據(jù),ack = v + 1,然后進(jìn)入到
TIME-WAIT(時(shí)間等待)狀態(tài),請注意,這個(gè)時(shí)候 TCP 連接還沒有釋放。必須經(jīng)過時(shí)間等待的設(shè)置,也就是2MSL后,客戶端才會進(jìn)入CLOSED狀態(tài),時(shí)間 MSL 叫做最長報(bào)文段壽命(Maximum Segment Lifetime)。服務(wù)端主要收到了客戶端的斷開連接確認(rèn)后,就會進(jìn)入 CLOSED 狀態(tài)。因?yàn)榉?wù)端結(jié)束 TCP 連接時(shí)間要比客戶端早,而整個(gè)連接斷開過程需要發(fā)送四個(gè)報(bào)文段,因此釋放連接的過程也被稱為四次揮手。
TCP 連接的任意一方都可以發(fā)起關(guān)閉操作,只不過通常情況下發(fā)起關(guān)閉連接操作一般都是客戶端。然而,一些服務(wù)器比如 Web 服務(wù)器在對請求作出相應(yīng)后也會發(fā)起關(guān)閉連接的操作。TCP 協(xié)議規(guī)定通過發(fā)送一個(gè) FIN 報(bào)文來發(fā)起關(guān)閉操作。
所以綜上所述,建立一個(gè) TCP 連接需要三個(gè)報(bào)文段,而關(guān)閉一個(gè) TCP 連接需要四個(gè)報(bào)文段。TCP 協(xié)議還支持一種半開啟(half-open) 狀態(tài),雖然這種情況并不多見。
TCP 半開啟
TCP 連接處于半開啟的這種狀態(tài)是因?yàn)檫B接的一方關(guān)閉或者終止了這個(gè) TCP 連接卻沒有通知另一方,也就是說兩個(gè)人正在微信聊天,cxuan 你下線了你不告訴我,我還在跟你侃八卦呢。此時(shí)就認(rèn)為這條連接處于半開啟狀態(tài)。這種情況發(fā)生在通信中的一方處于主機(jī)崩潰的情況下,你 xxx 的,我電腦死機(jī)了我咋告訴你?只要處于半連接狀態(tài)的一方不傳輸數(shù)據(jù)的話,那么是無法檢測出來對方主機(jī)已經(jīng)下線的。
另外一種處于半開啟狀態(tài)的原因是通信的一方關(guān)閉了主機(jī)電源 而不是正常關(guān)機(jī)。這種情況下會導(dǎo)致服務(wù)器上有很多半開啟的 TCP 連接。
TCP 半關(guān)閉
既然 TCP 支持半開啟操作,那么我們可以設(shè)想 TCP 也支持半關(guān)閉操作。同樣的,TCP 半關(guān)閉也并不常見。TCP 的半關(guān)閉操作是指僅僅關(guān)閉數(shù)據(jù)流的一個(gè)傳輸方向。兩個(gè)半關(guān)閉操作合在一起就能夠關(guān)閉整個(gè)連接。在一般情況下,通信雙方會通過應(yīng)用程序互相發(fā)送 FIN 報(bào)文段來結(jié)束連接,但是在 TCP 半關(guān)閉的情況下,應(yīng)用程序會表明自己的想法:"我已經(jīng)完成了數(shù)據(jù)的發(fā)送發(fā)送,并發(fā)送了一個(gè) FIN 報(bào)文段給對方,但是我依然希望接收來自對方的數(shù)據(jù)直到它發(fā)送一個(gè) FIN 報(bào)文段給我"。下面是一個(gè) TCP 半關(guān)閉的示意圖。

解釋一下這個(gè)過程:
首先客戶端主機(jī)和服務(wù)器主機(jī)一直在進(jìn)行數(shù)據(jù)傳輸,一段時(shí)間后,客戶端發(fā)起了 FIN 報(bào)文,要求主動(dòng)斷開連接,服務(wù)器收到 FIN 后,回應(yīng) ACK ,由于此時(shí)發(fā)起半關(guān)閉的一方也就是客戶端仍然希望服務(wù)器發(fā)送數(shù)據(jù),所以服務(wù)器會繼續(xù)發(fā)送數(shù)據(jù),一段時(shí)間后服務(wù)器發(fā)送另外一條 FIN 報(bào)文,在客戶端收到 FIN 報(bào)文回應(yīng) ACK 給服務(wù)器后,斷開連接。
TCP 的半關(guān)閉操作中,連接的一個(gè)方向被關(guān)閉,而另一個(gè)方向仍在傳輸數(shù)據(jù)直到它被關(guān)閉為止。只不過很少有應(yīng)用程序使用這一特性。
同時(shí)打開和同時(shí)關(guān)閉
還有一種比較非常規(guī)的操作,這就是兩個(gè)應(yīng)用程序同時(shí)主動(dòng)打開連接。雖然這種情況看起來不太可能,但是在特定的安排下卻是有可能發(fā)生的。我們主要講述這個(gè)過程。
通信雙方在接收到來自對方的 SYN 之前會首先發(fā)送一個(gè) SYN,這個(gè)場景還要求通信雙方都知道對方的 IP 地址 + 端口號。
下面是同時(shí)打開的例子

如上圖所示,通信雙方都在收到對方報(bào)文前主動(dòng)發(fā)送了 SYN 報(bào)文,都在收到彼此的報(bào)文后回復(fù)了一個(gè) ACK 報(bào)文。
一個(gè)同時(shí)打開過程需要交換四個(gè)報(bào)文段,比普通的三次握手增加了一個(gè),由于同時(shí)打開沒有客戶端和服務(wù)器一說,所以這里我用了通信雙方來稱呼。
像同時(shí)打開一樣,同時(shí)關(guān)閉也是通信雙方同時(shí)提出主動(dòng)關(guān)閉請求,發(fā)送 FIN 報(bào)文,下圖顯示了一個(gè)同時(shí)關(guān)閉的過程。

同時(shí)關(guān)閉過程中需要交換和正常關(guān)閉相同數(shù)量的報(bào)文段,只不過同時(shí)關(guān)閉不像四次揮手那樣順序進(jìn)行,而是交叉進(jìn)行的。
聊一聊初始序列號
也許是我上面圖示或者文字描述的不專業(yè),初始序列號它是有專業(yè)術(shù)語表示的,初始序列號的英文名稱是Initial sequence numbers (ISN),所以我們上面表示的 seq = v,其實(shí)就表示的 ISN。
在發(fā)送 SYN 之前,通信雙方會選擇一個(gè)初始序列號。初始序列號是隨機(jī)生成的,每一個(gè) TCP 連接都會有一個(gè)不同的初始序列號。RFC 文檔指出初始序列號是一個(gè) 32 位的計(jì)數(shù)器,每 4 us(微秒) + 1。因?yàn)槊總€(gè) TCP 連接都是一個(gè)不同的實(shí)例,這么安排的目的就是為了防止出現(xiàn)序列號重疊的情況。
當(dāng)一個(gè) TCP 連接建立的過程中,只有正確的 TCP 四元組和正確的序列號才會被對方接收。這也反應(yīng)了 TCP 報(bào)文段容易被偽造 的脆弱性,因?yàn)橹灰覀卧炝艘粋€(gè)相同的四元組和初始序列號就能夠偽造 TCP 連接,從而打斷 TCP 的正常連接,所以抵御這種攻擊的一種方式就是使用初始序列號,另外一種方法就是加密序列號。
TCP 狀態(tài)轉(zhuǎn)換
我們上面聊到了三次握手和四次揮手,提到了一些關(guān)于 TCP 連接之間的狀態(tài)轉(zhuǎn)換,那么下面我就從頭開始和你好好梳理一下這些狀態(tài)之間的轉(zhuǎn)換。
首先第一步,剛開始時(shí)服務(wù)器和客戶端都處于 CLOSED 狀態(tài),這時(shí)需要判斷是主動(dòng)打開還是被動(dòng)打開,如果是主動(dòng)打開,那么客戶端向服務(wù)器發(fā)送 SYN 報(bào)文,此時(shí)客戶端處于 SYN-SEND 狀態(tài),SYN-SEND 表示發(fā)送連接請求后等待匹配的連接請求,服務(wù)器被動(dòng)打開會處于 LISTEN 狀態(tài),用于監(jiān)聽 SYN 報(bào)文。如果客戶端調(diào)用了 close 方法或者經(jīng)過一段時(shí)間沒有操作,就會重新變?yōu)?CLOSED 狀態(tài),這一步轉(zhuǎn)換圖如下

這里有個(gè)疑問,為什么處于 LISTEN 狀態(tài)下的客戶端還會發(fā)送 SYN 變?yōu)?SYN_SENT 狀態(tài)呢?
知乎看到了車小胖大佬的回答,這種情況可能出現(xiàn)在 FTP 中,LISTEN -> SYN_SENT 是因?yàn)檫@個(gè)連接可能是由于服務(wù)器端的應(yīng)用有數(shù)據(jù)發(fā)送給客戶端所觸發(fā)的,客戶端被動(dòng)接受連接,連接建立后,開始傳輸文件。也就是說,處于 LISTEN 狀態(tài)的服務(wù)器也是有可能發(fā)送 SYN 報(bào)文的,只不過這種情況非常少見。
處于 SYN_SEND 狀態(tài)的服務(wù)器會接收 SYN 并發(fā)送 SYN 和 ACK 轉(zhuǎn)換成為 SYN_RCVD 狀態(tài),同樣的,處于 LISTEN 狀態(tài)的客戶端也會接收 SYN 并發(fā)送 SYN 和 ACK 轉(zhuǎn)換為 SYN_RCVD 狀態(tài)。如果處于 SYN_RCVD 狀態(tài)的客戶端收到 RST 就會變?yōu)?LISTEN 狀態(tài)。

這兩張圖一起看會比較好一些。
這里需要解釋下什么是 RST
這里有一種情況是當(dāng)主機(jī)收到 TCP 報(bào)文段后,其 IP 和端口號不匹配的情況。假設(shè)客戶端主機(jī)發(fā)送一個(gè)請求,而服務(wù)器主機(jī)經(jīng)過 IP 和端口號的判斷后發(fā)現(xiàn)不是給這個(gè)服務(wù)器的,那么服務(wù)器就會發(fā)出一個(gè) RST 特殊報(bào)文段給客戶端。

因此,當(dāng)服務(wù)端發(fā)送一個(gè) RST 特殊報(bào)文段給客戶端的時(shí)候,它就會告訴客戶端沒有匹配的套接字連接,請不要再繼續(xù)發(fā)送了。
RST:(Reset the connection)用于復(fù)位因某種原因引起出現(xiàn)的錯(cuò)誤連接,也用來拒絕非法數(shù)據(jù)和請求。如果接收到 RST 位時(shí)候,通常發(fā)生了某些錯(cuò)誤。
上面沒有識別正確的 IP 端口是一種導(dǎo)致 RST 出現(xiàn)的情況,除此之外,RST 還可能由于請求超時(shí)、取消一個(gè)已存在的連接等出現(xiàn)。
位于 SYN_RCVD 的服務(wù)器會接收 ACK 報(bào)文,SYN_SEND 的客戶端會接收 SYN 和 ACK 報(bào)文,并發(fā)送 ACK 報(bào)文,由此,客戶端和服務(wù)器之間的連接就建立了。

這里還要注意一點(diǎn),同時(shí)打開的狀態(tài)我在上面沒有刻意表示出來,實(shí)際上,在同時(shí)打開的情況下,它的狀態(tài)變化是這樣的。

為什么會是這樣呢?因?yàn)槟阆耄谕瑫r(shí)打開的情況下,兩端主機(jī)都發(fā)起 SYN 報(bào)文,而主動(dòng)發(fā)起 SYN 的主機(jī)會處于 SYN-SEND 狀態(tài),發(fā)送完成后,會等待接收 SYN 和 ACK , 在雙方主機(jī)都發(fā)送了 SYN + ACK 后,雙方都處于 SYN-RECEIVED(SYN-RCVD) 狀態(tài),然后等待 SYN + ACK 的報(bào)文到達(dá)后,雙方就會處于 ESTABLISHED 狀態(tài),開始傳輸數(shù)據(jù)。
好了,到現(xiàn)在為止,我給你敘述了一下 TCP 連接建立過程中的狀態(tài)轉(zhuǎn)換,現(xiàn)在你可以泡一壺茶喝點(diǎn)水,等著數(shù)據(jù)傳輸了。
好了,現(xiàn)在水喝夠了,這時(shí)候數(shù)據(jù)也傳輸完成了,數(shù)據(jù)傳輸完成后,這條 TCP 連接就可以斷開了。
現(xiàn)在我們把時(shí)鐘往前撥一下,調(diào)整到服務(wù)端處于 SYN_RCVD 狀態(tài)的時(shí)刻,因?yàn)閯偸盏搅?SYN 包并發(fā)送了 SYN + ACK 包,此時(shí)服務(wù)端很開心,但是這時(shí),服務(wù)端應(yīng)用進(jìn)程關(guān)閉了,然后應(yīng)用進(jìn)程發(fā)了一個(gè) FIN 包,就會讓服務(wù)器從 SYN_RCVD -> FIN_WAIT_1 狀態(tài)。

然后把時(shí)鐘調(diào)到現(xiàn)在,客戶端和服務(wù)器現(xiàn)在已經(jīng)傳輸完數(shù)據(jù)了 ,此時(shí)客戶端發(fā)送了一條 FIN 報(bào)文希望斷開連接,此時(shí)客戶端也會變?yōu)?nbsp;FIN_WAIT_1 狀態(tài),對于服務(wù)器來說,它接收到了 FIN 報(bào)文段并回復(fù)了 ACK 報(bào)文,就會從 ESTABLISHED -> CLOSE_WAIT 狀態(tài)。

位于 CLOSE_WAIT 狀態(tài)的服務(wù)端會發(fā)送 FIN 報(bào)文,然后把自己置于 LAST_ACK 狀態(tài)。處于 FIN_WAIT_1 的客戶端接收 ACK 消息就會變?yōu)?FIN_WAIT_2 狀態(tài)。
這里需要先解釋一下 CLOSING 這個(gè)狀態(tài),F(xiàn)IN_WAIT_1 -> CLOSING 的轉(zhuǎn)換比較特殊
CLOSING 這種狀態(tài)比較特殊,實(shí)際情況中應(yīng)該是很少見,屬于一種比較罕見的例外狀態(tài)。正常情況下,當(dāng)你發(fā)送FIN 報(bào)文后,按理來說是應(yīng)該先收到(或同時(shí)收到)對方的 ACK 報(bào)文,再收到對方的 FIN 報(bào)文。但是 CLOSING 狀態(tài)表示你發(fā)送 FIN 報(bào)文后,并沒有收到對方的 ACK 報(bào)文,反而卻也收到了對方的 FIN 報(bào)文。
什么情況下會出現(xiàn)此種情況呢?其實(shí)細(xì)想一下,也不難得出結(jié)論:那就是如果雙方在同時(shí)關(guān)閉一個(gè)鏈接的話,那么就出現(xiàn)了同時(shí)發(fā)送 FIN 報(bào)文的情況,也即會出現(xiàn) CLOSING 狀態(tài),表示雙方都正在關(guān)閉連接。
FIN_WAIT_2 狀態(tài)的客戶端接收服務(wù)端主機(jī)發(fā)送的 FIN + ACK 消息,并發(fā)送 ACK 響應(yīng)后,會變?yōu)?nbsp;TIME_WAIT 狀態(tài)。處于 CLOSE_WAIT 的客戶端發(fā)送 FIN 會處于 LAST_ACK 狀態(tài)。
這里不少圖和博客雖然在圖上畫的是 FIN + ACK 報(bào)文后才會處于 LAST_ACK 狀態(tài),但是描述的時(shí)候,一般通常只對于 FIN 進(jìn)行描述。也就是說 CLOSE_WAIT 發(fā)送 FIN 才會處于 LAST_ACK 狀態(tài)。

所以這里 FIN_WAIT_1 -> TIME_WAIT 的狀態(tài)也就是接收 FIN 和 ACK 并發(fā)送 ACK 之后,客戶端處于的狀態(tài)。
然后位于 CLOSINIG 狀態(tài)的客戶端這時(shí)候還有 ACK 接收的話,會繼續(xù)處于 TIME_WAIT 狀態(tài),可以看到,TIME_WAIT 狀態(tài)相當(dāng)于是客戶端在關(guān)閉前的最后一個(gè)狀態(tài),它是一種主動(dòng)關(guān)閉的狀態(tài);而 LAST_ACK 是服務(wù)端在關(guān)閉前的最后一個(gè)狀態(tài),它是一種被動(dòng)打開的狀態(tài)。
上面有幾個(gè)狀態(tài)比較特殊,這里我們詳細(xì)解釋下。
TIME_WAIT 狀態(tài)
通信雙方建立 TCP 連接后,主動(dòng)關(guān)閉連接的一方就會進(jìn)入 TIME_WAIT 狀態(tài)。TIME_WAIT 狀態(tài)也稱為 2MSL 的等待狀態(tài)。在這個(gè)狀態(tài)下,TCP 將會等待最大段生存期(Maximum Segment Lifetime, MSL) 時(shí)間的兩倍。
這里需要解釋下 MSL
MSL 是 TCP 段期望的最大生存時(shí)間,也就是在網(wǎng)絡(luò)中存在的最長時(shí)間。這個(gè)時(shí)間是有限制的,因?yàn)槲覀冎?TCP 是依靠 IP 數(shù)據(jù)段來進(jìn)行傳輸?shù)?,IP 數(shù)據(jù)報(bào)中有 TTL 和跳數(shù)的字段,這兩個(gè)字段決定了 IP 的生存時(shí)間,一般情況下,TCP 的最大生存時(shí)間是 2 分鐘,不過這個(gè)數(shù)值是可以修改的,根據(jù)不同操作系統(tǒng)可以修改此值。
基于此,我們來探討 TIME_WAIT 的狀態(tài)。
當(dāng) TCP 執(zhí)行一個(gè)主動(dòng)關(guān)閉并發(fā)送最終的 ACK 時(shí),TIME_WAIT 應(yīng)該以 2 * 最大生存時(shí)間存在,這樣就能夠讓 TCP 重新發(fā)送最終的 ACK 以避免出現(xiàn)丟失的情況。重新發(fā)送最終的 ACK 并不是因?yàn)?TCP 重傳了 ACK,而是因?yàn)橥ㄐ帕硪环街貍髁?FIN,客戶端經(jīng)常會發(fā)送 FIN,因?yàn)樗枰?ACK 的響應(yīng)才能夠關(guān)閉連接,如果生存時(shí)間超過了 2MSL 的話,客戶端就會發(fā)送 RST,使服務(wù)端出錯(cuò)。
TCP 超時(shí)和重傳
沒有永遠(yuǎn)不出錯(cuò)誤的通信,這句話表明著不管外部條件多么完備,永遠(yuǎn)都會有出錯(cuò)的可能。所以,在 TCP 的正常通信過程中,也會出現(xiàn)錯(cuò)誤,這種錯(cuò)誤可能是由于數(shù)據(jù)包丟失引起的,也可能是由于數(shù)據(jù)包重復(fù)引起的,甚至可能是由于數(shù)據(jù)包失序 引起的。
TCP 的通信過程中,會由 TCP 的接收端返回一系列的確認(rèn)信息來判斷是否出現(xiàn)錯(cuò)誤,一旦出現(xiàn)丟包等情況,TCP 就會啟動(dòng)重傳操作,重傳尚未確認(rèn)的數(shù)據(jù)。
TCP 的重傳有兩種方式,一種是基于時(shí)間,一種是基于確認(rèn)信息,一般通過確認(rèn)信息要比通過時(shí)間更加高效。
所以從這點(diǎn)就可以看出,TCP 的確認(rèn)和重傳,都是基于數(shù)據(jù)包是否被確認(rèn)為前提的。
TCP 在發(fā)送數(shù)據(jù)時(shí)會設(shè)置一個(gè)定時(shí)器,如果在定時(shí)器指定的時(shí)間內(nèi)未收到確認(rèn)信息,那么就會觸發(fā)相應(yīng)的超時(shí)或者基于計(jì)時(shí)器的重傳操作,計(jì)時(shí)器超時(shí)通常被稱為重傳超時(shí)(RTO)。
但是有另外一種不會引起延遲的方式,這就是快速重傳。
TCP 在每次重傳一次報(bào)文后,其重傳時(shí)間都會加倍,這種"間隔時(shí)間加倍"被稱為二進(jìn)制指數(shù)補(bǔ)償(binary exponential backoff) 。等到間隔時(shí)間加倍到 15.5 min 后,客戶端會顯示
Connection closed by foreign host.
TCP 擁有兩個(gè)閾值來決定如何重傳一個(gè)報(bào)文段,這兩個(gè)閾值被定義在 RFC[RCF1122] 中,第一個(gè)閾值是 R1,它表示愿意嘗試重傳的次數(shù),閾值 R2 表示 TCP 應(yīng)該放棄連接的時(shí)間。R1 和 R2 應(yīng)至少設(shè)為三次重傳和 100 秒放棄 TCP 連接。
這里需要注意下,對連接建立報(bào)文 SYN 來說,它的 R2 至少應(yīng)該設(shè)置為 3 分鐘,但是在不同的系統(tǒng)中,R1 和 R2 值的設(shè)置方式也不同。
在 Linux 系統(tǒng)中,R1 和 R2 的值可以通過應(yīng)用程序來設(shè)置,或者是修改 net.ipv4.tcp_retries1 和 net.ipv4.tcp_retries2 的值來設(shè)置。變量值就是重傳次數(shù)。
tcp_retries2 的默認(rèn)值是 15,這個(gè)充實(shí)次數(shù)的耗時(shí)大約是 13 - 30 分鐘,這只是一個(gè)大概值,最終耗時(shí)時(shí)間還要取決于 RTO ,也就是重傳超時(shí)時(shí)間。tcp_retries1 的默認(rèn)值是 3 。
對于 SYN 段來說,net.ipv4.tcp_syn_retries 和 net.ipv4.tcp_synack_retries 這兩個(gè)值限制了 SYN 的重傳次數(shù),默認(rèn)是 5,大約是 180 秒。
Windows 操作系統(tǒng)下也有 R1 和 R2 變量,它們的值被定義在下方的注冊表中
HKLM\System\CurrentControlSet\Services\Tcpip\Parameters
HKLM\System\CurrentControlSet\Services\Tcpip6\Parameters
其中有一個(gè)非常重要的變量就是 TcpMaxDataRetransmissions,這個(gè) TcpMaxDataRetransmissions 對應(yīng) Linux 中的 tcp_retries2 變量,默認(rèn)值是 5。這個(gè)值的意思表示的是 TCP 在現(xiàn)有連接上未確認(rèn)數(shù)據(jù)段的次數(shù)。
快速重傳
我們上面提到了快速重傳,實(shí)際上快速重傳機(jī)制是基于接收端的反饋信息來觸發(fā)的,它并不受重傳計(jì)時(shí)器的影響。所以與超時(shí)重傳相比,快速重傳能夠有效的修復(fù)丟包情況。當(dāng) TCP 連接的過程中接收端出現(xiàn)亂序的報(bào)文(比如 2 - 4 - 3)到達(dá)時(shí),TCP 需要立刻生成確認(rèn)消息,這種確認(rèn)消息也被稱為重復(fù) ACK。
當(dāng)失序報(bào)文到達(dá)時(shí),重復(fù) ACK 要做到立刻返回,不允許延遲發(fā)送,此舉的目的是要告訴發(fā)送方某段報(bào)文失序到達(dá)了,希望發(fā)送方指出失序報(bào)文段的序列號。
還有一種情況也會導(dǎo)致重復(fù) ACK 發(fā)給發(fā)送方,那就是當(dāng)前報(bào)文段的后續(xù)報(bào)文發(fā)送至接收端,由此可以判斷當(dāng)前發(fā)送方的報(bào)文段丟失或者延遲到達(dá)。因?yàn)檫@兩種情況導(dǎo)致的后果都是接收方?jīng)]有收到報(bào)文,但是我們卻無法判斷到底是報(bào)文段丟失還是報(bào)文段沒有送達(dá)。因此 TCP 發(fā)送端會等待一定數(shù)目的重復(fù) ACK 被接受來決定數(shù)據(jù)是否丟失并觸發(fā)快速重傳。一般這個(gè)判斷的數(shù)量是 3,這段文字表述可能無法清晰理解,我們舉個(gè)例子。

如上圖所示,報(bào)文段 1 成功接收并被確認(rèn)為 ACK 2,接收端的期待序號為 2,當(dāng)報(bào)文段 2 丟失后,報(bào)文段 3。失序到達(dá),但是與接收端的期望不匹配,所以接收端會重復(fù)發(fā)送冗余 ACK 2。
這樣,在超時(shí)重傳定時(shí)器到期之前,接收收到連續(xù)三個(gè)相同的 ACK 后,發(fā)送端就知道哪個(gè)報(bào)文段丟失了,于是發(fā)送方會重發(fā)這個(gè)丟失的報(bào)文段,這樣就不用等待重傳定時(shí)器的到期,大大提高了效率。
SACK
在標(biāo)準(zhǔn)的 TCP 確認(rèn)機(jī)制中,如果發(fā)送方發(fā)送了 0 - 10000 序號之間的數(shù)據(jù),但是接收方只接收到了 0 -1000, 3000 - 10000 之間的數(shù)據(jù),而 1000 - 3000 之間的數(shù)據(jù)沒有到達(dá)接收端,此時(shí)發(fā)送方會重傳 1000 - 10000 之間的數(shù)據(jù),實(shí)際上這是沒有必要的,因?yàn)?3000 后面的數(shù)據(jù)已經(jīng)被接收了。但是發(fā)送方無法感知這種情況的存在。
如何避免或者說解決這種問題呢?
為了優(yōu)化這種情況,我們有必要讓客戶端知道更多的消息,在 TCP 報(bào)文段中,有一個(gè) SACK 選項(xiàng)字段,這個(gè)字段是一種選擇性確認(rèn)(selective acknowledgment)機(jī)制,這個(gè)機(jī)制能告訴 TCP 客戶端,用我們的俗語來解釋就是:“我這里最多允許接收 1000 之后的報(bào)文段,但是我卻收到了 3000 - 10000 的報(bào)文段,請給我 1000 - 3000 之間的報(bào)文段”。
但是,這個(gè)選擇性確認(rèn)機(jī)制的是否開啟還受一個(gè)字段的影響,這個(gè)字段就是 SACK 允許選項(xiàng)字段,通信雙方在 SYN 段或者 SYN + ACK 段中添加 SACK 允許選項(xiàng)字段來通知對端主機(jī)是否支持 SACK,如果雙方都支持的話,后續(xù)在 SYN 段中就可以使用 SACK 選項(xiàng)了。
這里需要注意下:SACK 選項(xiàng)字段只能出現(xiàn)在 SYN 段中。
偽超時(shí)和重傳
在某些情況下,即使沒有出現(xiàn)報(bào)文段的丟失也可能會引發(fā)報(bào)文重傳。這種重傳行為被稱為 偽重傳(spurious retransmission) ,這種重傳是沒有必要的,造成這種情況的因素可能是由于偽超時(shí)(spurious timeout),偽超時(shí)的意思就是過早的判定超時(shí)發(fā)生。造成偽超時(shí)的因素有很多,比如報(bào)文段失序到達(dá),報(bào)文段重復(fù),ACK 丟失等情況。

檢測和處理偽超時(shí)的方法有很多,這些方法統(tǒng)稱為檢測算法和響應(yīng)算法。檢測算法用于判斷是否出現(xiàn)了超時(shí)現(xiàn)象或出現(xiàn)了計(jì)時(shí)器的重傳現(xiàn)象。一旦出現(xiàn)了超時(shí)或者重傳的情況,就會執(zhí)行響應(yīng)算法撤銷或者減輕超時(shí)帶來的影響,下面是幾種算法,此篇文章暫不深入這些實(shí)現(xiàn)細(xì)節(jié)
重復(fù) SACK 擴(kuò)展- DSACK
Eifel 檢測算法
前移 RTO 恢復(fù) - F-RTO
Eifel 響應(yīng)算法
包失序和包重復(fù)
上面我們討論的都是 TCP 如何處理丟包的問題,我們下面來討論一下包失序和包重復(fù)的問題。
包失序
數(shù)據(jù)包的失序到達(dá)是互聯(lián)網(wǎng)中極其容易出現(xiàn)的一種情況,由于 IP 層并不能保證數(shù)據(jù)包的有序性,每個(gè)數(shù)據(jù)包的發(fā)送都可能會選擇當(dāng)前情況傳輸速度最快的鏈路,所以很有可能出現(xiàn)發(fā)送了 A - > B -> C 的三個(gè)數(shù)據(jù)包,到達(dá)接收端的數(shù)據(jù)包順序是 C -> A -> B 或者 B -> C -> A 等等。這就是包失序的一種現(xiàn)象。
在包傳輸中,主要分為兩種鏈路:正向鏈路(SYN)和反向鏈路(ACK)
如果失序發(fā)生在正向鏈路,TCP 是無法正確判斷數(shù)據(jù)包是否丟失的,數(shù)據(jù)的丟失和失序都會導(dǎo)致接收端收到無序的數(shù)據(jù)包,造成數(shù)據(jù)之間的空缺。如果這種空缺不夠大的話,這種情況影響不大;但是如果空缺比較大的話,可能會導(dǎo)致偽重傳。
如果失序發(fā)生在反向鏈路,就會使 TCP 的窗口前移,然后收到重復(fù)而應(yīng)該被丟棄的 ACK,導(dǎo)致發(fā)送端出現(xiàn)不必要的流量突發(fā),影響可用網(wǎng)絡(luò)帶寬。
回到我們上面討論的快速重傳,由于快速重傳是根據(jù)重復(fù) ACK 推斷出現(xiàn)丟包而啟動(dòng)的,它不用等到重傳計(jì)時(shí)器超時(shí)。由于 TCP 接收端會對接收到的失序報(bào)文立刻返回 ACK,所以網(wǎng)絡(luò)中任何一個(gè)失序到達(dá)的報(bào)文都可能會造成重復(fù) ACK。假設(shè)一旦收到 ACK,就會啟動(dòng)快速重傳機(jī)制,當(dāng) ACK 數(shù)量激增,就會導(dǎo)致大量不必要的重傳發(fā)生,所以快速重傳應(yīng)該達(dá)到重復(fù)閾值(dupthresh) 再觸發(fā)。但是在互聯(lián)網(wǎng)中,嚴(yán)重的失序并不常見,因此 dupthresh 的值可以設(shè)置的盡量小,一般來說 3 就能處理絕大部分情況。
包重復(fù)
包重復(fù)也是互聯(lián)網(wǎng)中出現(xiàn)很少的一種情況,它指的是在網(wǎng)絡(luò)傳輸過程中,包可能會出現(xiàn)傳輸多次的情況,當(dāng)重傳生成時(shí),TCP 可能會出現(xiàn)混淆。
包的重復(fù)可以使接收端生成一系列的重復(fù) ACK,這種情況可以使用 SACK 協(xié)商來解決。
TCP 數(shù)據(jù)流和窗口管理
我們在 40 張圖帶你搞懂 TCP 和 UDP 這篇文章中知道了可以使用滑動(dòng)窗口來實(shí)現(xiàn)流量控制,也就是說,客戶端和服務(wù)器可以相互提供數(shù)據(jù)流信息的交換,數(shù)據(jù)流的相關(guān)信息主要包括報(bào)文段序列號、ACK 號和窗口大小。

圖中的兩個(gè)箭頭表示數(shù)據(jù)流方向,數(shù)據(jù)流方向也就是 TCP 報(bào)文段的傳輸方向??梢钥吹?,每個(gè) TCP 報(bào)文段中都包括了序列號、ACK 和窗口信息,可能還會有用戶數(shù)據(jù)。TCP 報(bào)文段中的窗口大小表示接收端還能夠接收的緩存空間的大小,以字節(jié)為單位。這個(gè)窗口大小是一種動(dòng)態(tài)的,因?yàn)闊o時(shí)無刻都會有報(bào)文段的接收和消失,這種動(dòng)態(tài)調(diào)整的窗口大小我們稱之為滑動(dòng)窗口,下面我們就來具體認(rèn)識一下滑動(dòng)窗口。
滑動(dòng)窗口
TCP 連接的每一端都可以發(fā)送數(shù)據(jù),但是數(shù)據(jù)的發(fā)送不是沒有限制的,實(shí)際上,TCP 連接的兩端都各自維護(hù)了一個(gè)發(fā)送窗口結(jié)構(gòu) (send window structure) 和 接收窗口結(jié)構(gòu) (receive window structure),這兩個(gè)窗口結(jié)構(gòu)就是數(shù)據(jù)發(fā)送的限制。
發(fā)送方窗口
下圖是一個(gè)發(fā)送方窗口的示例。

在這幅圖中,涉及滑動(dòng)窗口的四種概念:
已經(jīng)發(fā)送并確認(rèn)的報(bào)文段:發(fā)送給接收方后,接收方回回復(fù) ACK 來對報(bào)文段進(jìn)行響應(yīng),圖中標(biāo)注綠色的報(bào)文段就是已經(jīng)經(jīng)過接收方確認(rèn)的報(bào)文段。
已經(jīng)發(fā)送但是還沒確認(rèn)的報(bào)文段:圖中綠色區(qū)域是經(jīng)過接收方確認(rèn)的報(bào)文段,而淺藍(lán)色這段區(qū)域指的是已經(jīng)發(fā)送但是還未經(jīng)過接收方確認(rèn)的報(bào)文段。
等待發(fā)送的報(bào)文段:圖中深藍(lán)色區(qū)域是等待發(fā)送的報(bào)文段,它屬于發(fā)送窗口結(jié)構(gòu)的一部分,也就是說,發(fā)送窗口結(jié)構(gòu)其實(shí)是由已發(fā)送未確認(rèn) + 等待發(fā)送的報(bào)文段構(gòu)成。
窗口滑動(dòng)時(shí)才能發(fā)送的報(bào)文段:如果圖中的 [4,9] 這個(gè)集合內(nèi)的報(bào)文段發(fā)送完畢后,整個(gè)滑動(dòng)窗口會向右移動(dòng),圖中橙色區(qū)域就是窗口右移時(shí)才能發(fā)送的報(bào)文段。
滑動(dòng)窗口也是有邊界的,這個(gè)邊界是 Left edge 和 Right edge,Left edge 是窗口的左邊界,Right edge 是窗口的右邊界。
當(dāng) Left edge 向右移動(dòng)而 Right edge 不變時(shí),這個(gè)窗口可能處于 close 關(guān)閉狀態(tài)。隨著已發(fā)送的數(shù)據(jù)逐漸被確認(rèn)從而導(dǎo)致窗口變小時(shí),就會發(fā)生這種情況。

當(dāng) Right edge 向右移動(dòng)時(shí),窗口會處于 open 打開狀態(tài),允許發(fā)送更多的數(shù)據(jù)。當(dāng)接收端進(jìn)程讀取緩沖區(qū)數(shù)據(jù),從而使緩沖區(qū)接收更多數(shù)據(jù)時(shí),就會處于這種狀態(tài)。

還可能會發(fā)生 Right edge 向左移動(dòng)的情況,會導(dǎo)致發(fā)送并確認(rèn)的報(bào)文段變小,這種情況被稱為糊涂窗口綜合癥,這種情況是我們不愿意看到的。出現(xiàn)糊涂窗口綜合癥時(shí),通信雙方用于交換的數(shù)據(jù)段大小會變小,而網(wǎng)絡(luò)固定的開銷卻沒有變化,每個(gè)報(bào)文段中有用數(shù)據(jù)相對于頭部信息的比例較小,導(dǎo)致傳輸效率非常低。
這就相當(dāng)于之前你明明有能力花一天時(shí)間寫完一個(gè)復(fù)雜的頁面,現(xiàn)在你花了一天的時(shí)間卻改了一個(gè)標(biāo)題的 bug,大材小用。
每個(gè) TCP 報(bào)文段都包含ACK 號和窗口通告信息,所以每當(dāng)收到響應(yīng)時(shí),TCP 接收方都會根據(jù)這兩個(gè)參數(shù)調(diào)整窗口結(jié)構(gòu)。
TCP 滑動(dòng)窗口的 Left edge 永遠(yuǎn)不可能向左移動(dòng),因?yàn)榘l(fā)送并確認(rèn)的報(bào)文段永遠(yuǎn)不可能被取消,就像這世界上沒有后悔藥一樣。這條邊緣是由另一段發(fā)送的 ACK 號控制的。當(dāng) ACK 標(biāo)號使窗口向右移動(dòng)但是窗口大小沒有改變時(shí),則稱該窗口向前滑動(dòng)。
如果 ACK 的編號增加但是窗口通告信息隨著其他 ACK 的到達(dá)卻變小了,此時(shí) Left edge 會接近 Right edge。當(dāng) Left edge 和 Right edge 重合時(shí),此時(shí)發(fā)送方不會再傳輸任何數(shù)據(jù),這種情況被稱為零窗口。此時(shí) TCP 發(fā)送方會發(fā)起窗口探測,等待合適的時(shí)機(jī)再發(fā)送數(shù)據(jù)。
接收方窗口
接收方也維護(hù)了一個(gè)窗口結(jié)構(gòu),這個(gè)窗口要比發(fā)送方的簡單很多。這個(gè)窗口記錄了已經(jīng)接收并確認(rèn)的數(shù)據(jù),以及它能夠接收的最大序列號。接收方的窗口結(jié)構(gòu)不會存儲重復(fù)的報(bào)文段和 ACK,同時(shí)接收方的窗口也不會記錄不應(yīng)該收到的報(bào)文段和 ACK。下面是 TCP 接收方的窗口結(jié)構(gòu)。

與發(fā)送端的窗口一樣,接收方窗口結(jié)構(gòu)也維護(hù)了一個(gè) Left edge 和 Right edge。位于 Left edge 左邊的被稱為已經(jīng)接收并確認(rèn)的報(bào)文段,位于 Right edge 右邊的被稱為不能接收的報(bào)文段。
對于接收端來說,到達(dá)序列號小于 Left efge 的被認(rèn)為是已經(jīng)重復(fù)的數(shù)據(jù),需要丟棄。超過 Right edge 的被認(rèn)為超出處理范圍。只有當(dāng)?shù)竭_(dá)的報(bào)文段等于 Left edge 時(shí),數(shù)據(jù)才不會被丟棄,窗口才能夠向前滑動(dòng)。
接收方窗口結(jié)構(gòu)也會存在零窗口的情況,如果某個(gè)應(yīng)用進(jìn)程消耗數(shù)據(jù)很慢,而 TCP 發(fā)送方卻發(fā)送了大量的數(shù)據(jù)給接收方,會造成 TCP 緩沖區(qū)溢出,通告發(fā)送方不要再發(fā)送數(shù)據(jù)了,但是應(yīng)用進(jìn)程卻以非常慢的速度消耗緩沖區(qū)的數(shù)據(jù)(比如 1 字節(jié)),就會告訴接收端只能發(fā)送一個(gè)字節(jié)的數(shù)據(jù),這個(gè)過程慢慢持續(xù),造成網(wǎng)絡(luò)開銷大,效率很低。
我們上面提到了窗口存在 Left edge = Right edge 的情況,此時(shí)被稱為零窗口,下面我們就來具體研究一下零窗口。
零窗口
TCP 是通過接收端的窗口通告信息來實(shí)現(xiàn)流量控制的。通告窗口告訴了 TCP ,接收端能夠接收的數(shù)據(jù)量。當(dāng)接收方的窗口變?yōu)?0 時(shí),可以有效的阻止發(fā)送端繼續(xù)發(fā)送數(shù)據(jù)。當(dāng)接收端重新獲得可用空間時(shí),它會給發(fā)送端傳輸一個(gè) 窗口更新 告知自己能夠接收數(shù)據(jù)了。窗口更新一般是純 ACK ,即不帶任何數(shù)據(jù)。但是純 ACK 不能保證一定會到達(dá)發(fā)送端,于是需要有相關(guān)的措施能夠處理這種丟包。
如果純 ACK 丟失的話,通信雙方就會一直處于等待狀態(tài),發(fā)送方心想拉垮的接收端怎么還讓我發(fā)送數(shù)據(jù)!接收端心想天殺的發(fā)送方怎么還不發(fā)數(shù)據(jù)!為了防止這種情況,發(fā)送方會采用一個(gè)持續(xù)計(jì)時(shí)器來間歇性的查詢接收方,看看其窗口是否已經(jīng)增長。持續(xù)計(jì)時(shí)器會觸發(fā)窗口探測,強(qiáng)制要求接收方返回帶有更新窗口的 ACK。
窗口探測包含一個(gè)字節(jié)的數(shù)據(jù),采用的是 TCP 丟失重傳的方式。當(dāng) TCP 持續(xù)計(jì)時(shí)器超時(shí)后,就會觸發(fā)窗口探測的發(fā)送。一個(gè)字節(jié)的數(shù)據(jù)能否被接收端接收,還要取決于其緩沖區(qū)的大小。
擁塞控制
有了 TCP 的窗口控制后,使計(jì)算機(jī)網(wǎng)絡(luò)中兩個(gè)主機(jī)之間不再是以單個(gè)數(shù)據(jù)段的形式發(fā)送了,而是能夠連續(xù)發(fā)送大量的數(shù)據(jù)包。然而,大量數(shù)據(jù)包同時(shí)也伴隨著其他問題,比如網(wǎng)絡(luò)負(fù)載、網(wǎng)絡(luò)擁堵等問題。TCP 為了防止這類問題的出現(xiàn),使用了 擁塞控制 機(jī)制,擁塞控制機(jī)制會在面臨網(wǎng)絡(luò)擁塞時(shí)遏制發(fā)送方的數(shù)據(jù)發(fā)送。
擁塞控制主要有兩種方法
端到端的擁塞控制: 因?yàn)榫W(wǎng)絡(luò)層沒有為運(yùn)輸層擁塞控制提供顯示支持。所以即使網(wǎng)絡(luò)中存在擁塞情況,端系統(tǒng)也要通過對網(wǎng)絡(luò)行為的觀察來推斷。TCP 就是使用了端到端的擁塞控制方式。IP 層不會向端系統(tǒng)提供有關(guān)網(wǎng)絡(luò)擁塞的反饋信息。那么 TCP 如何推斷網(wǎng)絡(luò)擁塞呢?如果超時(shí)或者三次冗余確認(rèn)就被認(rèn)為是網(wǎng)絡(luò)擁塞,TCP 會減小窗口的大小,或者增加往返時(shí)延來避免。網(wǎng)絡(luò)輔助的擁塞控制: 在網(wǎng)絡(luò)輔助的擁塞控制中,路由器會向發(fā)送方提供關(guān)于網(wǎng)絡(luò)中擁塞狀態(tài)的反饋。這種反饋信息就是一個(gè)比特信息,它指示鏈路中的擁塞情況。
下圖描述了這兩種擁塞控制方式

TCP 擁塞控制
如果你看到這里,那我就暫定認(rèn)為你了解了 TCP 實(shí)現(xiàn)可靠性的基礎(chǔ)了,那就是使用序號和確認(rèn)號。除此之外,另外一個(gè)實(shí)現(xiàn) TCP 可靠性基礎(chǔ)的就是 TCP 的擁塞控制。如果說
TCP 所采用的方法是讓每一個(gè)發(fā)送方根據(jù)所感知到的網(wǎng)絡(luò)的擁塞程度來限制發(fā)出報(bào)文段的速率,如果 TCP 發(fā)送方感知到?jīng)]有什么擁塞,則 TCP 發(fā)送方會增加發(fā)送速率;如果發(fā)送方感知沿著路徑有阻塞,那么發(fā)送方就會降低發(fā)送速率。
但是這種方法有三個(gè)問題
TCP 發(fā)送方如何限制它向其他連接發(fā)送報(bào)文段的速率呢?
一個(gè) TCP 發(fā)送方是如何感知到網(wǎng)絡(luò)擁塞的呢?
當(dāng)發(fā)送方感知到端到端的擁塞時(shí),采用何種算法來改變其發(fā)送速率呢?
我們先來探討一下第一個(gè)問題,TCP 發(fā)送方如何限制它向其他連接發(fā)送報(bào)文段的速率呢?
我們知道 TCP 是由接收緩存、發(fā)送緩存和變量(LastByteRead, rwnd,等)組成。發(fā)送方的 TCP 擁塞控制機(jī)制會跟蹤一個(gè)變量,即 擁塞窗口(congestion window) 的變量,擁塞窗口表示為 cwnd,用于限制 TCP 在接收到 ACK 之前可以發(fā)送到網(wǎng)絡(luò)的數(shù)據(jù)量。而接收窗口(rwnd) 是一個(gè)用于告訴接收方能夠接受的數(shù)據(jù)量。
一般來說,發(fā)送方未確認(rèn)的數(shù)據(jù)量不得超過 cwnd 和 rwnd 的最小值,也就是
LastByteSent - LastByteAcked <= min(cwnd,rwnd)
由于每個(gè)數(shù)據(jù)包的往返時(shí)間是 RTT,我們假設(shè)接收端有足夠的緩存空間用于接收數(shù)據(jù),我們就不用考慮 rwnd 了,只專注于 cwnd,那么,該發(fā)送方的發(fā)送速率大概是 cwnd/RTT 字節(jié)/秒 。通過調(diào)節(jié) cwnd,發(fā)送方因此能調(diào)整它向連接發(fā)送數(shù)據(jù)的速率。
一個(gè) TCP 發(fā)送方是如何感知到網(wǎng)絡(luò)擁塞的呢?
這個(gè)我們上面討論過,是 TCP 根據(jù)超時(shí)或者 3 個(gè)冗余 ACK 來感知的。
當(dāng)發(fā)送方感知到端到端的擁塞時(shí),采用何種算法來改變其發(fā)送速率呢 ?
這個(gè)問題比較復(fù)雜,且容我娓娓道來,一般來說,TCP 會遵循下面這幾種指導(dǎo)性原則
如果在報(bào)文段發(fā)送過程中丟失,那就意味著網(wǎng)絡(luò)擁堵,此時(shí)需要適當(dāng)降低 TCP 發(fā)送方的速率。
一個(gè)確認(rèn)報(bào)文段指示發(fā)送方正在向接收方傳遞報(bào)文段,因此,當(dāng)對先前未確認(rèn)報(bào)文段的確認(rèn)到達(dá)時(shí),能夠增加發(fā)送方的速率。為啥呢?因?yàn)槲创_認(rèn)的報(bào)文段到達(dá)接收方也就表示著網(wǎng)絡(luò)不擁堵,能夠順利到達(dá),因此發(fā)送方擁塞窗口長度會變大,所以發(fā)送速率會變快
帶寬探測,帶寬探測說的是 TCP 可以通過調(diào)節(jié)傳輸速率來增加/減小 ACK 到達(dá)的次數(shù),如果出現(xiàn)丟包事件,就會減小傳輸速率。因此,為了探測擁塞開始出現(xiàn)的頻率, TCP 發(fā)送方應(yīng)該增加它的傳輸速率。然后慢慢使傳輸速率降低,進(jìn)而再次開始探測,看看擁塞開始速率是否發(fā)生了變化。
在了解完 TCP 擁塞控制后,下面我們就該聊一下 TCP 的 擁塞控制算法(TCP congestion control algorithm) 了。TCP 擁塞控制算法主要包含三個(gè)部分:慢啟動(dòng)、擁塞避免、快速恢復(fù),下面我們依次來看一下
慢啟動(dòng)
當(dāng)一條 TCP 開始建立連接時(shí),cwnd 的值就會初始化為一個(gè) MSS 的較小值。這就使得初始發(fā)送速率大概是 MSS/RTT 字節(jié)/秒 ,比如要傳輸 1000 字節(jié)的數(shù)據(jù),RTT 為 200 ms ,那么得到的初始發(fā)送速率大概是 40 kb/s 。實(shí)際情況下可用帶寬要比這個(gè) MSS/RTT 大得多,因此 TCP 想要找到最佳的發(fā)送速率,可以通過 慢啟動(dòng)(slow-start) 的方式,在慢啟動(dòng)的方式中,cwnd 的值會初始化為 1 個(gè) MSS,并且每次傳輸報(bào)文確認(rèn)后就會增加一個(gè) MSS,cwnd 的值會變?yōu)?2 個(gè) MSS,這兩個(gè)報(bào)文段都傳輸成功后每個(gè)報(bào)文段 + 1,會變?yōu)?4 個(gè) MSS,依此類推,每成功一次 cwnd 的值就會翻倍。如下圖所示

發(fā)送速率不可能會一直增長,增長總有結(jié)束的時(shí)候,那么何時(shí)結(jié)束呢?慢啟動(dòng)通常會使用下面這幾種方式結(jié)束發(fā)送速率的增長。
如果在慢啟動(dòng)的發(fā)送過程出現(xiàn)丟包的情況,那么 TCP 會將發(fā)送方的 cwnd 設(shè)置為 1 并重新開始慢啟動(dòng)的過程,此時(shí)會引入一個(gè)
ssthresh(慢啟動(dòng)閾值)的概念,它的初始值就是產(chǎn)生丟包的 cwnd 的值 / 2,即當(dāng)檢測到擁塞時(shí),ssthresh 的值就是窗口值的一半。第二種方式是直接和 ssthresh 的值相關(guān)聯(lián),因?yàn)楫?dāng)檢測到擁塞時(shí),ssthresh 的值就是窗口值的一半,那么當(dāng) cwnd > ssthresh 時(shí),每次翻番都可能會出現(xiàn)丟包,所以最好的方式就是 cwnd 的值 = ssthresh ,這樣 TCP 就會轉(zhuǎn)為擁塞控制模式,結(jié)束慢啟動(dòng)。
慢啟動(dòng)結(jié)束的最后一種方式就是如果檢測到 3 個(gè)冗余 ACK,TCP 就會執(zhí)行一種快速重傳并進(jìn)入恢復(fù)狀態(tài)。
擁塞避免
當(dāng) TCP 進(jìn)入擁塞控制狀態(tài)后,cwnd 的值就等于擁塞時(shí)值的一半,也就是 ssthresh 的值。所以,無法每次報(bào)文段到達(dá)后都將 cwnd 的值再翻倍。而是采用了一種相對保守的方式,每次傳輸完成后只將 cwnd 的值增加一個(gè) MSS,比如收到了 10 個(gè)報(bào)文段的確認(rèn),但是 cwnd 的值只增加一個(gè) MSS。這是一種線性增長模式,它也會有增長逾值,它的增長逾值和慢啟動(dòng)一樣,如果出現(xiàn)丟包,那么 cwnd 的值就是一個(gè) MSS,ssthresh 的值就等于 cwnd 的一半;或者是收到 3 個(gè)冗余的 ACK 響應(yīng)也能停止 MSS 增長。如果 TCP 將 cwnd 的值減半后,仍然會收到 3 個(gè)冗余 ACK,那么就會將 ssthresh 的值記錄為 cwnd 值的一半,進(jìn)入 快速恢復(fù) 狀態(tài)。
快速恢復(fù)
在快速恢復(fù)中,對于使 TCP 進(jìn)入快速恢復(fù)狀態(tài)缺失的報(bào)文段,對于每個(gè)收到的冗余 ACK,cwnd 的值都會增加一個(gè) MSS 。當(dāng)對丟失報(bào)文段的一個(gè) ACK 到達(dá)時(shí),TCP 在降低 cwnd 后進(jìn)入擁塞避免狀態(tài)。如果在擁塞控制狀態(tài)后出現(xiàn)超時(shí),那么就會遷移到慢啟動(dòng)狀態(tài),cwnd 的值被設(shè)置為 1 個(gè) MSS,ssthresh 的值設(shè)置為 cwnd 的一半。
后記
這篇文章的內(nèi)容對你有幫助嗎?歡迎大家點(diǎn)贊、在看、分享、轉(zhuǎn)發(fā)這篇文章,你的支持是我寫作最大的動(dòng)力!
這里是程序員cxuan,我們下期再見!
完
往期推薦
??
另外,cxuan 肝了六本 PDF,公號回復(fù) cxuan ,領(lǐng)取作者全部 PDF 。

