天天看點

TCP(二) 連接配接的建立和斷開

轉載自:小林coding 公衆号中的圖解網絡書籍,寫的很詳細,總結一部分記錄下。

TCP 頭部格式

TCP(二) 連接配接的建立和斷開

序列号:在建立連接配接時由計算機組成的随機數作為其初始值,通過SYN包傳給接收端主機,每發送一次資料,就累加一次該資料位元組數的大小。用來解決網絡包亂序問題。

确認應答号:指下一次期望收到的資料的序列号,發送端收到這個确認應答以後可以認為在這個序号以前的數

據都已經被正常接收。用來解決不丢包的問題。

控制位:

ACK:該位為 1 時,「确認應答」的字段變為有效,TCP 規定除了最初建立連接配接時的 SYN 包之外該位必須設定為 1 。

RST:該位為 1 時,表示 TCP 連接配接中出現異常必須強制斷開連接配接。

SYN:該位為 1 時,表示希望建立連接配接,并在其「序列号」的字段進行序列号初始值的設定。

FIN:該位為 1 時,表示今後不會再有資料發送,希望斷開連接配接。當通信結束希望斷開連接配接時,通信雙方的主機之間就可以互相交換 FIN 位為 1 的 TCP 段。

TCP連接配接建立

TCP的三次握手

TCP 是面向連接配接的協定,是以使用 TCP 前必須先建立連接配接,而建立連接配接是通過三次握手來進行的。

TCP(二) 連接配接的建立和斷開

一開始,用戶端和服務端都處于 CLOSED 狀态。先是服務端主動監聽某個端口,處于 LISTEN 狀态。

TCP(二) 連接配接的建立和斷開

用戶端會随機初始化序号( client_isn ),将此序号置于 TCP 首部的序列号字段中,同時把 SYN 标志位置為 1 ,表示 SYN 封包。接着把第一個 SYN 封包發送給服務端,表示向服務端發起連接配接,該封包不包含應用層資料,之後用戶端處于 SYN-SENT 狀态。

TCP(二) 連接配接的建立和斷開

服務端收到用戶端的 SYN 封包後,首先服務端也随機初始化自己的序号( server_isn ),将此序号填入TCP 首部的序列号字段中,其次把 TCP 首部的确認應答号字段填入 client_isn + 1 , 接着把 SYN和 ACK 标志位置為 1 。最後把該封包發給用戶端,該封包也不包含應用層資料,之後服務端處于 SYN_RCVD 狀态。

TCP(二) 連接配接的建立和斷開

用戶端收到服務端封包後,還要向服務端回應最後一個應答封包,首先該應答封包 TCP 首部 ACK 标志位置為 1 ,其次确認應答号字段填入 server_isn + 1 ,最後把封包發送給服務端,這次封包可以攜帶客戶到伺服器的資料,之後用戶端處于 ESTABLISHED 狀态。

伺服器收到用戶端的應答封包後,也進入 ESTABLISHED 狀态。

為什麼是三次握手?

TCP 連接配接:用于保證可靠性和流量控制維護的某些狀态資訊,這些資訊的組合,包括Socket、序列号和視窗大小。

為什麼三次握手才可以初始化Socket、序列号和視窗大小并建立 TCP 連接配接。

接下來以三個方面分析三次握手的原因:

  • 三次握手才可以阻止重複曆史連接配接的初始化(主要原因);
  • 三次握手才可以同步雙方的初始序列号;
  • 三次握手才可以避免資源浪費。
避免曆史連接配接

我們來看看 RFC 793 指出的 TCP 連接配接使用三次握手的首要原因:

The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing

confusion.

簡單來說,三次握手的首要原因是為了防止舊的重複連接配接初始化造成混亂。

網絡環境是錯綜複雜的,往往并不是如我們期望的一樣,先發送的資料包,就先到達目标主機,可能會由于網絡擁堵等亂七八糟的原因,會使得舊的資料包,先到達目标主機,那麼這種情況下 TCP 三次握手是如何避免的呢?

