TCP協議知識整理(報文、握手、揮手、重傳、窗口、擁塞)


1.概念

傳輸控制協議(TCP,Transmission Control Protocol)是一種面向連接的、可靠的、基於字節流的傳輸層通信協議。

  • 面向連接:一定是「一對一」才能連接,不能像 UDP 協議 可以一個主機同時向多個主機發送消息,也就是一對多是無法做到的;

  • 可靠的:無論的網絡鏈路中出現了怎樣的鏈路變化,TCP 都可以保證一個報文一定能夠到達接收端;

  • 字節流:消息是「沒有邊界」的,所以無論我們消息有多大都可以進行傳輸。並且消息是「有序的」,當「前一個」消息沒有收到的時候,即使它先收到了后面的字節已經收到,那么也不能扔給應用層去處理,同時對「重復」的報文會自動丟棄。

為什么要使用TCP協議?

網絡層的IP協議不可靠,需要傳輸層的TCP協議來保證。

 


 2.TCP報文內容

IP數據報=IP頭部+數據=IP頭部+(TCP頭部+數據)

源端口號:源計算機上的應用程序的端口號

目的端口號:目標計算機的應用程序端口號

序號Seq:本報文發送的數據組的第一個字節的序號,例如本報文要發送200-399這段200字節的內容,序號就是200,下一次報文的序號就是400,這保證了TCP傳輸的有序性。

確認號Ack:表示接收方期望收到發送方下一個報文段的第一個字節數據的編號。例如接收方收到了200-399的內容,那么下一次就希望能收到400+的內容,接收方返回的確認號就是400。

頭部長度:也叫數據偏移,由於[選項及填充]這部分長度可變,所以整個TCP頭部的大小是不確定的,不知道哪個位置是要傳送的數據,頭部長度指示TCP頭部的大小,那么下一個字節開始就是要傳輸的內容。

保留:為將來定義新的用途保留,現在一般置0。

控制位URG:緊急指針標志,為1時表示緊急指針有效,為0則忽略緊急指針。

控制位ACK:確認序號標志,為1時表示確認號有效,為0表示報文中不含確認信息,忽略確認號字段。

控制位PSH:push標志,為1表示是帶有push標志的數據,指示接收方在接收到該報文段以后,應盡快將這個報文段交給應用程序,而不是在緩沖區排隊。

控制位RST:重置連接標志,用於重置由於主機崩潰或其他原因而出現錯誤的連接。或者用於拒絕非法的報文段和拒絕連接請求。

控制位SYN:同步序號,用於建立連接過程,在連接請求中,SYN=1和ACK=0表示該數據段沒有使用捎帶的確認域,而連接應答捎帶一個確認,即SYN=1和ACK=1。

控制位FIN:finish標志,用於釋放連接,為1時表示發送方已經沒有數據發送了,即關閉本方數據流。

窗口大小:用來告知發送端接受端的緩存大小,以此控制發送端發送數據的速率,從而達到流量控制。窗口大小是一個16字節字段,因而窗口大小最大為65535。

校驗和:奇偶校驗,此校驗和是對整個的 TCP 報文段,包括 TCP 頭部和 TCP 數據,以 16 位字進行計算所得。由發送端計算和存儲,並由接收端進行驗證。

緊急指針:只有當 URG 標志置 1 時緊急指針才有效。緊急指針是一個正的偏移量,和順序號字段中的值相加表示緊急數據最后一個字節的序號。TCP 的緊急方式是發送端向接收端發送緊急數據的一種方式。

選項及填充:最常見的可選字段是最長報文大小,又稱為MSS(Maximum Segment Size),每個連接方通常都在通信的第一個報文段(為建立連接而設置SYN標志為1的那個段)中指明這個選項,它表示本端所能接受的最大報文段的長度。選項長度不一定是32位的整數倍,所以要加填充位,即在這個字段中加入額外的零,以保證TCP頭是32的整數倍。然后通過數據偏移來確定數據部分的位置。

數據:這部分也是可選的,例如在建立連接和連接終止時,僅發送TCP首部,不帶數據。

 


 3.套接字Socket是個什么東西

套接字=IP地址+端口號。傳輸層實現的端到端的通信,這里所謂的"端"不是IP地址,也不是端口號,而是套接字。

套接字(socket)是一個抽象層,應用程序可以通過它發送或接收數據,可對其進行像對文件一樣的打開、讀寫和關閉等操作。套接字允許應用程序將I/O插入到網絡中,並與網絡中的其他應用程序進行通信。

