TCP協議原理詳解

在閱讀之前,你需要了解網絡協議的基本知識,這篇博文並不會具體介紹,只是粗淺的總結Tcp協議相關知識。


一.TCP的概念

TCP協議是建立在傳輸層上的協議。不同於它的兄弟udp協議,它是面向連接的協議,即:必須兩方建立了連接之後纔可以傳輸數據。
這裏通過一個圖片來描述應用程序是如何通過tcp/ip協議通信。
這裏寫圖片描述

同爲傳輸層協議的tcp協議相對於udp協議,它可以保證數據傳輸的完整性,彌補了ip協議的best-effort性。
具體來看tcp協議是如何工作的:

TCP協議的連接和釋放
要講清楚tcp協議是如何開始工作的,首先先看數據在兩個應用之間是如何通過封裝分用來協調各個協議進行通信的:
數據封裝
這幅圖來自 《tcp/ip協議 卷一》,從這幅圖可以看到在傳輸層數據由tcp首部和應用數據組成。很明顯tcp首部包含了一切tcp工作機制所需要的信息。
接下來來看一下tcp首部格式
tcp首部

主要組成:

  • 4個字節的源端口和目的端口
  • 4個字節的序號
  • 4個字節的確認號
  • 6bits標誌位 urg,ack,psh,rst,syh,fin
  • 窗口

以上組成是接下來分析需要用到的。先來簡單的說一下:

  1. tcp首部只包含源端口和目的端口,因爲源ip和目的ip是放在ip首部裏面的。
  2. 4個字節的序號,主要存放傳輸數據的第一個字節的序號。TCP規定,tcp連接上傳輸的數據流中每一個數據都要編上一個序號。
    (如何通過序號保證數據傳輸的順序性呢?)
  3. 4個字節的確認號是和序號合作使用,來保證數據傳輸的正確性。
  4. 6個0/1標誌位用來表示tcp報文段所屬類別。
  5. 窗口,用來控制流量。

二.TCP的連接和釋放

簡單分析了tcp首部之後,開始分析tcp的連接,即:三次握手。
這裏寫圖片描述
分析上圖的過程:
1.客戶端A和服務器B上的TCP都處於CLOSED狀態。
2.一個客戶端A主動進程發起連接請求(主動打開),這時本地TCP實體就創建傳輸控制快(TCB),發送一個發送一段 報文段1 給服務器B ,報文段1中SYN標誌位爲1,序號爲seq=x;(一個 SYN將佔用一個序號),客戶端A狀態變成SYN-SENT。(第一次握手)
3.服務器進程發出被動打開,進入監聽狀態LISTEN。服務器B收到A發送的syn報文段後,同樣返回一個syn報文段2, 報文段2中,SYN=1,ACK=1,序號爲seq=y; ackSeq(確認號)=x+1; 確認號表示,希望客戶端下一次發送的seq。 發送之後,服務器B狀態編程SYN-RCVD。 (第二次握手)
4.客戶端A在收到服務器發回來的報文段2,發送一個ack報文段3,報文段3中,SYN=0,ACK=1, seq=x+1; ack=y+1;(注意,這裏SYN=0,因此這裏不佔用一個序號,tcp連接建立之後發送數據序號還是從seq=x+1 開始)。在發送之後,客戶端狀態變成established,等到服務器端接受到報文段3之後,狀態也變成established,tcp連接建立,可以開始數據傳輸了。(第三次握手)

Q1:爲什麼需要三次握手,如果確認兩方連接,兩次握手就可以保證了?
A1: 主要是爲了防止已失效的連接請求報文段突然又傳到了B,因而產生錯誤。假定出現一種異常情況,即A發出的第一個連接請求報文段並沒有丟失,而是由於網絡延遲長時間滯留了,一直延遲到連接釋放以後的某個時間纔到達B,本來這是一個早已失效的報文段。但B收到此失效的連接請求報文段後,就誤認爲是A又發出一次新的連接請求,於是就向A發出確認報文段,同意建立連接。假定不採用三次握手,那麼只要B發出確認,新的連接就建立了,這樣一直等待A發來數據,B的許多資源就這樣白白浪費了。
Q2:如果第三次握手沒有成功,服務端會發生什麼?
A1:這要查看tcp狀態機變化(在博文最後會給出狀態變化),當失敗時服務器會根據設定的重傳定時器,重傳ack報文,但是之後客戶端的第三次握手的ack報文還是失敗,超過了可允許重傳的時間愛你,那麼服務端就發送RTS報文段,自身進入CLOSED狀態。當客戶端接收到RTS報文段後,也由established狀態進入closed狀態。這樣做的目的是爲了防止SYN洪泛攻擊。

