成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

徹底弄懂TCP協(xié)議:從三次握手說(shuō)起

開(kāi)發(fā) 開(kāi)發(fā)工具
本文就是來(lái)說(shuō)說(shuō)這些頭疼點(diǎn)的,淺談一些 TCP 的疑難雜癥。那么從哪說(shuō)起呢?當(dāng)然是從三次握手和四次揮手說(shuō)起啦,可能大家都知道 TCP 是三次交互完成連接的建立,四次交互來(lái)斷開(kāi)一個(gè)連接,那為什么是三次握手和四次揮手呢?反過(guò)來(lái)不行嗎?

[[339497]]

作者:morganhuang,騰訊 IEG 后臺(tái)開(kāi)發(fā)工程師

說(shuō)到 TCP 協(xié)議,相信大家都比較熟悉了,對(duì)于 TCP 協(xié)議總能說(shuō)個(gè)一二三來(lái),但是 TCP 協(xié)議又是一個(gè)非常復(fù)雜的協(xié)議,其中有不少細(xì)節(jié)點(diǎn)讓人頭疼點(diǎn)。本文就是來(lái)說(shuō)說(shuō)這些頭疼點(diǎn)的,淺談一些 TCP 的疑難雜癥。那么從哪說(shuō)起呢?當(dāng)然是從三次握手和四次揮手說(shuō)起啦,可能大家都知道 TCP 是三次交互完成連接的建立,四次交互來(lái)斷開(kāi)一個(gè)連接,那為什么是三次握手和四次揮手呢?反過(guò)來(lái)不行嗎?

疑癥(1)TCP 的三次握手、四次揮手

下面兩圖大家再熟悉不過(guò)了,TCP 的三次握手和四次揮手見(jiàn)下面左邊的”TCP 建立連接”、”TCP 數(shù)據(jù)傳送”、”TCP 斷開(kāi)連接”時(shí)序圖和右邊的”TCP 協(xié)議狀態(tài)機(jī)” 。

TCP三次握手、四次揮手時(shí)序圖

TCP協(xié)議狀態(tài)機(jī)

要弄清 TCP 建立連接需要幾次交互才行,我們需要弄清建立連接進(jìn)行初始化的目標(biāo)是什么。TCP 進(jìn)行握手初始化一個(gè)連接的目標(biāo)是:分配資源、初始化序列號(hào)(通知 peer 對(duì)端我的初始序列號(hào)是多少),知道初始化連接的目標(biāo),那么要達(dá)成這個(gè)目標(biāo)的過(guò)程就簡(jiǎn)單了,握手過(guò)程可以簡(jiǎn)化為下面的四次交互:

1)client 端首先發(fā)送一個(gè) SYN 包告訴 Server 端我的初始序列號(hào)是 X;2)Server 端收到 SYN 包后回復(fù)給 client 一個(gè) ACK 確認(rèn)包,告訴 client 說(shuō)我收到了;3)接著 Server 端也需要告訴 client 端自己的初始序列號(hào),于是 Server 也發(fā)送一個(gè) SYN 包告訴 client 我的初始序列號(hào)是 Y;4)Client 收到后,回復(fù) Server 一個(gè) ACK 確認(rèn)包說(shuō)我知道了。

整個(gè)過(guò)程 4 次交互即可完成初始化,但是,細(xì)心的同學(xué)會(huì)發(fā)現(xiàn)兩個(gè)問(wèn)題:

  • Server 發(fā)送 SYN 包是作為發(fā)起連接的 SYN 包,還是作為響應(yīng)發(fā)起者的 SYN 包呢?怎么區(qū)分?比較容易引起混淆
  • Server 的 ACK 確認(rèn)包和接下來(lái)的 SYN 包可以合成一個(gè) SYN ACK 包一起發(fā)送的,沒(méi)必要分別單獨(dú)發(fā)送,這樣省了一次交互同時(shí)也解決了問(wèn)題[1].這樣 TCP 建立一個(gè)連接,三次握手在進(jìn)行最少次交互的情況下完成了 Peer 兩端的資源分配和初始化序列號(hào)的交換。

大部分情況下建立連接需要三次握手,也不一定都是三次,有可能出現(xiàn)四次握手來(lái)建立連接的。如下圖,當(dāng) Peer 兩端同時(shí)發(fā)起 SYN 來(lái)建立連接的時(shí)候,就出現(xiàn)了四次握手來(lái)建立連接(對(duì)于有些 TCP/IP 的實(shí)現(xiàn),可能不支持這種同時(shí)打開(kāi)的情況)。

在三次握手過(guò)程中,細(xì)心的同學(xué)可能會(huì)有以下疑問(wèn):

  • 初始化序列號(hào) X、Y 是可以是寫(xiě)死固定的嗎,為什么不能呢?
  • 假如 Client 發(fā)送一個(gè) SYN 包給 Server 后就掛了或是不管了,這個(gè)時(shí)候這個(gè)連接處于什么狀態(tài)呢?會(huì)超時(shí)嗎?為什么呢?

TCP 進(jìn)行斷開(kāi)連接的目標(biāo)是:回收資源、終止數(shù)據(jù)傳輸。由于 TCP 是全雙工的,需要 Peer 兩端分別各自拆除自己通向 Peer 對(duì)端的方向的通信信道。這樣需要四次揮手來(lái)分別拆除通信信道,就比較清晰明了了。

1)Client 發(fā)送一個(gè) FIN 包來(lái)告訴 Server 我已經(jīng)沒(méi)數(shù)據(jù)需要發(fā)給 Server 了;2)Server 收到后回復(fù)一個(gè) ACK 確認(rèn)包說(shuō)我知道了;3)然后 server 在自己也沒(méi)數(shù)據(jù)發(fā)送給 client 后,Server 也發(fā)送一個(gè) FIN 包給 Client 告訴 Client 我也已經(jīng)沒(méi)數(shù)據(jù)發(fā)給 client 了;4)Client 收到后,就會(huì)回復(fù)一個(gè) ACK 確認(rèn)包說(shuō)我知道了。

到此,四次揮手,這個(gè) TCP 連接就可以完全拆除了。在四次揮手的過(guò)程中,細(xì)心的同學(xué)可能會(huì)有以下疑問(wèn):

  • Client 和 Server 同時(shí)發(fā)起斷開(kāi)連接的 FIN 包會(huì)怎么樣呢,TCP 狀態(tài)是怎么轉(zhuǎn)移的?
  • 左側(cè)圖中的四次揮手過(guò)程中,Server 端的 ACK 確認(rèn)包能不能和接下來(lái)的 FIN 包合并成一個(gè)包呢,這樣四次揮手就變成三次揮手了。
  • 四次揮手過(guò)程中,首先斷開(kāi)連接的一端,在回復(fù)最后一個(gè) ACK 后,為什么要進(jìn)行 TIME_WAIT 呢(超時(shí)設(shè)置是 2*MSL,RFC793 定義了 MSL 為 2 分鐘,Linux 設(shè)置成了 30s),在 TIME_WAIT 的時(shí)候又不能釋放資源,白白讓資源占用那么長(zhǎng)時(shí)間,能不能省了 TIME_WAIT 呢,為什么?

疑癥(2)TCP 連接的初始化序列號(hào)能否固定

如果初始化序列號(hào)(縮寫(xiě)為 ISN:Inital Sequence Number)可以固定,我們來(lái)看看會(huì)出現(xiàn)什么問(wèn)題。假設(shè) ISN 固定是 1,Client 和 Server 建立好一條 TCP 連接后,Client 連續(xù)給 Server 發(fā)了 10 個(gè)包,這 10 個(gè)包不知怎么被鏈路上的路由器緩存了(路由器會(huì)毫無(wú)先兆地緩存或者丟棄任何的數(shù)據(jù)包),這個(gè)時(shí)候碰巧 Client 掛掉了,然后 Client 用同樣的端口號(hào)重新連上 Server,Client 又連續(xù)給 Server 發(fā)了幾個(gè)包,假設(shè)這個(gè)時(shí)候 Client 的序列號(hào)變成了 5。

接著,之前被路由器緩存的 10 個(gè)數(shù)據(jù)包全部被路由到 Server 端了,Server 給 Client 回復(fù)確認(rèn)號(hào) 10,這個(gè)時(shí)候,Client 整個(gè)都不好了,這是什么情況?我的序列號(hào)才到 5,你怎么給我的確認(rèn)號(hào)是 10 了,整個(gè)都亂了。RFC793 中,建議 ISN 和一個(gè)假的時(shí)鐘綁在一起,這個(gè)時(shí)鐘會(huì)在每 4 微秒對(duì) ISN 做加一操作,直到超過(guò) 2^32,又從 0 開(kāi)始,這需要 4 小時(shí)才會(huì)產(chǎn)生 ISN 的回繞問(wèn)題,這幾乎可以保證每個(gè)新連接的 ISN 不會(huì)和舊的連接的 ISN 產(chǎn)生沖突。這種遞增方式的 ISN,很容易讓攻擊者猜測(cè)到 TCP 連接的 ISN,現(xiàn)在的實(shí)現(xiàn)大多是在一個(gè)基準(zhǔn)值的基礎(chǔ)上進(jìn)行隨機(jī)的。

疑癥(3)初始化連接的 SYN 超時(shí)問(wèn)題

Client 發(fā)送 SYN 包給 Server 后掛了,Server 回給 Client 的 SYN-ACK 一直沒(méi)收到 Client 的 ACK 確認(rèn),這個(gè)時(shí)候這個(gè)連接既沒(méi)建立起來(lái),也不能算失敗。這就需要一個(gè)超時(shí)時(shí)間讓 Server 將這個(gè)連接斷開(kāi),否則這個(gè)連接就會(huì)一直占用 Server 的 SYN 連接隊(duì)列中的一個(gè)位置,大量這樣的連接就會(huì)將 Server 的 SYN 連接隊(duì)列耗盡,讓正常的連接無(wú)法得到處理。目前,Linux 下默認(rèn)會(huì)進(jìn)行 5 次重發(fā) SYN-ACK 包,重試的間隔時(shí)間從 1s 開(kāi)始,下次的重試間隔時(shí)間是前一次的雙倍,5 次的重試時(shí)間間隔為 1s,2s, 4s, 8s,16s,總共 31s,第 5 次發(fā)出后還要等 32s 都知道第 5 次也超時(shí)了,所以,總共需要 1s + 2s +4s+ 8s+ 16s + 32s =63s,TCP 才會(huì)把斷開(kāi)這個(gè)連接。

由于,SYN 超時(shí)需要 63 秒,那么就給攻擊者一個(gè)攻擊服務(wù)器的機(jī)會(huì),攻擊者在短時(shí)間內(nèi)發(fā)送大量的 SYN 包給 Server(俗稱(chēng) SYN flood 攻擊),用于耗盡 Server 的 SYN 隊(duì)列。對(duì)于應(yīng)對(duì) SYN 過(guò)多的問(wèn)題,linux 提供了幾個(gè) TCP 參數(shù):tcp_syncookies、tcp_synack_retries、tcp_max_syn_backlog、tcp_abort_on_overflow 來(lái)調(diào)整應(yīng)對(duì)。

疑癥(4) TCP 的 Peer 兩端同時(shí)斷開(kāi)連接

由上面的”TCP 協(xié)議狀態(tài)機(jī)“圖可以看出,TCP 的 Peer 端在收到對(duì)端的 FIN 包前發(fā)出了 FIN 包,那么該 Peer 的狀態(tài)就變成了 FIN_WAIT1,Peer 在 FIN_WAIT1 狀態(tài)下收到對(duì)端 Peer 對(duì)自己 FIN 包的 ACK 包的話(huà),那么 Peer 狀態(tài)就變成 FIN_WAIT2,Peer 在 FIN_WAIT2 下收到對(duì)端 Peer 的 FIN 包,在確認(rèn)已經(jīng)收到了對(duì)端 Peer 全部的 Data 數(shù)據(jù)包后,就響應(yīng)一個(gè) ACK 給對(duì)端 Peer,然后自己進(jìn)入 TIME_WAIT 狀態(tài)。