TCP(二) 連接配接的建立和斷開

用戶端連續發送多次 SYN 建立連接配接的封包,在網絡擁堵情況下:

一個舊 SYN 封包比最新的 SYN 封包早到達了服務端,那麼此時服務端就會回一個 SYN + ACK 封包給用戶端。用戶端收到後可以根據自身的上下文,判斷這是一個曆史連接配接(序列号過期或逾時),那麼用戶端就會發送 RST 封包給服務端,表示中止這一次連接配接。

如果是兩次握手連接配接,就不能判斷目前連接配接是否是曆史連接配接,三次握手則可以在用戶端(發送方)準備發送第三次封包時,用戶端因有足夠的上下文來判斷目前連接配接是否是曆史連接配接。

如果是曆史連接配接(序列号過期或逾時),則第三次握手發送的封包是 RST 封包,以此中止曆史連接配接。如果不是曆史連接配接,則第三次發送的封包是 ACK 封包,通信雙方就會成功建立連接配接。

是以,TCP 使用三次握手建立連接配接的最主要原因是防止曆史連接配接初始化了連接配接。

同步雙方初始序列号

TCP 協定的通信雙方, 都必須維護一個序列号, 序列号是可靠傳輸的一個關鍵因素,它的作用:

  • 接收方可以去除重複的資料;
  • 接收方可以根據資料包的序列号按序接收;
  • 可以辨別發送出去的資料包中, 哪些是已經被對方收到的。

可見,序列号在 TCP 連接配接中占據着非常重要的作用,是以當用戶端發送攜帶初始序列号的 SYN 封包的時候,需要服務端回一個 ACK 應答封包,表示用戶端的 SYN 封包已被服務端成功接收,那當服務端發送初始序列号給用戶端的時候,依然也要得到用戶端的應答回應,這樣一來一回,才能確定雙方的初始序列号能被可靠的同步。

TCP(二) 連接配接的建立和斷開

四次握手其實也能夠可靠的同步雙方的初始化序号,但由于第二步和第三步可以優化成一步,是以就成了三次握手。而兩次握手隻保證了一方的初始序列号能被對方成功接收,沒辦法保證雙方的初始序列号都能被确認接收。

避免資源浪費

如果隻有兩次握手,當用戶端的 SYN 請求連接配接在網絡中阻塞,用戶端沒有接收到 ACK 封包,就會重新發送 SYN ,由于沒有第三次握手,伺服器不清楚用戶端是否收到了自己發送的建立連接配接的 ACK 确認信号,是以每收到一個 SYN 就隻能先主動建立一個連接配接,這會造成什麼情況呢?

如果用戶端的 SYN 阻塞了,重複發送多次 SYN 封包,那麼伺服器在收到請求後就會建立多個備援的無效連結,造成不必要的資源浪費。

TCP(二) 連接配接的建立和斷開

即兩次握手會造成消息滞留情況下,伺服器重複接受無用的連接配接請求 SYN 封包,而造成重複配置設定資源。

為什麼用戶端和服務端的初始序列号 ISN 是不相同的?

如果一個已經失效的連接配接被重用了,但是該舊連接配接的曆史封包還殘留在網絡中,如果序列号相同,那麼就無法分辨 出該封包是不是曆史封包,如果曆史封包被新的連接配接接收了,則會産生資料錯亂。

初始序列号 ISN 是如何随機産生的?

是以,每次建立連接配接前重新初始化一個序列号主要是為了通信雙方能夠根據序号将不屬于本連接配接的封包段丢棄。 另一方面是為了安全性,防止黑客僞造的相同序列号的 TCP 封包被對方接收。

ISN起始是基于時鐘的,每 4 毫秒 + 1,轉一圈要 4.55 個小時。

RFC1948 中提出了一個較好的初始化序列号 ISN 随機生成算法。

