TCP傳輸協議詳解

最近重讀的 Stevens 老先生的TCP/IP詳解,梳理了一下,打算把本身理解的寫出來。
TCP/IP是一種面向鏈接的、可靠的、基於字節流的傳輸層通訊協議,它會保證數據不丟包、不亂序。TCP全名是Transmission Control Protocol,它是位於網絡OSI模型中的第四層(Transport layer)。html

TCP 首部

tcp header.jpg

  • Port
    每一個TCP數據段都包含源端口和目的端口號,用於尋找發送端和接收端的應用進程。這兩個值加上IP首部中的源端IP和目的端IP地址惟有時候咱們也會把它稱爲socket四元組(源IP地址、目的IP地址、源端口、目的端口)
  • Sequence number
    序列號用來標識從TCP發送端向TCO接收端發送的數據字節流,它標識在這個報文段中的第一個數據字節。序號是32 bit的無符號數,序號到達232-1後又從0開始。TCP爲應用層提供全雙工服務。這意味數據能在兩個方向上獨立地進行傳輸。所以,鏈接的每一端必須保持每一個方向上的傳輸數據序號。
  • Acknowledgment number
    確認號,和序列號相似,不過它是用來確認已經收到的序號並下次想收到的序號。這兩個序號保證了TCP傳輸過程當中不亂序、不丟包的問題。
  • TCP Flag
    NS:隱藏保護。
    CWR:發送主機設置擁塞窗口減小(CWR)標誌,以指示它收到了一個設置了ECE標誌的TCP段,並在擁塞控制機制中做出響應。
    ECE:ECN-Echo具備雙重角色,具體取決於SYN標誌的值。它代表:
    若是SYN標誌設置爲(1),則TCP對等體具備ECN能力。
    若是SYN標誌爲清除(0),則在正常傳輸期間接收到IP報頭中具備擁塞經歷標誌設置(ECN = 11)的分組。這用做TCP發送方的網絡擁塞(或即將發生的擁塞)的指示。
    URG:表示緊急指針字段是重要的
    ACK:表示確認字段是重要的。客戶端發送的初始SYN數據包以後的全部數據包都應設置此標誌。
    PSH:推送功能。要求將緩衝的數據推送到接收應用程序。
    RST:重置鏈接
    SYN:同步序列號。只有從每一端發送的第一個數據包應該設置此標誌。其餘一些標誌和字段會根據此標誌更改含義,有些僅在設置時有效,有些僅在明確時有效。
    FIN:來自發送方的最後一個數據包。
    關於SYN和FIN能夠參考我這篇文章TCP三次握手和四次揮手
  • Window size
    TCP的流量控制是由鏈接的每一端經過聲明窗口大小來提供。Window size 是一個16bit的字段,因此窗口最大爲65535。
  • Checksum 它是有發送端計算,而後接收端進行驗證。其目的是爲了保證在傳輸過程當中出現什麼差錯,若是校驗和驗證失敗,TCP直接丟棄這個數據段(校驗過程當中會涉及到一個僞首部,僞首部的數據都是從IP數據報頭獲取的,其目的就是爲了檢測TCP數據段是否已經正確到達,只是單純用來作校驗的)。
  • Urgent pointer 緊急指針,它只在URG標誌設置爲1的時候生效。

TCP 數據傳輸