但是如果 Peer 在 FIN_WAIT1 狀態(tài)下首先收到對(duì)端 Peer 的 FIN 包的話(huà),那么該 Peer 在確認(rèn)已經(jīng)收到了對(duì)端 Peer 全部的 Data 數(shù)據(jù)包后,就響應(yīng)一個(gè) ACK 給對(duì)端 Peer,然后自己進(jìn)入 CLOSEING 狀態(tài),Peer 在 CLOSEING 狀態(tài)下收到自己的 FIN 包的 ACK 包的話(huà),那么就進(jìn)入 TIME WAIT 狀態(tài)。于是,TCP 的 Peer 兩端同時(shí)發(fā)起 FIN 包進(jìn)行斷開(kāi)連接,那么兩端 Peer 可能出現(xiàn)完全一樣的狀態(tài)轉(zhuǎn)移 FIN_WAIT1——>CLOSEING——->TIME_WAIT,也就會(huì) Client 和 Server 最后同時(shí)進(jìn)入 TIME_WAIT 狀態(tài)。同時(shí)關(guān)閉連接的狀態(tài)轉(zhuǎn)移如下圖所示:

疑癥(5)四次揮手能不能變成三次揮手呢??

答案是可能的。TCP 是全雙工通信,Cliet 在自己已經(jīng)不會(huì)在有新的數(shù)據(jù)要發(fā)送給 Server 后,可以發(fā)送 FIN 信號(hào)告知 Server,這邊已經(jīng)終止 Client 到對(duì)端 Server 那邊的數(shù)據(jù)傳輸。但是,這個(gè)時(shí)候?qū)Χ?Server 可以繼續(xù)往 Client 這邊發(fā)送數(shù)據(jù)包。于是,兩端數(shù)據(jù)傳輸?shù)慕K止在時(shí)序上是獨(dú)立并且可能會(huì)相隔比較長(zhǎng)的時(shí)間,這個(gè)時(shí)候就必須最少需要 2+2= 4 次揮手來(lái)完全終止這個(gè)連接。但是,如果 Server 在收到 Client 的 FIN 包后,在也沒(méi)數(shù)據(jù)需要發(fā)送給 Client 了,那么對(duì) Client 的 ACK 包和 Server 自己的 FIN 包就可以合并成為一個(gè)包發(fā)送過(guò)去,這樣四次揮手就可以變成三次了(似乎 linux 協(xié)議棧就是這樣實(shí)現(xiàn)的)

疑癥(6) TCP 的頭號(hào)疼癥 TIME_WAIT 狀態(tài)

要說(shuō)明 TIME_WAIT 的問(wèn)題,需要解答以下幾個(gè)問(wèn)題

一、Peer 兩端,哪一端會(huì)進(jìn)入 TIME_WAIT 呢?為什么?

相信大家都知道,TCP 主動(dòng)關(guān)閉連接的那一方會(huì)最后進(jìn)入 TIME_WAIT。那么怎么界定主動(dòng)關(guān)閉方呢?是否主動(dòng)關(guān)閉是由 FIN 包的先后決定的,就是在自己沒(méi)收到對(duì)端 Peer 的 FIN 包之前自己發(fā)出了 FIN 包,那么自己就是主動(dòng)關(guān)閉連接的那一方。對(duì)于疑癥(4)中描述的情況,那么 Peer 兩邊都是主動(dòng)關(guān)閉的一方,兩邊都會(huì)進(jìn)入 TIME_WAIT。為什么是主動(dòng)關(guān)閉的一方進(jìn)行 TIME_WAIT 呢,被動(dòng)關(guān)閉的進(jìn)入 TIME_WAIT 可以不呢?我們來(lái)看看 TCP 四次揮手可以簡(jiǎn)單分為下面三個(gè)過(guò)程:

過(guò)程一.主動(dòng)關(guān)閉方發(fā)送 FIN;過(guò)程二.被動(dòng)關(guān)閉方收到主動(dòng)關(guān)閉方的 FIN 后發(fā)送該 FIN 的 ACK,被動(dòng)關(guān)閉方發(fā)送 FIN;過(guò)程三.主動(dòng)關(guān)閉方收到被動(dòng)關(guān)閉方的 FIN 后發(fā)送該 FIN 的 ACK,被動(dòng)關(guān)閉方等待自己 FIN 的 ACK。

問(wèn)題就在過(guò)程三中,據(jù) TCP 協(xié)議規(guī)范,不對(duì) ACK 進(jìn)行 ACK,如果主動(dòng)關(guān)閉方不進(jìn)入 TIME_WAIT,那么主動(dòng)關(guān)閉方在發(fā)送完 ACK 就走了的話(huà),如果最后發(fā)送的 ACK 在路由過(guò)程中丟掉了,最后沒(méi)能到被動(dòng)關(guān)閉方,這個(gè)時(shí)候被動(dòng)關(guān)閉方?jīng)]收到自己 FIN 的 ACK 就不能關(guān)閉連接,接著被動(dòng)關(guān)閉方會(huì)超時(shí)重發(fā) FIN 包,但是這個(gè)時(shí)候已經(jīng)沒(méi)有對(duì)端會(huì)給該 FIN 回 ACK,被動(dòng)關(guān)閉方就無(wú)法正常關(guān)閉連接了,所以主動(dòng)關(guān)閉方需要進(jìn)入 TIME_WAIT 以便能夠重發(fā)丟掉的被動(dòng)關(guān)閉方 FIN 的 ACK。

二、TIME_WAIT 狀態(tài)是用來(lái)解決或避免什么問(wèn)題呢?

TIME_WAIT 主要是用來(lái)解決以下幾個(gè)問(wèn)題:

1)上面解釋為什么主動(dòng)關(guān)閉方需要進(jìn)入 TIME_WAIT 狀態(tài)中提到的:主動(dòng)關(guān)閉方需要進(jìn)入 TIME_WAIT 以便能夠重發(fā)丟掉的被動(dòng)關(guān)閉方 FIN 包的 ACK。如果主動(dòng)關(guān)閉方不進(jìn)入 TIME_WAIT,那么在主動(dòng)關(guān)閉方對(duì)被動(dòng)關(guān)閉方 FIN 包的 ACK 丟失了的時(shí)候,被動(dòng)關(guān)閉方由于沒(méi)收到自己 FIN 的 ACK,會(huì)進(jìn)行重傳 FIN 包,這個(gè) FIN 包到主動(dòng)關(guān)閉方后,由于這個(gè)連接已經(jīng)不存在于主動(dòng)關(guān)閉方了,這個(gè)時(shí)候主動(dòng)關(guān)閉方無(wú)法識(shí)別這個(gè) FIN 包,協(xié)議棧會(huì)認(rèn)為對(duì)方瘋了,都還沒(méi)建立連接你給我來(lái)個(gè) FIN 包?,于是回復(fù)一個(gè) RST 包給被動(dòng)關(guān)閉方,被動(dòng)關(guān)閉方就會(huì)收到一個(gè)錯(cuò)誤(我們見(jiàn)的比較多的:connect reset by peer,這里順便說(shuō)下 Broken pipe,在收到 RST 包的時(shí)候,還往這個(gè)連接寫(xiě)數(shù)據(jù),就會(huì)收到 Broken pipe 錯(cuò)誤了),原本應(yīng)該正常關(guān)閉的連接,給我來(lái)個(gè)錯(cuò)誤,很難讓人接受。

2)防止已經(jīng)斷開(kāi)的連接 1 中在鏈路中殘留的 FIN 包終止掉新的連接 2(重用了連接 1 的所有的 5 元素(源 IP,目的 IP,TCP,源端口,目的端口)),這個(gè)概率比較低,因?yàn)樯婕暗揭粋€(gè)匹配問(wèn)題,遲到的 FIN 分段的序列號(hào)必須落在連接 2 的一方的期望序列號(hào)范圍之內(nèi),雖然概率低,但是確實(shí)可能發(fā)生,因?yàn)槌跏夹蛄刑?hào)都是隨機(jī)產(chǎn)生的,并且這個(gè)序列號(hào)是 32 位的,會(huì)回繞。

3)防止鏈路上已經(jīng)關(guān)閉的連接的殘余數(shù)據(jù)包(a lost duplicate packet or a wandering duplicate packet) 干擾正常的數(shù)據(jù)包,造成數(shù)據(jù)流的不正常。這個(gè)問(wèn)題和 2)類(lèi)似。

三、TIME_WAIT 會(huì)帶來(lái)哪些問(wèn)題呢?

TIME_WAIT 帶來(lái)的問(wèn)題注意是源于:一個(gè)連接進(jìn)入 TIME_WAIT 狀態(tài)后需要等待 2*MSL(一般是 1 到 4 分鐘)那么長(zhǎng)的時(shí)間才能斷開(kāi)連接釋放連接占用的資源,會(huì)造成以下問(wèn)題:

  • 作為服務(wù)器,短時(shí)間內(nèi)關(guān)閉了大量的 Client 連接,就會(huì)造成服務(wù)器上出現(xiàn)大量的 TIME_WAIT 連接,占據(jù)大量的 tuple,嚴(yán)重消耗著服務(wù)器的資源。
  • 作為客戶(hù)端,短時(shí)間內(nèi)大量的短連接,會(huì)大量消耗的 Client 機(jī)器的端口,畢竟端口只有 65535 個(gè),端口被耗盡了,后續(xù)就無(wú)法在發(fā)起新的連接了。

由于上面兩個(gè)問(wèn)題,作為客戶(hù)端需要連本機(jī)的一個(gè)服務(wù)的時(shí)候,首選 UNIX 域套接字而不是 TCP)。TIME_WAIT 很令人頭疼,很多問(wèn)題是由 TIME_WAIT 造成的,但是 TIME_WAIT 又不是多余的不能簡(jiǎn)單將 TIME_WAIT 去掉,那么怎么來(lái)解決或緩解 TIME_WAIT 問(wèn)題呢?可以進(jìn)行 TIME_WAIT 的快速回收和重用來(lái)緩解 TIME_WAIT 的問(wèn)題。有沒(méi)一些清掉 TIME_WAIT 的技巧呢?

四、TIME_WAIT 的快速回收和重用

【1】TIME_WAIT 快速回收l(shuí)inux 下開(kāi)啟 TIME_WAIT 快速回收需要同時(shí)打開(kāi) tcp_tw_recycle 和 tcp_timestamps(默認(rèn)打開(kāi))兩選項(xiàng)。Linux 下快速回收的時(shí)間為 3.5* RTO(Retransmission Timeout),而一個(gè) RTO 時(shí)間為 200ms 至 120s。開(kāi)啟快速回收 TIME_WAIT,可能會(huì)帶來(lái)(問(wèn)題一、)中說(shuō)的三點(diǎn)危險(xiǎn),為了避免這些危險(xiǎn),要求同時(shí)滿(mǎn)足以下三種情況的新連接要被拒絕掉:

  • 來(lái)自同一個(gè)對(duì)端 Peer 的 TCP 包攜帶了時(shí)間戳;
  • 之前同一臺(tái) peer 機(jī)器(僅僅識(shí)別 IP 地址,因?yàn)檫B接被快速釋放了,沒(méi)了端口信息)的某個(gè) TCP 數(shù)據(jù)在 MSL 秒之內(nèi)到過(guò)本 Server;
  • Peer 機(jī)器新連接的時(shí)間戳小于 peer 機(jī)器上次 TCP 到來(lái)時(shí)的時(shí)間戳,且差值大于重放窗口戳(TCP_PAWS_WINDOW)。

