TCP 詳解
參考:https://blog.csdn.net/sinat_36629696/article/details/80740678
https://www.jianshu.com/p/ef892323e68f
https://mp.weixin.qq.com/s/LUtk6u_zv0w8g8GIGWEuCw
TCP協議
TCP協議全稱: 傳輸控制協議,顧名思義,就是要對數據的傳輸進行一定的控制.
先來看看它的報頭
每部分的含義和說明
-
源端口和目的端口
各占16位,端口是傳輸層與應用層的服務接口,傳輸層的復用和分用功能都要通過端口才能實現。
-
序號
占 32位,TCP 連接中傳送的數據流中的每一個字節都編上一個序號(seq),序號字段的值則指的是本報文段所發送的數據的第一個字節的序號。 Seq 就是 Sequence Number 即序號,它是用來解決亂序問題的
-
確認號
占 32位,是期望收到對方的下一個報文段的數據的第一個字節的序號(ACK)。 ACK 就是 Acknowledgement Numer 即確認號,它是用來解決丟包情況的,告訴發送方這個包我收到啦。
-
數據偏移/首部長度
占 4位,它指出 TCP 報文段的數據起始處距離 TCP 報文段的起始處有多遠,“數據偏移”的單位是 32 位字
-
保留
占 6 位,保留為今后使用,但目前應置為 0,六位分別是URG,ACK,PSH,RST,SYN,FIN。
URG: 標識緊急指針是否有效 ,表示此報文段中有緊急數據,應盡快傳送(相當於高優先級的數據)
ACK: 標識確認序號是否有效,表示只有當 ACK有效時確認號字段才有效
PSH: 提示接收端應用程序讀取tcp緩沖區數據,表示接收應用進程應盡快地讀取緩存區數據,而不再等到整個緩存都填滿了后再讀取
RST: 請求重新建立連接. 我們把含有RST標識的報文稱為復位報文段,表示 TCP 連接中出現嚴重差錯,必須釋放連接,然后再重新建立運輸連接
SYN: 請求建立連接. 我們把含有SYN標識的報文稱為同步報文段,表示這是一個連接請求或連接接受報文
FIN: 請求釋放連接. 我們把含有FIN標識的報文稱為結束報文段,表示此報文段的發送端的數據已發送完畢,並要求釋放運輸連接 -
檢驗和
占 16為,檢驗和字段檢驗的范圍包括首部和數據這兩部分,由發送端填充,,檢驗形式有CRC校驗等。 如果接收端校驗不通過,則認為數據有問題。
-
緊急指針
占 16 位, 用來標識哪部分數據是緊急數據,指出在本報文段中緊急數據共有多少個字節(緊急數據放在本報文段數據的最前面)
特點
TCP 是面向連接的傳輸層協議 每一條 TCP 連接只能有兩個端點(endpoint),每一條 TCP 連接只能是點對點的(一對一) TCP 提供可靠交付的服務 TCP 提供全雙工通信 面向字節流
連接管理機制
正常情況下, tcp需要經過三次握手建立連接,四次揮手斷開連接
三次握手
第一次:
客戶端 - - > 服務器 此時服務器知道了客戶端要建立連接了
第二次:
客戶端 < - - 服務器 此時客戶端知道服務器收到連接請求了
第三次:
客戶端 - - > 服務器 此時服務器知道客戶端收到了自己的回應
到這里,就可以認為客戶端與服務器已經建立了連接.
三次握手的詳細過程
開始,客戶端和服務器都處於 CLOSE 狀態.
此時,客戶端向服務器主動發出連接請求,服務器被動接受連接請求.
1、TCP服務器進程先創建傳輸控制塊TCB,時刻准備接受客戶端進程的連接請求,此時服務器就進入了 LISTEN(監聽)狀態
2、TCP客戶端進程也是先創建傳輸控制塊TCB,然后向服務器發出連接請求報文,此時報文首部中的同步標志位SYN=1,同時選擇一個初始序列號 seq = x,此時,TCP客戶端進程進入了 SYN-SENT(同步已發送狀態)狀態。TCP規定,SYN報文段(SYN=1的報文段)不能攜帶數據,但需要消耗掉一個序號。
3、TCP服務器收到請求報文后,如果同意連接,則發出確認報文。確認報文中的 ACK=1,SYN=1,確認序號是 x+1,同時也要為自己初始化一個序列號 seq = y,此時,TCP服務器進程進入了SYN-RCVD(同步收到)狀態。這個報文也不能攜帶數據,但是同樣要消耗一個序號。
4、TCP客戶端進程收到確認后還,要向服務器給出確認。確認報文的ACK=1,確認序號是 y+1,自己的序列號是 x+1.
5、此時,TCP連接建立,客戶端進入ESTABLISHED(已建立連接)狀態。當服務器收到客戶端的確認后也進入ESTABLISHED狀態,此后雙方就可以開始通信了。
初始序列號 ISN 的取值
RFC793 中認為 ISN 要和一個假的時鍾綁定在一起ISN 每四微秒加一,當超過 2 的 32 次方之后又從 0 開始,要四個半小時左右發生 ISN 回繞。
SYN 超時了怎么處理
在 Linux 中就是默認重試 5 次,並且就是階梯性的重試,間隔就是1s、2s、4s、8s、16s,再第五次發出之后還得等 32s 才能知道這次重試的結果,所以說總共等63s 才能斷開連接。
SYN Flood 攻擊
SYN Flood 攻擊: 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 隊列滿了直接拒絕連接。
四次揮手
四次揮手的詳細過程
數據傳輸完畢后,雙方都可以釋放連接.
此時客戶端和服務器都是處於ESTABLISHED狀態,然后客戶端主動斷開連接,服務器被動斷開連接.
1、客戶端進程發出連接釋放報文,並且停止發送數據。釋放數據報文首部,FIN=1,其序列號為seq=u(等於前面已經傳送過來的數據的最后一個字節的序號加1),此時客戶端進入FIN-WAIT-1(終止等待1)狀態。 TCP規定,FIN報文段即使不攜帶數據,也要消耗一個序號。
2、服務器收到連接釋放報文,發出確認報文,ACK=1,確認序號為 u+1,並且帶上自己的序列號seq=v,此時服務端就進入了CLOSE-WAIT(關閉等待)狀態。TCP服務器通知高層的應用進程,客戶端向服務器的方向就釋放了,這時候處於半關閉狀態,即客戶端已經沒有數據要發送了,但是服務器若發送數據,客戶端依然要接受。這個狀態還要持續一段時間,也就是整個CLOSE-WAIT狀態持續的時間。
3、客戶端收到服務器的確認請求后,此時客戶端就進入FIN-WAIT-2(終止等待2)狀態,等待服務器發送連接釋放報文(在這之前還需要接受服務器發送的最終數據)
4、服務器將最后的數據發送完畢后,就向客戶端發送連接釋放報文,FIN=1,確認序號為v+1,由於在半關閉狀態,服務器很可能又發送了一些數據,假定此時的序列號為seq=w,此時,服務器就進入了LAST-ACK(最后確認)狀態,等待客戶端的確認。
5、客戶端收到服務器的連接釋放報文后,必須發出確認,ACK=1,確認序號為w+1,而自己的序列號是u+1,此時,客戶端就進入了TIME-WAIT(時間等待)狀態。注意此時TCP連接還沒有釋放,必須經過2∗MSL(最長報文段壽命)的時間后,當客戶端撤銷相應的TCB后,才進入CLOSED狀態。
6、服務器只要收到了客戶端發出的確認,立即進入CLOSED狀態。同樣,撤銷TCB后,就結束了這次的TCP連接。可以看到,服務器結束TCP連接的時間要比客戶端早一些。
為什么有等待 2*MSL的時間呢
MSL 是 Maximum Segment Lifetime,即報文最長生存時間,RFC 793 定義的 MSL 時間是 2 分鍾,Linux 實際實現是 30s,那么 2MSL 是一分鍾。
- 就是怕被動關閉方沒有收到最后的 ACK,如果被動方由於網絡原因沒有到,那么它會再次發送 FIN, 此時如果主動關閉方已經 CLOSED 那就傻了,因此等一會兒。
- 假設立馬斷開連接,但是又重用了這個連接,就是五元組完全一致,並且序號還在合適的范圍內,雖然概率很低但理論上也有可能,那么新的連接會被已關閉連接鏈路上的一些殘留數據干擾,因此給予一定的時間來處理一些殘留數據。
等待 2MSL 會產生什么問題?
如果服務器主動關閉大量的連接,那么會出現大量的資源占用,需要等到 2MSL 才會釋放資源。
如果是客戶端主動關閉大量的連接,那么在 2MSL 里面那些端口都是被占用的,端口只有 65535 個,如果端口耗盡了就無法發起送的連接了,不過我覺得這個概率很低,這么多端口你這是要建立多少個連接?
確認應答機制(ACK機制)
TCP將每個字節的數據都進行了編號,即為序列號.
每一個ACK都帶有對應的確認序列號,意思是告訴發送者,我已經收到了哪些數據; 下一次你要從哪里開始發.
比如,客戶端向服務器發送了1005字節的數據,服務器返回給客戶端的確認序號是1003,那么說明服務器只收到了1-1002的數據.1003,1004,1005都沒收到.
此時客戶端就會從1003開始重發.
超時重傳機制
這種情況下,主機B會收到很多重復數據.
那么TCP協議需要識別出哪些包是重復的,並且把重復的丟棄.
這時候利用前面提到的序列號,就可以很容易做到去重.
超時時間如何確定?
最理想的情況下,找到一個最小的時間,保證 “確認應答一定能在這個時間內返回”.
但是這個時間的長短,隨着網絡環境的不同,是有差異的.
如果超時時間設的太長,會影響整體的重傳效率; 如果超時時間設的太短,有可能會頻繁發送重復的包.
TCP為了保證任何環境下都能保持較高性能的通信,因此會動態計算這個最大超時時間.
這個最大超時時間就叫 RTT,即 Round Trip Time,然后根據這個時間制定超時重傳的時間 RTO,即 Retransmission Timeout。
Linux中(BSD Unix和Windows也是如此),超時以500ms為一個單位進行控制,每次判定超時重發的超時時間都是500ms的整數倍.
如果重發一次之后,仍然得不到應答,等待 2500ms 后再進行重傳. 如果仍然得不到應答,等待 4500ms 進行重傳.
依次類推,以指數形式遞增. 累計到一定的重傳次數,TCP認為網絡異常或者對端主機出現異常,強制關閉連接.
為什么還需要快速重傳機制?
超時重傳是按時間來驅動的,如果是網絡狀況真的不好的情況,超時重傳沒問題,但是如果網絡狀況好的時候,只是恰巧丟包了,那等這么長時間就沒必要。
於是又引入了數據驅動的重傳叫快速重傳,什么意思呢?就是發送方如果連續三次收到對方相同的確認號,那么馬上重傳數據。
因為連續收到三次相同 ACK 證明當前網絡狀況是 ok 的,那么確認是丟包了,於是立馬重發,沒必要等這么久。
看起來好像挺完美的,但是你有沒有想過我發送1、2、3、4這4個包,就 2 對方沒收到,1、3、4都收到了,然后不管是超時重傳還是快速重傳反正對方就回 ACK 2。
這時候要重傳 2、3、4 呢還是就 2 呢?
SACK 的引入是為了解決什么問題?
SACK 即 Selective Acknowledgment,它的引入就是為了解決發送方不知道該重傳哪些數據的問題。
我們來看一下下面的圖就知道了。
SACK 就是接收方會回傳它已經接受到的數據,這樣發送方就知道哪一些數據對方已經收到了,所以就可以選擇性的發送丟失的數據。
滑動窗口
剛才我們討論了確認應答機制,對每一個發送的數據段,都要給一個ACK確認應答. 收到ACK后再發送下一個數據段.
這樣做有一個比較大的缺點,就是性能較差. 尤其是數據往返時間較長的時候.
那么我們可不可以一次發送多個數據段呢?
一個概念: 窗口
窗口大小指的是無需等待確認應答就可以繼續發送數據的最大值.
上圖的窗口大小就是4000個字節 (四個段).
發送前四個段的時候,不需要等待任何ACK,直接發送
收到第一個ACK確認應答后,窗口向后移動,繼續發送第五六七八段的數據…
因為這個窗口不斷向后滑動,所以叫做滑動窗口.
操作系統內核為了維護這個滑動窗口,需要開辟發送緩沖區來記錄當前還有哪些數據沒有應答
只有ACK確認應答過的數據,才能從緩沖區刪掉.
如果出現了丟包,那么該如何進行重傳呢?
1、數據包已經收到,但確認應答ACK丟了.
這種情況下,部分ACK丟失並無大礙,因為還可以通過后續的ACK來確認對方已經收到了哪些數據包.
2、數據包丟失
可以通過高速重發控制控制
流量控制
接收端處理數據的速度是有限的. 如果發送端發的太快,導致接收端的緩沖區被填滿,這個時候如果發送端繼續發送,就會造成丟包,進而引起丟包重傳等一系列連鎖反應.
因此TCP支持根據接收端的處理能力,來決定發送端的發送速度.
這個機制就叫做 流量控制(Flow Control)
接收端將自己可以接收的緩沖區大小放入 TCP 首部中的 “窗口大小” 字段,
通過ACK通知發送端;
窗口大小越大,說明網絡的吞吐量越高;
接收端一旦發現自己的緩沖區快滿了,就會將窗口大小設置成一個更小的值通知給發送端;
發送端接受到這個窗口大小的通知之后,就會減慢自己的發送速度;
如果接收端緩沖區滿了,就會將窗口置為0;
這時發送方不再發送數據,但是需要定期發送一個窗口探測數據段,讓接收端把窗口大小再告訴發送端.
擁塞控制
雖然TCP有了滑動窗口這個大殺器,能夠高效可靠地發送大量數據.
但是如果在剛開始就發送大量的數據,仍然可能引發一些問題.
因為網絡上有很多計算機,可能當前的網絡狀態已經比較擁堵.
在不清楚當前網絡狀態的情況下,貿然發送大量數據,很有可能雪上加霜.
因此,TCP引入 慢啟動 機制,先發少量的數據,探探路,摸清當前的網絡擁堵狀態以后,再決定按照多大的速度傳輸數據.
在此引入一個概念 擁塞窗口
發送開始的時候,定義擁塞窗口大小為1;
每次收到一個ACK應答,擁塞窗口加1;
每次發送數據包的時候,將擁塞窗口和接收端主機反饋的窗口大小做比較,取較小的值作為實際發送的窗口
像上面這樣的擁塞窗口增長速度,是指數級別的.
“慢啟動” 只是指初使時慢,但是增長速度非常快.
為了不增長得那么快,此處引入一個名詞叫做慢啟動的閾值,當擁塞窗口的大小超過這個閾值的時候,不再按照指數方式增長,而是按照線性方式增長.
- 當TCP開始啟動的時候,慢啟動閾值等於窗口最大值
- 在每次超時重發的時候,慢啟動閾值會變成原來的一半,同時擁塞窗口置回1
少量的丟包,我們僅僅是觸發超時重傳;
大量的丟包,我們就認為是網絡擁塞;
當TCP通信開始后,網絡吞吐量會逐漸上升;
隨着網絡發生擁堵,吞吐量會立刻下降.
擁塞控制,歸根結底是TCP協議想盡可能快的把數據傳輸給對方,但是又要避免給網絡造成太大壓力的折中方案.
延遲應答
如果接收數據的主機立刻返回ACK應答,這時候返回的窗口可能比較小.
假設接收端緩沖區為1M. 一次收到了500K的數據;
如果立刻應答,返回的窗口大小就是500K;
但實際上可能處理端處理的速度很快,10ms之內就把500K數據從緩沖區消費掉了; 在這種情況下,接收端處理還遠沒有達到自己的極限,即使窗口再放大一些,也能處理過來;
如果接收端稍微等一會兒再應答,比如等待200ms再應答,那么這個時候返回的窗口大小就是1M
窗口越大,網絡吞吐量就越大,傳輸效率就越高.
TCP的目標是在保證網絡不擁堵的情況下盡量提高傳輸效率;
那么所有的數據包都可以延遲應答么?
肯定也不是
有兩個限制
數量限制: 每隔N個包就應答一次
時間限制: 超過最大延遲時間就應答一次
具體的數量N和最大延遲時間,依操作系統不同也有差異
一般 N 取2,最大延遲時間取200ms
粘包問題
首先要明確,粘包問題中的 “包”,是指應用層的數據包.
在TCP的協議頭中,沒有如同UDP一樣的 “報文長度” 字段
但是有一個序號字段.
站在傳輸層的角度,TCP是一個一個報文傳過來的. 按照序號排好序放在緩沖區中.
站在應用層的角度,看到的只是一串連續的字節數據.
那么應用程序看到了這一連串的字節數據,就不知道從哪個部分開始到哪個部分是一個完整的應用層數據包.
此時數據之間就沒有了邊界,就產生了粘包問題
那么如何避免粘包問題呢?
歸根結底就是一句話,明確兩個包之間的邊界
對於定長的包
- 保證每次都按固定大小讀取即可
例如上面的Request結構,是固定大小的,那么就從緩沖區從頭開始按sizeof(Request)依次讀取即可
對於變長的包
- 可以在數據包的頭部,約定一個數據包總長度的字段,從而就知道了包的結束位置
還可以在包和包之間使用明確的分隔符來作為邊界(應用層協議,是程序員自己來定的,只要保證分隔符不和正文沖突即可)
對於UDP協議來說,是否也存在 “粘包問題” 呢?
對於UDP,如果還沒有向上層交付數據,UDP的報文長度仍然存在.
同時,UDP是一個一個把數據交付給應用層的,就有很明確的數據邊界.
站在應用層的角度,使用UDP的時候,要么收到完整的UDP報文,要么不收.不會出現收到 “半個” 的情況.