在socket編程中,客戶端執行connect()時,將觸發三次握手。

 


 4.TCP三次握手

就是指建立一個TCP連接時,需要客戶端和服務器總共發送3個包。進行三次握手的主要作用就是為了確認雙方的接收能力和發送能力是否正常、指定自己的初始化序列號為后面的可靠性傳送做准備。實質上其實就是連接服務器指定端口,建立TCP連接,並同步連接雙方的序列號和確認號,交換TCP窗口大小信息。

剛開始客戶端處於 Closed 的狀態,服務端處於 Listen 狀態。

  • 第一次握手:客戶端給服務端發一個 SYN 報文,並指明客戶端的初始化序列號 ISN。此時客戶端處於 SYN_SENT 狀態(在發送連接請求后等待匹配的連接請求)。首部的同步位SYN=1,隨機生成初始序號seq=x,SYN=1的報文段不能攜帶數據,但要消耗掉一個序號。
  • 第二次握手:服務器收到客戶端的 SYN 報文之后,會以自己的 SYN 報文作為應答,並且也是指定了自己的初始化序列號 ISN(s)。同時會把客戶端的 ISN + 1 作為ACK 的值,表示自己已經收到了客戶端的 SYN,此時服務器處於 SYN_RCVD 狀態()。在確認報文段中SYN=1,ACK=1,確認號ack=x+1,隨機生成初始序號seq=y。
  • 第三次握手:客戶端收到 SYN 報文之后,會發送一個 ACK 報文,當然,也是一樣把服務器的 ISN + 1 作為 ACK 的值,表示已經收到了服務端的 SYN 報文,此時客戶端處於 ESTABLISHED 狀態。服務器收到 ACK 報文之后,也處於 ESTABLISHED 狀態,此時,雙方已建立起了連接。確認報文段ACK=1,確認號ack=y+1,序號seq=x+1(初始為seq=x,第二個報文段所以要+1),ACK報文段可以攜帶數據,不攜帶數據則不消耗序號。

在三次握手的建立中,原本雙方都是closed狀態,率先發送請求連接報文的 發送端先主動打開(active open),接收端被動打開(passive open)。

客戶端:我們在一起吧

服務端:好

客戶端:嗯

面試過程中,三次握手衍生出一系列的問題

(1)一個服務器的的最大TCP連接數

理論最大TCP連接數=套接字數量=客戶端IP地址數×客戶端的端口數

當然,服務端最大並發 TCP 連接數遠不能達到理論上限。

  • 首先主要是文件描述符限制,Socket 都是文件,所以首先要通過 ulimit 配置文件描述符的數目;
  • 另一個是內存限制,每個 TCP 連接都要占用一定內存,操作系統是有限的。

(2)半連接隊列是什么?

服務器第一次收到客戶端的 SYN 之后,就會處於 SYN_RCVD 狀態,此時雙方還沒有完全建立其連接,服務器會把此種狀態下請求連接放在一個隊列里,我們把這種隊列稱之為半連接隊列。

還有一個全連接隊列,就是已經完成三次握手,建立起連接的就會放在全連接隊列中。如果隊列滿了就有可能會出現丟包現象。

(3)丟包情況

客戶端沒有收到第2個包的情況下 會一直周期性重傳第1個包,有兩種情況

  • 第1個包丟了,服務端沒有收到請求,自然也不會發送第2個包,客戶端更不可能收到第2個包
  • 第2個包丟了,服務器收到第1個包然后發送第2個包,但由於半路丟了導致客戶端收不到第2個包

服務端沒有收到第3個包的情況下,會一直周期性重傳第2個包,也是兩種情況,第2個包丟了或者第3個包丟了。

周期性重傳是個什么概念?每次重傳的等待時間逐漸變長,一般是指數式增長。例如1s、2s、4s、8s...過了一定時間就叫超時,不傳了。

(4)SYN攻擊

服務器端的資源分配是在二次握手時分配的,而客戶端的資源是在完成三次握手時分配的。

  • 客戶端偽造大量IP地址向服務端 瘋狂發送第1個包;
  • 然后服務器以為是正常的請求,會分配資源,並且發送第2個包回去;
  • 然后客戶端不鳥它,那么服務端就會周期性超時重傳,導致分配資源越來越多,重傳的包也越來越多,引起網絡擁塞甚至系統癱瘓。

常見抵御SYN攻擊的方法

  • 縮短超時時間
  • 增大半連接隊列的大小
  • 過濾網關防護
  • SYN cookies技術

(5)三個包的數據攜帶情況

