1. 程式人生 > 實用技巧 >TCP的基礎知識

TCP的基礎知識

iwehdio的部落格園:https://www.cnblogs.com/iwehdio/

學習自:

TCP基本認識

  • TCP頭部格式:

    • 序列號:在建立連線時由計算機生成的隨機數作為其初始值,通過 SYN 包傳給接收端主機,每傳送一次資料,就「累加」一次該「資料位元組數」的大小。用來解決網路包亂序問題。
    • 確認應答號:指下一次「期望」收到的資料的序列號,傳送端收到這個確認應答以後可以認為在這個序號以前的資料都已經被正常接收。用來解決不丟包的問題。
    • 控制位:
      • ACK:該位為 1 時,「確認應答」的欄位變為有效,TCP 規定除了最初建立連線時的 SYN包之外該位必須設定為 1 。
      • RST:該位為 1 時,表示 TCP 連線中出現異常必須強制斷開連線。
      • SYN:該位為 1 時,表示希望建立連線,並在其「序列號」的欄位進行序列號初始值的設定。
      • FIN:該位為 1 時,表示今後不會再有資料傳送,希望斷開連線。當通訊結束希望斷開連線時,通訊雙方的主機之間就可以相互交換 FIN 位為 1 的 TCP 段。
  • 為什麼需要 TCP 協議?

    • IP 層是「不可靠」的,它不保證網路包的交付、不保證網路包的按序交付、也不保證網路包中的資料的完整性。
    • 如果需要保障網路資料包的可靠性,那麼就需要由上層(傳輸層)的 TCP 協議來負責。
    • TCP 是一個工作在傳輸層的可靠資料傳輸的服務,它能確保接收端接收的網路包是無損壞、無間隔、非冗餘和按序的。
  • 什麼是TCP?

    • TCP 是面向連線的、可靠的、基於位元組流的傳輸層通訊協議。
    • 面向連線:一定是「一對一」才能連線,不能像 UDP 協議可以一個主機同時向多個主機發送訊息,也就是一對多是無法做到的;
    • 可靠的:無論的網路鏈路中出現了怎樣的鏈路變化,TCP 都可以保證一個報文一定能夠到達接收端;
    • 位元組流:訊息是「沒有邊界」的,所以無論我們訊息有多大都可以進行傳輸。並且訊息是「有序的」,當「前一個」訊息沒有收到的時候,即使它先收到了後面的位元組,那麼也不能扔給應用層去處理,同時對「重複」的報文會自動丟棄。
  • 什麼是TCP連線?

    • 用於保證可靠性和流量控制維護的某些狀態資訊,這些資訊的組合,包括Socket、序列號和視窗大小稱為連線。
    • 建立一個 TCP 連線是需要客戶端與伺服器端達成上述三個資訊的共識。
      • Socket:由 IP 地址和埠號組成
      • 序列號:用來解決亂序問題等
      • 視窗大小:用來做流量控制
  • 如何唯一確定一個 TCP 連線呢?

    • TCP 四元組可以唯一的確定一個連線,四元組包括如下:源地址、源埠、目的地址、目的埠。
    • 源地址和目的地址的欄位(32位)是在 IP 頭部中,作用是通過 IP 協議傳送報文給對方主機。
    • 源埠和目的埠的欄位(16位)是在 TCP 頭部中,作用是告訴 TCP 協議應該把報文發給哪個程序。
  • 有一個 IP 的伺服器監聽了一個埠,它的 TCP 的最大連線數是多少?

    • 伺服器通常固定在某個本地埠上監聽,等待客戶端的連線請求。因此,客戶端 IP 和 埠是可變的,其理論值計算公式為:最大TCP連線數=客戶端的IP數×客戶端的埠數。
    • 對 IPv4,客戶端的 IP 數最多為 2 的 32 次方,客戶端的埠數最多為 2 的 16 次方,也就是服務端單機最大 TCP 連線數,約為 2 的 48 次方。
    • 當然,服務端最大併發 TCP 連線數遠不能達到理論上限。
      • 首先主要是檔案描述符限制,Socket 都是檔案,所以首先要通過 ulimit 配置檔案描述符的數目。包括系統級、使用者級和程序級的配置;
      • 另一個是記憶體限制,每個 TCP 連線都要佔用一定記憶體,作業系統的記憶體是有限的。
      • 一條空的TCP連線至少要佔用3.3KB,而要接受和傳送資料,至少需要各4KB的空間,而且還需要CPU資源。
  • UDP 和 TCP 有什麼區別呢?分別的應用場景是?

    • UDP 不提供複雜的控制機制,利用 IP 提供面向「無連線」的通訊服務。UDP 協議真的非常簡單,頭部只有 8 個位元組( 64 位)。
      • 目標和源埠:主要是告訴 UDP 協議應該把報文發給哪個程序。
      • 包長度:該欄位儲存了 UDP 首部的長度跟資料的長度之和。
      • 校驗和:校驗和是為了提供可靠的 UDP 首部和資料而設計。
    • TCP和UDP的區別:
      • 連線:TCP 是面向連線的傳輸層協議,傳輸資料前先要建立連線。UDP 是不需要連線,即刻傳輸資料。
      • 服務物件:TCP 是一對一的兩點服務,即一條連線只有兩個端點。UDP 支援一對一、一對多、多對多的互動通訊
      • 可靠性:TCP 是可靠交付資料的,資料可以無差錯、不丟失、不重複、按需到達。UDP 是盡最大努力交付,不保證可靠交付資料。
      • 擁塞控制、流量控制:TCP 有擁塞控制和流量控制機制,保證資料傳輸的安全性。UDP 則沒有,即使網路非常擁堵了,也不會影響 UDP 的傳送速率。
      • 首部開銷:TCP 首部長度較長,會有一定的開銷,首部在沒有使用「選項」欄位時是 20 個位元組,如果使用了「選項」欄位則會變長的。UDP 首部只有 8 個位元組,並且是固定不變的,開銷較小。
      • 傳輸方式:TCP 是流式傳輸,沒有邊界,但保證順序和可靠。UDP 是一個包一個包的傳送,是有邊界的,但可能會丟包和亂序。
      • 分片不同:TCP 的資料大小如果大於 MSS 大小,則會在傳輸層進行分片,目標主機收到後,也同樣在傳輸
        層組裝 TCP 資料包,如果中途丟失了一個分片,只需要傳輸丟失的這個分片。UDP 的資料大小如果大於 MTU 大小,則會在 IP 層進行分片,目標主機收到後,在 IP 層組裝完資料,接著再傳給傳輸層,但是如果中途丟了一個分片,則就需要重傳所有的資料包,這樣傳輸效率非常差,所以通常 UDP 的報文應該小於 MTU。
    • 應用場景:
      • 由於 TCP 是面向連線,能保證資料的可靠性交付,因此經常用於:FTP 檔案傳輸、HTTP / HTTPS。
      • 由於 UDP 面向無連線,它可以隨時傳送資料,再加上UDP本身的處理既簡單又高效,因此經常用於:包總量較少的通訊,如 DNS 、SNMP 等;視訊、音訊等多媒體通訊;廣播通訊。
  • 為什麼 UDP 頭部沒有「首部長度」欄位,而 TCP 頭部有「首部長度」欄位呢?

    • 原因是 TCP 有可變長的「選項」欄位,而 UDP 頭部長度則是不會變化的,無需多一個欄位去記錄UDP 的首部長度。
  • 為什麼 UDP 頭部有「包長度」欄位,而 TCP 頭部則沒有「包長度」欄位呢?

    • TCP 是如何計算負載資料長度:TCP資料長度=IP包總長度-IP首部長度-TCP首部長度。
    • 其中 IP 總長度 和 IP 首部長度,在 IP 首部格式是已知的。TCP 首部長度,則是在 TCP 首部格式已知的,所以就可以求得 TCP 資料的長度。
    • UDP 也是基於 IP 層的呀, UDP 的資料長度也可以通過這個公式計算。DP 「包長度」是冗餘的,可能是為了網路裝置硬體設計和處理方便,首部長度需要是 4 位元組的整數倍。