TCP的創建鏈接以前寫過一篇文章,因此就不在這裏細贅了,咱們直接聊TCP數據傳輸中如何保證數據的傳輸順序和丟包問題的,以及怎麼提升TCP傳輸的吞吐量。node

  • Acknowledgement of delay
    一般TCP在收到數據的時候不會馬上發送一個ACK確認,它會延遲發送,能夠和對方須要的數據一塊兒發送(數據捎帶ACK)或者是等待第二個數據來了直接回復第二個ACK,一般的實現採用的延遲是200ms(就是說它會等待200ms有沒有數據一塊兒發送)
  • Nagle
    在數據傳輸過程當中,一般會遇到一些小分組的傳輸(好比 41 bit的數據分組,除去TCP首部和IP首部真正傳輸的數據只有1 bit),像這種小分組多的話,在網絡上傳輸就加大了形成網絡擁塞的可能。爲了提傳輸效率,因此提出了Nagle算法。
    這個算法要求一個TCP鏈接最多隻能有一個未被確認的未完成的小分組,在該分組到達以前不能發送其餘的小分組。而後,TCP會收集這些小分組,並在確認到來時以一個分組的方式發送出去,這樣就能夠有效的減小了小分組。 在一些實時性要求比較高的場景下,採用了Nagle算法會讓用戶感受到時延,因此咱們能夠選擇關閉Nagle算法,Socket API 能夠用 TCP_NODELAY 選項來關閉,nginx上的 tcp_nodely也是採用的這個系統調用。
  • Retransmission
    TCP爲了保證數據不丟失所採用的重傳策略。 TCP超時重傳比較嚴重,它表示已經超時了尚未收到數據確認的回覆,因此他會進入到慢啓動,而快速重傳則不用。
    TCP超時重傳:TCP發送方首先會維護一個TCP的重傳定時器(有的也叫超時時間RTO),這個定時器是根據往返時間(RTT)進行計算,具體算法的實現能夠參考 RFC 6298,當RTO到了尚未收到數據的確認,那麼TCP就認爲數據已經丟失了。TCP會重傳數據,接着進入到擁塞控制裏的慢啓動(關於擁塞控制會在後面講)。
    TCP快速重傳:它主要是收到了三個重複的ACK後(接受方若是收到的數據是亂序的。它會重發本身最近接收到的正確順序的ACK)進行重傳,由於收到重複的ACK表明數據已經發送過去了,其中的一個數據可能由於其餘緣由(如數據傳輸中換了比較遠的路由,或者是數據乾脆直接丟了)形成數據沒有收到。因此這個狀況不算太嚴重,它不會進入到慢啓動,它會進入到快速恢復。
    TCP 在收到連續重複ACK後會重傳最後順序確認包的下一個,這樣原先已經正確傳輸的包可能會重複發送,下降了TCP性能。爲改善這種狀況,發展出SACK(Selective Acknowledgement)技術,使用SACK選項能夠告知發包方收到了哪些數據,發包方收到這些信息後就會知道哪些數據丟失,而後當即重傳丟失的部分。

TCP 滑動窗口

  • 滑動窗口
    TCP在雙方數據傳輸的過程當中,都會維護一個窗口,它表明了我還能夠接受的數據的大小。若是接收方窗口大小爲0,發送方就會中止發送。之因此叫滑動窗口(Sliding Window)是由於它是動態可變的,不是固定的(張開、合攏、收縮)。它保證了數據的可靠傳遞、它確保數據按順序傳遞、而且它強制發送者之間的流量控制。
    window

    上圖中咱們能夠看到:
    發送端的LastByteAcked指向了接收端最後一次順序ACK的位置,LastByteSent指向了發送出的數據,可是尚未收到確認ACK。
    接收端的NextByteExpected指向了已經收到的最後一個連續數據,LastByteAcked指向了接收到的最後一個數據,其中的空白表明還未收到的數據。
    下面看一張滑動窗口的示意圖:
    tcpswpointers.png
    SND.UNA:已發送但還沒有確認的數據的第一個字節的序列號。 這標誌着傳輸類別#2的第一個字節; 全部先前的序列號都是指傳輸類別#1中的字節。
    SND.NXT:要發送到另外一個設備(在這種狀況下是服務器)的下一個數據字節的序列號。 這標誌着傳輸類別#3的第一個字節。
    SND.WND:發送窗口的大小。 回想一下,窗口指定任何設備在任什麼時候候均可能具備「未完成」( 未確認 )的總字節數。 所以,添加第一個未確認字節( SND.UNA )和發送窗口(SND.WND )的序列號標記發送類別#4的第一個字節。 SND.UNA:已經發送可是還沒有確認的 SND.NXT:將要發送的 SND.WND:發送窗口的大小 #1 表示已經確認過的數據,因此窗口右移,黑色表明窗口大小。
    #2 表示已經發送的,可是尚未收到確認。
    #3 表示尚未發送的,接受方能夠接收的數據。
    #4 表示不能發送的數據,接收方不能接收的數據。

下面看一張TCP窗口滑動的示意圖: nginx

tcpswexampleserver.png

  • 糊塗窗口
    咱們看到了TCP經過讓接收方指明窗口來進行流量控制,這將有效的組織發送方放鬆數據,直到窗口變爲非0爲止。可是其中會遇到一個問題,就是接收方發送的的的窗口更新數據丟失,這樣會讓發送方進入到無限等待狀態,由於他要等待窗口更新爲非0。爲了解決這個問題TCP採用了堅持定時器(persist timer)去探測窗口更新。 這樣又會致使一種被稱爲「糊塗窗口綜合症SWS (Silly Window Syndrome)」的情況。若是發生這種狀況,則少許的數據將經過鏈接進行交換,而不是滿長度的報文段。
    該現象可發生在兩端中的任何一段,接受方能夠通告一個小的窗口(而不是一直等待有大的窗口才通告),發送方也能夠發送少許的數據(而不是等待其餘的數據以便發送一個大的數據段)。能夠在任何一端採起避免SWS的現象。
    1.接收方不通告小窗口。一般的算法是接收方不通告一個比當前窗口大的窗口(能夠爲0),除非窗口能夠增長一個報文段大小(也就是將要接收的MSS)或者能夠增長接收方緩存空間的一半,不論實際有多少。
    2.發送方避免出現糊塗窗口綜合症的措施是隻有如下條件之一知足時才發送數據:(a)能夠發送一個滿長度的報文段;(b)能夠發送至少是接收方通告窗口大小一半的報文段;(c)能夠發送任何數據而且不但願接收ACK(也就是說,咱們沒有還未被確認的數據)或者該鏈接上不能使用Nagle算法。