第1、2個包不可以攜帶數據,第3個可以。

  • 如果第1個包可以攜帶數據,那么SYN攻擊發送的第1個包攜帶大量數據浪費服務端的資源去接收
  • 第2個包不可以攜帶數據沒有查到相關資料。個人猜測,既然第1個包為防止SYN攻擊不可以攜帶數據,客戶端發起請求的連接 即客戶端要發東西給服務端,東西都沒發過來服務端帶什么數據給它,應該是稱為第2個包沒必要攜帶數據。不僅帶了也沒用,如果遇到SYN攻擊更浪費資源。
  • 收到第2個包才會發送第3個包,收到第2個包說明服務器已經准備好要連接了,再發一個包就連接了,所以第3個包順便帶點數據沒什么毛病。

(6)兩次握手不行嗎?

先弄明白三次握手的目的是什么,能不能只用兩次握手來達到同樣的目的。

  • 第一次握手:客戶端發送網絡包,服務端收到了。
    這樣服務端就能得出結論:客戶端的發送能力、服務端的接收能力是正常的。
  • 第二次握手:服務端發包,客戶端收到了。
    這樣客戶端就能得出結論:服務端的接收、發送能力,客戶端的接收、發送能力是正常的。不過此時服務器並不能確認客戶端的接收能力是否正常。
  • 第三次握手:客戶端發包,服務端收到了。
    這樣服務端就能得出結論:客戶端的接收、發送能力正常,服務器自己的發送、接收能力也正常。

因此,需要三次握手才能確認雙方的接收與發送能力是否正常。

如果只有兩次握手,會出現以下情況:

客戶端發出連接請求,但因連接請求報文丟失而未收到確認,於是客戶端再重傳一次連接請求。后來收到了確認,建立了連接。數據傳輸完畢后,就釋放了連接,客戶端共發出了兩個連接請求報文段,其中第一個丟失,第二個到達了服務端,但是第一個丟失的報文段只是在某些網絡結點長時間滯留了,延誤到連接釋放以后的某個時間才到達服務端,此時服務端誤認為客戶端又發出一次新的連接請求,於是就向客戶端發出確認報文段,同意建立連接,不采用三次握手,只要服務端發出確認,就建立新的連接了,此時客戶端忽略服務端發來的確認,也不發送數據,則服務端一致等待客戶端發送數據,浪費資源。這樣產生的效果就是序列號不同步

(7)第三次握手失敗怎么辦?

服務端發送很多次第2個包之后沒有收到第3個包,直到超時,此時才算是握手失敗,不會重傳ACK報文,直接發送RST報文然后進入closed狀態。這樣是防止SYN攻擊。(超時即失敗,不超時並且沒有收到第3個包還會重傳)

 


 5.四次揮手

剛開始雙方都處於ESTABLISHED 狀態,假如是客戶端先發起關閉請求。四次揮手的過程如下:

客戶端:我們分手吧!

服務端:等我發完東西先。

服務端:渣男!分手!

客戶端:再見了您嘞!

(1)揮手為什么要4次?

因為當服務端收到客戶端的SYN連接請求報文后,可以直接發送SYN+ACK報文。其中ACK報文是用來應答的,SYN報文是用來同步的。

但是關閉連接時,當服務端收到FIN報文時,很可能並不會立即關閉SOCKET,所以只能先回復一個ACK報文,告訴客戶端,”你發的FIN報文我收到了”。只有等到我服務端所有的報文都發送完了,我才能發送FIN報文,因此不能一起發送。故需要四次揮手。

(2)半關閉狀態

發送FIN的一端單方面宣布本端不會再發數據了,但可以收數據,這就是半關閉狀態。

(3)2MSL等待狀態

TIME_WAIT狀態也稱為2MSL等待狀態。每個具體TCP實現必須選擇一個報文段最大生存時間MSL(Maximum Segment Lifetime),它是任何報文段被丟棄前在網絡內的最長時間。這個時間是有限的,因為TCP報文段以IP數據報在網絡內傳輸,而IP數據報則有限制其生存時間的TTL字段。這個2MSL等待狀態會更新的,每發出一次第4個包,都會重置2MSL。

(4)為什么要等2MSL時間?

  • 如果客戶端在發送第4個包后直接關閉,如果這個包丟了,則服務端會再發送第3個包,但此時客戶端已經關閉了,沒辦法收到包也就不會再發第4個包,服務端就一直重發,超過一定次數應該就不會重發,浪費資源。  等待的效果就是如果第4個包丟了,服務端再發一個過來,客戶端能扛到那個時候,再發一次第4個包。每發一次第4個包就會重新計時等待,發出去的ACK包丟了的話還能扛到下一次服務端發來FIN包。
  • 如果沒有這個2MSL的話,客戶端這邊的套接字直接關閉了又重新發起連接,套接字相同,原本滯留在網絡中的包會對新的連接造成影響。2MSL保證本次連接的所有數據都從網絡中消失。