初看起來(lái)正常的數(shù)據(jù)包同時(shí)滿(mǎn)足下面 3 條幾乎不可能,因?yàn)闄C(jī)器的時(shí)間戳不可能倒流的,出現(xiàn)上述的 3 點(diǎn)均滿(mǎn)足時(shí),一定是老的重復(fù)數(shù)據(jù)包又回來(lái)了,丟棄老的 SYN 包是正常的。到此,似乎啟用快速回收就能很大程度緩解 TIME_WAIT 帶來(lái)的問(wèn)題。但是,這里忽略了一個(gè)東西就是 NAT。

在一個(gè) NAT 后面的所有 Peer 機(jī)器在 Server 看來(lái)都是一個(gè)機(jī)器,NAT 后面的那么多 Peer 機(jī)器的系統(tǒng)時(shí)間戳很可能不一致,有些快,有些慢。這樣,在 Server 關(guān)閉了與系統(tǒng)時(shí)間戳快的 Client 的連接后,在這個(gè)連接進(jìn)入快速回收的時(shí)候,同一 NAT 后面的系統(tǒng)時(shí)間戳慢的 Client 向 Server 發(fā)起連接,這就很有可能同時(shí)滿(mǎn)足上面的三種情況,造成該連接被 Server 拒絕掉。所以,在是否開(kāi)啟 tcp_tw_recycle 需要慎重考慮了

【2】TIME_WAIT 重用

linux 上比較完美的實(shí)現(xiàn)了 TIME_WAIT 重用問(wèn)題。只要滿(mǎn)足下面兩點(diǎn)中的一點(diǎn),一個(gè) TW 狀態(tài)的四元組(即一個(gè) socket 連接)可以重新被新到來(lái)的 SYN 連接使用。

[1]. 新連接 SYN 告知的初始序列號(hào)比 TIME_WAIT 老連接的末序列號(hào)大;[2]. 如果開(kāi)啟了 tcp_timestamps,并且新到來(lái)的連接的時(shí)間戳比老連接的時(shí)間戳大。

要同時(shí)開(kāi)啟 tcp_tw_reuse 選項(xiàng)和 tcp_timestamps 選項(xiàng)才可以開(kāi)啟 TIME_WAIT 重用,還有一個(gè)條件是:重用 TIME_WAIT 的條件是收到最后一個(gè)包后超過(guò) 1s。細(xì)心的同學(xué)可能發(fā)現(xiàn) TIME_WAIT 重用對(duì) Server 端來(lái)說(shuō)并沒(méi)解決大量 TIME_WAIT 造成的資源消耗的問(wèn)題,因?yàn)椴还?TIME_WAIT 連接是否被重用,它依舊占用著系統(tǒng)資源。即便如此,TIME_WAIT 重用還是有些用處的,它解決了整機(jī)范圍拒絕接入的問(wèn)題,雖然一般一個(gè)單獨(dú)的 Client 是不可能在 MSL 內(nèi)用同一個(gè)端口連接同一個(gè)服務(wù)的,但是如果 Client 做了 bind 端口那就是同個(gè)端口了。時(shí)間戳重用 TIME_WAIT 連接的機(jī)制的前提是 IP 地址唯一性,得出新請(qǐng)求發(fā)起自同一臺(tái)機(jī)器,但是如果是 NAT 環(huán)境下就不能這樣保證了,于是在 NAT 環(huán)境下,TIME_WAIT 重用還是有風(fēng)險(xiǎn)的。

有些同學(xué)可能會(huì)混淆 tcp_tw_reuse 和 SO_REUSEADDR 選項(xiàng),認(rèn)為是相關(guān)的一個(gè)東西,其實(shí)他們是兩個(gè)完全不同的東西,可以說(shuō)兩個(gè)半毛錢(qián)關(guān)系都沒(méi)。tcp_tw_reuse 是內(nèi)核選項(xiàng),而 SO_REUSEADDR 用戶(hù)態(tài)的選項(xiàng),使用 SO_REUSEADDR 是告訴內(nèi)核,如果端口忙,但 TCP 狀態(tài)位于 TIME_WAIT,可以重用端口。如果端口忙,而 TCP 狀態(tài)位于其他狀態(tài),重用端口時(shí)依舊得到一個(gè)錯(cuò)誤信息,指明 Address already in use”。如果你的服務(wù)程序停止后想立即重啟,而新套接字依舊使用同一端口,此時(shí) SO_REUSEADDR 選項(xiàng)非常有用。但是,使用這個(gè)選項(xiàng)就會(huì)有(問(wèn)題二、)中說(shuō)的三點(diǎn)危險(xiǎn),雖然發(fā)生的概率不大。

五、清掉 TIME_WAIT 的奇技怪巧

可以用下面兩種方式控制服務(wù)器的 TIME_WAIT 數(shù)量:

【1】修改 tcp_max_tw_buckets

tcp_max_tw_buckets 控制并發(fā)的 TIME_WAIT 的數(shù)量,默認(rèn)值是 180000。如果超過(guò)默認(rèn)值,內(nèi)核會(huì)把多的 TIME_WAIT 連接清掉,然后在日志里打一個(gè)警告。官網(wǎng)文檔說(shuō)這個(gè)選項(xiàng)只是為了阻止一些簡(jiǎn)單的 DoS 攻擊,平常不要人為的降低它。

【2】利用 RST 包從外部清掉 TIME_WAIT 鏈接

根據(jù) TCP 規(guī)范,收到任何的發(fā)送到未偵聽(tīng)端口、已經(jīng)關(guān)閉的連接的數(shù)據(jù)包、連接處于任何非同步狀態(tài)(LISTEN,SYS-SENT,SYN-RECEIVED)并且收到的包的 ACK 在窗口外,或者安全層不匹配,都要回執(zhí)以 RST 響應(yīng)(而收到滑動(dòng)窗口外的序列號(hào)的數(shù)據(jù)包,都要丟棄這個(gè)數(shù)據(jù)包,并回復(fù)一個(gè) ACK 包),內(nèi)核收到 RST 將會(huì)產(chǎn)生一個(gè)錯(cuò)誤并終止該連接。我們可以利用 RST 包來(lái)終止掉處于 TIME_WAIT 狀態(tài)的連接,其實(shí)這就是所謂的 RST 攻擊了。

為了描述方便:假設(shè) Client 和 Server 有個(gè)連接 Connect1,Server 主動(dòng)關(guān)閉連接并進(jìn)入了 TIME_WAIT 狀態(tài),我們來(lái)描述一下怎么從外部使得 Server 的處于 TIME_WAIT 狀態(tài)的連接 Connect1 提前終止掉。要實(shí)現(xiàn)這個(gè) RST 攻擊,首先我們要知道 Client 在 Connect1 中的端口 port1(一般這個(gè)端口是隨機(jī)的,比較難猜到,這也是 RST 攻擊較難的一個(gè)點(diǎn)),利用 IP_TRANSPARENT 這個(gè) socket 選項(xiàng),它可以 bind 不屬于本地的地址,因此可以從任意機(jī)器綁定 Client 地址以及端口 port1,然后向 Server 發(fā)起一個(gè)連接,Server 收到了窗口外的包于是響應(yīng)一個(gè) ACK,這個(gè) ACK 包會(huì)路由到 Client 處。

這個(gè)時(shí)候 99%的可能 Client 已經(jīng)釋放連接 connect1 了,這個(gè)時(shí)候 Client 收到這個(gè) ACK 包,會(huì)發(fā)送一個(gè) RST 包,server 收到 RST 包然后就釋放連接 connect1 提前終止 TIME_WAIT 狀態(tài)了。提前終止 TIME_WAIT 狀態(tài)是可能會(huì)帶來(lái)(問(wèn)題二)中說(shuō)的三點(diǎn)危害,具體的危害情況可以看下 RFC1337。RFC1337 中建議,不要用 RST 過(guò)早的結(jié)束 TIME_WAIT 狀態(tài)。

至此,上面的疑癥都解析完畢,然而細(xì)心的同學(xué)會(huì)有下面的疑問(wèn):

  • TCP 的可靠傳輸是確認(rèn)號(hào)來(lái)實(shí)現(xiàn)的,那么 TCP 的確認(rèn)機(jī)制是怎樣的呢?是收到一個(gè)包就馬上確認(rèn),還是可以稍等一下在確認(rèn)呢?
  • 假如發(fā)送一個(gè)包,一直都沒(méi)收到確認(rèn)呢?什么時(shí)候重傳呢?超時(shí)機(jī)制的怎樣的?
  • TCP 兩端 Peer 的處理能力不對(duì)等的時(shí)候,比如發(fā)送方處理能力很強(qiáng),接收方處理能力很弱,這樣發(fā)送方是否能夠不管接收方死活狂發(fā)數(shù)據(jù)呢?如果不能,流量控制機(jī)制的如何的?
  • TCP 是端到端的協(xié)議,也就是 TCP 對(duì)端 Peer 只看到對(duì)方,看不到網(wǎng)絡(luò)上的其他點(diǎn),那么 TCP 的兩端怎么對(duì)網(wǎng)絡(luò)情況做出反映呢?發(fā)生擁塞的時(shí)候,擁塞控制機(jī)制是如何的?

疑癥(7)TCP 的延遲確認(rèn)機(jī)制

按照 TCP 協(xié)議,確認(rèn)機(jī)制是累積的,也就是確認(rèn)號(hào) X 的確認(rèn)指示的是所有 X 之前但不包括 X 的數(shù)據(jù)已經(jīng)收到了。確認(rèn)號(hào)(ACK)本身就是不含數(shù)據(jù)的分段,因此大量的確認(rèn)號(hào)消耗了大量的帶寬,雖然大多數(shù)情況下,ACK 還是可以和數(shù)據(jù)一起捎帶傳輸?shù)模侨绻麤](méi)有捎帶傳輸,那么就只能單獨(dú)回來(lái)一個(gè) ACK,如果這樣的分段太多,網(wǎng)絡(luò)的利用率就會(huì)下降。為緩解這個(gè)問(wèn)題,RFC 建議了一種延遲的 ACK,也就是說(shuō),ACK 在收到數(shù)據(jù)后并不馬上回復(fù),而是延遲一段可以接受的時(shí)間,延遲一段時(shí)間的目的是看能不能和接收方要發(fā)給發(fā)送方的數(shù)據(jù)一起回去,因?yàn)?TCP 協(xié)議頭中總是包含確認(rèn)號(hào)的,如果能的話(huà),就將數(shù)據(jù)一起捎帶回去,這樣網(wǎng)絡(luò)利用率就提高了。

延遲 ACK 就算沒(méi)有數(shù)據(jù)捎帶,那么如果收到了按序的兩個(gè)包,那么只要對(duì)第二包做確認(rèn)即可,這樣也能省去一個(gè) ACK 消耗。由于 TCP 協(xié)議不對(duì) ACK 進(jìn)行 ACK 的,RFC 建議最多等待 2 個(gè)包的積累確認(rèn),這樣能夠及時(shí)通知對(duì)端 Peer,我這邊的接收情況。Linux 實(shí)現(xiàn)中,有延遲 ACK 和快速 ACK,并根據(jù)當(dāng)前的包的收發(fā)情況來(lái)在這兩種 ACK 中切換。一般情況下,ACK 并不會(huì)對(duì)網(wǎng)絡(luò)性能有太大的影響,延遲 ACK 能減少發(fā)送的分段從而節(jié)省了帶寬,而快速 ACK 能及時(shí)通知發(fā)送方丟包,避免滑動(dòng)窗口停等,提升吞吐率。

關(guān)于 ACK 分段,有個(gè)細(xì)節(jié)需要說(shuō)明一下,ACK 的確認(rèn)號(hào),是確認(rèn)按序收到的最后一個(gè)字節(jié)序,對(duì)于亂序到來(lái)的 TCP 分段,接收端會(huì)回復(fù)相同的 ACK 分段,只確認(rèn)按序到達(dá)的最后一個(gè) TCP 分段。TCP 連接的延遲確認(rèn)時(shí)間一般初始化為最小值 40ms,隨后根據(jù)連接的重傳超時(shí)時(shí)間(RTO)、上次收到數(shù)據(jù)包與本次接收數(shù)據(jù)包的時(shí)間間隔等參數(shù)進(jìn)行不斷調(diào)整。