TCP 擁塞控制

TCP不只能夠能夠控制端到端的數據傳輸,還能夠對網絡上的傳輸進行監控。這使得TCP很是強大智能,它會根據網絡狀況來調整本身的收發速度。網絡順暢時就能夠發的快,擁塞時就發的相對慢一些。擁塞控制算法主要有四種:慢啓動,擁塞避免,快速重傳和快速恢復。git

  • 慢啓動和擁塞避免
    慢啓動和擁塞避免算法必須被TCP發送端用來控制正在向網絡輸送的數據量。爲了 實現這些算法,必須向TCP每鏈接狀態加入兩個參量。擁塞窗口(cwnd)是對發送端收到確 認(ACK)以前能向網絡傳送的最大數據量的一個發送端限制,接收端通知窗口(rwnd)是對 未完成數據量的接收端限制。cwnd和rwnd的最小值決定了數據傳送。 另外一個狀態參量,慢啓動閥值(ssthresh),被用來肯定是用慢啓動仍是用擁塞避免 算法來控制數據傳送。 在不清楚環境的狀況下向網絡傳送數據,要求TCP緩慢地探測網絡以肯定可用流量,避免忽然傳送大量數據而使網絡擁塞。在開始慢啓動時cwnd爲1,每收到一個用於確認新數據的ACK至多增長SMSS(SENDER MAXIMUM SEGMENT SIZE)字節。 慢啓動算法在cwnd<ssthresh時使用,擁塞避免算法在cwnd>ssthresh時使用。當cwnd和ssthresh相等時,發送端既可使用慢啓動也可使用擁塞避免。 當擁塞發生時,ssthresh被設置爲當前窗口大小的一半(cwnd和接收方通告窗口大小的最小值,但最少爲2個報文段)。若是是超時重傳,cwnd被設置爲1個報文段(這就是慢啓動,其實慢啓動也不慢,它是指數性增加,只是它的起始比較低)當達到ssthresh時,進入擁塞避免算法(擁塞避免是線性增加)。
    congwin.jpg

在該圖中咱們能夠清楚的看到,ssthresh最初等於8 MSS 。 擁塞窗口在慢啓動期間以指數方式快速上升並在第三次傳輸時達到ssthresh。 而後,擁塞窗口線性地爬升,直到發生丟失(超時),就在發送7以後。當發生丟失時,擁塞窗口是12 MSS 。 而後將ssthresh設置爲6 MSS而且將cwnd設置爲1,而且該過程繼續。github

  • 快速重傳和快速恢復
    當接收端收到一個順序混亂的數據,它應該馬上回復一個重複的ACK。這個ACK的目的是通知發送端收到了一個順序紊亂的數據段,以及指望的序列號。發送端收到這個重複的ACK可能有多種緣由,可能丟失或者是網絡對數據從新排序等。在收到三個重複ACK以後(包含第一次收到的一共四個一樣的ACK),TCP不等重傳定時器超時就重傳看起來已經丟失(可能數據繞路並無丟失)的數據段。由於這個在網絡上並無超時重傳那麼惡劣,因此不會進入慢啓動,而進入快速恢復。快速恢復首先會把ssthresh減半(通常還會四捨五入到數據段的倍數),而後cwnd=ssthresh+收到重複ACK報文段累計的大小。
    1553401836470.jpg
    這個圖上咱們能夠看出,在三次重複ACK後cwnd並無進入到慢啓動,而是進入到了快速重傳。在第二段超時重傳時,進入到了慢啓動cwnd置1。

總結

原本打算以最少的文字去解釋TCP,可是並非很成功。TCP發展至今已經有幾十年了,其中的技術點均可以出好幾本書了。你能夠把它當個索引,快速瀏覽一遍。下面我列一下在寫這篇文章時參考的文檔,都很不錯,值得一讀。
TCP Congestion Control
Transmission Control Protocol
TCP Sliding Window
TCP/IP Guide
rfc 5681算法

相關文章
相關標籤/搜索