繼續分析TCP連接的終止,即 四次揮手:
四次揮手
分析上圖過程:
1.客戶端進程發起主動關閉,客戶端A發送一個FIN報文段1,報文段1中 FIN=1 seq=u;同時客戶端狀態由established 變成 FIN-WAIT-1 ,此時客戶端這邊連接已經關閉,不會再傳輸數據。
2.服務器收到了報文段1,發送確認ack報文段2,報文段2的ACK=1,seq=v,ackSeq=u+1;並在此時通知服務器進程,同時進入close-wait狀態。
3.客戶端在收到報文段2之後,自身進入FIN-WAIT-2 狀態。
4.服務器進程發送關閉指令給服務器後,服務器發送關閉FIN報文段3,報文段3中 FIN=1,ACK=1,seq=w,ackSeq=u+1; (這裏要注意,報文段2的seq=v,而這裏seq=w,而不是v+1,這是因爲在close-wait狀態的時候,服務器連接沒有關閉仍可以向客戶端發送數據)。在發完報文段3之後,服務器進入last-ack狀態。
4.客戶端在fin-wait-2狀態的時候,收到fin+ack報文段後,發送ack報文段4,報文段4 ACK=1,seq=u+1,ackSeq=w+1 進入TIME-WAIT狀態,在經過2MSL(最大報文段生存時間),進入closed狀態。
**MSL:任何報文段被丟棄前在網絡內的最長時間,一般設置爲30s。
5.服務器端在收到確認ack報文段後,進入closed狀態。

Q1:爲什麼要設置一個TIME-WAIT狀態然後經過2MSL進入到closed狀態呢?
A1:這是爲了保證最後一個ack一定能傳輸到服務器。如果服務器沒有收到ack,會重發一次報文段3給客戶端,然後客戶端在發一次報文段4給服務器。一來一回,兩個報文段最大生存時間是2MSL。
Q2:如果服務器進程不發送關閉指令給服務器,那麼tcp連接是否一直處於半關閉狀態?
A2:是的,如果沒有報文段3發送過來,確實一直處於半關閉狀態,只有服務器會向客戶端發送數據。
解決辦法:如果執行主動關閉的應用層將進行全關閉,而不是半關閉來說明它還想接收數據,就設置一個定時器。時間到了,就進入到closed狀態。
Q3:tcp連接釋放一定是經過4次揮手嗎?
A3:不一定,tcp連接的釋放會由於各種異常的情況導致,不過都是通過RST報文段來實現的。(也可以在數據傳輸完成後,直接發送RST報文段來關閉tcp連接)

當然,TCP的四次揮手是一方主動關閉,一方被動關閉,有可能會出現兩方都主動關閉的情景:
這裏寫圖片描述
由圖中可以看到狀態fin-wait-1時候,如果收到ack報文段就會進入fin-wait-2狀態,如果收到fin報文段就會進入closing狀態。然後在收到ack報文段就會進入time-wait,最終進入closed,關閉連接。

三.TCP的數據傳輸