疑癥(8)TCP 的重傳機(jī)制以及重傳的超時(shí)計(jì)算

【1】TCP 的重傳超時(shí)計(jì)算

TCP 交互過(guò)程中,如果發(fā)送的包一直沒(méi)收到 ACK 確認(rèn),是要一直等下去嗎?顯然不能一直等(如果發(fā)送的包在路由過(guò)程中丟失了,對(duì)端都沒(méi)收到又如何給你發(fā)送確認(rèn)呢?),這樣協(xié)議將不可用,既然不能一直等下去,那么該等多久呢?等太長(zhǎng)時(shí)間的話(huà),數(shù)據(jù)包都丟了很久了才重發(fā),沒(méi)有效率,性能差;等太短時(shí)間的話(huà),可能 ACK 還在路上快到了,這時(shí)候卻重傳了,造成浪費(fèi),同時(shí)過(guò)多的重傳會(huì)造成網(wǎng)絡(luò)擁塞,進(jìn)一步加劇數(shù)據(jù)的丟失。也是,我們不能去猜測(cè)一個(gè)重傳超時(shí)時(shí)間,應(yīng)該是通過(guò)一個(gè)算法去計(jì)算,并且這個(gè)超時(shí)時(shí)間應(yīng)該是隨著網(wǎng)絡(luò)的狀況在變化的。為了使我們的重傳機(jī)制更高效,如果我們能夠比較準(zhǔn)確知道在當(dāng)前網(wǎng)絡(luò)狀況下,一個(gè)數(shù)據(jù)包從發(fā)出去到回來(lái)的時(shí)間 RTT——Round Trip Time,那么根據(jù)這個(gè) RTT 我們就可以方便設(shè)置 TimeOut——RTO(Retransmission TimeOut)了。

為了計(jì)算這個(gè) RTO,RFC793 中定義了一個(gè)經(jīng)典算法,算法如下:

  • [1] 首先采樣計(jì)算RTT值
  • [2] 然后計(jì)算平滑的RTT,稱(chēng)為Smoothed Round Trip Time (SRTT),SRTT = ( ALPHA * SRTT ) + ((1-ALPHA) * RTT)
  • [3] RTO = min[UBOUND,max[LBOUND,(BETA*SRTT)]]

其中:UBOUND 是 RTO 值的上限;例如:可以定義為 1 分鐘,LBOUND 是 RTO 值的下限,例如,可以定義為 1 秒;ALPHA is a smoothing factor (e.g., .8 to .9), and BETA is a delay variance factor(e.g., 1.3 to 2.0).

然而這個(gè)算法有個(gè)缺點(diǎn)就是:在算 RTT 樣本的時(shí)候,是用第一次發(fā)數(shù)據(jù)的時(shí)間和 ack 回來(lái)的時(shí)間做 RTT 樣本值,還是用重傳的時(shí)間和 ACK 回來(lái)的時(shí)間做 RTT 樣本值?不管是怎么選擇,總會(huì)造成會(huì)要么把 RTT 算過(guò)長(zhǎng)了,要么把 RTT 算過(guò)短了。如下圖:(a)就計(jì)算過(guò)長(zhǎng)了,而(b)就是計(jì)算過(guò)短了。

針對(duì)上面經(jīng)典算法的缺陷,于是提出 Karn / Partridge Algorithm 對(duì)經(jīng)典算法進(jìn)行了改進(jìn)(算法大特點(diǎn)是——忽略重傳,不把重傳的 RTT 做采樣),但是這個(gè)算法有問(wèn)題:如果在某一時(shí)間,網(wǎng)絡(luò)閃動(dòng),突然變慢了,產(chǎn)生了比較大的延時(shí),這個(gè)延時(shí)導(dǎo)致要重轉(zhuǎn)所有的包(因?yàn)橹暗?RTO 很小),于是,因?yàn)橹剞D(zhuǎn)的不算,所以,RTO 就不會(huì)被更新,這是一個(gè)災(zāi)難。于是,為解決上面兩個(gè)算法的問(wèn)題,又有人推出來(lái)了一個(gè)新的算法,這個(gè)算法叫 Jacobson / Karels Algorithm(參看 FC6289),這個(gè)算法的核心是:除了考慮每?jī)纱螠y(cè)量值的偏差之外,其變化率也應(yīng)該考慮在內(nèi),如果變化率過(guò)大,則通過(guò)以變化率為自變量的函數(shù)為主計(jì)算 RTT(如果陡然增大,則取值為比較大的正數(shù),如果陡然減小,則取值為比較小的負(fù)數(shù),然后和平均值加權(quán)求和),反之如果變化率很小,則取測(cè)量平均值。

公式如下:(其中的 DevRTT 是 Deviation RTT 的意思)

  1. SRTT = SRTT + α (RTT – SRTT)  —— 計(jì)算平滑RTT 
  2. DevRTT = (1-β)*DevRTT + β*(|RTT-SRTT|) ——計(jì)算平滑RTT和真實(shí)的差距(加權(quán)移動(dòng)平均) 
  3. RTO= µ * SRTT + ∂ *DevRTT —— 神一樣的公式 
  4. (其中:在Linux下,α = 0.125,β = 0.25, μ = 1,∂ = 4 ——這就是算法中的“調(diào)得一手好參數(shù)”,nobody knows why, it just works…) 最后的這個(gè)算法在被用在今天的TCP協(xié)議中并工作非常好 

最后的這個(gè)算法在被用在今天的 TCP 協(xié)議中并工作非常好。

知道超時(shí)怎么計(jì)算后,很自然就想到定時(shí)器的設(shè)計(jì)問(wèn)題。一個(gè)簡(jiǎn)單直觀的方案就是為 TCP 中的每一個(gè)數(shù)據(jù)包維護(hù)一個(gè)定時(shí)器,在這個(gè)定時(shí)器到期前沒(méi)收到確認(rèn),則進(jìn)行重傳。這種設(shè)計(jì)理論上是很合理的,但是實(shí)現(xiàn)上,這種方案將會(huì)有非常多的定時(shí)器,會(huì)帶來(lái)巨大內(nèi)存開(kāi)銷(xiāo)和調(diào)度開(kāi)銷(xiāo)。既然不能每個(gè)包一個(gè)定時(shí)器,那么多少個(gè)包一個(gè)定時(shí)器才好呢,這個(gè)似乎比較難確定。可以換個(gè)思路,不要以包量來(lái)確定定時(shí)器,以連接來(lái)確定定時(shí)器會(huì)不會(huì)比較合理呢?目前,采取每一個(gè) TCP 連接單一超時(shí)定時(shí)器的設(shè)計(jì)則成了一個(gè)默認(rèn)的選擇,并且 RFC2988 給出了每連接單一定時(shí)器的設(shè)計(jì)建議算法規(guī)則:

[1].每一次一個(gè)包含數(shù)據(jù)的包被發(fā)送(包括重發(fā)),如果還沒(méi)開(kāi)啟重傳定時(shí)器,則開(kāi)啟它,使得它在 RTO 秒之后超時(shí)(按照當(dāng)前的 RTO 值)。[2]. 當(dāng)接收到一個(gè) ACK 確認(rèn)一個(gè)新的數(shù)據(jù);如果所有的發(fā)出數(shù)據(jù)都被確認(rèn)了,關(guān)閉重傳定時(shí)器;[3].當(dāng)接收到一個(gè) ACK 確認(rèn)一個(gè)新的數(shù)據(jù),還有數(shù)據(jù)在傳輸,也就是還有沒(méi)被確認(rèn)的數(shù)據(jù),重新啟動(dòng)重傳定時(shí)器,使得它在 RTO 秒之后超時(shí)(按照當(dāng)前的 RTO 值)。

當(dāng)重傳定時(shí)器超時(shí)后,依次做下列 3 件事情:[4.1]. 重傳最早的尚未被 TCP 接收方 ACK 的數(shù)據(jù)包;[4.2]. 重新設(shè)置 RTO 為 RTO *2(“還原定時(shí)器”),但是新 RTO 不應(yīng)該超過(guò) RTO 的上限(RTO 有個(gè)上限值,這個(gè)上限值最少為 60s);[4.3]. 重啟重傳定時(shí)器。

上面的建議算法體現(xiàn)了一個(gè)原則:沒(méi)被確認(rèn)的包必須可以超時(shí),并且超時(shí)的時(shí)間不能太長(zhǎng),同時(shí)也不要過(guò)早重傳。規(guī)則[1][3][4.3]共同說(shuō)明了只要還有數(shù)據(jù)包沒(méi)被確認(rèn),那么定時(shí)器一定會(huì)是開(kāi)啟著的(這樣滿(mǎn)足沒(méi)被確認(rèn)的包必須可以超時(shí)的原則)。規(guī)則[4.2]說(shuō)明定時(shí)器的超時(shí)值是有上限的(滿(mǎn)足超時(shí)的時(shí)間不能太長(zhǎng))。

規(guī)則[3]說(shuō)明,在一個(gè) ACK 到來(lái)后重置定時(shí)器可以保護(hù)后發(fā)的數(shù)據(jù)不被過(guò)早重傳;因?yàn)橐粋€(gè) ACK 到來(lái)了,說(shuō)明后續(xù)的 ACK 很可能會(huì)依次到來(lái),也就是說(shuō)丟失的可能性并不大。規(guī)則[4.2]也是在一定程度上避免過(guò)早重傳,因?yàn)椋诔霈F(xiàn)定時(shí)器超時(shí)后,有可能是網(wǎng)絡(luò)出現(xiàn)擁塞了,這個(gè)時(shí)候應(yīng)該延長(zhǎng)定時(shí)器,避免出現(xiàn)大量的重傳進(jìn)一步加劇網(wǎng)絡(luò)的擁塞。

【2】TCP 的重傳機(jī)制

通過(guò)上面我們可以知道,TCP 的重傳是由超時(shí)觸發(fā)的,這會(huì)引發(fā)一個(gè)重傳選擇問(wèn)題,假設(shè) TCP 發(fā)送端連續(xù)發(fā)了 1、2、3、4、5、6、7、8、9、10 共 10 包,其中 4、6、8 這 3 個(gè)包全丟失了,由于 TCP 的 ACK 是確認(rèn)最后連續(xù)收到序號(hào),這樣發(fā)送端只能收到 3 號(hào)包的 ACK,這樣在 TIME_OUT 的時(shí)候,發(fā)送端就面臨下面兩個(gè)重傳選擇:[1].僅重傳 4 號(hào)包 [2].重傳 3 號(hào)后面所有的包,也就是重傳 4~10 號(hào)包

對(duì)于,上面兩個(gè)選擇的優(yōu)缺點(diǎn)都比較明顯。方案[1],優(yōu)點(diǎn):按需重傳,能夠最大程度節(jié)省帶寬。缺點(diǎn):重傳會(huì)比較慢,因?yàn)橹貍?4 號(hào)包后,需要等下一個(gè)超時(shí)才會(huì)重傳 6 號(hào)包。方案[2],優(yōu)點(diǎn):重傳較快,數(shù)據(jù)能夠較快交付給接收端。缺點(diǎn):重傳了很多不必要重傳的包,浪費(fèi)帶寬,在出現(xiàn)丟包的時(shí)候,一般是網(wǎng)絡(luò)擁塞,大量的重傳又可能進(jìn)一步加劇擁塞。