ISN = M + F (localhost, localport, remotehost, remoteport)

M是一個計時器,這個計時器每隔 4 毫秒加 1。

F是一個 Hash 算法,根據源 IP、目的 IP、源端口、目的端口生成一個随機數值。要保證 Hash 算法不能被外部輕易推算得出,用 MD5 算法是一個比較好的選擇。

既然 IP 層會分片,為什麼 TCP 層還需要 MSS 呢?

我們先來認識下 MTU 和 MSS

TCP(二) 連接配接的建立和斷開

MTU:一個網絡包的最大長度,以太網中一般為1500位元組;

MSS:除去 IP 和 TCP 頭部之後,一個網絡包所能容納的 TCP 資料的最大長度;

如果在 TCP 的整個封包(頭部 + 資料)交給 IP 層進行分片,會有什麼異常呢?

當 IP 層有一個超過MTU大小的資料(TCP 頭部 + TCP 資料)要發送,那麼 IP 層就要進行分片,把資料分片成若幹片,保證每一個分片都小于 MTU。把一份 IP 資料報進行分片以後,由目标主機的 IP 層來進行重新組裝後, 再交給上一層 TCP 傳輸層。

這看起來井然有序,但這存在隐患的,那麼當如果一個 IP 分片丢失,整個IP封包的所有分片都得重傳。因為 IP 層本身沒有逾時重傳機制,它由傳輸層的 TCP 來負責逾時和重傳。

當接收方發現 TCP 封包(頭部 + 資料)的某一片丢失後,則不會響應 ACK 給對方,那麼發送方的 TCP 在逾時後,就會重發整個 TCP 封包(頭部 + 資料)。

是以,可以得知由 IP 層進行分片傳輸,是非常沒有效率的。是以,為了達到最佳的傳輸效能 TCP 協定在建立連接配接的時候通常要協商雙方的MSS值,當 TCP 層發現資料超過MSS 時,則就先會進行分片,當然由它形成的 IP 包的長度也就不會大于 MTU ,自然也就不用 IP 分片了。

TCP(二) 連接配接的建立和斷開

經過 TCP 層分片後,如果一個 TCP 分片丢失後,進行重發時也是以MSS為機關,而不用重傳所有的分片,大大增加了重傳的效率。

TCP 連接配接斷開

天下沒有不散的宴席,對于 TCP 連接配接也是這樣, TCP 斷開連接配接是通過四次揮手方式。雙方都可以主動斷開連接配接,斷開連接配接後主機中的資源将被釋放。

TCP(二) 連接配接的建立和斷開
  • 用戶端打算關閉連接配接,此時會發送一個 TCP 首部 FIN 标志位被置為 1 的封包,也即 FIN 封包,之後用戶端進入FIN_WAIT_1狀态。
  • 服務端收到該封包後,就向用戶端發送 ACK 應答封包,接着服務端進入 CLOSED_WAIT 狀态。
  • 用戶端收到服務端的 ACK 應答封包後,之後進入 FIN_WAIT_2 狀态。
  • 等待服務端處理完資料後,也向用戶端發送 FIN 封包,之後服務端進入 LAST_ACK 狀态。
  • 用戶端收到服務端的 FIN 封包後,回一個 ACK 應答封包,之後進入 TIME_WAIT 狀态。
  • 伺服器收到了 ACK 應答封包後,就進入了 CLOSED 狀态,至此服務端已經完成連接配接的關閉。
  • 用戶端在經過 2MSL 一段時間後,自動進入 CLOSED 狀态,至此用戶端也完成連接配接的關閉。

你可以看到,每個方向都需要一個FIN和一個ACK,是以通常被稱為四次揮手。這裡一點需要注意是:主動關閉連接配接的,才有TIME_WAIT狀态。

為什麼揮手需要四次?

再來回顧下四次揮手雙方發 FIN 包的過程,就能了解為什麼需要四次了。

