三次握手
三次握手協議的過程:
a.客戶端 向 服務器端 發送一個 SYN 包,請求一個主動打開。該包攜帶客戶端為這個連接請求設定的隨機數A作為消息列號。
b.服務器端接收到一個SYN包后,把該包放入SYN隊列中;回送一個SYN/ACK。ACK的確認碼應為A+1,SYN/ACK包本身攜帶一個隨機產生的序號B。
c.客戶端收到SYN/ACK包后,發送一個ACK的包,該包的序號被設定為A+1,而ACK的確認碼為B+1。當服務器端收到這個ACK包的時候,把請求幀從SYN隊列中移出,放置ACCEPT隊列中。
場景:當服務器端接收到客戶端發送過來的SYN后, 回了SYN-ACK后,客戶端掉線了,服務器端沒有收到客戶端回來的ACK,那這個連接 就 處於 一個中間狀態,沒成功也沒失敗。
但是,服務器端如果在一定時間內沒有收到TCP會重新發SYN-ACK。
- 主機收到一個TCP包時,用兩端的IP地址與端口號來標識這個TCP包屬於哪個session。使用一張表來存儲所有的session,表中的每條稱作TCB(Transmit Control Block)。
- TCB結構的定義包含:連接使用的源端口, 目的端口,目的ip, 序號, 應答序號, 對方窗口大小, 已方窗口大小, tcp狀態, tcp輸入/輸出隊列, 應用層輸出隊列, tcp的重傳有關變量等。
- 服務器端的連接數量是無限的,只受內存的限制。
tcp報文頭
- 來源連接端口,16位長,識別發送連接端口
- 目的連接端口,16位長,識別接收連接端口
- 序列號(seq,32位長)
- 確認號(ack,32位長),期望收到的數據的開始序列號,也即已經收到的數據的字節長度加1
- 資料偏移(4位長),以4字節為單位計算出的數據段開始地址的偏移值。
- 保留,需置0
- ACK—為1表示確認號字段有效
- SYN—為1表示這是連接請求或是連接接受請求,用於創建連接和使順序號同步
- FIN—為1表示發送方沒有數據要傳輸了,要求釋放連接
- RST—為1表示出現嚴重差錯。可能需要重新創建TCP連接。還可以用於拒絕非法的報文段和拒絕連接請求
- 緊急指針(16位長)—本報文段中的緊急數據的最后一個字節的序號
- 窗口(WIN,16位長)—表示從確認號開始,本報文的發送方可以接收的字節數,即接收窗口大小。用於流量控制
- 校驗和(Checksum,16位長)—對整個的TCP報文段,包括TCP頭部和TCP數據,以16位字進行計算所得。這是一個強制性的字段
重傳機制
方式
-
超時重傳
-
概念
- 發送數據時設定一個定時器,若在指定時間內沒有收到應答報文,就會重發數據
-
發生超時重傳的時機
- 數據包丟失
- 確認應答丟失
-
超時時間RTO選擇
- 略大於RTT
- 重傳超時策略:超時時間間隔加倍
-
-
快速重傳
-
概念
- 發送方可以一次發送多個數據包,若中間的某個數據包丟失了,接收方會一直回復這個丟失的數據包應答報文,接收方若收到三次這個數據包的應答報文,就知道該報文還沒有被接收方收到,可以重傳這個數據包
-
問題
- 因為接收方對后續的數據包也返回了丟失的那個應答報文,所以發送方不知道后續的數據包是否丟失,也就不知道應該重傳丟失的數據包還是把后續的數據包都重傳
-
-
SACK
-
概念
- 選擇性重傳,解決快速重傳不知道重傳哪些報文的缺點
-
實現方式
- 在tcp頭部“選項”字段里加一個SACK,將緩存的地圖發送給發送方,這樣發送方就知道哪些數據接收方收到了,哪些數據接收方沒有收到,以便重傳接收方沒有收到的數據
-
參數
- 要開啟SACK,需要發送方和接收方都支持:net.ipv4.tcp_sack,linux2.4以后默認打開
-
-
D-SACK
-
概念
- 對SACK的擴展,通過SACK告訴發送方哪些數據被重復接收了
-
實現方式
- 若有發送方有重復發送數據包,會通過SACK告訴發送方這這個數據包已被發送過
-
好處
-
發送方能夠知道是發出去的包丟了還是接收方發送的ACK丟了
- SACK若告知這個包已被發送過,那么說明是接收方發送的ACK丟了
-
發送方可以知道發出去的數據包是否被網絡延遲了
- 發送方在延遲之后會重發數據包,之前的數據包若一段時間后到達了接收方,接收方返回的應答報文中能看到該數據包已經被接收了,是個重復的報文,說明被網絡延遲了,而重發的數據包已被收到
-
-
序列號與確認應答(ACK)保證了tcp的可靠傳輸
滑動窗口
引入原因
- 每發送一個數據都需要進行確認應答,收到了再發送下一個,效率比較低。在窗口大小限制范圍內,可以無需等待上一個數據包應答,就可以繼續發送下一個數據包
錯誤處理
-
累計應答
-
發送數據包丟失
- 若發送方發送了一批數據包,中間的某個數據包丟失,那么接收方回復應答時ACK會回復丟失的那個數據包,這樣接收方就知道丟失的數據包是哪一個,因此可以重新發送;
-
應答報文丟失
- 若接收方返回的這一組數據包中,某一個應答報文丟失了,只要最后一個應答報文發送成功了,接收方就知道這個數據包的其實是收到了的
-
窗口大小
-
tcp頭里的字段window
- 接收方通過這個字段告訴接收方自己還有多少緩沖區可以接收數據,發送方就可以根據接收方的處理來發送數據,以免導致接收方處理不過來,因此窗口的大小是由接收方決定的
-
接收方和發送方的窗口
- 接收方和發送方的窗口大小基本相等,因為發送方的窗口大小取決於接收方,當接收方處理能力快,窗口變大,通過tcp報文中的window字段告訴接收方,若傳輸過程中出現了延遲,所以這時兩個窗口大小不一致
擁塞控制
概念
- 流量控制是避免發送方填滿接收方的緩存,但若因為其他主機之間的通信造成網絡擁堵,會有超時和丟包發生,這樣會導致重傳 ,網絡負擔會更大,進入惡性循環,所以tcp不能忽略網絡上發生的事,當網絡發生擁堵時,它會降低數據的發送量。擁塞控制的目的就是避免發送方的數據填滿整個網絡
擁塞窗口cwnd
-
發送窗口swnd和接收窗口rwnd是約等於的關系,有了擁塞窗口的概念后,發送窗口swnd=min(cwnd, rwnd)
- 網絡中沒有出現擁塞,cwnd會增大,反之會減小
-
判斷網絡擁塞的方法
- 發生超時重傳
-
相關算法
-
慢啟動
- tcp剛建立連接完成,會有個慢啟動的過程,一點一點提高發送數據包的數量
- 每接收到一個ack,cwnd大小就會加1,初始化時,cwnd大小為1,即cwnd大小按指數級增長
-
ssthreshold,slow start thresold,慢啟動門限
- 當cwnd < ssthreshold時,會采用慢啟動算法
- 當cwnd >= ssthreshold時,會使用擁塞避免算法
-
擁塞避免
- 每收到一個ack,cwnd增加1/cwnd。即囤積了cwnd這么多個包后,一次性發送過去。之后都會這樣發送,cwnd按線性增長
- 當一直這么增長,會慢慢進入擁塞狀況,於是開始出現丟包現象,這時需要對丟失的數據進行重傳,當觸發了重傳機制也就進入了擁塞發生算法
-
擁塞發生
-
超時重傳
- ssthreshold設為cwnd/2
- cwnd設為1
- 即一旦發生超時重傳就重新進入慢啟動
-
快速重傳
- 當接收方發現丟了一個中間包時,會發送三次丟失包的ack,發送方收到后就會快速重傳丟失的包
-
tcp認為這時候擁塞並不嚴重,只丟了一小部分包,於是
- cwnd = cwnd/2
- ssthreshold變為cwnd
- 然后進入快速恢復算法
-
-
快速恢復
- cwnd = ssthresold+3
- 重傳丟失的數據包
- 如果再收到重復的ack,那么cwnd+1
- 收到新的ack后,cwnd變為ssthreshold,然后進入擁塞避免算法
-
流量控制
概念
- 流量控制是基於滑動窗口實現的,tcp通過讓接收方指明希望從發送方接收的數據大小(窗口大小)來進行流量控制
問題
-
描述
- 接收方收到了太多數據,暫時不能接收數據了,於是返回了window大小為0
- 發送方發現window大小為0,於是暫時不再發送數據包了
- 接收方處理完數據,可以繼續處理了,於是在之前已處理完的數據包的應答報文中更新窗口大小,但是該應答報文丟失了,導致接收方沒能收到,於是就一直不發送新的數據包了,出現了死鎖
-
解決方法
- tcp為每個連接設定一個持續計時器,只有發起連接的一方從對方收到零窗口通知,就啟動計時器;如果持續計時器超時,就會發送窗口探測報文,對方會給出自己現在接收窗口的大小
- 窗口探測次數一般為3次,每次大約30-60秒,如果三次后窗口還是0,有的tcp實現會發起RST報文來中斷連接
糊塗窗口綜合症
-
發送方
- 發送方雖然知道窗口很大,但是每次都只發送很少的數據
-
接收方
- 接收方太忙,每次都只能從window中取出很少的數據,然后通知發送方,這樣發送方每次也只發送很少的數據
-
問題
- 每次都只傳輸小包,效率低
-
解決方法
-
讓接收方不通知小窗口給發送方
-
當窗口大小小於min(mss,(緩存空間/2))時,就會向發送方通知窗口為0,阻止發送方后續發送數據過來
- 注mss,Maximum Segment Size,最大報文長度,MSS是TCP報文段中的數據字段的最大長度,不包括TCP首部的長度
-
-
讓發送方不發送小包
-
Nagel算法
-
發送條件
- 等到窗口大小>=mss或數據大小>=mss
- 收到之前發送數據的ack應答
- 滿足發送條件的兩點才會發送
-
設置關閉
- Nagel算法默認開啟,若想要關閉,需要在socket設置TCP_NODELAY來關閉。沒有全局參數,需要每個應用根據自己的特點來關閉
-
-
-
Silly Window Syndrome翻譯成中文就是“糊塗窗口綜合症”。正如你上面看到的一樣,如果我們的接收方太忙了,來不及取走Receive Windows里的數據,那么,就會導致發送方越來越小。到最后,如果接收方騰出幾個字節並告訴發送方現在有幾個字節的window,而我們的發送方會義無反顧地發送這幾個字節。
要知道,我們的TCP+IP頭有40個字節,為了幾個字節,要達上這么大的開銷,這太不經濟了。
另外,你需要知道網絡上有個MTU,對於以太網來說,MTU是1500字節,除去TCP+IP頭的40個字節,真正的數據傳輸可以有1460,這就是所謂的MSS(Max Segment Size)注意,TCP的RFC定義這個MSS的默認值是536,這是因為 RFC 791里說了任何一個IP設備都得最少接收576尺寸的大小(實際上來說576是撥號的網絡的MTU,而576減去IP頭的20個字節就是536)。
如果你的網絡包可以塞滿MTU,那么你可以用滿整個帶寬,如果不能,那么你就會浪費帶寬。(大於MTU的包有兩種結局,一種是直接被丟了,另一種是會被重新分塊打包發送) 你可以想像成一個MTU就相當於一個飛機的最多可以裝的人,如果這飛機里滿載的話,帶寬最高,如果一個飛機只運一個人的話,無疑成本增加了,也而相當二。
所以,Silly Windows Syndrome這個現像就像是你本來可以坐200人的飛機里只做了一兩個人。 要解決這個問題也不難,就是避免對小的window size做出響應,直到有足夠大的window size再響應,這個思路可以同時實現在sender和receiver兩端。
- 如果這個問題是由Receiver端引起的,那么就會使用 David D Clark’s 方案。在receiver端,如果收到的數據導致window size小於某個值,可以直接ack(0)回sender,這樣就把window給關閉了,也阻止了sender再發數據過來,等到receiver端處理了一些數據后windows size 大於等於了MSS,或者,receiver buffer有一半為空,就可以把window打開讓send 發送數據過來。
- 如果這個問題是由Sender端引起的,那么就會使用著名的 Nagle’s algorithm。這個算法的思路也是延時處理,他有兩個主要的條件:1)要等到 Window Size>=MSS 或是 Data Size >=MSS,2)收到之前發送數據的ack回包,他才會發數據,否則就是在攢數據。
另外,Nagle算法默認是打開的,所以,對於一些需要小包場景的程序——比如像telnet或ssh這樣的交互性比較強的程序,你需要關閉這個算法。你可以在Socket設置TCP_NODELAY選項來關閉這個算法(關閉Nagle算法沒有全局參數,需要根據每個應用自己的特點來關閉)
另外,網上有些文章說TCP_CORK的socket option是也關閉Nagle算法,這不對。TCP_CORK其實是更新激進的Nagle算漢,完全禁止小包發送,而Nagle算法沒有禁止小包發送,只是禁止了大量的小包發送。最好不要兩個選項都設置。
https://coolshell.cn/articles/11609.html
http://zdyi.com/books/unp/s1/1.3.5.html