TCP連線建立

  • TCP的三次握手過程:

    • 一開始,客戶端和服務端都處於 CLOSED 狀態。先是服務端主動監聽某個埠,處於 LISTEN狀態。
    • 客戶端會隨機初始化序號( client_isn ),將此序號置於 TCP 首部的「序號」欄位中,同時把SYN 標誌位置為 1 ,表示 SYN 報文。接著把第一個 SYN 報文傳送給服務端,表示向服務端發起連線,該報文不包含應用層資料,之後客戶端處於 SYN-SENT 狀態。
    • 服務端收到客戶端的 SYN 報文後,首先服務端也隨機初始化自己的序號( server_isn ),將此序號填入 TCP 首部的「序號」欄位中,其次把 TCP 首部的「確認應答號」欄位填入 client_isn +1 , 接著把 SYN 和 ACK 標誌位置為 1 。最後把該報文發給客戶端,該報文也不包含應用層資料,之後服務端處於 SYN-RCVD 狀態。
    • 客戶端收到服務端報文後,還要向服務端迴應最後一個應答報文,首先該應答報文 TCP 首部ACK 標誌位置為 1 ,其次「確認應答號」欄位填入 server_isn + 1 ,最後把報文傳送給服務端,這次報文可以攜帶客戶到伺服器的資料,之後客戶端處於 ESTABLISHED 狀態。

    • 伺服器收到客戶端的應答報文後,也進入 ESTABLISHED 狀態。
    • 第三次握手是可以攜帶資料的,前兩次握手是不可以攜帶資料的。
    • 一旦完成三次握手,雙方都處於 ESTABLISHED 狀態,此時連線就已建立完成,客戶端和服務端就可以相互發送資料了。
  • 如何在 Linux 系統中檢視 TCP 狀態?

    • TCP 的連線狀態檢視,在 Linux 可以通過 netstat -napt 命令檢視。

  • 為什麼是三次握手?不是兩次、四次?

    • 首先,因為三次握手才能保證雙方具有接收和傳送的能力。
    • 其次,重點在於為什麼三次握手才可以初始化Socket、序列號和視窗大小並建立 TCP 連線。
    • 避免歷史連線:三次握手的首要原因是為了防止舊的重複連線初始化造成混亂。
      • 客戶端連續傳送多次 SYN 建立連線的報文,在網路擁堵情況下:
        • 一個「舊 SYN 報文」比「最新的 SYN 」 報文早到達了服務端;
        • 那麼此時服務端就會回一個 SYN + ACK 報文給客戶端;
        • 客戶端收到後可以根據自身的上下文,判斷這是一個歷史連線(序列號過期或超時),那麼客戶端就會發送 RST 報文給服務端,表示中止這一次連線。
      • 如果是兩次握手連線,就不能判斷當前連線是否是歷史連線,三次握手則可以在客戶端(傳送方)準備傳送第三次報文時,客戶端因有足夠的上下文來判斷當前連線是否是歷史連線:
        • 如果是歷史連線(序列號過期或超時),則第三次握手傳送的報文是 RST 報文,以此中止歷史
          連線;
        • 如果不是歷史連線,則第三次傳送的報文是 ACK 報文,通訊雙方就會成功建立連線。
    • 同步雙方初始序列號:兩次握手只能同步一方。
      • TCP 協議的通訊雙方, 都必須維護一個「序列號」, 序列號是可靠傳輸的一個關鍵因素,它的作用:
        • 接收方可以去除重複的資料;
        • 接收方可以根據資料包的序列號按序接收;
        • 可以標識傳送出去的資料包中, 哪些是已經被對方收到的。
      • 當客戶端傳送攜帶「初始序列號」的 SYN報文的時候,需要服務端回一個 ACK 應答報文,表示客戶端的 SYN 報文已被服務端成功接收,那當服務端傳送「初始序列號」給客戶端的時候,依然也要得到客戶端的應答迴應,這樣一來一回,才能確保雙方的初始序列號能被可靠的同步。
      • 四次握手中,服務端的ACK應答和SYN請求可以合成一次。
      • 而兩次握手只保證了一方的初始序列號能被對方成功接收,沒辦法保證雙方的初始序列號都能被確認接收。
    • 避免資源浪費:兩次握手造成冗餘連線。
      • 如果只有「兩次握手」,當客戶端的 SYN 請求連線在網路中阻塞,客戶端沒有接收到 ACK 報文,就會重新發送 SYN ,由於沒有第三次握手,伺服器不清楚客戶端是否收到了自己傳送的建立連線的ACK 確認訊號,所以每收到一個 SYN 就只能先主動建立一個連線。
      • 這會造成,如果客戶端的 SYN 阻塞了,重複傳送多次 SYN 報文,那麼伺服器在收到請求後就會建立多個冗餘
        的無效連結,造成不必要的資源浪費。
    • 小結:
      • TCP 建立連線時,通過三次握手能防止歷史連線的建立,能減少雙方不必要的資源開銷,能幫助雙方同步初始化序列號。序列號能夠保證資料包不重複、不丟棄和按序傳輸。
      • 不使用「兩次握手」和「四次握手」的原因:
        • 「兩次握手」:無法防止歷史連線的建立,會造成雙方資源的浪費,也無法可靠的同步雙方序列
          號;
        • 「四次握手」:三次握手就已經理論上最少可靠連線建立,所以不需要使用更多的通訊次數。
  • 為什麼每次的初始序列號 ISN 是不相同的?

    • 如果一個已經失效的連線被重用了,但是該舊連線的歷史報文還殘留在網路中,如果序列號相同,那麼就無法分辨出該報文是不是歷史報文,如果歷史報文被新的連線接收了,則會產生資料錯亂。
    • 所以,每次建立連線前重新初始化一個序列號主要是為了通訊雙方能夠根據序號將不屬於本連線的報文段丟棄。
    • 另一方面是為了安全性,防止黑客偽造的相同序列號的 TCP 報文被對方接收。
  • 初始序列號 ISN 是如何隨機產生的?

    • 起始 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:

    • 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 分片丟失後,進行重發時也是以 MSS 為單位,而不用重傳所有的分片,大大增加了重傳的效率。

  • 什麼是 SYN 攻擊?如何避免 SYN 攻擊?

    • SYN攻擊:

      • 我們都知道 TCP 連線建立是需要三次握手,假設攻擊者短時間偽造不同 IP 地址的 SYN 報文,服務端每接收到一個 SYN 報文,就進入SYN_RCVD 狀態。
      • 但服務端傳送出去的 ACK + SYN 報文,無法得到未知 IP 主機的 ACK 應答,久而久之就會佔滿服務端的 SYN 接收佇列(未連線佇列),使得伺服器不能為正常使用者服務。
    • Linux 核心的 SYN (未完成連線建立)佇列與 Accpet (已完成連線建立)佇列是如何工作的?

      • 當服務端接收到客戶端的 SYN 報文時,會將其加入到核心的「 SYN 佇列」;
        接著傳送 SYN + ACK 給客戶端,等待客戶端迴應 ACK 報文;
      • 服務端接收到 ACK 報文後,從「 SYN 佇列」移除放入到「 Accept 佇列」;
    • 應用通過呼叫 accpet() socket 介面,從「 Accept 佇列」取出連線。

    • 如果應用程式過慢時,就會導致「 Accept 佇列」被佔滿。

    • 如果不斷受到 SYN 攻擊,就會導致「 SYN 佇列」被佔滿。

    • 解決方式一:

      • 通過修改 Linux 核心引數,控制佇列大小和當佇列滿時應做什麼處理。當網絡卡接收資料包的速度大於核心處理的速度時,會有一個佇列儲存這些資料包。

        # 該佇列的最大值
        net.core.netdev_max_backlog
        # SYN_RCVD 狀態連線的最大個數
        net.ipv4.tcp_max_syn_backlog
        # 超出處理能力時,對新的 SYN 直接回報 RST,丟棄連線:
        net.ipv4.tcp_abort_on_overflow
        
    • 解決方式二:使用SYN Cookie演算法。

      • 當 「 SYN 佇列」滿之後,後續伺服器收到 SYN 包,不進入「 SYN 佇列」;
      • 伺服器計算出一個 cookie 值,再以 SYN + ACK 中的「序列號」返回客戶端,
      • 服務端接收到客戶端的應答報文時,伺服器會檢查這個 ACK 包的合法性。如果合法,直接放入到「 Accept 佇列」。

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 報文時,先回一個 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 的時間,以確保報文已被自然消亡。
    • 首先,保證老的連線的報文段在網路中消失,使下一個新的連線中不會出現這種舊的連線請求的報文段。 網路中可能存在來自發送方的資料包,當這些傳送方的資料包被接收方處理後又會向對方傳送響應,所以一來一回需要等待 2 倍的時間。
    • 其次,保證全雙工連線的關閉。如果被動關閉方沒有收到斷開連線的最後的 ACK 報文,就會觸發超時重發 Fin 報文,另一方接收到 FIN 後,會重發 ACK 給被動關閉方。
    • 2MSL 的時間是從客戶端接收到 FIN 後傳送 ACK 開始計時的。如果在 TIME-WAIT 時間內,因為客戶端的 ACK 沒有傳輸到服務端,客戶端又接收到了服務端重發的 FIN 報文,那麼 2MSL 時間將重新計時。
    • 在 Linux 系統裡 2MSL 預設是 60 秒,那麼一個 MSL 也就是 30 秒。Linux 系統停留在TIME_WAIT 的時間為固定的 60 秒。
  • 為什麼需要 TIME_WAIT 狀態?

    • 主動發起關閉連線的一方,才會有 TIME-WAIT 狀態。
    • 防止具有相同「四元組」的「舊」資料包被收到。
      • 假設 TIME-WAIT 沒有等待時間或時間過短,被延遲的資料包抵達後,如果有相同埠的 TCP 連線被複用後,被延遲的資料包抵達了客戶端,那麼客戶端是有可能正常接收這個過期的報文,這就會產生資料錯亂等嚴重的問題。
      • 經過 2MSL 這個時間,足以讓兩個方向上的資料包都被丟棄,使得原來連線的資料包在網路中都自然消失,再出現的資料包一定都是新建立連線所產生的。
    • 保證「被動關閉連線」的一方能被正確的關閉,即保證最後的 ACK 能讓被動關閉方接收,從而幫助其正常關閉。
      • 四次揮手的最後一個 ACK 報文如果在網路中被丟失了,此時如果客戶端TIME-WAIT 過短或沒有,則就直接進入了 CLOSED 狀態了,那麼服務端則會一直處在LASE_ACK 狀態。
      • 當客戶端發起建立連線的 SYN 請求報文後,服務端會發送 RST 報文給客戶端,連線建立的過程就會被終止。
  • TIME_WAIT 過多有什麼危害?

    • 過多的 TIME-WAIT 狀態主要的危害有兩種:
      • 第一是記憶體資源佔用;
      • 第二是對埠資源的佔用,一個 TCP 連線至少消耗一個本地埠。
    • 客戶端受埠資源限制:
      • 客戶端TIME_WAIT過多,就會導致埠資源被佔用,因為埠就65536個,被佔滿就會導致無法建立新的連線。
    • 服務端受系統資源限制:
      • 由於一個四元組表示 TCP 連線,理論上服務端可以建立很多連線,服務端卻只監聽一個埠 但是會把連線扔給處理執行緒,所以理論上監聽的埠可以繼續監聽。
      • 但是執行緒池處理不了那麼多一直不斷的連線了。所以當服務端出現大量 TIME_WAIT 時,系統資源被佔滿時,會導致處理不過來新的連線。
  • 如何優化 TIME_WAIT?

    • 開啟 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 選項。
      • 複用處於 TIME_WAIT 的 socket 為新的連線所用。
    • net.ipv4.tcp_max_tw_buckets。
      • 當系統中處於 TIME_WAIT 的連線一旦超過某個值時,系統就會將所有的TIME_WAIT 連線狀態重置
    • 程式中使用 SO_LINGER ,應用強制使用 RST 關閉。
      • 該TCP 連線將跳過四次揮手,也就跳過了TIME_WAIT 狀態,直接關閉。
  • 如果已經建立了連線,但是客戶端突然出現故障了怎麼辦?

    • TCP 有一個機制是保活機制。這個機制的原理是這樣的:
      • 定義一個時間段,在這個時間段內,如果沒有任何連線相關的活動,TCP 保活機制會開始作用,每隔一個時間間隔,傳送一個探測報文。
      • 該探測報文包含的資料非常少,如果連續幾個探測報文都沒有得到響應,則認為當前的 TCP 連線已經死亡,系統核心將錯誤資訊通知給上層應用程式。
    • 在 Linux 核心可以有對應的引數可以設定保活時間、保活探測的次數、保活探測的時間間隔,預設下2h+9*75s即2小時11分15秒才可以發現一個死亡連線。
    • 如果開啟了 TCP 保活,需要考慮以下幾種情況:
      • 第一種,對端程式是正常工作的。當 TCP 保活的探測報文傳送給對端, 對端會正常響應,這樣 TCP 保活時間會被重置,等待下一個 TCP 保活時間的到來。
      • 第二種,對端程式崩潰並重啟。當 TCP 保活的探測報文傳送給對端後,對端是可以響應的,但由於沒有該連線的有效資訊,會產生一個 RST 報文,這樣很快就會發現 TCP 連線已經被重置。
      • 第三種,是對端程式崩潰,或對端由於其他原因導致報文不可達。當 TCP 保活的探測報文傳送給對端後,石沉大海,沒有響應,連續幾次,達到保活探測次數後,TCP 會報告該 TCP 連線已經死亡。

TCP的Socket程式設計

重傳機制

  • TCP 實現可靠傳輸的方式之一,是通過序列號與確認應答。在 TCP 中,當傳送端的資料到達接收主機時,接收端主機會返回一個確認應答訊息,表示已收到訊息。(傳送資料1~1000,返回確認並確認號為1001)

  • 超時重傳:

    • 重傳機制的其中一個方式,就是在傳送資料時,設定一個定時器,當超過指定的時間後,沒有收到對方的 ACK 確認應答報文,就會重發該資料,也就是我們常說的超時重傳。
    • TCP 會在以下兩種情況發生超時重傳:
      • 資料包丟失
      • 確認應答丟失
  • 超時時間應該設定為多少呢?

    • RTT 就是資料從網路一端傳送到另一端所需的時間,也就是包的往返時間。

    • 超時重傳時間是以 RTO (Retransmission Timeout 超時重傳時間)表示。

      • 當超時時間 RTO 較大時,重發就慢,丟了半天才重發,沒有效率,效能差;
      • 當超時時間 RTO 較小時,會導致可能並沒有丟就重發,於是重發的就快,會增加網路擁塞,導致更多的超時,更多的超時導致更多的重發。
    • 超時重傳時間 RTO 的值應該略大於報文往返 RTT 的值。

    • 「報文往返 RTT 的值」是經常變化的,因為我們的網路也是時常變化的。也就因為「報文往返RTT 的值」 是經常波動變化的,所以「超時重傳時間 RTO 的值」應該是一個動態變化的值。

    • 估計往返時間,通常需要取樣以下兩個:

      • 需要 TCP 通過取樣 RTT 的時間,然後進行加權平均,算出一個平滑 RTT 的值,而且這個值還是要不斷變化的,因為網路狀況不斷地變化。
      • 除了取樣 RTT,還要取樣 RTT 的波動範圍,這樣就避免如果 RTT 有一個大的波動的話,很難被發現的情況。
    • RTO的計算:

    • 如果超時重發的資料,再次超時的時候,又需要重傳的時候,TCP 的策略是超時間隔加倍。也就是每當遇到一次超時重傳的時候,都會將下一次超時時間間隔設為先前值的兩倍。兩次超時,就說明網路環境差,不宜頻繁反覆傳送。

    • 超時觸發重傳存在的問題是,超時週期可能相對較長。

  • 快速重傳:

    • TCP 還有另外一種快速重傳(Fast Retransmit)機制,它不以時間為驅動,而是以資料驅動重傳。

    • 第一份 Seq1 先送到了,於是就 Ack 回 2;

    • 結果 Seq2 因為某些原因沒收到,Seq3 到達了,於是還是 Ack 回 2;

    • 後面的 Seq4 和 Seq5 都到了,但還是 Ack 回 2,因為 Seq2 還是沒有收到;

    • 傳送端收到了三個 Ack = 2 的確認,知道了 Seq2 還沒有收到,就會在定時器過期之前,重傳丟失的 Seq2。

    • 最後,收到了 Seq2,此時因為 Seq3,Seq4,Seq5 都收到了,於是 Ack 回 6 。

    • 快速重傳的工作方式是當收到三個相同的 ACK 報文時,會在定時器過期之前,重傳丟失的報文段。

    • 快速重傳機制只解決了一個問題,就是超時時間的問題,但是它依然面臨著另外一個問題。就是重傳的時候,是重傳之前的一個,還是重傳所有的問題。

  • SACK方法:Selective Acknowledgment 選擇性確認

    • 要在 TCP 頭部「選項」欄位里加一個 SACK 的東西,它可以將快取的地圖傳送給傳送方,這樣傳送方就可以知道哪些資料收到了,哪些資料沒收到,知道了這些資訊,就可以只重傳丟失的資料。

    • sack大於ack就表明有些資料沒收到。

    • 如果要支援 SACK ,必須雙方都要支援。在 Linux 下,可以通過 net.ipv4.tcp_sack 引數開啟這個功能(Linux 2.4 後預設開啟)。

  • Duplicate SACK:

    • 又稱 D-SACK ,其主要使用了 SACK 來告訴「傳送方」有哪些資料被重複接收了。
    • 解決ack丟包:
      • 「傳送方」傳送了(30003499)和(35003999)兩個資料包,都被成功接收了。
      • 「接收方」發給「傳送方」的兩個 ACK 確認應答都丟失了,所以傳送方超時後,重傳第一個資料包(3000 ~ 3499)
      • 於是「接收方」發現數據是重複收到的,於是回了一個 SACK = 30003500,告訴「傳送方」30003500 的資料早已被接收了,因為 ACK 都到了 4000 了,已經意味著 4000 之前的所有資料都已收到,所以這個 SACK 就代表著 D-SACK 。
      • 也就是SACK 小於ack就代表著 D-SACK,資料重新發送了 。這樣「傳送方」就知道了,資料沒有丟,是「接收方」的 ACK 確認報文丟了。
    • 解決網路延時:
      • 資料包(1000~1499) 被網路延遲了,導致「傳送方」沒有收到 Ack 1500 的確認報文。
      • 而後面報文到達的三個相同的 ACK 確認報文,就觸發了快速重傳機制,但是在重傳後,被延遲的資料包(1000~1499)又到了「接收方」;
      • 所以「接收方」回了一個 SACK=1000~1500,因為 ACK 已經到了 3000,所以這個 SACK 是 DSACK,表示收到了重複的包。
      • 這樣傳送方就知道快速重傳觸發的原因不是發出去的包丟了,也不是因為迴應的 ACK 包丟了,而是因為網路延遲了。
    • D-SACK 有這麼幾個好處:
      1. 可以讓「傳送方」知道,是發出去的包丟了,還是接收方迴應的 ACK 包丟了;
      2. 可以知道是不是「傳送方」的資料包被網路延遲了;
      3. 可以知道網路中是不是把「傳送方」的資料包給複製了;
    • 在 Linux 下可以通過 net.ipv4.tcp_dsack 引數開啟/關閉這個功能(Linux 2.4 後預設開啟)。

滑動視窗

  • 為什麼引入視窗:

    • TCP 是每傳送一個數據,都要進行一次確認應答。當上一個數據包收到了應答了, 再發送下一個。

    • 這種方式的缺點是效率比較低的。而且資料包的往返時間越長,通訊的效率就越低。

    • TCP 引入了視窗這個概念。即使在往返時間較長的情況下,它也不會降低網路通訊的效率。

    • 視窗大小就是指無需等待確認應答,而可以繼續傳送資料的最大值。

    • 視窗的實現實際上是作業系統開闢的一個快取空間,傳送方主機在等到確認應答返回之前,必須在緩衝區中保留已傳送的資料。如果按期收到確認應答,此時資料就可以從快取區清除。

    • 假設視窗大小為 3 個 TCP 段,那麼傳送方就可以「連續傳送」 3 個 TCP 段,並且中途若有 ACK丟失,可以通過「下一個確認應答進行確認」。

    • 只要傳送方收到了 ACK 700 確認應答,就意味著 700 之前的所有資料「接收方」都收到了。這個模式就叫累計確認或者累計應答。

  • 視窗大小由哪一方決定?

    • TCP 頭裡有一個欄位叫 Window ,也就是視窗大小。這個欄位是接收端告訴傳送端自己還有多少緩衝區可以接收資料。於是傳送端就可以根據這個接收端的處理能力來發送資料,而不會導致接收端處理不過來。
    • 所以,通常視窗的大小是由接收方的視窗大小來決定的。傳送方傳送的資料大小不能超過接收方的視窗大小,否則接收方就無法正常接收到資料。
  • 傳送方的滑動視窗:

    • 傳送方快取的資料,根據處理的情況分成四個部分,其中深藍色方框是傳送視窗,紫色方框是可用視窗:

      • 1 是已傳送並收到 ACK確認的資料:1~31 位元組
      • 2 是已傳送但未收到 ACK確認的資料:32~45 位元組
      • 3 是未傳送但總大小在接收方處理範圍內(接收方還有空間):46~51位元組
      • 4 是未傳送但總大小超過接收方處理範圍(接收方沒有空間):52位元組以後
    • 當傳送方把可用視窗中的資料「全部」都一下發送出去後,可用視窗的大小就為 0 了,表明可用視窗耗盡,在沒收到 ACK 確認之前是無法繼續傳送資料了。

    • 當收到之前傳送的資料 32~36 位元組的 ACK 確認應答後,如果傳送視窗的大小沒有變化,則滑動視窗往右邊移動 5 個位元組,因為有 5 個位元組的資料被應答確認,接下來 52~56 位元組又變成了可用視窗,那麼後續也就可以傳送 52~56 這 5 個位元組的資料了。

  • 程式是如何表示傳送方的四個部分的呢?

    • TCP 滑動視窗方案使用三個指標來跟蹤在四個傳輸類別中的每一個類別中的位元組。其中兩個指標是絕對指標(指特定的序列號),一個是相對指標(需要做偏移)。

      • SND.WND :表示傳送視窗的大小(大小是由接收方指定的);
      • SND.UNA :是一個絕對指標,它指向的是已傳送但未收到確認的第一個位元組的序列號,也就是#2 的第一個位元組。
      • SND.NXT :也是一個絕對指標,它指向未傳送但可傳送範圍的第一個位元組的序列號,也就是 #3的第一個位元組。
      • 指向 #4 的第一個位元組是個相對指標,它需要 SND.UNA 指標加上 SND.WND 大小的偏移量,就可以指向 #4 的第一個位元組了。
    • 那麼可用視窗大小的計算就是:可用視窗大 = SND.WND -(SND.NXT - SND.UNA)

  • 接收方的滑動視窗:

    • 接收方快取的資料,根據情況分為三個部分:

      • 1 + 2 是已成功接收並確認的資料(等待應用程序讀取);
      • 3 是未收到資料但可以接收的資料(包括已接收但未確認的資料);
      • 4 未收到資料並不可以接收的資料;
    • 使用兩個指標進行劃分:

      • RCV.WND :表示接收視窗的大小,它會通告給傳送方。
      • RCV.NXT :是一個指標,它指向期望從傳送方傳送來的下一個資料位元組的序列號,也就是 #3 的第一個位元組。
      • 指向 #4 的第一個位元組是個相對指標,它需要 RCV.NXT 指標加上 RCV.WND 大小的偏移量,就可以指向 #4 的第一個位元組了。
  • 接收視窗和傳送視窗的大小是相等的嗎?

    • 並不是完全相等,接收視窗的大小是約等於傳送視窗的大小的。
    • 因為滑動視窗並不是一成不變的。比如,當接收方的應用程序讀取資料的速度非常快的話,這樣的話接收視窗可以很快的就空缺出來。那麼新的接收視窗大小,是通過 TCP 報文中的 Windows 欄位來告訴傳送方。這個傳輸過程是存在時延的,所以接收視窗和傳送視窗是約等於的關係。