上面的問(wèn)題是由于單純以時(shí)間驅(qū)動(dòng)來(lái)進(jìn)行重傳的,都必須等待一個(gè)超時(shí)時(shí)間,不能快速對(duì)當(dāng)前網(wǎng)絡(luò)狀況做出響應(yīng),如果加入以數(shù)據(jù)驅(qū)動(dòng)呢?TCP 引入了一種叫 Fast Retransmit(快速重傳)的算法,就是在連續(xù)收到 3 次相同確認(rèn)號(hào)的 ACK,那么就進(jìn)行重傳。這個(gè)算法基于這么一個(gè)假設(shè),連續(xù)收到 3 個(gè)相同的 ACK,那么說(shuō)明當(dāng)前的網(wǎng)絡(luò)狀況變好了,可以重傳丟失的包了。

快速重傳解決了 timeout 的問(wèn)題,但是沒(méi)解決重傳一個(gè)還是重傳多個(gè)的問(wèn)題。出現(xiàn)難以決定是否重傳多個(gè)包問(wèn)題的根源在于,發(fā)送端不知道那些非連續(xù)序號(hào)的包已經(jīng)到達(dá)接收端了,但是接收端是知道的,如果接收端告訴一下發(fā)送端不就可以解決這個(gè)問(wèn)題嗎?于是,RFC2018 提出了 Selective Acknowledgment(SACK,選擇確認(rèn))機(jī)制,SACK 是 TCP 的擴(kuò)展選項(xiàng),包括(1)SACK 允許選項(xiàng)(Kind=4,Length=2,選項(xiàng)只允許在有 SYN 標(biāo)志的 TCP 包中),(2)SACK 信息選項(xiàng) Kind=5,Length)。一個(gè) SACK 的例子如下圖,紅框說(shuō)明:接收端收到了 0-5500,8000-8500,7000-7500,6000-6500 的數(shù)據(jù)了,這樣發(fā)送端就可以選擇重傳丟失的 5500-6000,6500-7000,7500-8000 的包。

SACK 依靠接收端的接收情況反饋,解決了重傳風(fēng)暴問(wèn)題,這樣夠了嗎?接收端能不能反饋更多的信息呢?顯然是可以的,于是,RFC2883 對(duì)對(duì) SACK 進(jìn)行了擴(kuò)展,提出了 D-SACK,也就是利用第一塊 SACK 數(shù)據(jù)中描述重復(fù)接收的不連續(xù)數(shù)據(jù)塊的序列號(hào)參數(shù),其他 SACK 數(shù)據(jù)則描述其他正常接收到的不連續(xù)數(shù)據(jù)。這樣發(fā)送方利用第一塊 SACK,可以發(fā)現(xiàn)數(shù)據(jù)段被網(wǎng)絡(luò)復(fù)制、錯(cuò)誤重傳、ACK 丟失引起的重傳、重傳超時(shí)等異常的網(wǎng)絡(luò)狀況,使得發(fā)送端能更好調(diào)整自己的重傳策略。D-SACK,有幾個(gè)優(yōu)點(diǎn):

1)發(fā)送端可以判斷出,是發(fā)包丟失了,還是接收端的 ACK 丟失了。(發(fā)送方,重傳了一個(gè)包,發(fā)現(xiàn)并沒(méi)有 D-SACK 那個(gè)包,那么就是發(fā)送的數(shù)據(jù)包丟了;否則就是接收端的 ACK 丟了,或者是發(fā)送的包延遲到達(dá)了);2)發(fā)送端可以判斷自己的 RTO 是不是有點(diǎn)小了,導(dǎo)致過(guò)早重傳(如果收到比較多的 D-SACK 就該懷疑是 RTO 小了);3)發(fā)送端可以判斷自己的數(shù)據(jù)包是不是被復(fù)制了。(如果明明沒(méi)有重傳該數(shù)據(jù)包,但是收到該數(shù)據(jù)包的 D-SACK);4)發(fā)送端可以判斷目前網(wǎng)絡(luò)上是不是出現(xiàn)了有些包被 delay 了,也就是出現(xiàn)先發(fā)的包卻后到了。

疑癥(9)TCP 的流量控制

我們知道 TCP 的窗口(window)是一個(gè) 16bit 位字段,它代表的是窗口的字節(jié)容量,也就是 TCP 的標(biāo)準(zhǔn)窗口最大為 2^16-1=65535 個(gè)字節(jié)。另外在 TCP 的選項(xiàng)字段中還包含了一個(gè) TCP 窗口擴(kuò)大因子,option-kind 為 3,option-length 為 3 個(gè)字節(jié),option-data 取值范圍 0-14。窗口擴(kuò)大因子用來(lái)擴(kuò)大 TCP 窗口,可把原來(lái) 16bit 的窗口,擴(kuò)大為 31bit。

這個(gè)窗口是接收端告訴發(fā)送端自己還有多少緩沖區(qū)可以接收數(shù)據(jù)。于是發(fā)送端就可以根據(jù)這個(gè)接收端的處理能力來(lái)發(fā)送數(shù)據(jù),而不會(huì)導(dǎo)致接收端處理不過(guò)來(lái)。也就是,發(fā)送端是根據(jù)接收端通知的窗口大小來(lái)調(diào)整自己的發(fā)送速率的,以達(dá)到端到端的流量控制。盡管流量控制看起來(lái)簡(jiǎn)單明了,就是發(fā)送端根據(jù)接收端的限制來(lái)控制自己的發(fā)送就好了,但是細(xì)心的同學(xué)還是會(huì)有些疑問(wèn)的。

1)發(fā)送端是怎么做到比較方便知道自己哪些包可以發(fā),哪些包不能發(fā)呢?2)如果接收端通知一個(gè)零窗口給發(fā)送端,這個(gè)時(shí)候發(fā)送端還能不能發(fā)送數(shù)據(jù)呢?如果不發(fā)數(shù)據(jù),那一直等接收端口通知一個(gè)非 0 窗口嗎,如果接收端一直不通知呢?3)如果接收端處理能力很慢,這樣接收端的窗口很快被填滿(mǎn),然后接收處理完幾個(gè)字節(jié),騰出幾個(gè)字節(jié)的窗口后,通知發(fā)送端,這個(gè)時(shí)候發(fā)送端馬上就發(fā)送幾個(gè)字節(jié)給接收端嗎?發(fā)送的話(huà)會(huì)不會(huì)太浪費(fèi)了,就像一艘萬(wàn)噸油輪只裝上幾斤的油就開(kāi)去目的地一樣。對(duì)于發(fā)送端產(chǎn)生數(shù)據(jù)的能力很弱也一樣,如果發(fā)送端慢吞吞產(chǎn)生幾個(gè)字節(jié)的數(shù)據(jù)要發(fā)送,這個(gè)時(shí)候該不該立即發(fā)送呢?還是累積多點(diǎn)在發(fā)送?

疑問(wèn) 1)的解決:

發(fā)送方要知道那些可以發(fā),哪些不可以發(fā),一個(gè)簡(jiǎn)明的方案就是按照接收方的窗口通告,發(fā)送方維護(hù)一個(gè)一樣大小的發(fā)送窗口就可以了,在窗口內(nèi)的可以發(fā),窗口外的不可以發(fā),窗口在發(fā)送序列上不斷后移,這就是 TCP 中的滑動(dòng)窗口。如下圖所示,對(duì)于 TCP 發(fā)送端其發(fā)送緩存內(nèi)的數(shù)據(jù)都可以分為 4 類(lèi):[1]-已經(jīng)發(fā)送并得到接收端 ACK 的;[2]-已經(jīng)發(fā)送但還未收到接收端 ACK 的;[3]-未發(fā)送但允許發(fā)送的(接收方還有空間);[4]-未發(fā)送且不允許發(fā)送(接收方?jīng)]空間了)。

其中,[2]和[3]兩部分合起來(lái)稱(chēng)之為發(fā)送窗口。

下面兩圖演示的窗口的滑動(dòng)情況,收到 36 的 ACK 后,窗口向后滑動(dòng) 5 個(gè) byte。

疑問(wèn) 2)的解決

由問(wèn)題 1)我們知道,發(fā)送端的發(fā)送窗口是由接收端控制的。下圖,展示了一個(gè)發(fā)送端是怎么受接收端控制的。

由上圖我們知道,當(dāng)接收端通知一個(gè) zero 窗口的時(shí)候,發(fā)送端的發(fā)送窗口也變成了 0,也就是發(fā)送端不能發(fā)數(shù)據(jù)了。如果發(fā)送端一直等待,直到接收端通知一個(gè)非零窗口在發(fā)數(shù)據(jù)的話(huà),這似乎太受限于接收端,如果接收端一直不通知新的窗口呢?顯然發(fā)送端不能干等,起碼有一個(gè)主動(dòng)探測(cè)的機(jī)制。為解決 0 窗口的問(wèn)題,TCP 使用了 Zero Window Probe 技術(shù),縮寫(xiě)為 ZWP。發(fā)送端在窗口變成 0 后,會(huì)發(fā) ZWP 的包給接收方,來(lái)探測(cè)目前接收端的窗口大小,一般這個(gè)值會(huì)設(shè)置成 3 次,每次大約 30-60 秒(不同的實(shí)現(xiàn)可能會(huì)不一樣)。

如果 3 次過(guò)后還是 0 的話(huà),有的 TCP 實(shí)現(xiàn)就會(huì)發(fā) RST 掉這個(gè)連接。正如有人的地方就會(huì)有商機(jī),那么有等待的地方就很有可能出現(xiàn) DDoS 攻擊點(diǎn)。攻擊者可以在和 Server 建立好連接后,就向 Server 通告一個(gè) 0 窗口,然后 Server 端就只能等待進(jìn)行 ZWP,于是攻擊者會(huì)并發(fā)大量的這樣的請(qǐng)求,把 Server 端的資源耗盡。

疑問(wèn)點(diǎn) 3)的解決

疑點(diǎn) 3)本質(zhì)就是一個(gè)避免發(fā)送大量小包的問(wèn)題。造成這個(gè)問(wèn)題原因有二:

1)接收端一直在通知一個(gè)小的窗口;2)發(fā)送端本身問(wèn)題,一直在發(fā)送小包。這個(gè)問(wèn)題,TCP 中有個(gè)術(shù)語(yǔ)叫 Silly Window Syndrome(糊涂窗口綜合癥)。

解決這個(gè)問(wèn)題的思路有兩種,1)接收端不通知小窗口,2)發(fā)送端積累一下數(shù)據(jù)在發(fā)送。

思路 1)是在接收端解決這個(gè)問(wèn)題,David D Clark’s 方案,如果收到的數(shù)據(jù)導(dǎo)致 window size 小于某個(gè)值,就 ACK 一個(gè) 0 窗口,這就阻止發(fā)送端在發(fā)數(shù)據(jù)過(guò)來(lái)。等到接收端處理了一些數(shù)據(jù)后 windows size 大于等于了 MSS,或者 buffer 有一半為空,就可以通告一個(gè)非 0 窗口。

思路 2)是在發(fā)送端解決這個(gè)問(wèn)題,有個(gè)著名的 Nagle’s algorithm。Nagle 算法的規(guī)則:[1]如果包長(zhǎng)度達(dá)到 MSS ,則允許發(fā)送;[2]如果該包含有 FIN ,則允許發(fā)送;[3]設(shè)置了 TCP_NODELAY 選項(xiàng),則允許發(fā)送;[4]設(shè)置 TCP_CORK 選項(xiàng)時(shí),若所有發(fā)出去的小數(shù)據(jù)包(包長(zhǎng)度小于 MSS)均被確認(rèn),則允許發(fā)送;[5]上述條件都未滿(mǎn)足,但發(fā)生了超時(shí)(一般為 200ms ),則立即發(fā)送。

