您好,登錄后才能下訂單哦!
這篇文章主要講解了“linux的TCP問題有哪些”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“linux的TCP問題有哪些”吧!
TCP 即 Transmission Control Protocol,可以看到是一個傳輸控制協議,重點就在這個控制。
控制什么?
控制可靠、按序地傳輸以及端與端之間的流量控制。夠了么?還不夠,它需要更加智能,因此還需要加個擁塞控制,需要為整體網絡的情況考慮。
這就是出行你我他,安全靠大家。
我們知道網絡是分層實現的,網絡協議的設計就是為了通信,從鏈路層到 IP 層其實就已經可以完成通信了。
你看鏈路層不可或缺畢竟咱們電腦都是通過鏈路相互連接的,然后 IP 充當了地址的功能,所以通過 IP 咱們找到了對方就可以進行通信了。
那加個 TCP 層干啥?IP 層實現控制不就完事了嘛?
之所以要提取出一個 TCP 層來實現控制是因為 IP 層涉及到的設備更多,一條數據在網絡上傳輸需要經過很多設備,而設備之間需要靠 IP 來尋址。
假設 IP 層實現了控制,那是不是涉及到的設備都需要關心很多事情?整體傳輸的效率是不是大打折扣了?
我舉個例子,假如 A 要傳輸給 F 一個積木,但是無法直接傳輸到,需要經過 B、C、D、E 這幾個中轉站之手。這里有兩種情況:
你覺得哪種效率高?明顯是第二種,轉發的設備不需要關心這些事,只管轉發就完事!
所以把控制的邏輯獨立出來成 TCP 層,讓真正的接收端來處理,這樣網絡整體的傳輸效率就高了。
我們已經知道了為什么需要獨立出 TCP 這一層,并且這一層主要是用來干嘛的,接下來就來看看它到底是怎么干的。
我們都知道 TCP 是面向連接的,那這個連接到底是個什么東西?真的是拉了一條線讓端與端之間連起來了?
所謂的連接其實只是雙方都維護了一個狀態,通過每一次通信來維護狀態的變更,使得看起來好像有一條線關聯了對方。
在具體深入之前我們需要先來看看一些 TCP 頭的格式,這很基礎也很重要。
我就不一一解釋了,挑重點的說。
首先可以看到 TCP 包只有端口,沒有 IP。
Seq 就是 Sequence Number 即序號,它是用來解決亂序問題的。
ACK 就是 Acknowledgement Numer 即確認號,它是用來解決丟包情況的,告訴發送方這個包我收到啦。
標志位就是 TCP flags 用來標記這個包是什么類型的,用來控制 TPC 的狀態。
窗口就是滑動窗口,Sliding Window,用來流控。
明確了協議頭的要點之后,我們再來看三次握手。
三次握手真是個老生常談的問題了,但是真的懂了么?不是浮在表面?能不能延伸出一些點別的?
我們先來看一下熟悉的流程。
首先為什么要握手,其實主要就是為了初始化Seq Numer,SYN 的全稱是 Synchronize Sequence Numbers,這個序號是用來保證之后傳輸數據的順序性。
你要說是為了測試保證雙方發送接收功能都正常,我覺得也沒毛病,不過我認為重點在于同步序號。
那為什么要三次,就拿我和你這兩個角色來說,首先我告訴你我的初始化序號,你聽到了和我說你收到了。
然后你告訴我你的初始序號,然后我對你說我收到了。
這好像四次了?如果真的按一來一回就是四次,但是中間一步可以合在一起,就是你和我說你知道了我的初始序號的時候同時將你的初始序號告訴我。
因此四次握手就可以減到三次了。
不過你沒有想過這么一種情形,我和你同時開口,一起告訴對方各自的初始序號,然后分別回應收到了,這不就是四次握手了?
我來畫個圖,清晰一點。
看看是不是四次握手了? 不過具體還是得看實現,有些實現可能不允許這種情況出現,但是這不影響我們思考,因為握手的重點就是同步初始序列號,這種情況也完成了同步的目標。
不知道大家有沒有想過 ISN 的值要設成什么?代碼寫死從零開始?
想象一下如果寫死一個值,比如 0 ,那么假設已經建立好連接了,client 也發了很多包比如已經第 20 個包了,然后網絡斷了之后 client 重新,端口號還是之前那個,然后序列號又從 0 開始,此時服務端返回第 20 個包的ack,客戶端是不是傻了?
所以 RFC793 中認為 ISN 要和一個假的時鐘綁定在一起ISN 每四微秒加一,當超過 2 的 32 次方之后又從 0 開始,要四個半小時左右發生 ISN 回繞。
所以 ISN 變成一個遞增值,真實的實現還需要加一些隨機值在里面,防止被不法份子猜到 ISN。
也就是 client 發送 SYN 至 server 然后就掛了,此時 server 發送 SYN+ACK 就一直得不到回復,怎么辦?
我腦海中一想到的就是重試,但是不能連續快速重試多次,你想一下,假設 client 掉線了,你總得給它點時間恢復吧,所以呢需要慢慢重試,階梯性重試。
在 Linux 中就是默認重試 5 次,并且就是階梯性的重試,間隔就是1s、2s、4s、8s、16s,再第五次發出之后還得等 32s 才能知道這次重試的結果,所以說總共等63s 才能斷開連接。
你看到沒 SYN 超時需要耗費服務端 63s 的時間斷開連接,也就說 63s 內服務端需要保持這個資源,所以不法分子就可以構造出大量的 client 向 server 發 SYN 但就是不回 server。
使得 server 的 SYN 隊列耗盡,無法處理正常的建連請求。
所以怎么辦?
可以開啟 tcp_syncookies,那就用不到 SYN 隊列了。
SYN 隊列滿了之后 TCP 根據自己的 ip、端口、然后對方的 ip、端口,對方 SYN 的序號,時間戳等一波操作生成一個特殊的序號(即 cookie)發回去,如果對方是正常的 client 會把這個序號發回來,然后 server 根據這個序號建連。
或者調整 tcp_synack_retries 減少重試的次數,設置 tcp_max_syn_backlog 增加 SYN 隊列數,設置 tcp_abort_on_overflow SYN 隊列滿了直接拒絕連接。
四次揮手和三次握手成雙成對,同樣也是 TCP 中的一線明星,讓我們重溫一下熟悉的圖。
為什么揮手需要四次?因為 TCP 是全雙工協議,也就是說雙方都要關閉,每一方都向對方發送 FIN 和回應 ACK。
就像我對你說我數據發完了,然后你回復好的你收到了。然后你對我說你數據發完了,然后我向你回復我收到了。
所以看起來就是四次。
從圖中可以看到主動關閉方的狀態是 FIN_WAIT_1 到 FIN_WAIT_2 然后再到 TIME_WAIT,而被動關閉方是 CLOSE_WAIT 到 LAST_ACK。
狀態一定是這樣變遷的嗎?讓我們再來看個圖。
可以看到雙方都主動發起斷開請求所以各自都是主動發起方,狀態會從 FIN_WAIT_1 都進入到 CLOSING 這個過度狀態然后再到 TIME_WAIT。
假設 client 已經沒有數據發送給 server 了,所以它發送 FIN 給 server 表明自己數據發完了,不再發了,如果這時候 server 還是有數據要發送給 client 那么它就是先回復 ack ,然后繼續發送數據。
等 server 數據發送完了之后再向 client 發送 FIN 表明它也發完了,然后等 client 的 ACK 這種情況下就會有四次揮手。
那么假設 client 發送 FIN 給 server 的時候 server 也沒數據給 client,那么 server 就可以將 ACK 和它的 FIN 一起發給client ,然后等待 client 的 ACK,這樣不就三次揮手了?
斷開連接發起方在接受到接受方的 FIN 并回復 ACK 之后并沒有直接進入 CLOSED 狀態,而是進行了一波等待,等待時間為 2MSL。
MSL 是 Maximum Segment Lifetime,即報文最長生存時間,RFC 793 定義的 MSL 時間是 2 分鐘,Linux 實際實現是 30s,那么 2MSL 是一分鐘。
那么為什么要等 2MSL 呢?
就是怕被動關閉方沒有收到最后的 ACK,如果被動方由于網絡原因沒有到,那么它會再次發送 FIN, 此時如果主動關閉方已經 CLOSED 那就傻了,因此等一會兒。
假設立馬斷開連接,但是又重用了這個連接,就是五元組完全一致,并且序號還在合適的范圍內,雖然概率很低但理論上也有可能,那么新的連接會被已關閉連接鏈路上的一些殘留數據干擾,因此給予一定的時間來處理一些殘留數據。
如果服務器主動關閉大量的連接,那么會出現大量的資源占用,需要等到 2MSL 才會釋放資源。
如果是客戶端主動關閉大量的連接,那么在 2MSL 里面那些端口都是被占用的,端口只有 65535 個,如果端口耗盡了就無法發起送的連接了,不過我覺得這個概率很低,這么多端口你這是要建立多少個連接?
快速回收,即不等 2MSL 就回收, Linux 的參數是 tcp_tw_recycle,還有 tcp_timestamps 不過默認是打開的。
其實上面我們已經分析過為什么需要等 2MSL,所以如果等待時間果斷就是出現上面說的那些問題。
所以不建議開啟,而且 Linux 4.12 版本后已經咔擦了這個參數了。
前不久剛有位朋友在群里就提到了這玩意。
一問果然有 NAT 的身影。
現象就是請求端請求服務器的靜態資源偶爾會出現 20-60 秒左右才會有響應的情況,從抓包看請求端連續三個 SYN 都沒有回應。
比如你在學校,對外可能就一個公網 IP,然后開啟了 tcp_tw_recycle(tcp_timestamps 也是打開的情況下),在 60 秒內對于同源 IP 的連接請求中 timestamp 必須是遞增的,不然認為其是過期的數據包就會丟棄。
學校這么多機器,你無法保證時間戳是一致的,因此就會出問題。
所以這玩意不推薦使用。
重用,即開啟 tcp_tw_reuse 當然也是需要 tcp_timestamps 的。
這里有個重點,tcp_tw_reuse 是用在連接發起方的,而我們的服務端基本上是連接被動接收方。
tcp_tw_reuse 是發起新連接的時候,可以復用超過 1s 的處于 TIME_WAIT 狀態的連接,所以它壓根沒有減少我們服務端的壓力。
它重用的是發起方處于 TIME_WAIT 的連接。
這里還有一個 SO_REUSEADDR ,這玩意有人會和 tcp_tw_reuse 混為一談,首先 tcp_tw_reuse 是內核選項而 SO_REUSEADDR 是用戶態選項。
然后 SO_REUSEADDR 主要用在你啟動服務的時候,如果此時的端口被占用了并且這個連接處于 TIME_WAIT 狀態,那么你可以重用這個端口,如果不是 TIME_WAIT,那就是給你個 Address already in use。
所以這兩個玩意好像都不行,而且 tcp_tw_reuse 和tcp_tw_recycle,其實是違反 TCP 協議的,說好的等我到天荒地老,你卻偷偷放了手?
要么就是調小 MSL 的時間,不過也不太安全,要么調整 tcp_max_tw_buckets 控制 TIME_WAIT 的數量,不過默認值已經很大了 180000,這玩意應該是用來對抗 DDos 攻擊的。
所以我給出的建議是服務端不要主動關閉,把主動關閉方放到客戶端。畢竟咱們服務器是一對很多很多服務,我們的資源比較寶貴。
還有一個很騷的解決方案,我自己瞎想的,就是自己攻擊自己。
Socket 有一個選項叫 IP_TRANSPARENT ,可以綁定一個非本地的地址,然后服務端把建連的 ip 和端口都記下來,比如寫入本地某個地方。
然后啟動一個服務,假如現在服務端資源很緊俏,那么你就定個時間,過了多久之后就將處于 TIME_WAIT 狀態的對方 ip 和端口告訴這個服務。
然后這個服務就利用 IP_TRANSPARENT 偽裝成之前的那個 client 向服務端發起一個請求,然后服務端收到會給真的 client 一個 ACK, 那 client 都關了已經,說你在搞啥子,于是回了一個 RST,然后服務端就中止了這個連接。
前面我們提到 TCP 要提供可靠的傳輸,那么網絡又是不穩定的如果傳輸的包對方沒收到卻又得保證可靠那么就必須重傳。
TCP 的可靠性是靠確認號的,比如我發給你1、2、3、4這4個包,你告訴我你現在要 5 那說明前面四個包你都收到了,就是這么回事兒。
不過這里要注意,SeqNum 和 ACK 都是以字節數為單位的,也就是說假設你收到了1、2、4 但是 3 沒有收到你不能 ACK 5,如果你回了 5 那么發送方就以為你5之前的都收到了。
所以只能回復確認最大連續收到包,也就是 3。
而發送方不清楚 3、4 這兩個包到底是還沒到呢還是已經丟了,于是發送方需要等待,這等待的時間就比較講究了。
如果太心急可能 ACK 已經在路上了,你這重傳就是浪費資源了,如果太散漫,那么接收方急死了,這死鬼怎么還不發包來,我等的花兒都謝了。
所以這個等待超時重傳的時間很關鍵,怎么搞?聰明的小伙伴可能一下就想到了,你估摸著正常來回一趟時間是多少不就好了,我就等這么長。
這就來回一趟的時間就叫 RTT,即 Round Trip Time,然后根據這個時間制定超時重傳的時間 RTO,即 Retransmission Timeout。
不過這里大概只好了 RTO 要參考下 RTT ,但是具體要怎么算?首先肯定是采樣,然后一波加權平均得到 RTO。
RFC793 定義的公式如下:
1、先采樣 RTT 2、SRTT = ( ALPHA * SRTT ) + ((1-ALPHA) * RTT) 3、RTO = min[UBOUND,max[LBOUND,(BETA*SRTT)]]
ALPHA 是一個平滑因子取值在 0.8~0.9之間,UBOUND 就是超時時間上界-1分鐘,LBOUND 是下界-1秒鐘,BETA 是一個延遲方差因子,取值在 1.3~2.0。
但是還有個問題,RTT 采樣的時間用一開始發送數據的時間到收到 ACK 的時間作為樣本值還是重傳的時間到 ACK 的時間作為樣本值?
從圖中就可以看到,一個時間算長了,一個時間算短了,這有點難,因為你不知道這個 ACK 到底是回復誰的。
所以怎么辦?發生重傳的來回我不采樣不就好了,我不知道這次 ACK 到底是回復誰的,我就不管他,我就采樣正常的來回。
這就是 Karn / Partridge 算法,不采樣重傳的RTT。
但是不采樣重傳會有問題,比如某一時刻網絡突然就是很差,你要是不管重傳,那么還是按照正常的 RTT 來算 RTO, 那么超時的時間就過短了,于是在網絡很差的情況下還瘋狂重傳加重了網絡的負載。
因此 Karn 算法就很粗暴的搞了個發生重傳我就將現在的 RTO 翻倍,哼!就是這么簡單粗暴。
但是這種平均的計算很容易把一個突然間的大波動,平滑掉,所以又搞了個算法,叫 Jacobson / Karels Algorithm。
它把最新的 RTT 和平滑過的 SRTT 做了波計算得到合適的 RTO,公式我就不貼了,反正我不懂,不懂就不嗶嗶了。
超時重傳是按時間來驅動的,如果是網絡狀況真的不好的情況,超時重傳沒問題,但是如果網絡狀況好的時候,只是恰巧丟包了,那等這么長時間就沒必要。
于是又引入了數據驅動的重傳叫快速重傳,什么意思呢?就是發送方如果連續三次收到對方相同的確認號,那么馬上重傳數據。
因為連續收到三次相同 ACK 證明當前網絡狀況是 ok 的,那么確認是丟包了,于是立馬重發,沒必要等這么久。
看起來好像挺完美的,但是你有沒有想過我發送1、2、3、4這4個包,就 2 對方沒收到,1、3、4都收到了,然后不管是超時重傳還是快速重傳反正對方就回 ACK 2。
這時候要重傳 2、3、4 呢還是就 2 呢?
SACK 即 Selective Acknowledgment,它的引入就是為了解決發送方不知道該重傳哪些數據的問題。
我們來看一下下面的圖就知道了。
SACK 就是接收方會回傳它已經接受到的數據,這樣發送方就知道哪一些數據對方已經收到了,所以就可以選擇性的發送丟失的數據。
如圖,通過 ACK 告知我接下來要 5500 開始的數據,并一直更新 SACK,6000-6500 我收到了,6000-7000的數據我收到了,6000-7500的數據我收到了,發送方很明確的知道,5500-5999 的那一波數據應該是丟了,于是重傳。
而且如果數據是多段不連續的, SACK 也可以發送,比如 SACK 0-500,1000-1500,2000-2500。就表明這幾段已經收到了。
D-SACK 其實是 SACK 的擴展,它利用 SACK 的第一段來描述重復接受的不連續的數據序號,如果第一段描述的范圍被 ACK 覆蓋,說明重復了,比如我都 ACK 到6000了你還給我回 SACK 5000-5500 呢?
說白了就是從第一段的反饋來和已經接受到的 ACK 比一比,參數是 tcp_dsack,Linux 2.4 之后默認開啟。
那知道重復了有什么用呢?
1、知道重復了說明對方收到剛才那個包了,所以是回來的 ACK 包丟了。2、是不是包亂序的,先發的包后到?3、是不是自己太著急了,RTO 太小了?4、是不是被數據復制了,搶先一步呢?
我們已經知道了 TCP 有序號,并且還有重傳,但是這還不夠,因為我們不是愣頭青,還需要根據情況來控制一下發送速率,因為網絡是復雜多變的,有時候就會阻塞住,而有時候又很通暢。
所以發送方需要知道接收方的情況,好控制一下發送的速率,不至于蒙著頭一個勁兒的發然后接受方都接受不過來。
因此 TCP 就有個叫滑動窗口的東西來做流量控制,也就是接收方告訴發送方我還能接受多少數據,然后發送方就可以根據這個信息來進行數據的發送。
以下是發送方維護的窗口,就是黑色圈起來的。
圖中的 #1 是已收到 ACK 的數據,#2 是已經發出去但是還沒收到 ACK 的數據,#3 就是在窗口內可以發送但是還沒發送的數據。#4 就是還不能發送的數據。
然后此時收到了 36 的 ACK,并且發出了 46-51 的字節,于是窗口向右滑動了。
TCP/IP Guide 上還有一張完整的圖,畫的十分清晰,大家看一下。
上文已經說了發送方式根據接收方回應的 window 來控制能發多少數據,如果接收方一直回應 0,那發送方就杵著?
你想一下,發送方發的數據都得到 ACK 了,但是呢回應的窗口都是 0 ,這發送方此時不敢發了啊,那也不能一直等著啊,這 Window 啥時候不變 0 啊?
于是 TCP 有一個 Zero Window Probe 技術,發送方得知窗口是 0 之后,會去探測探測這個接收方到底行不行,也就是發送 ZWP 包給接收方。
具體看實現了,可以發送多次,然后還有間隔時間,多次之后都不行可以直接 RST。
你想象一下,如果每次接收方都說我還能收 1 個字節,發送方該不該發?
TCP + IP 頭部就 40 個字節了,這傳輸不劃算啊,如果傻傻的一直發這就叫 Silly Window。
那咋辦,一想就是發送端等著,等養肥了再發,要么接收端自己自覺點,數據小于一個閾值就告訴發送端窗口此時是 0 算了,也等養肥了再告訴發送端。
發送端等著的方案就是納格算法,這個算法相信看一下代碼就知道了。
簡單的說就是當前能發送的數據和窗口大于等于 MSS 就立即發送,否則再判斷一下之前發送的包 ACK 回來沒,回來再發,不然就攢數據。
接收端自覺點的方案是 David D Clark’s 方案,如果窗口數據小于某個閾值就告訴發送方窗口 0 別發,等緩過來數據大于等于 MSS 或者接受 buffer 騰出一半空間了再設置正常的 window 值給發送方。
對了提到納格算法不得不再提一下延遲確認,納格算法在等待接收方的確認,而開啟延遲確認則會延遲發送確認,會等之后的包收到了再一起確認或者等待一段時候真的沒了再回復確認。
這就相互等待了,然后延遲就很大了,兩個不可同時開啟。
前面我已經提到了,加了擁塞控制是因為 TCP 不僅僅就管兩端之間的情況,還需要知曉一下整體的網絡情形,畢竟只有大家都守規矩了道路才會通暢。
前面我們提到了重傳,如果不管網絡整體的情況,肯定就是對方沒給 ACK ,那我就無腦重傳。
如果此時網絡狀況很差,所有的連接都這樣無腦重傳,是不是網絡情況就更差了,更加擁堵了?
然后越擁堵越重傳,一直沖沖沖!然后就 GG 了。
所以需要個擁塞控制,來避免這種情況的發送。
主要有以下幾個步驟來搞:
1、慢啟動,探探路。2、擁塞避免,感覺差不多了減速看看 3、擁塞發生快速重傳/恢復
慢啟動,就是新司機上路慢慢來,初始化 cwnd(Congestion Window)為 1,然后每收到一個 ACK 就 cwnd++ 并且每過一個 RTT ,cwnd = 2*cwnd 。
線性中帶著指數,指數中又夾雜著線性增。
然后到了一個閾值,也就是 ssthresh(slow start threshold)的時候就進入了擁塞避免階段。
這個階段是每收到一個 ACK 就 cwnd = cwnd + 1/cwnd并且每一個 RTT 就 cwnd++。
可以看到都是線性增。
然后就是一直增,直到開始丟包的情況發生,前面已經分析到重傳有兩種,一種是超時重傳,一種是快速重傳。
如果發生超時重傳的時候,那說明情況有點糟糕,于是直接把 ssthresh 置為當前 cwnd 的一半,然后 cwnd 直接變為 1,進入慢啟動階段。
如果是快速重傳,那么這里有兩種實現,一種是 TCP Tahoe ,和超時重傳一樣的處理。
一種是 TCP Reno,這個實現是把 cwnd = cwnd/2 ,然后把 ssthresh 設置為當前的 cwnd 。
然后進入快速恢復階段,將 cwnd = cwnd + 3(因為快速重傳有三次),重傳 DACK 指定的包,如果再收到一個DACK則 cwnd++,如果收到是正常的 ACK 那么就將 cwnd 設為 ssthresh 大小,進入擁塞避免階段。
可以看到快速恢復就重傳了指定的一個包,那有可能是很多包都丟了,然后其他的包只能等待超時重傳,超時重傳就會導致 cwnd 減半,多次觸發就指數級下降。
所以又搞了個 New Reno,多加了個 New,它是在沒有SACK 的情況下改進快速恢復,它會觀察重傳 DACK 指定的包的響應 ACK 是否是已經發送的最大 ACK,比如你發了1、2、3、4,對方沒收到 2,但是 3、4都收到了,于是你重傳 2 之后 ACK 肯定是 5,說明就丟了這一個包。
不然就是還有其他包丟了,如果就丟了一個包就是之前的過程一樣,如果還有其他包丟了就繼續重傳,直到 ACK 是全部的之后再退出快速恢復階段。
簡單的說就是一直探測到全部包都收到了再結束這個環節。
還有個 FACK,它是基于 SACK 用來作為重傳過程中的擁塞控制,相對于上面的 New Reno 我們就知道它有 SACK 所以不需要一個一個試過去,具體我不展開了。
從維基上看有這么多。
感謝各位的閱讀,以上就是“linux的TCP問題有哪些”的內容了,經過本文的學習后,相信大家對linux的TCP問題有哪些這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。