關閉連接配接時,用戶端向服務端發送 FIN 時,僅僅表示用戶端不再發送資料了但是還能接收資料。伺服器收到用戶端的 FIN 封包時,先回一個 ACK 應答封包,而服務端可能還有資料需要處理和發送,等服務端不再發送資料時,才發送 FIN 封包給用戶端來表示同意現在關閉連接配接。

從上面過程可知,服務端通常需要等待完成資料的發送和處理,是以服務端的 ACK 和 FIN 一般都會分開發送,進而比三次握手導緻多了一次。

為什麼 TIME_WAIT 等待的時間是 2MSL?

MSL 是 Maximum Segment Lifetime,封包最大生存時間,它是任何封包在網絡上存在的最長時間,超過這個時

間封包将被丢棄。因為 TCP 封包基于是 IP 協定的,而 IP 頭中有一個 TTL 字段,是 IP 資料報可以經過的最大路由數,每經過一個處理他的路由器此值就減 1,當此值為 0 則資料報将被丢棄,同時發送 ICMP 封包通知源主機。

MSL 與 TTL 的差別: MSL 的機關是時間,而 TTL 是經過路由跳數。是以 MSL 應該要大于等于 TTL 消耗為 0 的

時間,以確定封包已被自然消亡。

TIME_WAIT 等待 2 倍的 MSL,比較合理的解釋是: 網絡中可能存在來自發送方的資料包,當這些發送方的資料包

被接收方處理後又會向對方發送響應,是以一來一回需要等待 2 倍的時間。

比如如果被動關閉方沒有收到斷開連接配接的最後的 ACK 封包,就會觸發逾時重發 FIN 封包,另一方接收到 FIN 後,會重發 ACK 給被動關閉方, 一來一去正好 2 個 MSL。2MSL 的時間是從用戶端接收到 FIN 後發送 ACK 開始計時的。如果在 TIME-WAIT 時間内,因為用戶端的 ACK沒有傳輸到服務端,用戶端又接收到了服務端重發的 FIN 封包,那麼 2MSL 時間将重新計時。 在 Linux 系統裡 2MSL 預設是 60 秒,那麼一個 MSL 也就是 30 秒。

Linux 系統停留在 TIME_WAIT 的時間為固定的 60 秒。其定義在 Linux 核心代碼裡的名稱為TCP_TIMEWAIT_LEN:

*#define TCP_TIMEWAIT_LEN (60*HZ) /*how long to wait to destroy TIME-WAIT state, about 60 seconds /

如果要修改 TIME_WAIT 的時間長度,隻能修改 Linux 核心代碼裡 TCP_TIMEWAIT_LEN 的值,并重新編譯 Linux核心。

為什麼需要 TIME_WAIT 狀态?

主動發起關閉連接配接的一方,才會有 TIME-WAIT 狀态。

需要 TIME-WAIT 狀态,主要是兩個原因:

  • 防止具有相同四元組(目的位址,源位址,目的端口,源端口)的舊資料包被收到;
  • 保證被動關閉連接配接的一方能被正确的關閉,即保證最後的 ACK 能讓被動關閉方接收,進而幫助其正常關閉;

原因一:防止舊連接配接的資料包

假設 TIME-WAIT 沒有等待時間或時間過短,被延遲的資料包抵達後會發生什麼呢?

TCP(二) 連接配接的建立和斷開

如上圖黃色框框服務端在關閉連接配接之前發送的 SEQ = 301 封包,被網絡延遲了。

這時有相同端口的 TCP 連接配接被複用後,被延遲的 SEQ = 301 抵達了用戶端,那麼用戶端是有可能正常接收這個過期的封包,這就會産生資料錯亂等嚴重的問題。

是以,TCP 就設計出了這麼一個機制,經過 2MSL 這個時間,足以讓兩個方向上的資料包都被丢棄,使得原來連接配接的資料包在網絡中都自然消失,再出現的資料包一定都是建立立連接配接所産生的。