規(guī)則[4]指出 TCP 連接上最多只能有一個(gè)未被確認(rèn)的小數(shù)據(jù)包。從規(guī)則[4]可以看出 Nagle 算法并不禁止發(fā)送小的數(shù)據(jù)包(超時(shí)時(shí)間內(nèi)),而是避免發(fā)送大量小的數(shù)據(jù)包。由于 Nagle 算法是依賴(lài) ACK 的,如果 ACK 很快的話(huà),也會(huì)出現(xiàn)一直發(fā)小包的情況,造成網(wǎng)絡(luò)利用率低。TCP_CORK 選項(xiàng)則是禁止發(fā)送小的數(shù)據(jù)包(超時(shí)時(shí)間內(nèi)),設(shè)置該選項(xiàng)后,TCP 會(huì)盡力把小數(shù)據(jù)包拼接成一個(gè)大的數(shù)據(jù)包(一個(gè) MTU)再發(fā)送出去,當(dāng)然也不會(huì)一直等,發(fā)生了超時(shí)(一般為 200ms),也立即發(fā)送。Nagle 算法和 CP_CORK 選項(xiàng)提高了網(wǎng)絡(luò)的利用率,但是增加是延時(shí)。從規(guī)則[3]可以看出,設(shè)置 TCP_NODELAY 選項(xiàng),就是完全禁用 Nagle 算法了。

這里要說(shuō)一個(gè)小插曲,Nagle 算法和延遲確認(rèn)(Delayed Acknoledgement)一起,當(dāng)出現(xiàn)(write-write-read)的時(shí)候會(huì)引發(fā)一個(gè) 40ms 的延時(shí)問(wèn)題,這個(gè)問(wèn)題在 HTTP svr 中體現(xiàn)的比較明顯。場(chǎng)景如下:

客戶(hù)端在請(qǐng)求下載 HTTP svr 中的一個(gè)小文件,一般情況下,HTTP svr 都是先發(fā)送 HTTP 響應(yīng)頭部,然后在發(fā)送 HTTP 響應(yīng) BODY(特別是比較多的實(shí)現(xiàn)在發(fā)送文件的實(shí)施采用的是 sendfile 系統(tǒng)調(diào)用,這就出現(xiàn) write-write-read 模式了)。當(dāng)發(fā)送頭部的時(shí)候,由于頭部較小,于是形成一個(gè)小的 TCP 包發(fā)送到客戶(hù)端,這個(gè)時(shí)候開(kāi)始發(fā)送 body,由于 body 也較小,這樣還是形成一個(gè)小的 TCP 數(shù)據(jù)包,根據(jù) Nagle 算法,HTTP svr 已經(jīng)發(fā)送一個(gè)小的數(shù)據(jù)包了,在收到第一個(gè)小包的 ACK 后或等待 200ms 超時(shí)后才能在發(fā)小包,HTTP svr 不能發(fā)送這個(gè) body 小 TCP 包。

客戶(hù)端收到 http 響應(yīng)頭后,由于這是一個(gè)小的 TCP 包,于是客戶(hù)端開(kāi)啟延遲確認(rèn),客戶(hù)端在等待 Svr 的第二個(gè)包來(lái)在一起確認(rèn)或等待一個(gè)超時(shí)(一般是 40ms)在發(fā)送 ACK 包;這樣就出現(xiàn)了你等我、然而我也在等你的死鎖狀態(tài),于是出現(xiàn)最多的情況是客戶(hù)端等待一個(gè) 40ms 的超時(shí),然后發(fā)送 ACK 給 HTTP svr,HTTP svr 收到 ACK 包后在發(fā)送 body 部分。大家在測(cè) HTTP svr 的時(shí)候就要留意這個(gè)問(wèn)題了。

疑癥(10)TCP 的擁塞控制

談到擁塞控制,就要先談?wù)創(chuàng)砣囊蛩睾捅举|(zhì)。本質(zhì)上,網(wǎng)絡(luò)上擁塞的原因就是大家都想獨(dú)享整個(gè)網(wǎng)絡(luò)資源,對(duì)于 TCP,端到端的流量控制必然會(huì)導(dǎo)致網(wǎng)絡(luò)擁堵。這是因?yàn)? TCP 只看到對(duì)端的接收空間的大小,而無(wú)法知道鏈路上的容量,只要雙方的處理能力很強(qiáng),那么就可以以很大的速率發(fā)包,于是鏈路很快出現(xiàn)擁堵,進(jìn)而引起大量的丟包,丟包又引發(fā)發(fā)送端的重傳風(fēng)暴,進(jìn)一步加劇鏈路的擁塞。

另外一個(gè)擁塞的因素是鏈路上的轉(zhuǎn)發(fā)節(jié)點(diǎn),例如路由器,再好的路由器只要接入網(wǎng)絡(luò),總是會(huì)拉低網(wǎng)絡(luò)的總帶寬,如果在路由器節(jié)點(diǎn)上出現(xiàn)處理瓶頸,那么就很容易出現(xiàn)擁塞。由于 TCP 看不到網(wǎng)絡(luò)的狀況,那么擁塞控制是必須的并且需要采用試探性的方式來(lái)控制擁塞,于是擁塞控制要完成兩個(gè)任務(wù):[1]公平性;[2]擁塞過(guò)后的恢復(fù)。

TCP 發(fā)展到現(xiàn)在,擁塞控制方面的算法很多,其中 Reno 是目前應(yīng)用最廣泛且較為成熟的算法,下面著重介紹一下 Reno 算法(RFC5681)。介紹該算法前,首先介紹一個(gè)概念 duplicate acknowledgment(冗余 ACK、重復(fù) ACK)一般情況下一個(gè) ACK 被稱(chēng)為冗余 ACK,要同時(shí)滿(mǎn)足下面幾個(gè)條件(對(duì)于 SACK,那么根據(jù) SACK 的一些信息來(lái)進(jìn)一步判斷)。

[1] 接收 ACK 的那端已經(jīng)發(fā)出了一些還沒(méi)被 ACK 的數(shù)據(jù)包;[2] 該 ACK 沒(méi)有捎帶 data;[3] 該 ACK 的 SYN 和 FIN 位都是 off 的,也就是既不是 SYN 包的 ACK 也不是 FIN 包的 ACK;[4] 該 ACK 的確認(rèn)號(hào)等于接收 ACK 那端已經(jīng)收到的 ACK 的最大確認(rèn)號(hào);[5] 該 ACK 通知的窗口等接收該 ACK 的那端上一個(gè)收到的 ACK 的窗口。

Reno 算法包含 4 個(gè)部分:[1]慢熱啟動(dòng)算法 – Slow Start;[2]擁塞避免算法 – Congestion Avoidance;[3]快速重傳 - Fast Retransimit;[4]快速恢復(fù)算法 – Fast Recovery。

TCP 的擁塞控制主要原理依賴(lài)于一個(gè)擁塞窗口(cwnd)來(lái)控制,根據(jù)前面的討論,我們知道有一個(gè)接收端通告的接收窗口(rwnd)用于流量控制;加上擁塞控制后,發(fā)送端真正的發(fā)送窗口=min(rwnd,cwnd)。關(guān)于 cwnd 的單位,在 TCP 中是以字節(jié)來(lái)做單位的,我們假設(shè) TCP 每次傳輸都是按照 MSS 大小來(lái)發(fā)送數(shù)據(jù),因此你可以認(rèn)為 cwnd 按照數(shù)據(jù)包個(gè)數(shù)來(lái)做單位也可以理解,下面如果沒(méi)有特別說(shuō)明是字節(jié),那么 cwnd 增加 1 也就是相當(dāng)于字節(jié)數(shù)增加 1 個(gè) MSS 大小。

【1】慢熱啟動(dòng)算法 – Slow Start

慢啟動(dòng)體現(xiàn)了一個(gè)試探的過(guò)程,剛接入網(wǎng)絡(luò)的時(shí)候先發(fā)包慢點(diǎn),探測(cè)一下網(wǎng)絡(luò)情況,然后在慢慢提速。不要一上來(lái)就拼命發(fā)包,這樣很容易造成鏈路的擁堵,出現(xiàn)擁堵了在想到要降速來(lái)緩解擁堵這就有點(diǎn)成本高了,畢竟無(wú)數(shù)的先例告誡我們先污染后治理的成本是很高的。慢啟動(dòng)的算法如下(cwnd 全稱(chēng) Congestion Window):1)連接建好的開(kāi)始先初始化 cwnd = N,表明可以傳 N 個(gè) MSS 大小的數(shù)據(jù);2)每當(dāng)收到一個(gè) ACK,++cwnd; 呈線(xiàn)性上升;3)每當(dāng)過(guò)了一個(gè) RTT,cwnd = cwnd*2; 呈指數(shù)讓升;4)還有一個(gè)慢啟動(dòng)門(mén)限 ssthresh(slow start threshold),是一個(gè)上限,當(dāng) cwnd >= ssthresh 時(shí),就會(huì)進(jìn)入"擁塞避免算法 - Congestion Avoidance"。

根據(jù) RFC5681,如果 MSS > 2190 bytes,則 N = 2;如果 MSS < 1095 bytes,則 N =4;如果 2190 bytes >= MSS >= 1095 bytes,則 N = 3;一篇 Google 的論文《An Argument for Increasing TCP’s Initial Congestion Window》建議把 cwnd 初始化成了 10 個(gè) MSS。Linux 3.0 后采用了這篇論文的建議。

【2】擁塞避免算法 – Congestion Avoidance

慢啟動(dòng)的時(shí)候說(shuō)過(guò),cwnd 是指數(shù)快速增長(zhǎng)的,但是增長(zhǎng)是有個(gè)門(mén)限 ssthresh(一般來(lái)說(shuō)大多數(shù)的實(shí)現(xiàn) ssthresh 的值是 65535 字節(jié))的,到達(dá)門(mén)限后進(jìn)入擁塞避免階段。在進(jìn)入擁塞避免階段后,cwnd 值變化算法如下:1)每收到一個(gè) ACK,調(diào)整 cwnd 為 (cwnd + 1/cwnd) * MSS 個(gè)字節(jié);2)每經(jīng)過(guò)一個(gè) RTT 的時(shí)長(zhǎng),cwnd 增加 1 個(gè) MSS 大小。

TCP 是看不到網(wǎng)絡(luò)的整體狀況的,那么 TCP 認(rèn)為網(wǎng)絡(luò)擁塞的主要依據(jù)是它重傳了報(bào)文段。前面我們說(shuō)過(guò) TCP 的重傳分兩種情況:1)出現(xiàn) RTO 超時(shí),重傳數(shù)據(jù)包。這種情況下,TCP 就認(rèn)為出現(xiàn)擁塞的可能性就很大,于是它反應(yīng)非常'強(qiáng)烈' [1] 調(diào)整門(mén)限 ssthresh 的值為當(dāng)前 cwnd 值的 1/2;[2] reset 自己的 cwnd 值為 1;[3] 然后重新進(jìn)入慢啟動(dòng)過(guò)程。

2)在 RTO 超時(shí)前,收到 3 個(gè) duplicate ACK 進(jìn)行重傳數(shù)據(jù)包。這種情況下,收到 3 個(gè)冗余 ACK 后說(shuō)明確實(shí)有中間的分段丟失,然而后面的分段確實(shí)到達(dá)了接收端,因?yàn)檫@樣才會(huì)發(fā)送冗余 ACK,這一般是路由器故障或者輕度擁塞或者其它不太嚴(yán)重的原因引起的,因此此時(shí)擁塞窗口縮小的幅度就不能太大,此時(shí)進(jìn)入快速重傳。

【3】快速重傳 - Fast Retransimit 做的事情有:

1) 調(diào)整門(mén)限 ssthresh 的值為當(dāng)前 cwnd 值的 1/2;2) 將 cwnd 值設(shè)置為新的 ssthresh 的值;3) 重新進(jìn)入擁塞避免階段。