前面已經分析了tcp的連接和釋放,那麼下面來分析tcp的數據傳輸是如何保證數據的完整性和順序性。
已經提到,tcp傳輸的數據流中每一個字節都有一個序號,而tcp首部中序號是指,這個報文段中傳輸數據的第一個字節的序號。明確了這個前提,我們來看一段tcp傳輸數據的實例:
這裏寫圖片描述
前面三段指的是三次握手,從第四次開始客戶端向服務器發送一個包含長度爲1440字節流的報文段1。(注意傳輸數據的時候SYN=0,ACK=1,此時seq=1,ackSeq=1,),然後第四次又繼續傳輸了一個1440的報文段2。(這裏注意到,並沒有在收到確認報文ack後才能繼續發送,但是tcp中針對有的情況會要求需要發送ack才能繼續發送報文段,後面會講到),報文段2的seq=1+1440,ack=1。第5次,服務器發送了一個ack確認回來,這個確認報文段3,seq=1;ack=1441,說明服務器希望客戶端再發送的數據是1441.後面就不具體分析了。
上面的傳輸是一個沒有出現錯誤的傳輸。
但是,網絡上會由於各種各樣的原因,導致數據傳送出現錯誤。那麼tcp是怎麼保證數據不會出現錯誤呢?
tcp中引入了超時重傳機制和快速重傳機制。
超時重傳算法:
加入客戶端發送報文段1 的時候,出現了問題,服務器沒有收到。那麼服務器不會發送ack,而是一直等3傳輸過來,等到超時還沒發送過來,就會重傳1,但是由於ack不能跳着確認,只能確認最大連續收到的包,因此後面2,3,4,根本不能確認,那麼我們是否應該重傳2,3,4呢?而且每收不到一個包就要等超時重傳,這樣導致效率非常的低。也許會想,把超時重傳的時間閥設置的非常小,那麼是不是效率就快呢?這有可能會導致,還沒接收到就重發。
快速重傳機制:
我們可以看到超時重傳算法,有很大的不足,所以tcp在重傳這件事上又引用了快速重傳機制,這個機制說的是,當客戶端發送報文段1,出現了問題,服務器沒有收到,這時候服務器會有一個定時器,時間到了就會繼續發送ack,當客戶端連續收到3個相同的ack報文,就會觸發快速重傳機制,重新傳輸報文段1.
這裏寫圖片描述
但這樣仍有不足,就是重傳的時候,我們仍不知道後面有幾份收到了,所以仍然可能會重傳後面很多份數據。快速重傳相對於超時重傳只是解決了重傳快慢的效率問題,而重傳後面的數據仍沒有解決。
爲了解決這個問題,tcp引入sack機制。這個問題,我感覺我講不清楚。想詳細瞭解的可以看這篇博客:
http://coolshell.cn/articles/11564.html
這篇博客在我看tcp/ip卷一的過程中給了我很多幫助。

四.TCP和網絡的配合

在第三節,分析了tcp是如何保證數據的完整性的,這一節來分析TCP協議針對各種不同的網絡環境是通過什麼機制和算法來確定數據傳輸的速率。
在具體分析之前,先弄懂tcp傳輸的數據一般有兩種:

  • 1.交互數據流
  • 2.成塊數據流

交互數據流

指的是客戶端輸入一個指令,服務器根據得到的報文段相應一個指令,一般是起控制作用的數據,這種數據有一個特點就是字節很少,但是由於tcp本身首部就有20個字節,ip首部也有20個字節,那麼一個數據報文段就是41個字節(假設發送的控制數據爲一個字節),很明顯,如果不停的發送這種只含一個這樣數據的小報文段,那麼很容易造成網絡的擁塞。對於這種數據流,tcp的處理方式有兩種:

  • Nagle算法:(應用在客戶端)
  • ack延時機制(應用在服務端)

    1. Nagle算法的設計原則:

(1)如果包長度達到最大報文長度(MSS,Maximum Segment Size),則允許發送;
(2)如果該包含有FIN,則允許發送;
(3)設置了TCP_NODELAY選項,則允許發送;
(4)未設置TCP_CORK選項時,若所有發出去的小數據包(包長度小於MSS)均被確認,則允許發送;
(5)上述條件都未滿足,但發生了超時(一般爲200ms),則立即發送。

Nagle算法的作用是,減少網絡上小包的數量,它的機制就是,數據小包如果小於mss,那麼不發送,等到把數據合在一起超過了mss就發送。在多個小包合在一起的時間內,如果收到了服務器端對先前發送數據的確認,那麼立即發送剛合成好的數據包(即使它不大於mss);

Nagle的僞代碼.
if there is new data to send
  if the window size >= MSS and available data is >= MSS
    send complete MSS segment now
  else
    if there is unconfirmed data still in the pipe
      enqueue data in the buffer until an acknowledge is received
    else
      send data immediately
    end if
  end if
end if

2. ack延時機制

同樣是爲了減少網絡中數據小包數量,它的機制是,在收到客戶端發送的數據,不立即發送ack確認報文段,而是在等待一段時間後(一般爲200ms),希望應用程序會對剛剛收到的數據進行應答,這樣就可以用新數據將ACK捎帶過去。這樣通過把對客戶端相應的數據和ack確認數據合成一個報文段。這樣就減少了tcp協議的開銷。

