1. 程式人生 > >TCP協議設計原理 TCP協議設計原理

TCP協議設計原理 TCP協議設計原理

TCP協議設計原理


 

  最近去了解TCP協議,發現這是一個特別值得深思的協議。在本篇部落格中,不會長篇大論的給大家介紹TCP協議特點、包頭格式以及TCP的連線和斷開等基本原理,而是會帶大家深入理解為什麼要這麼設計,如果不這麼設計,會產生什麼後果,希望能幫助大家對TCP協議的理解。TCP彌補了IP盡力而為服務的不足,實現了面向連線、高可靠性、報文按序到達、端到端流量控制。

  • 面向連線

  一提到TCP是面向連線的協議,必然是介紹其的3次握手和4次揮手,為了說明為什麼需要三次握手和四次揮手,我們還是拿兩個圖來說明連線建立和斷開的過程。

 

  為什麼要三次握手呢?若兩次握手怎樣。假設客戶端發起連線請求(SYN=1,seq=client_isn),伺服器端收到請求後返回訊息(SYN=1,seq=server_isn,ack=client+1)連線建立。

  現在說明為什麼兩次握手不可以。若客戶端傳送連線請求request1(SYN=1,seq=client_isn),這時這個請求由於網路阻塞沒有及時到達伺服器端,而客戶端一段時間後又傳送了一個連線請求request2 (SYN=1,seq=client_isn),該request2建立了連線完成了本次通訊,然後斷開連線。此時客戶端傳送的第一個連線請求request1到達了伺服器端,此時伺服器端發現是一個連線請求,服務端並不知道這是由於網路阻塞導致已經無用的連線請求,伺服器收到request1則給客戶端傳送訊息(SYN=1,seq=server_isn,ack=client_isn+1)。如果是兩次握手那麼客戶端在收到這條訊息後則客戶端和伺服器端建立連線。但客戶端並不是真正想建立連線,所以不能通過兩次握手就建立連線。

  那為什麼需要四次揮手呢?如果三次揮手又會怎樣。我們假設客戶端向伺服器傳送了斷開請求,伺服器在收到斷開請求後也向客戶端傳送斷開請求(FIN=1,ACK=1,seq=w,ack=u+1),客戶端收到此訊息後向伺服器傳送斷開連線(ACK=1,seq=u+1,ack=w+1)。可想而知這種方法是不可行的。因為當客戶端沒有資料需要傳送給伺服器時,客戶端主動發起了斷開請求,但是並不代表伺服器端沒有資料發給客戶端。所以為了保證伺服器端正常傳輸完資料,伺服器端在收到客戶端傳送的斷開請求後先發送一個ACK(ACK=1,seq=v,ack=u+1)給客戶端,當伺服器端資料傳輸完後傳送斷開請求(FIN=1,ACK=1,seq=w,ack=u+1)。

  不知道大家有沒有注意到客戶端在傳送了最後一個斷開請求的ACK後,又等待了2MSL的時間才關閉連線。為什麼不直接關閉連線呢?如果客戶端直接關閉連線,而此時客戶端最後傳送的ACK又在網路中丟失,從而可能導致伺服器端的連線無法正常關閉。那為什麼又要設定為2MSL呢?1MSL表示一個IP資料報在網路中的最多存活時間。假設客戶端最後傳送的ACK經過將近1MSL快要到達伺服器端的時候丟失了,那麼伺服器端在規定的時間內未收到最後客戶端傳送的ACK,則伺服器端重新發送最後的FIN給客戶端,請求客戶端重發ACK,該FIN經過1MSL到達客戶端。所以如上最壞情況,如果客戶端在2MSL內沒有收到FIN請求,則表明伺服器端已經斷開連線。

  • 傳輸高可靠性

  不用多說,大家都知道TCP的傳輸可靠性是依據確認號實現的。簡單說就是客戶端每傳送一個分段給伺服器端,服務端收到後會給客戶端傳送一個確認號,表示伺服器端收到該分段。如果客戶端在RTT時間週期內未收到伺服器端的確認號,則引發超時重傳。因此TCP協議中需要計時器。那麼問題就來了,TCP有那麼多分段,是要給每一個分段都生成一個計時器嗎?

  給每個分段都生成一個計時器當然是最簡單也最好理解的,每個計時器在RTT時間後到期,如果沒有收到確認號則重傳該分段。然而給每個分段都生成計時器將帶來巨大的記憶體開銷和排程開銷。因此在實際中採取給每個TCP連線生成一個計時器,那麼問題又來了,一個TCP連線有那麼多分段,如何利用一個計時器管理這麼多分段呢?設計原則如下(大家可以思考一下為什麼這麼設計):

  1. 傳送TCP分段時,如果沒有開啟重傳定時器,則開啟;
  2. 傳送TCP分段時,如果有重傳定時器開啟,則不再開啟;
  3. 收到一個非冗餘的ACK時,如果有資料在傳輸,重新啟動重傳定時器;
  4. 收到一個非冗餘的ACK時,如果沒有資料在傳輸,則關閉重傳定時器;
  5. 如果連續收到3個冗餘ACK時,則不用等到重傳定時器超時,直接重傳。
  • 報文按序到達

  確認號是TCP兩端通訊的資料傳輸的“標誌”,TCP的傳送端在收到一個確認號後,就認為接收端已經收到了該確認號之前的所有資料。早期的TCP標準中,只要TCP有一個分段丟失,該分段後的其他分段即使正確到達接收端,傳送端還是會重傳丟失分段後的所有分段,從而導致了大量不必要的超時重傳。現在的TCP實現了一種選擇確認的方式,接收端會顯示的告訴傳送端重傳哪些分段,不需要重傳哪些分段,避免了重傳風暴。

  不知道大家在學習TCP協議時,有沒有考慮TCP序列號迴繞的問題。從TCP報文頭部知道序列號佔32位,能傳輸2的32次方個位元組。如果一個1Gbps的網路,TCP端1s會發送125MB的資料,從而在32s內可傳送2的32次方個位元組,導致序列號迴繞,而32s是小於MSL值的。一旦序列號迴繞會導致接收端對TCP報文的排序發生錯亂。當然可以通過加時間戳的方式來輔助序列號的識別,在接收端發現序列號迴繞時,比較時間戳欄位的值,如果迴繞的序列號時間戳較大,則說明確實發生了迴繞,從而將該資料放在最大的序列號之後。TCP還有其他方法判斷序列號是否發生迴繞,從而有效的確定資料報的排列順序。

  • 端到端流量控制

  端到端流量控制使用滑動視窗來實現,一提到滑動視窗大家張口就來的是慢開始、擁塞避免、快重傳、快恢復。那麼問題來了:①快重傳和快恢復確實提高了TCP的傳輸效率,但是如果傳送端每次傳送的TCP報文中僅有少量的資料,而包含大量的報頭欄位,從而也會影響效率,那麼如何增大發送端傳送資料的大小呢。②接收端在收到資料後返回給傳送端一個ACK,如果接收端針對每個分段都返回ACK的話,網路中的ACK也會消耗大量的頻寬,那麼如何減少網路中ACK的傳送呢。

  大家可能看到這樣的長篇大論,已經沒有了任何興趣,那就放一張卡車拉煤圖吧。我想通過卡車拉煤來說明如何解決這兩個問題。其中括號中的是TCP中問題用拉煤的例子解釋。

  我們先說第一個問題,就是TCP每次攜帶資料量少(卡車每次都拉一點煤,都不夠油錢的)的問題。TCP中為什麼會存在這個問題呢?接收端通過ack告訴傳送端接收端視窗大小,決定傳送端還可以傳送多少資料(北京發電廠告訴山西煤場我這最多還可以接受5kg煤,你下次就送5kg煤就可以了,然後山西煤場就真的開著卡車送來了5kg煤)。這種情況顯然需要從接收端著手解決,如果接收視窗為0,則告訴傳送端不要在傳送資料了,只有當接收端可接受的資料達到接收視窗的一半時,再告訴傳送視窗傳送資料(也就是說北京發電廠已經騰出了一半的空地可放煤了,才告知山西煤場送煤)。那還存在問題,雖然接受視窗已經有一半空閒,但是傳送視窗傳送的TCP攜帶的資料量還是較少(雖然發電廠已經有一半的地可以放煤了,但是煤場每次只送5kg煤)。這就是傳送端的問題了,從而利用Nagle演算法解決傳送端持續傳送小塊資料分段的問題。如下我們就來看看這個Nagle演算法:

複製程式碼
IF 資料的大小和視窗的大小都超過了MSS
Then 傳送資料分段
ELSE
  IF 還有發出的不足MSS大小的TCP分段沒有收到確認
    Then 積累資料到傳送佇列的末尾的TCP分段
  ELSE
    傳送資料分段
  EndIF
EndIF
複製程式碼

  第二個問題就是網路中ACK消耗大量頻寬的問題(也就是說卡車把煤拉到北京,直接帶著北京的口信,空著車就回山西了)。RFC建議了一種延遲的ACK,也就是說接收端在收到資料並不立即回覆ACK,而是等一段時間,看看接收端是否也有資料要傳送給傳送端,同時通過要傳送的資料一同傳輸給傳送端。等一段時間,可能後續的TCP分段到達,這樣就可以取最大者一起返回,從而也能減少網路中ACK的數量。當然RFC的建議延遲的ACK最多等待兩個分段的積累確認。


 

  最近去了解TCP協議,發現這是一個特別值得深思的協議。在本篇部落格中,不會長篇大論的給大家介紹TCP協議特點、包頭格式以及TCP的連線和斷開等基本原理,而是會帶大家深入理解為什麼要這麼設計,如果不這麼設計,會產生什麼後果,希望能幫助大家對TCP協議的理解。TCP彌補了IP盡力而為服務的不足,實現了面向連線、高可靠性、報文按序到達、端到端流量控制。

  • 面向連線

  一提到TCP是面向連線的協議,必然是介紹其的3次握手和4次揮手,為了說明為什麼需要三次握手和四次揮手,我們還是拿兩個圖來說明連線建立和斷開的過程。

 

  為什麼要三次握手呢?若兩次握手怎樣。假設客戶端發起連線請求(SYN=1,seq=client_isn),伺服器端收到請求後返回訊息(SYN=1,seq=server_isn,ack=client+1)連線建立。

  現在說明為什麼兩次握手不可以。若客戶端傳送連線請求request1(SYN=1,seq=client_isn),這時這個請求由於網路阻塞沒有及時到達伺服器端,而客戶端一段時間後又傳送了一個連線請求request2 (SYN=1,seq=client_isn),該request2建立了連線完成了本次通訊,然後斷開連線。此時客戶端傳送的第一個連線請求request1到達了伺服器端,此時伺服器端發現是一個連線請求,服務端並不知道這是由於網路阻塞導致已經無用的連線請求,伺服器收到request1則給客戶端傳送訊息(SYN=1,seq=server_isn,ack=client_isn+1)。如果是兩次握手那麼客戶端在收到這條訊息後則客戶端和伺服器端建立連線。但客戶端並不是真正想建立連線,所以不能通過兩次握手就建立連線。

  那為什麼需要四次揮手呢?如果三次揮手又會怎樣。我們假設客戶端向伺服器傳送了斷開請求,伺服器在收到斷開請求後也向客戶端傳送斷開請求(FIN=1,ACK=1,seq=w,ack=u+1),客戶端收到此訊息後向伺服器傳送斷開連線(ACK=1,seq=u+1,ack=w+1)。可想而知這種方法是不可行的。因為當客戶端沒有資料需要傳送給伺服器時,客戶端主動發起了斷開請求,但是並不代表伺服器端沒有資料發給客戶端。所以為了保證伺服器端正常傳輸完資料,伺服器端在收到客戶端傳送的斷開請求後先發送一個ACK(ACK=1,seq=v,ack=u+1)給客戶端,當伺服器端資料傳輸完後傳送斷開請求(FIN=1,ACK=1,seq=w,ack=u+1)。

  不知道大家有沒有注意到客戶端在傳送了最後一個斷開請求的ACK後,又等待了2MSL的時間才關閉連線。為什麼不直接關閉連線呢?如果客戶端直接關閉連線,而此時客戶端最後傳送的ACK又在網路中丟失,從而可能導致伺服器端的連線無法正常關閉。那為什麼又要設定為2MSL呢?1MSL表示一個IP資料報在網路中的最多存活時間。假設客戶端最後傳送的ACK經過將近1MSL快要到達伺服器端的時候丟失了,那麼伺服器端在規定的時間內未收到最後客戶端傳送的ACK,則伺服器端重新發送最後的FIN給客戶端,請求客戶端重發ACK,該FIN經過1MSL到達客戶端。所以如上最壞情況,如果客戶端在2MSL內沒有收到FIN請求,則表明伺服器端已經斷開連線。

  • 傳輸高可靠性

  不用多說,大家都知道TCP的傳輸可靠性是依據確認號實現的。簡單說就是客戶端每傳送一個分段給伺服器端,服務端收到後會給客戶端傳送一個確認號,表示伺服器端收到該分段。如果客戶端在RTT時間週期內未收到伺服器端的確認號,則引發超時重傳。因此TCP協議中需要計時器。那麼問題就來了,TCP有那麼多分段,是要給每一個分段都生成一個計時器嗎?

  給每個分段都生成一個計時器當然是最簡單也最好理解的,每個計時器在RTT時間後到期,如果沒有收到確認號則重傳該分段。然而給每個分段都生成計時器將帶來巨大的記憶體開銷和排程開銷。因此在實際中採取給每個TCP連線生成一個計時器,那麼問題又來了,一個TCP連線有那麼多分段,如何利用一個計時器管理這麼多分段呢?設計原則如下(大家可以思考一下為什麼這麼設計):

  1. 傳送TCP分段時,如果沒有開啟重傳定時器,則開啟;
  2. 傳送TCP分段時,如果有重傳定時器開啟,則不再開啟;
  3. 收到一個非冗餘的ACK時,如果有資料在傳輸,重新啟動重傳定時器;
  4. 收到一個非冗餘的ACK時,如果沒有資料在傳輸,則關閉重傳定時器;
  5. 如果連續收到3個冗餘ACK時,則不用等到重傳定時器超時,直接重傳。
  • 報文按序到達

  確認號是TCP兩端通訊的資料傳輸的“標誌”,TCP的傳送端在收到一個確認號後,就認為接收端已經收到了該確認號之前的所有資料。早期的TCP標準中,只要TCP有一個分段丟失,該分段後的其他分段即使正確到達接收端,傳送端還是會重傳丟失分段後的所有分段,從而導致了大量不必要的超時重傳。現在的TCP實現了一種選擇確認的方式,接收端會顯示的告訴傳送端重傳哪些分段,不需要重傳哪些分段,避免了重傳風暴。

  不知道大家在學習TCP協議時,有沒有考慮TCP序列號迴繞的問題。從TCP報文頭部知道序列號佔32位,能傳輸2的32次方個位元組。如果一個1Gbps的網路,TCP端1s會發送125MB的資料,從而在32s內可傳送2的32次方個位元組,導致序列號迴繞,而32s是小於MSL值的。一旦序列號迴繞會導致接收端對TCP報文的排序發生錯亂。當然可以通過加時間戳的方式來輔助序列號的識別,在接收端發現序列號迴繞時,比較時間戳欄位的值,如果迴繞的序列號時間戳較大,則說明確實發生了迴繞,從而將該資料放在最大的序列號之後。TCP還有其他方法判斷序列號是否發生迴繞,從而有效的確定資料報的排列順序。

  • 端到端流量控制

  端到端流量控制使用滑動視窗來實現,一提到滑動視窗大家張口就來的是慢開始、擁塞避免、快重傳、快恢復。那麼問題來了:①快重傳和快恢復確實提高了TCP的傳輸效率,但是如果傳送端每次傳送的TCP報文中僅有少量的資料,而包含大量的報頭欄位,從而也會影響效率,那麼如何增大發送端傳送資料的大小呢。②接收端在收到資料後返回給傳送端一個ACK,如果接收端針對每個分段都返回ACK的話,網路中的ACK也會消耗大量的頻寬,那麼如何減少網路中ACK的傳送呢。

  大家可能看到這樣的長篇大論,已經沒有了任何興趣,那就放一張卡車拉煤圖吧。我想通過卡車拉煤來說明如何解決這兩個問題。其中括號中的是TCP中問題用拉煤的例子解釋。

  我們先說第一個問題,就是TCP每次攜帶資料量少(卡車每次都拉一點煤,都不夠油錢的)的問題。TCP中為什麼會存在這個問題呢?接收端通過ack告訴傳送端接收端視窗大小,決定傳送端還可以傳送多少資料(北京發電廠告訴山西煤場我這最多還可以接受5kg煤,你下次就送5kg煤就可以了,然後山西煤場就真的開著卡車送來了5kg煤)。這種情況顯然需要從接收端著手解決,如果接收視窗為0,則告訴傳送端不要在傳送資料了,只有當接收端可接受的資料達到接收視窗的一半時,再告訴傳送視窗傳送資料(也就是說北京發電廠已經騰出了一半的空地可放煤了,才告知山西煤場送煤)。那還存在問題,雖然接受視窗已經有一半空閒,但是傳送視窗傳送的TCP攜帶的資料量還是較少(雖然發電廠已經有一半的地可以放煤了,但是煤場每次只送5kg煤)。這就是傳送端的問題了,從而利用Nagle演算法解決傳送端持續傳送小塊資料分段的問題。如下我們就來看看這個Nagle演算法:

複製程式碼
IF 資料的大小和視窗的大小都超過了MSS
Then 傳送資料分段
ELSE
  IF 還有發出的不足MSS大小的TCP分段沒有收到確認
    Then 積累資料到傳送佇列的末尾的TCP分段
  ELSE
    傳送資料分段
  EndIF
EndIF
複製程式碼

  第二個問題就是網路中ACK消耗大量頻寬的問題(也就是說卡車把煤拉到北京,直接帶著北京的口信,空著車就回山西了)。RFC建議了一種延遲的ACK,也就是說接收端在收到資料並不立即回覆ACK,而是等一段時間,看看接收端是否也有資料要傳送給傳送端,同時通過要傳送的資料一同傳輸給傳送端。等一段時間,可能後續的TCP分段到達,這樣就可以取最大者一起返回,從而也能減少網路中ACK的數量。當然RFC的建議延遲的ACK最多等待兩個分段的積累確認。