在快速重傳的時(shí)候,一般網(wǎng)絡(luò)只是輕微擁堵,在進(jìn)入擁塞避免后,cwnd 恢復(fù)的比較慢。針對(duì)這個(gè),“快速恢復(fù)”算法被添加進(jìn)來(lái),當(dāng)收到 3 個(gè)冗余 ACK 時(shí),TCP 最后的[3]步驟進(jìn)入的不是擁塞避免階段,而是快速恢復(fù)階段。

【4】快速恢復(fù)算法 – Fast Recovery :

快速恢復(fù)的思想是“數(shù)據(jù)包守恒”原則,即帶寬不變的情況下,在網(wǎng)絡(luò)同一時(shí)刻能容納數(shù)據(jù)包數(shù)量是恒定的。當(dāng)“老”數(shù)據(jù)包離開(kāi)了網(wǎng)絡(luò)后,就能向網(wǎng)絡(luò)中發(fā)送一個(gè)“新”的數(shù)據(jù)包。既然已經(jīng)收到了 3 個(gè)冗余 ACK,說(shuō)明有三個(gè)數(shù)據(jù)分段已經(jīng)到達(dá)了接收端,既然三個(gè)分段已經(jīng)離開(kāi)了網(wǎng)絡(luò),那么就是說(shuō)可以在發(fā)送 3 個(gè)分段了。

于是只要發(fā)送方收到一個(gè)冗余的 ACK,于是 cwnd 加 1 個(gè) MSS。快速恢復(fù)步驟如下(在進(jìn)入快速恢復(fù)前,cwnd 和 sshthresh 已被更新為:sshthresh = cwnd /2,cwnd = sshthresh):1)把 cwnd 設(shè)置為 ssthresh 的值加 3,重傳 Duplicated ACKs 指定的數(shù)據(jù)包;2)如果再收到 duplicated Acks,那么 cwnd = cwnd +1;3)如果收到新的 ACK,而非 duplicated Ack,那么將 cwnd 重新設(shè)置為【3】中 1)的 sshthresh 的值。然后進(jìn)入擁塞避免狀態(tài)。

細(xì)心的同學(xué)可能會(huì)發(fā)現(xiàn)快速恢復(fù)有個(gè)比較明顯的缺陷就是:它依賴(lài)于 3 個(gè)冗余 ACK,并假定很多情況下,3 個(gè)冗余的 ACK 只代表丟失一個(gè)包。但是 3 個(gè)冗余 ACK 也很有可能是丟失了很多個(gè)包,快速恢復(fù)只是重傳了一個(gè)包,然后其他丟失的包就只能等待到 RTO 超時(shí)了。超時(shí)會(huì)導(dǎo)致 ssthresh 減半,并且退出了 Fast Recovery 階段,多個(gè)超時(shí)會(huì)導(dǎo)致 TCP 傳輸速率呈級(jí)數(shù)下降。出現(xiàn)這個(gè)問(wèn)題的主要原因是過(guò)早退出了 Fast Recovery 階段。

為解決這個(gè)問(wèn)題,提出了 New Reno 算法,該算法是在沒(méi)有 SACK 的支持下改進(jìn) Fast Recovery 算法(SACK 改變 TCP 的確認(rèn)機(jī)制,把亂序等信息會(huì)全部告訴對(duì)方,SACK 本身攜帶的信息就可以使得發(fā)送方有足夠的信息來(lái)知道需要重傳哪些包,而不需要重傳哪些包),具體改進(jìn)如下:

1)發(fā)送端收到 3 個(gè)冗余 ACK 后,重傳冗余 ACK 指示可能丟失的那個(gè)包 segment1,如果 segment1 的 ACK 通告接收端已經(jīng)收到發(fā)送端的全部已經(jīng)發(fā)出的數(shù)據(jù)的話(huà),那么就是只丟失一個(gè)包,如果沒(méi)有,那么就是有多個(gè)包丟失了;2)發(fā)送端根據(jù) segment1 的 ACK 判斷出有多個(gè)包丟失,那么發(fā)送端繼續(xù)重傳窗口內(nèi)未被 ACK 的第一個(gè)包,直到 sliding window 內(nèi)發(fā)出去的包全被 ACK 了,才真正退出 Fast Recovery 階段。

我們可以看到,擁塞控制在擁塞避免階段,cwnd 是加性增加的,在判斷出現(xiàn)擁塞的時(shí)候采取的是指數(shù)遞減。為什么要這樣做呢?這是出于公平性的原則,擁塞窗口的增加受惠的只是自己,而擁塞窗口減少受益的是大家。這種指數(shù)遞減的方式實(shí)現(xiàn)了公平性,一旦出現(xiàn)丟包,那么立即減半退避,可以給其他新建的連接騰出足夠的帶寬空間,從而保證整個(gè)的公平性。

至此,TCP 的疑難雜癥基本介紹完畢了,總的來(lái)說(shuō) TCP 是一個(gè)有連接的、可靠的、帶流量控制和擁塞控制的端到端的協(xié)議。TCP 的發(fā)送端能發(fā)多少數(shù)據(jù),由發(fā)送端的發(fā)送窗口決定(當(dāng)然發(fā)送窗口又被接收端的接收窗口、發(fā)送端的擁塞窗口限制)的,那么一個(gè) TCP 連接的傳輸穩(wěn)定狀態(tài)應(yīng)該體現(xiàn)在發(fā)送端的發(fā)送窗口的穩(wěn)定狀態(tài)上,這樣的話(huà),TCP 的發(fā)送窗口有哪些穩(wěn)定狀態(tài)呢?TCP 的發(fā)送窗口穩(wěn)定狀態(tài)主要有上面三種穩(wěn)定狀態(tài):

【1】接收端擁有大窗口的經(jīng)典鋸齒狀

大多數(shù)情況下都是處于這樣的穩(wěn)定狀態(tài),這是因?yàn)椋话闱闆r下機(jī)器的處理速度就是比較快,這樣 TCP 的接收端都是擁有較大的窗口,這時(shí)發(fā)送端的發(fā)送窗口就完全由其擁塞窗口 cwnd 決定了;網(wǎng)絡(luò)上擁有成千上萬(wàn)的 TCP 連接,它們?cè)谙嗷?zhēng)用網(wǎng)絡(luò)帶寬,TCP 的流量控制使得它想要獨(dú)享整個(gè)網(wǎng)絡(luò),而擁塞控制又限制其必要時(shí)做出犧牲來(lái)體現(xiàn)公平性。于是在傳輸穩(wěn)定的時(shí)候 TCP 發(fā)送端呈現(xiàn)出下面過(guò)程的反復(fù):

[1]用慢啟動(dòng)或者擁塞避免方式不斷增加其擁塞窗口,直到丟包的發(fā)生;[2]然后將發(fā)送窗口將下降到 1 或者下降一半,進(jìn)入慢啟動(dòng)或者擁塞避免階段(要看是由于超時(shí)丟包還是由于冗余 ACK 丟包);過(guò)程如下圖:

【2】接收端擁有小窗口的直線(xiàn)狀態(tài)

這種情況下是接收端非常慢速,接收窗口一直很小,這樣發(fā)送窗口就完全有接收窗口決定了。由于發(fā)送窗口小,發(fā)送數(shù)據(jù)少,網(wǎng)絡(luò)就不會(huì)出現(xiàn)擁塞了,于是發(fā)送窗口就一直穩(wěn)定的等于那個(gè)較小的接收窗口,呈直線(xiàn)狀態(tài)。

【3】?jī)蓚€(gè)直連網(wǎng)絡(luò)端點(diǎn)間的滿(mǎn)載狀態(tài)下的直線(xiàn)狀態(tài)

這種情況下,Peer 兩端直連,并且只有位于一個(gè) TCP 連接,那么這個(gè)連接將獨(dú)享網(wǎng)絡(luò)帶寬,這里不存在擁塞問(wèn)題,在他們處理能力足夠的情況下,TCP 的流量控制使得他們能夠跑慢整個(gè)網(wǎng)絡(luò)帶寬。

通過(guò)上面我們知道,在 TCP 傳輸穩(wěn)定的時(shí)候,各個(gè) TCP 連接會(huì)均分網(wǎng)絡(luò)帶寬的。相信大家學(xué)生時(shí)代經(jīng)常會(huì)發(fā)生這樣的場(chǎng)景,自己在看視頻的時(shí)候突然出現(xiàn)視頻卡頓,于是就大叫起來(lái),哪個(gè)開(kāi)了迅雷,趕緊給我停了。其實(shí)簡(jiǎn)單的下載加速就是開(kāi)啟多個(gè) TCP 連接來(lái)分段下載就達(dá)到加速的效果,假設(shè)宿舍的帶寬是 1000K/s,一開(kāi)始兩個(gè)在看視頻,每人平均網(wǎng)速是 500k/s,這速度看起視頻來(lái)那叫一個(gè)順溜。突然其中一個(gè)同學(xué)打打開(kāi)迅雷開(kāi)著 99 個(gè) TCP 連接在下載愛(ài)情動(dòng)作片,這個(gè)時(shí)候平均下來(lái)你能分到的帶寬就剩下 10k/s,這網(wǎng)速下你的視頻還不卡成幻燈片。

在通信鏈路帶寬固定(假設(shè)為 W),多人公用一個(gè)網(wǎng)絡(luò)帶寬的情況下,利用 TCP 協(xié)議的擁塞控制的公平性,多開(kāi)幾個(gè) TCP 連接就能多分到一些帶寬(當(dāng)然要忽略有些用 UDP 協(xié)議帶來(lái)的影響),然而不管怎么最多也就能把整個(gè)帶寬搶到,于是在占滿(mǎn)整個(gè)帶寬的情況下,下載一個(gè)大小為 FS 的文件,那么最快需要的時(shí)間是 FS/W,難道就沒(méi)辦法加速了嗎?

答案是有的,這樣因?yàn)榫W(wǎng)絡(luò)是網(wǎng)狀的,一個(gè)節(jié)點(diǎn)是要和很多幾點(diǎn)互聯(lián)的,這就存在多個(gè)帶寬為 W 的通信鏈路,如果我們能夠?qū)⒁螺d的文件,一半從 A 通信鏈路下載,另外一半從 B 通信鏈路下載,這樣整個(gè)下載時(shí)間就減半了為 FS/(2W),這就是 p2p 加速。相信大家學(xué)生時(shí)代在下載愛(ài)情動(dòng)作片的時(shí)候也遇到過(guò)這種情況,明明外網(wǎng)速度沒(méi)這么快的,自己下載的愛(ài)情動(dòng)作片的速度卻達(dá)到幾 M/s,那是因?yàn)椋愕淖蠛蠡蛴液蟮乃抻言趲湍慵铀僦小N覀兌贾?P2P 模式下載會(huì)快,并且越多人下載就越快,那么問(wèn)題來(lái)了,P2P 下載加速理論上的加速比是多少呢?

11.附加題 1:P2P 理論上的加速比

傳統(tǒng)的 C/S 模式傳輸文件,在跑滿(mǎn) Client 帶寬的情況下傳輸一個(gè)文件需要耗時(shí) FS/BW,如果有 n 個(gè)客戶(hù)端需要下載文件,那么總耗時(shí)是 n*(FS/BW),當(dāng)然啦,這并不一定是串行傳輸,可以并行來(lái)傳輸?shù)模@樣總耗時(shí)也就是 FS/BW 了,但是這需要服務(wù)器的帶寬是 n 個(gè) client 帶寬的總和 n*BW。C/S 模式一個(gè)明顯的缺點(diǎn)是服務(wù)要傳輸一個(gè)文件 n 次,這樣對(duì)服務(wù)器的性能和帶寬帶來(lái)比較大的壓力,我可以換下思路,服務(wù)器將文件傳給其中一個(gè) Client 后,讓這些互聯(lián)的 Client 自己來(lái)交互那個(gè)文件,那服務(wù)器的壓力就減少很多了。這就是 P2P 網(wǎng)絡(luò)的好處,P2P 利用各個(gè)節(jié)點(diǎn)間的互聯(lián),提倡“人人為我,我為人人”。