(5)2MSL時間有多長?

以下僅屬於個人猜測,查閱許多博客都是一筆帶過,沒有想要的答案

假設一個包在客戶端和服務端之間傳遞的單程時間是X,服務端重傳第3個包的等待時間是Y。模擬最后兩次揮手

服務端發送FIN包給客戶端,此時重傳時間Y開始計時;客戶端收到了並且發回ACK包,此時2MSL開始計時,重傳時間還剩Y-X。對於這個ACK包做出假設:

ACK包沒有丟,傳到了服務端,那么就是一次成功,服務端不會再發送ACK包,即重傳時間還沒到,Y-X-X>0,Y>2X

ACK包丟了,都說2MSL是為了防止ACK包丟了的情況,那這個2MSL一定能扛到下一次包發來。服務端沒有收到,在重傳時間Y后再發一次FIN包,此時在客戶端2MSL里過了Y-X,要扛到FIN包發來,至少再過X,2MSL>Y-X+X,2MSL>Y>2X。

如果服務端第2次發送的FIN包丟了咋辦?

那么2MLS要扛到Y+Y,2MSL>2Y>4X。感覺不太可能,丟包本來就少見,連續丟包更少見,為了這個"更少見"的情況延長2MSL,還不如規定服務端重傳2、3次就異常自動關閉。

 


6.重傳機制

TCP 實現可靠傳輸的方式之一,是通過序列號與確認應答,針對數據包丟失的情況,會用重傳機制解決。

(1)超時重傳

沒有收到應答就再發一次包,沒有收到應答的情況有兩種丟包情況(或者網絡滯留,如果滯留重傳數據包,則接收的一方會自動忽略重復的包)

  • 發出去的包半路丟了使得對方沒有發應答包過來
  • 發出去的包沒丟但對方的應答包丟了

例子在三次握手里,時間設置要權衡往返時間,公式看着挺麻煩的就不記錄了。

(2)快速重傳

不以時間為驅動,而是以數據驅動重傳。直接盜圖

如果發送方連續收到3個相同的ACK=2則會知道seq=2丟了,但是這3個ACK=2是(1,3,4))還是(1,4,5)或者(1,3,5)發來的就不清楚了,那要重發哪些包?

於是有了SACK

(3)SACK 帶選擇確認的重傳

即返回的ACK異常時,順便把收到了哪些包的信息也發回去,這些信息放在報頭的 "選項及填充" 里面。

(4)D-SACK

使用了 SACK 來告訴「發送方」有哪些數據被重復接收了。

 

 

D-SACK 有這么幾個好處:

可以讓「發送方」知道,是發出去的包丟了,還是接收方回應的 ACK 包丟了;

可以知道是不是「發送方」的數據包被網絡延遲了;

可以知道網絡中是不是把「發送方」的數據包給復制了;

 


 7.滑動窗口機制

最初始的信息傳遞就是一問一答,一個seq回應一個ack,然后再發seq,這樣效率不高。為了提高效率,幾個seq一起發,這就是滑動窗口。

滑動窗口是在緩沖區的基礎上建立的,一方發一方接,為什么要發?是為了讓對方接。所以窗口大小是根據接收方緩沖區大小來確定的。

(1)發送端滑動窗口

(2)接收端滑動窗口

滑動窗口並不是一成不變的。比如,當接收方的應用進程讀取數據的速度非常快的話,這樣的話接收窗口可以很快的就空缺出來。那么新的接收窗口大小,是通過 TCP 報文中的 Windows 字段來告訴發送方。那么這個傳輸過程是存在時延的,所以接收窗口和發送窗口是約等於的關系

(3)流量控制

如果接收窗口初始360,由於接收端繁忙,收到200的數據放在緩沖區沒有讀取,此時接收窗口剩下160,發個包回應發送端"我現在只能接160了",但發送端還沒收到這個消息就發了大小200的包過去,此時接收端 沒有能力接收 就導致包丟了。 

為了防止這種情況發生,TCP 規定是不允許同時減少緩存又收縮窗口的,而是采用先收縮窗口,過段時間再減少緩存,這樣就可以避免了丟包情況。(避免發送方的數據填滿了接收方的緩存)