原因二:保證連接配接正确關閉

在 RFC 793 指出 TIME-WAIT 另一個重要的作用是:

TIME-WAIT - represents waiting for enough time to pass to be sure the remote TCP received the acknowledgment of its connection termination request.

也就是說,TIME-WAIT 作用是等待足夠的時間以確定最後的ACK能讓被動關閉方接收,進而幫助其正常關閉。

假設 TIME-WAIT 沒有等待時間或時間過短,斷開連接配接會造成什麼問題呢?

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-iimGzt24-1630025850227)(https://cdn.jsdelivr.net/gh/jianningwu/picture/imgwps261.png)]

如上圖紅色框框用戶端四次揮手的最後一個 ACK 封包如果在網絡中被丢失了,此時如果用戶端 TIME-WAIT 過短或沒有,則就直接進入了 CLOSED 狀态了,那麼服務端則會一直處在 LASE_ACK 狀态。

當用戶端發起建立連接配接的 SYN 請求封包後,服務端會發送 RST 封包給用戶端,連接配接建立的過程就會被終止。

如果 TIME-WAIT 等待足夠長的情況就會遇到兩種情況:

  • 服務端正常收到四次揮手的最後一個 ACK 封包,則服務端正常關閉連接配接。
  • 服務端沒有收到四次揮手的最後一個 ACK 封包時,則會重發 FIN 關閉連接配接封包并等待新的 ACK 封包。

是以用戶端在 TIME-WAIT 狀态等待 2MSL 時間後,就可以保證雙方的連接配接都可以正常的關閉。

如果已經建立了連接配接,但是用戶端突然出現故障了怎麼辦?

TCP 有一個機制是保活機制。這個機制的原理是這樣的:

定義一個時間段,在這個時間段内,如果沒有任何連接配接相關的活動,TCP 保活機制會開始作用,每隔一個時間間隔,發送一個探測封包,該探測封包包含的資料非常少,如果連續幾個探測封包都沒有得到響應,則認為目前的 TCP 連接配接已經死亡,系統核心将錯誤資訊通知給上層應用程式。
           

在 Linux 核心可以有對應的參數可以設定保活時間、保活探測的次數、保活探測的時間間隔,以下都為預設值:

net.ipv4.tcp_keepalive_time=7200 
net.ipv4.tcp_keepalive_intvl=75 
net.ipv4.tcp_keepalive_probes=9
           

tcp_keepalive_time=7200:表示保活時間是 7200 秒(2⼩時),也就 2 小時内如果沒有任何連接配接相關的活動,則會啟動保活機制;

tcp_keepalive_intvl=75:表示每次檢測間隔 75 秒;

tcp_keepalive_probes=9:表示檢測 9 次無響應,認為對方是不可達的,進而中斷本次的連接配接。

也就是說在 Linux 系統中,最少需要經過 2 小時 11 分 15 秒才可以發現一個死亡連接配接。

TCP(二) 連接配接的建立和斷開

這個時間是有點長的,我們也可以根據實際的需求,對以上的保活相關的參數進行設定。如果開啟了 TCP 保活,需要考慮以下幾種情況:

第一種,對端程式是正常工作的。當 TCP 保活的探測封包發送給對端, 對端會正常響應,這樣 TCP保活時間會被重置,等待下一個 TCP 保活時間的到來。

第二種,對端程式崩潰并重新開機。當 TCP 保活的探測封包發送給對端後,對端是可以響應的,但由于沒有該連接配接的有效資訊,會産生一個RST封包,這樣很快就會發現 TCP 連接配接已經被重置。

第三種,是對端程式崩潰,或對端由于其他原因導緻封包不可達。當 TCP 保活的探測封包發送給對端後,石沉大海,沒有響應,連續幾次,達到保活探測次數後,TCP TCP連接配接已經死亡。

繼續閱讀