知道 P2P 傳輸?shù)暮锰幒螅覀儊?lái)談下理論上的最大加速比,為了簡(jiǎn)化討論,一個(gè)簡(jiǎn)單的網(wǎng)絡(luò)拓?fù)鋱D如下,有 4 個(gè)相互互聯(lián)的節(jié)點(diǎn),并且每個(gè)節(jié)點(diǎn)間的網(wǎng)絡(luò)帶寬是 BW,傳輸一個(gè)大小為 FS 的文件最快的時(shí)間是多少呢?假設(shè)節(jié)點(diǎn) N1 有個(gè)大小為 FS 的文件需要傳輸給 N2,N3,N4 節(jié)點(diǎn),一種簡(jiǎn)單的方式就是:節(jié)點(diǎn) N1 同時(shí)將文件傳輸給節(jié)點(diǎn) N2,N3,N4 耗時(shí) FS/BW,這樣大家都擁有文件 FS 了。大家可以看出,整個(gè)過(guò)程只有節(jié)點(diǎn) 1 在發(fā)送文件,其他節(jié)點(diǎn)都是在接收,完全違反了 P2P 的“人人為我,我為人人”的宗旨。那怎么才能讓大家都做出貢獻(xiàn)了呢?解決方案是切割文件。

[1]首先,節(jié)點(diǎn) N1 文件分成 3 個(gè)片段 FS2,FS3,FS4,接著將 FS2 發(fā)送給 N2,F(xiàn)S3 發(fā)送給 N3,F(xiàn)S4 發(fā)送給 N4,耗時(shí) FS/(3*BW);[2]然后,N2,N3,N4 執(zhí)行“人人為我,我為人人”的精神,將自己擁有的 F2,F3,F4 分別發(fā)給沒(méi)有的其他的節(jié)點(diǎn),這樣耗時(shí) FS/(3*BW)完成交換。

于是總耗時(shí)為 2FS/(3BW)完成了文件 FS 的傳輸,可以看出耗時(shí)減少為原來(lái)的 2/3 了,如果有 n 個(gè)節(jié)點(diǎn),那么時(shí)間就是原來(lái)的 2/(n-1),也就是加速比是 2/(n-1),這就是加速的理論上限了嗎?還沒(méi)發(fā)揮最多能量的,相信大家已經(jīng)看到分割文件的好處了,上面的文件分割粒度還是有點(diǎn)大,以至于,在第二階段[2]傳輸過(guò)程中,節(jié)點(diǎn) N1 無(wú)所事事。為了最大化發(fā)揮大家的作用,我們需要將 FS2,FS3,FS4 在進(jìn)行分割,假設(shè)將它們都均分為 K 等份,這樣就有 FS21,FS22…FS2K、FS31,FS32…FS3K、FS41,FS42…FS4K,一共 3K 個(gè)分段。于是下面就開(kāi)始進(jìn)行加速分發(fā):

[1]節(jié)點(diǎn) N1 將分段 FS21,F(xiàn)S31,F(xiàn)S41 分別發(fā)送給 N2,N3,N4 節(jié)點(diǎn)。耗時(shí),F(xiàn)S/(3K*BW) [2]節(jié)點(diǎn) N1 將分段 FS22,F(xiàn)S32,F(xiàn)S42 分別發(fā)送給 N2,N3,N4 節(jié)點(diǎn),同時(shí)節(jié)點(diǎn) N2,N3,N4 將階段[1]收到的分段相互發(fā)給沒(méi)有的節(jié)點(diǎn)。耗時(shí),F(xiàn)S/(3K*BW)。

[K]節(jié)點(diǎn) N1 將分段 FS2K,F(xiàn)S3K,F(xiàn)S4K 分別發(fā)送給 N2,N3,N4 節(jié)點(diǎn),同時(shí)節(jié)點(diǎn) N2,N3,N4 將階段[K-1]收到的分段相互發(fā)給沒(méi)有的節(jié)點(diǎn)。耗時(shí),F(xiàn)S/(3K*BW)。[K+1]節(jié)點(diǎn) N2,N3,N4 將階段[K]收到的分段相互發(fā)給沒(méi)有的節(jié)點(diǎn)。耗時(shí),F(xiàn)S/(3K*BW)。于是總的耗時(shí)為(K+1) (FS/(3KBW)) = FS/(3BW) +FS/(3KBW),當(dāng) K 趨于無(wú)窮大的時(shí)候,文件進(jìn)行無(wú)限細(xì)分的時(shí)候,耗時(shí)變成了 FS/(3*BW),也就是當(dāng)節(jié)點(diǎn)是 n+1 的時(shí)候,加速比是 n。這就是理論上的最大加速比了,最大加速比是 P2P 網(wǎng)絡(luò)節(jié)點(diǎn)個(gè)數(shù)減 1。

12.附加題 2:系統(tǒng)調(diào)用 listen() 的 backlog 參數(shù)指的是什么

要說(shuō)明 backlog 參數(shù)的含義,首先需要說(shuō)一下 Linux 的協(xié)議棧維護(hù)的 TCP 連接的兩個(gè)連接隊(duì)列:[1]SYN 半連接隊(duì)列;[2]accept 連接隊(duì)列。

[1]SYN 半連接隊(duì)列:Server 端收到 Client 的 SYN 包并回復(fù) SYN,ACK 包后,該連接的信息就會(huì)被移到一個(gè)隊(duì)列,這個(gè)隊(duì)列就是 SYN 半連接隊(duì)列(此時(shí) TCP 連接處于 非同步狀態(tài) )

[2]accept 連接隊(duì)列:Server 端收到 SYN,ACK 包的 ACK 包后,就會(huì)將連接信息從[1]中的隊(duì)列移到另外一個(gè)隊(duì)列,這個(gè)隊(duì)列就是 accept 連接隊(duì)列(這個(gè)時(shí)候 TCP 連接已經(jīng)建立,三次握手完成了)。

用戶(hù)進(jìn)程調(diào)用 accept()系統(tǒng)調(diào)用后,該連接信息就會(huì)從[2]中的隊(duì)列中移走。相信不少同學(xué)就 backlog 的具體含義進(jìn)行爭(zhēng)論過(guò),有些認(rèn)為 backlog 指的是[1]和[2]兩個(gè)隊(duì)列的和。而有些則認(rèn)為是 backlog 指的是[2]的大小。其實(shí),這兩個(gè)說(shuō)法都對(duì),在 linux kernel 2.2 之前 backlog 指的是[1]和[2]兩個(gè)隊(duì)列的和。而 2.2 以后,就指的是[2]的大小,那么在 kernel 2.2 以后,[1]的大小怎么確定的呢?兩個(gè)隊(duì)列的作用分別是什么呢?

【1】SYN 半連接隊(duì)列的作用

對(duì)于 SYN 半連接隊(duì)列的大小是由(/proc/sys/net/ipv4/tcp_max_syn_backlog)這個(gè)內(nèi)核參數(shù)控制的,有些內(nèi)核似乎也受 listen 的 backlog 參數(shù)影響,取得是兩個(gè)值的最小值。當(dāng)這個(gè)隊(duì)列滿(mǎn)了,Server 會(huì)丟棄新來(lái)的 SYN 包,而 Client 端在多次重發(fā) SYN 包得不到響應(yīng)而返回(connection time out)錯(cuò)誤。但是,當(dāng) Server 端開(kāi)啟了 syncookies,那么 SYN 半連接隊(duì)列就沒(méi)有邏輯上的最大值了,并且/proc/sys/net/ipv4/tcp_max_syn_backlog 設(shè)置的值也會(huì)被忽略。

【2】accept 連接隊(duì)列

accept 連接隊(duì)列的大小是由 backlog 參數(shù)和(/proc/sys/net/core/somaxconn)內(nèi)核參數(shù)共同決定,取值為兩個(gè)中的最小值。當(dāng) accept 連接隊(duì)列滿(mǎn)了,協(xié)議棧的行為根據(jù)(/proc/sys/net/ipv4/tcp_abort_on_overflow)內(nèi)核參數(shù)而定。如果 tcp_abort_on_overflow=1,server 在收到 SYN_ACK 的 ACK 包后,協(xié)議棧會(huì)丟棄該連接并回復(fù) RST 包給對(duì)端,這個(gè)是 Client 會(huì)出現(xiàn)(connection reset by peer)錯(cuò)誤。如果 tcp_abort_on_overflow=0,server 在收到 SYN_ACK 的 ACK 包后,直接丟棄該 ACK 包。這個(gè)時(shí)候 Client 認(rèn)為連接已經(jīng)建立了,一直在等 Server 的數(shù)據(jù),直到超時(shí)出現(xiàn) read timeout 錯(cuò)誤。

參考資料

http://blog.csdn.net/dog250/article/details/6612496

http://coolshell.cn/articles/11564.html

http://coolshell.cn/articles/11609.html

 

http://www.tcpipguide.com/free/t_TCPMessageSegmentFormat.html

【本文為51CTO專(zhuān)欄作者“騰訊技術(shù)工程”原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)聯(lián)系原作者(微信號(hào):Tencent_TEG)】

戳這里,看該作者更多好文

 

責(zé)任編輯:武曉燕 來(lái)源: 51CTO專(zhuān)欄
相關(guān)推薦

2023-09-02 21:57:52

網(wǎng)絡(luò)TCP協(xié)議

2017-09-25 21:27:07

TCP協(xié)議數(shù)據(jù)鏈

2023-09-07 16:46:54

TCP數(shù)據(jù)傳遞

2015-11-09 09:58:56

2023-10-24 15:22:09

TCPUDP

2020-12-08 06:34:16

TCP握手SYN 報(bào)文

2022-10-10 07:34:36

TCP三次握手區(qū)塊鏈

2023-11-01 08:04:08

WiresharkTCP協(xié)議

2015-10-13 09:42:52

TCP網(wǎng)絡(luò)協(xié)議

2019-06-12 11:26:37

TCP三次握手四次揮手

2024-01-12 08:23:11

TCPACK服務(wù)器

2022-07-25 07:07:35

TCP客戶(hù)端服務(wù)器

2021-03-08 18:08:08

TCP Connect 協(xié)議

2024-10-09 20:54:16

2018-07-05 14:25:01

TCP握手原理

2019-12-12 10:36:43

TCPSYNIP

2022-07-07 09:00:17

TCP 連接HTTP 協(xié)議

2018-10-15 08:06:33

TCP握手原理

2023-03-06 15:43:56

2020-03-02 14:41:04

運(yùn)維架構(gòu)技術(shù)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

主站蜘蛛池模板: 国产乱码精品1区2区3区 | 激情五月综合网 | 成年人网站在线观看视频 | 日韩在线播放一区 | 美日韩一区二区 | 国产三级 | 中日字幕大片在线播放 | 在线免费观看a级片 | 成人免费影院 | 国产精品区二区三区日本 | 啪啪精品| 色一级| 中文字幕视频在线 | 奇米久久 | 99免费 | 成人片免费看 | 伊人青青久久 | 91久久国产综合久久 | 亚洲精品电影在线观看 | 中文字幕成人 | 国产成人福利视频在线观看 | 91av视频| 亚洲欧美国产一区二区三区 | 综合激情av | 亚洲欧洲日韩精品 中文字幕 | 久久久久国产一区二区三区四区 | 久久久久国产一区二区三区 | 亚洲高清一区二区三区 | 一区二区高清不卡 | 久久久久资源 | 久久成人一区 | 国产成人jvid在线播放 | 国产成人精品久久 | 男女免费在线观看视频 | 狠狠躁天天躁夜夜躁婷婷老牛影视 | 精品久久久久久 | 久久久久久久久久久久久久国产 | 久久久高清| 亚洲精品视频免费 | 国产精品99久久久久久久vr | avav在线看|