Q1:這兩種機制加在一起是否會產生1+1大於2的作用呢?
A1:並不會,甚至有可能會導致性能變差。這種典型場景就是發送端寫-寫-讀。
即先向服務端發送一個寫操作小包,在服務器中由於ack延時機制,必須等待200ms才能發送ack確認。而發送端由於發送的小包觸發了nagle算法,因此第二個寫操作必須等待ack確認到了才能繼續發送。 寫-寫之間就花費了200ms+RRT,而讀操作,是等待服務器發送數據過來,寫-讀的時間就看服務器相應的時間了。
這裏寫圖片描述

成塊數據流

和交互數據流相反,成塊數據流是正常的一個tcp中帶很多字節的報文段。傳輸這種數據的時候,如果一直傳輸而不考慮網速問題,那麼就會導致傳送數據失敗,網絡擁塞的問題。那麼tcp是如何根據網速來決定它傳輸的速度呢?是如何控制自己的速度呢?
這利用了一個概念 滑動窗口。在前面介紹tcp首部的時候,有說到窗口這個東西。這個窗口就是滑動窗口,它會根據實際情況調整自己的大小,因此叫滑動窗口。
在tcp連接建立的時候,客戶端和服務端會協商一個窗口的大小,但在通信過程中,接收端可根據自己的資源情況,隨時動態的調整對方的發送窗口上限值。
什麼是滑動端口呢?
這裏寫圖片描述
黑框的就是滑動窗口的大小,這是一個抽象的概念,裏面包含發送未確認的數據(#2),還可以發送的數據(#3)。當#2中,例如確認數據到了38,那麼窗口的開始處就變成了39,窗口向右移動。
這裏可以明白一點,窗口的左邊是受確認序號控制的。而窗口的大小動態的受服務器返回的ack中窗口值來規定。
比如,原來窗口大小20個字節,但是由於服務器處理速度快,希望客戶端多發一點數據,於是在ack報文段中,窗口設置爲30個字節,那麼滑動窗口變成30個字節(其實,並不是這麼簡單的就是30個字節,這裏我們是假設不考慮擁塞窗口的問題,後面會繼續分析)。
當然,更典型的是,服務端處理速度慢,發送的數據總是不能得到及時的處理,因此爲了防止客戶端一直髮送數據,而服務器又不能處理,導致丟包重傳,甚至網絡擁塞,那麼服務器就會把窗口設置到很小,以至於小到mss,觸發nagle算法。

這個窗口的協商過程是什麼呢?
發送端在確定發送報文段的速率時,既要根據接收端的接收能力,又要考慮網絡擁塞問題。

min[rwnd,cwnd]

rwnd:接收窗口,也就是上面提到的ack報文段中的窗口。
cwnd:擁塞窗口,發送端根據自己估計的網絡擁塞程度而設置的窗口值。
當rwnd小於cwnd時,接收端的接收能力限制了發送窗口的最大值。
當cwnd小於rwnd時,網絡的擁塞限制了發送窗口的最大值。

因此tcp中有一系列針對網絡擁塞的處理算法。

擁塞控制算法:

  1. 慢啓動算法
  2. 擁塞避免算法
  3. 快重傳(這裏第三節裏已經講過)
  4. 快恢復(配合快重傳使用)

首先要明確擁塞控制算法的本質都在於控制擁塞窗口的大小,至於接收窗口是由服務器決定的,而不是算法可以決定的。

1.慢啓動
1.設定一個慢啓動閥值:ssthresh
2.在剛加入網絡開始傳輸數據的時候,cwnd=mss
3.每當收到一個新的ack確認報文段cwnd+=mss
4.每當過了一個RRT(發送接收的一個來回時間),cwnd=cwnd*2,成指數增長;
5.當cwnd超過ssthresh的時候,進入擁塞避免算法。
慢啓動算法,可以和實際練習起來,比如用迅雷下載東西的時候,剛加入的下載資源的速度總是從 0k 開始增長到帶寬最大的速度。即,剛加入網絡的資源要一點點提速。
2.擁塞避免算法
1.cwnd按線性增長,每經過一個RTT,cwnd+=mss;

這裏寫圖片描述
圖中有一個地方,發生超時的時候,重傳速率又是採用慢開始算法。那麼每次重傳都要用慢開始算法嗎?

3.快恢復算法
在前面瞭解到不必等到重傳計時器超時,只要連續收到三個相同的ack,就開始重傳數據,即不必等到超時,也不必重新採用慢開始算法。而是採用快恢復算法。
當可以在超時時間內連續收到3個ack,那麼說明網絡不至於那麼差。
1. 設置cwnd = sshthresh + 3 * MSS
2. 重傳Duplicated ACKs指定的數據包
3. 如果再收到 duplicated Acks,那麼cwnd = cwnd +1 (繼續慢增長,等待是否有新的ack或者超時)
4. 如果收到了新的Ack,那麼,cwnd= sshthresh ,然後就進入了擁塞避免的算法了。(說明已經進入網絡正常階段了)

至此,Tcp的基礎算是分析的差不多了,至於更高明的處理算法,有興趣可以詳讀tcp/ip協議。然後這裏提一下,tcp中超時重傳的時間(RTO)非常重要,以及tcp中有4個定時器,可以去查資料瞭解。因爲沒有什麼分析的價值。
最後給一個tcp狀態機的僞代碼:
tcp狀態機圖片我就不貼了,好累呀。用了一個禮拜才完成這篇blog。果然認真寫博客要命啊。

switch (狀態){
   case closed狀態 :
      if(收到“被動打開”報文)
         進入到 listen 報文;
      if (收到“主動打開”報文)
      {
         發送SYN報文段;
         進入SYN-SENT 狀態;
      }
      if(收到任何報文段){
         發送RST報文段
      }
      if(收到其他報文段) {
         發出差錯報文;
      }
      break;

   case LISTEN  狀態:
      if(收到"發送數據"報文){
         發送SYN報文段;
         進入SYN-SENT狀態;
      }
      if(收到任何SYN報文段) {
         發送SYN+ ACK 報文段
      }
      if(收到任何其他報文段或) {
         發出差錯報文
      }
      break;
   case SYS-SENT 狀態:
      if(超時)
         進入closed狀態;
      if(收到SYN報文段) {
         發送SYN+ACK 報文段;
         進入SYN-RCVD狀態;
      }
      if(收到SYN+ACK 報文段){
         發送ACK報文段;
         進入established狀態;
      }
      if(收到其他報文段) {
         發出差錯報文;
      }
      break;
   case SYN-RCVD 狀態:
      if(收到ACK報文段)
         進入established狀態;
      if(超時) {
         發送RST報文段;
         進入到closed狀態;
      }
      if(收到"關閉報文"){
         發送FIN報文段;
         進入到FIN-WAIT-1 狀態;
      }
      if(收到RTS報文段){
         進入到listen 狀態;
      }
      if(收到其他報文段) {
         發出差錯報文;
      }
      break;
   case ESTABLISHED 狀態:
      if(收到FIN報文段) {
         發送FIN報文段;
         進入closed-wait 狀態;
      }
      if(收到進程的關閉報文) {
         發送FIN報文段;
         進入FIN-WAIT-1 狀態;
      }
      if(收到RTS或者SYN報文段) {
         發出差錯報文;
      }
      if(收到數據或ACK報文段)
         調用輸入模塊;
      if(收到"發送"報文)
         調用輸出模塊塊;
      if(收到其他報文段) {
         發出差錯報文;
      }
      break;
   case FIN-WAIT-1 狀態:
      if(收到FIN報文段){
         發送ACK報文段;
         進入closing狀態;
      }
      if(收到FIN+ack 報文段){
         發送ack報文段;
         進入fin-wait狀態;
      }
      if(收到ack報文段)
         進入fin-wait-2;
      break;

   case FIN-WAIT-2 狀態:
      if(收到FIN報文段){
         發送ack報文段;
         進入time-wait狀態;
      }
      if(收到其他報文段) {
         發出差錯報文;
      }
      break;

   case closing 狀態:
      if(收到ack報文段)
         進入time-wait狀態;
      break;
   case time-wait 狀態:
      if(超時2msl)
         進入closed狀態;
      break;
   case closed-wait 狀態:
      if(收到"關閉"報文){
         發送FIN報文段;
         進入last-ack狀態;
      }
      break;

   case last-ack狀態:
      if(收到ack報文段)
         進入closed狀態;
      break;
   }

   }
   }
}

參考資料:
https://wenku.baidu.com/view/4d96cb718e9951e79b8927e9.html
http://coolshell.cn/articles/11609.html
http://blog.csdn.net/macrossdzh/article/details/5967676

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