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

TCP協議設計原理

left 阻塞 恢復 窗口 思考 com 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協議設計原理