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)。spa
不知道你們有沒有注意到客戶端在發送了最後一個斷開請求的ACK後,又等待了2MSL的時間才關閉鏈接。爲何不直接關閉鏈接呢?若是客戶端直接關閉鏈接,而此時客戶端最後發送的ACK又在網絡中丟失,從而可能致使服務器端的鏈接沒法正常關閉。那爲何又要設置爲2MSL呢?1MSL表示一個IP數據報在網絡中的最多存活時間。假設客戶端最後發送的ACK通過將近1MSL快要到達服務器端的時候丟失了,那麼服務器端在規定的時間內未收到最後客戶端發送的ACK,則服務器端從新發送最後的FIN給客戶端,請求客戶端重發ACK,該FIN通過1MSL到達客戶端。因此如上最壞狀況,若是客戶端在2MSL內沒有收到FIN請求,則代表服務器端已經斷開鏈接。設計
不用多說,你們都知道TCP的傳輸可靠性是依據確認號實現的。簡單說就是客戶端每發送一個分段給服務器端,服務端收到後會給客戶端發送一個確認號,表示服務器端收到該分段。若是客戶端在RTT時間週期內未收到服務器端的確認號,則引起超時重傳。所以TCP協議中須要計時器。那麼問題就來了,TCP有那麼多分段,是要給每個分段都生成一個計時器嗎?code
給每一個分段都生成一個計時器固然是最簡單也最好理解的,每一個計時器在RTT時間後到期,若是沒有收到確認號則重傳該分段。然而給每一個分段都生成計時器將帶來巨大的內存開銷和調度開銷。所以在實際中採起給每一個TCP鏈接生成一個計時器,那麼問題又來了,一個TCP鏈接有那麼多分段,如何利用一個計時器管理這麼多分段呢?設計原則以下(你們能夠思考一下爲何這麼設計):server
確認號是TCP兩端通訊的數據傳輸的「標誌」,TCP的發送端在收到一個確認號後,就認爲接收端已經收到了該確認號以前的全部數據。早期的TCP標準中,只要TCP有一個分段丟失,該分段後的其餘分段即便正確到達接收端,發送端仍是會重傳丟失分段後的全部分段,從而致使了大量沒必要要的超時重傳。如今的TCP實現了一種選擇確認的方式,接收端會顯示的告訴發送端重傳哪些分段,不須要重傳哪些分段,避免了重傳風暴。blog
不知道你們在學習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最多等待兩個分段的積累確認。