流量控制

  • 流量控制:
    • 傳送方不能無腦的發資料給接收方,要考慮接收方處理能力。
    • 如果一直無腦的發資料給對方,但對方處理不過來,那麼就會導致頻繁觸發重發機制,從而導致網路流量的無端的浪費。
    • 為了解決這種現象發生,TCP 提供一種機制可以讓「傳送方」根據「接收方」的實際接收能力控制傳送的資料量,這就是所謂的流量控制。
  • 作業系統緩衝區與滑動視窗的關係:
    • 我們假定了傳送視窗和接收視窗是不變的,但是實際上,傳送視窗和接收視窗中所存放的位元組數,都是放在作業系統記憶體緩衝區中的,而作業系統的緩衝區,會被作業系統調整。
    • 當應用程序沒辦法及時讀取緩衝區的內容時,也會對我們的緩衝區造成影響。
  • 操心繫統的緩衝區,是如何影響傳送視窗和接收視窗的呢?
    • 當應用程式沒有及時讀取快取時:
      • 服務端非常繁忙,應用程序只讀取了一部分資料,還有 一部分資料佔用著緩衝區,於是接收視窗相當於縮小了。
      • 服務端傳送確認資訊時,將視窗大小通告給客戶端。以便客戶端傳送適合該視窗大小的資料。
      • 如果接收端的接收視窗為0,在通告給傳送端後,傳送視窗減少為 0。也就是發生了視窗關閉。
      • 當傳送方可用視窗變為 0 時,傳送方實際上會定時傳送視窗探測報文,以便知道接收方的視窗是否發生了改變。
    • 當服務端系統資源非常緊張的時候:
      • 操心繫統可能會直接減少了接收緩衝區大小,這時應用程式又無法及時讀取快取資料,那麼這時候就有嚴重的事情發生了,會出現資料包丟失的現象。
      • 服務端收到了資料時,如果發現數據大小超過了接收視窗的大小,就會把資料包丟失了。
      • 而客戶端如果在接收到視窗為0的通告之前又傳送了資料,在接收到視窗為0的通告後可用視窗可能變為負值。
      • 如果發生了先減少快取,再收縮視窗,就會出現丟包的現象。
      • 為了防止這種情況發生,TCP 規定是不允許同時減少快取又收縮視窗的,而是採用先收縮視窗,過段時間再減少快取,這樣就可以避免了丟包情況。
  • 視窗關閉的潛在危險:
    • TCP 通過讓接收方指明希望從傳送方接收的資料大小(視窗大小)來進行流量控制。
    • 如果視窗大小為 0 時,就會阻止傳送方給接收方傳遞資料,直到視窗變為非 0 為止,這就是視窗關閉。
    • 接收方向傳送方通告視窗大小時,是通過 ACK 報文來通告的。
    • 那麼,當發生視窗關閉時,接收方處理完資料後,會向傳送方通告一個視窗非 0 的 ACK 報文,如果這個通告視窗的 ACK 報文在網路中丟失了,那麻煩就大了。
    • 這會導致傳送方一直等待接收方的非 0 視窗通知,接收方也一直等待發送方的資料,如不採取措施,這種相互等待的過程,會造成了死鎖的現象。
  • TCP 是如何解決視窗關閉時,潛在的死鎖現象呢?
    • TCP 為每個連線設有一個持續定時器,只要 TCP 連線一方收到對方的零視窗通知,就啟動持續計時器。
    • 如果持續計時器超時,就會發送視窗探測 ( Window probe ) 報文,而對方在確認這個探測報文時,給出自己現在的接收視窗大小。
      • 如果接收視窗仍然為 0,那麼收到這個報文的一方就會重新啟動持續計時器;如果接收視窗不是 0,那麼死鎖的局面就可以被打破了。
    • 視窗探測的次數一般為 3 次,每次大約 30-60 秒(不同的實現可能會不一樣)。如果 3 次過後接收視窗還是 0 的話,有的 TCP 實現就會發 RST 報文來中斷連線。
  • 糊塗視窗綜合症:
    • 如果接收方太忙了,來不及取走接收窗口裡的資料,那麼就會導致傳送方的傳送視窗越來越小。
    • 如果傳送視窗小到了一定的程度,為此傳送一個數據包是不經濟的。
    • 發生該行為的條件:
      • 接收方可以通告一個小的視窗
      • 而傳送方可以傳送小資料
    • 怎麼讓接收方不通告小視窗呢?
      • 當「視窗大小」小於 min( MSS,快取空間/2 ) ,也就是小於 MSS 與 1/2 快取大小中的最小值時,就會向傳送方通告視窗為 0 ,也就阻止了傳送方再發資料過來。
      • 等到接收方處理了一些資料後,視窗大小 >= MSS,或者接收方快取空間有一半可以使用,就可以把視窗開啟讓傳送方傳送資料過來。
    • 怎麼讓傳送方避免傳送小資料呢?
      • Nagle演算法主要是避免傳送小的資料包,要求TCP連線上最多隻能有一個未被確認的小分組,在該分組的確認到達之前不能傳送其他的小分組。
      • 使用 Nagle 演算法滿足以下兩個條件中的一條才可以傳送資料:
        • 要等到視窗大小 >= MSS 或是 資料大小 >= MSS
        • 收到之前傳送資料的 ack 回包
      • 只要沒滿足上面條件中的一條,傳送方一直在囤積資料,直到滿足上面的傳送條件。