(4)窗口關閉

為了解決這個死鎖問題,TCP 為每個連接設有一個持續定時器,只要 TCP 連接一方收到對方的零窗口通知,就啟動持續計時器

如果持續計時器超時,就會發送窗口探測 ( Window probe ) 報文,而對方在確認這個探測報文時,給出自己現在的接收窗口大小。

窗口探測

  • 如果接收窗口仍然為 0,那么收到這個報文的一方就會重新啟動持續計時器;
  • 如果接收窗口不是 0,那么死鎖的局面就可以被打破了。

窗口探查探測的次數一般為 3 此次,每次次大約 30-60 秒(不同的實現可能會不一樣)。如果 3 次過后接收窗口還是 0 的話,有的 TCP 實現就會發 RST 報文來中斷連接。

 


8.擁塞控制

(1)概念

在網絡出現擁堵時,如果繼續發送大量數據包,可能會導致數據包時延、丟失等,這時 TCP 就會重傳數據,但是一重傳就會導致網絡的負擔更重,於是會導致更大的延遲以及更多的丟包,這個情況就會進入惡性循環被不斷地放大。這就是擁塞。

為了避免這樣的悲劇發生,要及時控制數據包的發送量。這就是擁塞控制。

有這么幾個概念:慢啟動、擁塞避免、擁塞發生、快速恢復

(2)擁塞窗口

這是發送方維護的一個狀態變量,會根據網絡的擁塞情況動態變化,如果擁塞加重,窗口減小,擁塞減緩,窗口變大。

有了擁塞窗口之后,發送窗口 = min(擁塞窗口,接受窗口);

(3)慢啟動

每收到一個ACK則擁塞窗口大小+1

三次握手建立TCP連接后,第一次發1個數據包,收到1個ACK后;第二次發2個數據包,收到2個ACK;第三次發4個數據包,收到4個ACK;第四次發8個數據包... 指數式增長,試探網絡的底線。

(4)擁塞避免

指數式增長無疑會使得擁塞窗口會越來越大,當它達到一個閾值(ssthresh)時,不再以指數式增長,而是每次+1,線性增長,不然一下子就爆炸了,這就是擁塞避免。

(5)擁塞發生

即使線性增長,每輪包數量慢慢變多,總會有網絡扛不住的時候,此時重傳計時器超時/連續收到3個相同ACK,系統就判斷是網絡擁塞了,這就是擁塞發生。

(6)快速恢復

根據超時重傳和快速重傳的區別,快速恢復有2種方式。(同時使用)

超時重傳的快速恢復,下一次發送數據包只發1個,重新回到慢啟動,並且閾值設為 擁塞時窗口大小的一半(ssthresh=cwnd/2)。一夜回到解放前,這種方式太激進,會造成卡頓,已廢棄。

(《圖解TCP》第五版215頁:TCP通信開始時是沒有設置慢啟動閾值。而是在超時重發時,才會設置為當時擁塞窗口大小的一半。)

快速重傳的快速恢復,當接收方發現丟了一個中間包的時候,發送三次前一個包的 ACK,於是發送端就會快速地重傳,不必等待超時再重傳。TCP 認為這種情況不嚴重,因為大部分沒丟,只丟了一小部分,則 ssthresh 和 cwnd 變化如下:

  • cwnd = cwnd/2;
  • ssthresh = cwnd;

然后進入快速恢復算法

  • cwnd = ssthresh + 重傳ack個數(一般為3)
  • 重傳丟失的數據包
  • 如果再收到重復的 ACK,那么 cwnd 增加 1
  • 如果收到新數據的 ACK 后,把 cwnd 設置為第一步中的ssthresh的值,原因是該ACK確認了新的數據,說明丟失的數據都已收到。快速恢復算法結束,進入擁塞避免階段。
  • 缺點:這個算法依賴3個重傳的ack包,但3個重傳的ack不代表只丟了一個包,可能丟了好多好多個,但這個算法只會重傳一個,剩下的那些只能等到RTO超時。於是,超時一個阻塞窗口減半,多個超時會超成TCP的傳輸速度呈級數下降,而且也不會觸發快速恢復算法了。

再貼一張圖

 

 

 


參考&引用

https://yuanrengu.com/2020/77eef79f.html

http://cdn.yuanrengu.com/img/040315115571.png

https://blog.csdn.net/paincupid/article/details/79726795

https://www.cnblogs.com/xiaolincoding/p/12638546.html

https://www.cnblogs.com/xiaolincoding/p/12732052.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM