TCP 詳解

						<p>上回說到 UDP 協議, 與之對應的便是 TCP 協議</p>

TCP協議

TCP協議全稱: 傳輸控制協議, 顧名思義, 就是要對數據的傳輸進行一定的控制.
先來看看它的報頭
Alt text

我們來分析分析每部分的含義和作用

  • 源端口號/目的端口號: 表示數據從哪個進程來, 到哪個進程去.
  • 32位序號:
  • 4位首部長度: 表示該tcp報頭有多少個4字節(32個bit)
  • 6位保留: 顧名思義, 先保留着, 以防萬一
  • 6位標誌位

URG: 標識緊急指針是否有效
ACK: 標識確認序號是否有效
PSH: 用來提示接收端應用程序立刻將數據從tcp緩衝區讀走
RST: 要求重新建立連接. 我們把含有RST標識的報文稱爲復位報文段
SYN: 請求建立連接. 我們把含有SYN標識的報文稱爲同步報文段
FIN: 通知對端, 本端即將關閉. 我們把含有FIN標識的報文稱爲結束報文段

  • 16位窗口大小:

  • 16位檢驗和: 由發送端填充, 檢驗形式有CRC校驗等. 如果接收端校驗不通過, 則認爲數據有問題. 此處的校驗和不光包含TCP首部, 也包含TCP數據部分.
  • 16位緊急指針: 用來標識哪部分數據是緊急數據.
  • 選項和數據暫時忽略
  • 連接管理機制

    正常情況下, tcp需要經過三次握手建立連接, 四次揮手斷開連接.

    那麼什麼是三次握手? 什麼是四次揮手呢?

    三次握手

    第一次:
    客戶端 - - > 服務器 此時服務器知道了客戶端要建立連接了
    第二次:
    客戶端 < - - 服務器 此時客戶端知道服務器收到連接請求了
    第三次:
    客戶端 - - > 服務器 此時服務器知道客戶端收到了自己的迴應

    到這裏, 就可以認爲客戶端與服務器已經建立了連接.

    再來看個圖.
    Alt text

    剛開始, 客戶端和服務器都處於 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狀態,此後雙方就可以開始通信了。

    爲什麼不用兩次?

    • 主要是爲了防止已經失效的連接請求報文突然又傳送到了服務器,從而產生錯誤。如果使用的是兩次握手建立連接,假設有這樣一種場景,客戶端發送的第一個請求連接並且沒有丟失,只是因爲在網絡中滯留的時間太長了,由於TCP的客戶端遲遲沒有收到確認報文,以爲服務器沒有收到,此時重新向服務器發送這條報文,此後客戶端和服務器經過兩次握手完成連接,傳輸數據,然後關閉連接。此時之前滯留的那一次請求連接,因爲網絡通暢了, 到達了服務器,這個報文本該是失效的,但是,兩次握手的機制將會讓客戶端和服務器再次建立連接,這將導致不必要的錯誤和資源的費。
      如果採用的是三次握手,就算是那一次失效的報文傳送過來了,服務端接受到了那條失效報文並且回覆了確認報文,但是客戶端不會再次發出確認。由於服務器收不到確認,就知道客戶端並沒有請求連接。

    爲什麼不用四次?

    • 因爲三次已經可以滿足需要了, 四次就多餘了.

    再來看看何爲四次揮手.

    數據傳輸完畢後,雙方都可以釋放連接.
    此時客戶端和服務器都是處於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連接的時間要比客戶端早一些。

    再來看一張圖.
    Alt text

    爲什麼最後客戶端還要等待 2*MSL的時間呢?

    • MSL(Maximum Segment Lifetime),TCP允許不同的實現可以設置不同的MSL值。

    • 第一,保證客戶端發送的最後一個ACK報文能夠到達服務器,因爲這個ACK報文可能丟失,站在服務器的角度看來,我已經發送了FIN+ACK報文請求斷開了,客戶端還沒有給我回應,應該是我發送的請求斷開報文它沒有收到,於是服務器又會重新發送一次,而客戶端就能在這個2MSL時間段內收到這個重傳的報文,接着給出迴應報文,並且會重啓2MSL計時器。

    • 第二,防止類似與“三次握手”中提到了的“已經失效的連接請求報文段”出現在本連接中。客戶端發送完最後一個確認報文後,在這個2MSL時間中,就可以使本連接持續的時間內所產生的所有報文段都從網絡中消失。這樣新的連接中不會出現舊連接的請求報文。

    爲什麼建立連接是三次握手,關閉連接確是四次揮手呢?

    • 建立連接的時候, 服務器在LISTEN狀態下,收到建立連接請求的SYN報文後,把ACK和SYN放在一個報文裏發送給客戶端。
      而關閉連接時,服務器收到對方的FIN報文時,僅僅表示對方不再發送數據了但是還能接收數據,而自己也未必全部數據都發送給對方了,所以己方可以立即關閉,也可以發送一些數據給對方後,再發送FIN報文給對方來表示同意現在關閉連接,因此,己方ACK和FIN一般都會分開發送,從而導致多了一次。

    如果已經建立了連接, 但是客戶端突發故障了怎麼辦?

    • TCP設有一個保活計時器,顯然,客戶端如果出現故障,服務器不能一直等下去,白白浪費資源。服務器每收到一次客戶端的請求後都會重新復位這個計時器,時間通常是設置爲2小時,若兩小時還沒有收到客戶端的任何數據,服務器就會發送一個探測報文段,以後每隔75分鐘發送一次。若一連發送10個探測報文仍然沒反應,服務器就認爲客戶端出了故障,接着就關閉連接。

    理解TIME_WAIT狀態

    可以做一個實驗, 先運行server, 再運行client連接server, 然後斷開server, 再立馬運行server.
    我們會發現:
    這裏寫圖片描述

    綁定的時候出了問題.
    這是因爲,雖然server應用程序終止了,但TCP協議層的連接並沒有完全斷開,因此不能再次監聽綁定同樣的server端口.

    TCP協議規定,主動關閉連接的一方要處於TIME_ WAIT狀態,等待2*MSL(maximum segment lifetime)的時間後才能回到CLOSED狀態.
    我們使用Ctrl-C終止了server, 所以server是主動關閉連接的一方, 在TIME_WAIT期間仍然不能再次監聽同樣的server端口
    MSL在RFC1122中規定爲兩分鐘,但是各操作系統的實現不同, 在Centos7上默認配置的值是60s;
    可以通過 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看MSL的值
    這裏寫圖片描述
    解決TIME_WAIT引起的bind失敗問題
    在server的TCP連接沒有完全斷開之前不允許重新監聽, 某些情況下可能是不合理的.

    比如:

    服務器需要處理非常大量的客戶端的連接(每個連接的生存時間可能很短, 但是每秒都有大量的客戶端來請求).
    這個時候如果由服務器端主動關閉連接(比如某些客戶端不活躍, 就需要被服務器端主動清理掉), 就會產生大量TIME_WAIT連接.
    由於我們的請求量很大, 就可能導致TIME_WAIT的連接數很多, 導致服務器的端口不夠用, 無法處理新的連接.

    解決方法:
    - 使用setsockopt()設置socket描述符的選項SO_REUSEADDR爲1, 表示允許創建端口號相同但IP地址不同的多個socket描述符.

    用法:

    • 在server代碼的socket()和bind()調用之間插入如下代碼

    int opt = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    確認應答機制(ACK機制)

    這裏寫圖片描述

    TCP將每個字節的數據都進行了編號, 即爲序列號.
    這裏寫圖片描述

    每一個ACK都帶有對應的確認序列號, 意思是告訴發送者, 我已經收到了哪些數據; 下一次你要從哪裏開始發.
    比如, 客戶端向服務器發送了1005字節的數據, 服務器返回給客戶端的確認序號是1003, 那麼說明服務器只收到了1-1002的數據.
    1003, 1004, 1005都沒收到.
    此時客戶端就會從1003開始重發.

    超時重傳機制

    這裏寫圖片描述

    主機A發送數據給B之後, 可能因爲網絡擁堵等原因, 數據無法到達主機B
    如果主機A在一個特定時間間隔內沒有收到B發來的確認應答, 就會進行重發
    但是主機A沒收到確認應答也可能是ACK丟失了.
    這裏寫圖片描述

    這種情況下, 主機B會收到很多重複數據.
    那麼TCP協議需要識別出哪些包是重複的, 並且把重複的丟棄.
    這時候利用前面提到的序列號, 就可以很容易做到去重.

    超時時間如何確定?
    最理想的情況下, 找到一個最小的時間, 保證 “確認應答一定能在這個時間內返回”.
    但是這個時間的長短, 隨着網絡環境的不同, 是有差異的.
    如果超時時間設的太長, 會影響整體的重傳效率; 如果超時時間設的太短, 有可能會頻繁發送重複的包.

    TCP爲了保證任何環境下都能保持較高性能的通信, 因此會動態計算這個最大超時時間.

    • Linux中(BSD Unix和Windows也是如此), 超時以500ms爲一個單位進行控制, 每次判定超時重發的超時時間都是500ms的整數倍.
      如果重發一次之後, 仍然得不到應答, 等待 2*500ms 後再進行重傳. 如果仍然得不到應答, 等待 4*500ms 進行重傳.
      依次類推, 以指數形式遞增. 累計到一定的重傳次數, TCP認爲網絡異常或者對端主機出現異常, 強制關閉連接.

    滑動窗口

    剛纔我們討論了確認應答機制, 對每一個發送的數據段, 都要給一個ACK確認應答. 收到ACK後再發送下一個數據段.
    這樣做有一個比較大的缺點, 就是性能較差. 尤其是數據往返時間較長的時候.
    那麼我們可不可以一次發送多個數據段呢?
    例如這樣:
    這裏寫圖片描述
    一個概念: 窗口
    窗口大小指的是無需等待確認應答就可以繼續發送數據的最大值.
    上圖的窗口大小就是4000個字節 (四個段).

    發送前四個段的時候, 不需要等待任何ACK, 直接發送
    收到第一個ACK確認應答後, 窗口向後移動, 繼續發送第五六七八段的數據…

    因爲這個窗口不斷向後滑動, 所以叫做滑動窗口.
    操作系統內核爲了維護這個滑動窗口, 需要開闢發送緩衝區來記錄當前還有哪些數據沒有應答
    只有ACK確認應答過的數據, 才能從緩衝區刪掉.
    這裏寫圖片描述

    如果出現了丟包, 那麼該如何進行重傳呢?

    此時分兩種情況討論:

    1, 數據包已經收到, 但確認應答ACK丟了.
    這裏寫圖片描述
    這種情況下, 部分ACK丟失並無大礙, 因爲還可以通過後續的ACK來確認對方已經收到了哪些數據包.

    2, 數據包丟失
    這裏寫圖片描述

    當某一段報文丟失之後, 發送端會一直收到 1001 這樣的ACK, 就像是在提醒發送端 “我想要的是 1001”
    如果發送端主機連續三次收到了同樣一個 “1001” 這樣的應答, 就會將對應的數據 1001 - 2000 重新發送
    這個時候接收端收到了 1001 之後, 再次返回的ACK就是7001了
    因爲2001 - 7000接收端其實之前就已經收到了, 被放到了接收端操作系統內核的接收緩衝區中.

    這種機制被稱爲 “高速重發控制” ( 也叫 “快重傳” )

    流量控制

    接收端處理數據的速度是有限的. 如果發送端發的太快, 導致接收端的緩衝區被填滿, 這個時候如果發送端繼續發送, 就會造成丟包, 進而引起丟包重傳等一系列連鎖反應.
    因此TCP支持根據接收端的處理能力, 來決定發送端的發送速度.
    這個機制就叫做 流量控制(Flow Control)

    接收端將自己可以接收的緩衝區大小放入 TCP 首部中的 “窗口大小” 字段,
    通過ACK通知發送端;
    窗口大小越大, 說明網絡的吞吐量越高;
    接收端一旦發現自己的緩衝區快滿了, 就會將窗口大小設置成一個更小的值通知給發送端;
    發送端接受到這個窗口大小的通知之後, 就會減慢自己的發送速度;
    如果接收端緩衝區滿了, 就會將窗口置爲0;
    這時發送方不再發送數據, 但是需要定期發送一個窗口探測數據段, 讓接收端把窗口大小再告訴發送端.

    這裏寫圖片描述

    那麼接收端如何把窗口大小告訴發送端呢?
    我們的TCP首部中, 有一個16位窗口大小字段, 就存放了窗口大小的信息;
    16位數字最大表示65536, 那麼TCP窗口最大就是65536字節麼?
    實際上, TCP首部40字節選項中還包含了一個窗口擴大因子M, 實際窗口大小是窗口字段的值左移 M 位(左移一位相當於乘以2).

    擁塞控制

    雖然TCP有了滑動窗口這個大殺器, 能夠高效可靠地發送大量數據.
    但是如果在剛開始就發送大量的數據, 仍然可能引發一些問題.
    因爲網絡上有很多計算機, 可能當前的網絡狀態已經比較擁堵.
    在不清楚當前網絡狀態的情況下, 貿然發送大量數據, 很有可能雪上加霜.

    因此, TCP引入 慢啓動 機制, 先發少量的數據, 探探路, 摸清當前的網絡擁堵狀態以後, 再決定按照多大的速度傳輸數據.

    這裏寫圖片描述

    在此引入一個概念 擁塞窗口

    • 發送開始的時候, 定義擁塞窗口大小爲1;
    • 每次收到一個ACK應答, 擁塞窗口加1;
    • 每次發送數據包的時候, 將擁塞窗口和接收端主機反饋的窗口大小做比較, 取較小的值作爲實際發送的窗口

    像上面這樣的擁塞窗口增長速度, 是指數級別的.
    “慢啓動” 只是指初使時慢, 但是增長速度非常快.
    爲了不增長得那麼快, 此處引入一個名詞叫做慢啓動的閾值, 當擁塞窗口的大小超過這個閾值的時候, 不再按照指數方式增長, 而是按照線性方式增長.

    這裏寫圖片描述

    • 當TCP開始啓動的時候, 慢啓動閾值等於窗口最大值
    • 在每次超時重發的時候, 慢啓動閾值會變成原來的一半, 同時擁塞窗口置回1

    少量的丟包, 我們僅僅是觸發超時重傳;
    大量的丟包, 我們就認爲是網絡擁塞;
    當TCP通信開始後, 網絡吞吐量會逐漸上升;
    隨着網絡發生擁堵, 吞吐量會立刻下降.

    擁塞控制, 歸根結底是TCP協議想儘可能快的把數據傳輸給對方, 但是又要避免給網絡造成太大壓力的折中方案.

    延遲應答

    如果接收數據的主機立刻返回ACK應答, 這時候返回的窗口可能比較小.
    假設接收端緩衝區爲1M. 一次收到了500K的數據;
    如果立刻應答, 返回的窗口大小就是500K;
    但實際上可能處理端處理的速度很快, 10ms之內就把500K數據從緩衝區消費掉了; 在這種情況下, 接收端處理還遠沒有達到自己的極限, 即使窗口再放大一些, 也能處理過來;
    如果接收端稍微等一會兒再應答, 比如等待200ms再應答, 那麼這個時候返回的窗口大小就是1M

    窗口越大, 網絡吞吐量就越大, 傳輸效率就越高.
    TCP的目標是在保證網絡不擁堵的情況下儘量提高傳輸效率;

    那麼所有的數據包都可以延遲應答麼?
    肯定也不是
    有兩個限制

    • 數量限制: 每隔N個包就應答一次
    • 時間限制: 超過最大延遲時間就應答一次

    具體的數量N和最大延遲時間, 依操作系統不同也有差異
    一般 N 取2, 最大延遲時間取200ms

    捎帶應答

    在延遲應答的基礎上, 我們發現, 很多情況下
    客戶端和服務器在應用層也是 “一發一收” 的
    意味着客戶端給服務器說了 “How are you”
    服務器也會給客戶端回一個 “Fine, thank you”
    那麼這個時候ACK就可以搭順風車, 和服務器迴應的 “Fine, thank you” 一起發送給客戶端
    這裏寫圖片描述

    面向字節流

    創建一個TCP的socket, 同時在內核中創建一個 發送緩衝區 和一個 接收緩衝區;
    調用write時, 數據會先寫入發送緩衝區中;
    如果發送的字節數太大, 會被拆分成多個TCP的數據包發出;
    如果發送的字節數太小, 就會先在緩衝區裏等待, 等到緩衝區大小差不多了, 或者到了其他合適的時機再發送出去;
    接收數據的時候, 數據也是從網卡驅動程序到達內核的接收緩衝區;
    然後應用程序可以調用read從接收緩衝區拿數據;
    另一方面, TCP的一個連接, 既有發送緩衝區, 也有接收緩衝區,
    那麼對於這一個連接, 既可以讀數據, 也可以寫數據, 這個概念叫做 全雙工

    由於緩衝區的存在, 所以TCP程序的讀和寫不需要一一匹配
    例如:

    • 寫100個字節的數據, 可以調用一次write寫100個字節, 也可以調用100次write, 每次寫一個字節;
    • 讀100個字節數據時, 也完全不需要考慮寫的時候是怎麼寫的, 既可以一次read 100個字節, 也可以一次read一個字節, 重複100次;

    粘包問題

    首先要明確, 粘包問題中的 “包”, 是指應用層的數據包.
    在TCP的協議頭中, 沒有如同UDP一樣的 “報文長度” 字段
    但是有一個序號字段.
    站在傳輸層的角度, TCP是一個一個報文傳過來的. 按照序號排好序放在緩衝區中.
    站在應用層的角度, 看到的只是一串連續的字節數據.
    那麼應用程序看到了這一連串的字節數據, 就不知道從哪個部分開始到哪個部分是一個完整的應用層數據包.
    此時數據之間就沒有了邊界, 就產生了粘包問題

    那麼如何避免粘包問題呢?
    歸根結底就是一句話, 明確兩個包之間的邊界

    對於定長的包
    - 保證每次都按固定大小讀取即可
    例如上面的Request結構, 是固定大小的, 那麼就從緩衝區從頭開始按sizeof(Request)依次讀取即可

    對於變長的包
    - 可以在數據包的頭部, 約定一個數據包總長度的字段, 從而就知道了包的結束位置
    還可以在包和包之間使用明確的分隔符來作爲邊界(應用層協議, 是程序員自己來定的, 只要保證分隔符不和正文衝突即可)

    對於UDP協議來說, 是否也存在 “粘包問題” 呢?

    對於UDP, 如果還沒有向上層交付數據, UDP的報文長度仍然存在.
    同時, UDP是一個一個把數據交付給應用層的, 就有很明確的數據邊界.
    站在應用層的角度, 使用UDP的時候, 要麼收到完整的UDP報文, 要麼不收.
    不會出現收到 “半個” 的情況.

    TCP 異常情況

    • 進程終止: 進程終止會釋放文件描述符, 仍然可以發送FIN. 和正常關閉沒有什麼區別.
    • 機器重啓: 和進程終止的情況相同.
    • 機器掉電/網線斷開: 接收端認爲連接還在, 一旦接收端有寫入操作, 接收端發現連接已經不在了, 就會進行 reset. 即使沒有寫入操作, TCP自己也內置了一個保活定時器, 會定期詢問對方是否還在. 如果對方不在, 也會把連接釋放.
    • 另外, 應用層的某些協議, 也有一些這樣的檢測機制.
      例如HTTP長連接中, 也會定期檢測對方的狀態.
      例如QQ, 在QQ斷線之後, 也會定期嘗試重新連接.

    TCP 小結

    爲什麼TCP這麼複雜?

    因爲既要保證可靠性, 同時又要儘可能提高性能.

    保證可靠性的機制

    • 校驗和
    • 序列號(按序到達)
    • 確認應答
    • 超時重傳
    • 連接管理
    • 流量控制
    • 擁塞控制

    提高性能的機制

    • 滑動窗口
    • 快速重傳
    • 延遲應答
    • 捎帶應答

    定時器

    • 超時重傳定時器
    • 保活定時器
    • TIME_WAIT定時器

    基於 TCP 的應用層協議

    HTTP
    HTTPS
    SSH
    Telnet
    FTP
    SMTP

    當然, 也包括我們自己寫TCP程序時自定義的應用層協議

    TCP 和 UDP 對比

    我們說了TCP是可靠連接, 那麼是不是TCP一定就優於UDP呢?

    TCP和UDP之間的優點和缺點, 不能簡單絕對地進行比較
    TCP用於可靠傳輸的情況, 應用於文件傳輸, 重要狀態更新等場景
    UDP用於對高速傳輸和實時性要求較高的通信領域
    例如, 早期的QQ, 視頻傳輸等. 另外UDP可以用於廣播

    歸根結底, TCP和UDP都是一種工具, 什麼時機用, 具體怎麼用, 還是要根據具體的需求場景去決定.

    發表評論
    所有評論
    還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
    相關文章