擁塞控制

  • 為什麼要有擁塞控制呀,不是有流量控制了嗎?
    • 的流量控制是避免「傳送方」的資料填滿「接收方」的快取,但是並不知道網路的中發生了什麼。
    • 一般來說,計算機網路都處在一個共享的環境。因此也有可能會因為其他主機之間的通訊使得網路擁堵。
    • 在網路出現擁堵時,如果繼續傳送大量資料包,可能會導致資料包時延、丟失等,這時 TCP 就會重傳資料,但是一重傳就會導致網路的負擔更重,於是會導致更大的延遲以及更多的丟包,這個情況就會進入惡性迴圈被不斷地放大。
    • 於是,就有了擁塞控制,控制的目的就是避免「傳送方」的資料填滿整個網路。當網路傳送擁塞時,TCP 會自我犧牲,降低傳送的資料量。
    • 為了在「傳送方」調節所要傳送資料的量,定義了一個叫做「擁塞視窗」的概念。
  • 什麼是擁塞視窗?和傳送視窗有什麼關係呢?
    • 擁塞視窗 cwnd是傳送方維護的一個的狀態變數,它會根據網路的擁塞程度動態變化的。
    • 傳送視窗 swnd 和接收視窗 rwnd 是約等於的關係,那麼由於加入了擁塞視窗的概念後,此時傳送視窗的值是swnd = min(cwnd, rwnd),也就是傳送視窗是擁塞視窗和接收視窗中的最小值。
    • 擁塞視窗 cwnd 變化的規則:
      • 只要網路中沒有出現擁塞, cwnd 就會增大;
      • 但網路中出現了擁塞, cwnd 就減少。
  • 怎麼知道當前網路是否出現了擁塞呢?
    • 其實只要「傳送方」沒有在規定時間內接收到 ACK 應答報文,也就是發生了超時重傳,就會認為網路出現了用擁塞。
  • 擁塞控制有哪些控制演算法?
    • 擁塞控制主要是四個演算法:慢啟動、擁塞避免、擁塞發生、快速恢復。
    • 慢啟動:
      • TCP 在剛建立連線完成後,首先是有個慢啟動的過程,這個慢啟動的意思就是一點一點的提高發送資料包的數量。
      • 慢啟動的規則是:當傳送方每收到一個 ACK,擁塞視窗 cwnd 的大小就會加 1。(擁塞視窗為4,則返回4個ack後變為8)
      • 慢啟動演算法,發包的個數是指數性的增長。
    • 慢啟動漲到什麼時候是個頭呢?
      • 有一個叫慢啟動門限 ssthresh (slow start threshold)狀態變數。
        • 當 cwnd < ssthresh 時,使用慢啟動演算法。
        • 當 cwnd >= ssthresh 時,就會使用「擁塞避免演算法」。
    • 擁塞避免:
      • 當擁塞視窗 cwnd 「超過」慢啟動門限 ssthresh 就會進入擁塞避免演算法。一般來說 ssthresh 的大小是 65535 位元組。
      • 擁塞避免的規則是:每當收到一個 ACK 時,cwnd 增加 1/cwnd。
      • 擁塞避免演算法就是將原本慢啟動演算法的指數增長變成了線性增長,還是增長階段,但是增長速度緩慢了一些。
      • 一直增長著後,網路就會慢慢進入了擁塞的狀況了,於是就會出現丟包現象,這時就需要對丟失的資料包進行重傳。當觸發了重傳機制,也就進入了「擁塞發生演算法」。
    • 擁塞發生:
      • 發生超時重傳的擁塞發生演算法:
        • 使用擁塞發生演算法。這個時候,ssthresh 和 cwnd 的值會發生變化:
          • ssthresh 設為 cwnd/2 ,
          • cwnd 重置為 1
        • 接著,就重新開始慢啟動。這種方式太激進了,反應也很強烈,會造成網路卡頓。
      • 發生快速重傳的擁塞發生演算法:
        • TCP 認為這種情況不嚴重,因為大部分沒丟,只丟了一小部分,則 ssthresh 和 cwnd 變化如下:
          • cwnd = cwnd/2 ,也就是設定為原來的一半;
          • ssthresh = cwnd ;
          • 進入快速恢復演算法
    • 快速恢復:
      • 快速重傳和快速恢復演算法一般同時使用,快速恢復演算法是認為,還能收到 3 個重複 ACK 說明網路也不那麼糟糕,所以沒有必要像 RTO 超時那麼強烈。
      • 擁塞視窗 cwnd = ssthresh + 3 ( 3 的意思是確認有 3 個數據包被收到了);
      • 重傳丟失的資料包;
      • 如果再收到重複的 ACK,那麼 cwnd 增加 1;
      • 如果收到新資料的 ACK 後,把 cwnd 設定為快重傳擁塞發生中的 ssthresh 的值,原因是該 ACK 確認了新的資料,說明從 三次重發 ACK 時的資料都已收到,該恢復過程已經結束,可以回到恢復之前的狀態了,也即再次進入擁塞避免狀態。


iwehdio的部落格園:https://www.cnblogs.com/iwehdio/