TCP協議
TCP的特點
- TCP是面向連接的傳輸層協議。
- TCP是點對點的,每一條TCP連接只能有兩個端點。
- TCP提供可靠交付的服務。
- TCP提供全雙工通信。
- TCP面向字節流。
TCP報文段的首部格式
TCP報文首部與其功能息息相關,所以想深入瞭解TCP功能可以先看看TCP首部。如果有看不懂的地方,在熟悉了TCP功能之後再回顧一遍就會一目瞭然。
本文簡單概況各個字段的作用,如果想了解更細節的可以查閱其他書籍或博客。
-
源端口
2個字節,記錄發送方端口。 -
目的端口
2個字節,記錄接收方端口。 -
序號
4個字節,記錄本報文的第一個字節的序號,這樣就可辨別報文的順序。 -
確認號
4個字節,是期待收到對方下一個報文段第一個字節的序號。 -
數據偏移
4個比特,記錄了本報文中數據部分的起始位置。(其實也就是記錄了首部的長度,可以注意到首部某些字段長度可變,所以需要記錄長度。) -
保留
6個比特,保留爲今後使用,目前全爲0。 -
緊急URG標誌位
表示當前報文特別緊急,需要優先處理。(標誌位都佔1個比特) -
確認ACK標誌位
表示是否收到。(區別於確認號,確認號是ack,而確認標誌位是ACK,確認號記錄的是收到的序號,而確認標誌位表示的是是否收到,只有0和1兩種可能。) -
推送PSH標誌位
很少使用。當兩個應用進行交互式通信時,有時在一端的應用程序希望在鍵入一個命令後立即能夠收到對方的響應。這種情況就將該標誌位置1。 -
復位RST標誌位
當RST=1,表示TCP連接中出現嚴重差錯,必須釋放連接,然後重新建立連接。 -
同步SYN 標誌位
在建立TCP連接時用來同步序號。
當SYN=1,ACK=0,表示這是一個連接請求報文。
當SYN=1,ACK=1,表示這是一個同意連接的響應報文。 -
終止FIN標誌位
用於釋放一個連接。當FIN=1,表示數據發送完畢,請求釋放連接。 -
窗口
2個字節,與滑動窗口功能相關,用於記錄發送本報文的一端接收窗口的大小。窗口值作爲接收方設置發送窗口值大小的依據。 -
檢驗和
2個字節,檢驗首部和數據部分的正確性。在計算檢驗和時,要在TCP報文段的前面加上12字節的僞首部,格式與UDP相同。(可以搜索學習一下僞首部)。 -
緊急指針
兩個字節。在緊急URG標誌位爲1時有意義,記錄了緊急數據末尾所在的位置。值得注意的是:窗口爲0也可以發緊急數據。 -
選項
長度可變,最長40字節。(選項前面都是不可變的,一共20字節。)
選項有很多,比如最長報文段長度MSS,窗口擴大,時間戳等。 -
填充
長度可變。TCP首部長度必須是4字節的倍數,使用使用填充來湊長度。
TCP可靠傳輸的工作原理
TCP協議保證數據傳輸可靠性的方式主要有:
- 校驗和
- 序列號
- 確認應答
- 超時重傳
- 流量控制
- 擁塞控制
- 連接管理
校驗和
計算方式:在數據傳輸的過程中,將發送的數據段都當做一個16位的整數。將這些整數加起來。並且前面的進位不能丟棄,補在後面,最後取反,得到校驗和。
發送方:在發送數據之前計算檢驗和,並進行校驗和的填充。
接收方:收到數據後,對數據以同樣的方式進行計算,求出校驗和,與發送方的進行比對。
注意:如果接收方比對校驗和與發送方不一致,那麼數據一定傳輸有誤。但是如果接收方比對校驗和與發送方一致,數據不一定傳輸成功。
確認應答與序列號
TCP對要發送的所有數據的字節進行編號。
應答的流程:
- 客戶端發送數據中的1~1000個字節
- 服務器發送確認號ack=1001的報文,表示正確收到了前1000個字節,接下來請發送第1001位。
- 如果接收出錯,(比如收到了1003,但沒收到1002)會發送確認號爲出錯的編號。(失序不會影響接收到的數據,接收方會存儲並排序好。)
超時重傳
發送方報文發送後遲遲未收到接收方的相關的ack,可能是因爲某種原因報文未到達,當超過了某個設定的時間,就會觸發重傳。這就是所謂的超時重傳。
超時重傳的概念非常簡單,但複雜的問題其實是超時時間的選擇。重傳時間太小會造成很多不必要的重傳,增加網絡負荷,太長又會使網絡空閒,降低傳輸效率。
TCP採用了一種自適應算法。
計算RTT(報文段往返時間)的加權往返平均值RTTs。
新RTTs = ( 1 - α ) * ( 舊的RTTs ) + α * (新RTT)
RTTd是RTT的偏差的加權平均值。
新RTTd=(1-β) * (舊的RTTd)+β * |RTTs-新RTT|
超時重傳時間RTO=RTTs+4*RTTd
所以超時重傳時間便是RTO。
流量控制
所謂流量控制就是讓發送方的發送速率不要太快,要讓接收方來得及接收。利用滑動窗口機制可以很方便地在TCP連接上實現對發送方的流量控制。
滑動窗口
我們以單工連接爲例子,發送方有一個發送窗口,接收方有一個接收窗口。
事先,接收方會告知發送方窗口大小,即告訴對方自己能承受的窗口大小。如果窗口有變化,之後會再通知。
我們可以用p1,p2,p3三個指針表示發送窗口。
p1以前的表示已經發送但並收到確認的字節。
p1-p3表示窗口以內。窗口內的數據都是當前允許發送的。
p1-p2是已經發送過了,但還爲收到確認,爲了防止發送失敗(如超時重傳)暫時存在窗口內。
p2-p3是允許發送但還沒發送的字節。
p3以後是當前不允許發送的字節。
當某個字節被髮送p2指針會向前移動一格。
當某個字節發送並收到確認,p1和p3向前移動一格,一般來說,窗口大小是固定的。
窗口可能向後收縮,即p1不懂,p3向後移動,這發生在對方通知窗口縮小了。但TCP標準不贊成這樣做,因爲這樣可能發送方在收到這個通知以前語句發送了窗口中的許多數據,現在又收縮窗口,不然發送,就會產生錯誤。
接收方在接收到正確順序的字節會發送確認報文,並將窗口向前移動一格。
收到順序不正確的字節則會暫時存儲,等待正確報文到達,然後一併發送確認並移動窗口。
滑動窗口和TCP首部的一些聯繫(自己的理解)
當我們熟悉了滑動窗口後再看TCP報文首部,就能將一些字段聯繫起來。
主要是這些字段:序號,確認號,窗口。
在發送報文中,序號會是數據部分第一個字節的序號,這樣就可以讓對方知道報文順序。
在接收報文中,確認號是期待收到對方下一個報文段的第一個數據字節的序號。窗口值協商了接下來發送窗口的大小。
通過這樣的方式,就完成了滑動窗口的信息交換。
滑動窗口存在的糊塗窗口綜合徵
糊塗窗口綜合徵有時會使TCP的性能變壞。設想一種情況:TCP接收方的緩存已經滿了,而交互式引用進程一次只從接收緩存中讀取一個字節(接着就使接收緩存空間僅騰出1個字節),然後向發送方發送確認,並把窗口設置爲1個字節(但發送的數據報是40字節)。接着,發送方又發來了一個字節的數據(請注意,發送方發送的IP數據報是41字節長)。接收方發回確認,仍然將黃口設置爲1個字節,這樣網絡效率很低。
要解決這個問題可以讓接收方等到接收緩存有足夠的空間。
比如:
- 能夠接收一個最大報文,或者緩衝區的一半,再來發送窗口通告,這樣就不會產生小報文
- 如果一個窗口低於一半的Window Size,或者不夠一個MSS,就直接通告窗口爲0
滑動窗口的死鎖問題
這裏面涉及到一種情況,如果B已經告訴A自己的緩衝區已滿,於是A停止發送數據;等待一段時間後,B的緩衝區出現了富餘,於是給A發送報文告訴A我的rwnd大小爲400,但是這個報文不幸丟失了,於是就出現A等待B的通知||B等待A發送數據的死鎖狀態。爲了處理這種問題,TCP引入了持續計時器(Persistence timer),當A收到對方的零窗口通知時,就啓用該計時器,時間到則發送一個1字節的探測報文,對方會在此時迴應自身的接收窗口大小,如果結果仍未0,則重設持續計時器,繼續等待。
擁塞控制
在計算機網絡中的鏈路容量(即帶寬)、交換節點中的緩存和處理機等,都是網絡的資源。在某段時間,若堆網絡中某一資源的需求超過了該資源所能提供的可用部分,網絡的性能就會變差。這就是所謂擁塞。(供不應求)
所謂擁塞控制就是防止過多的數據諸如到網絡中,這樣可用使網絡中的路由器或鏈路不致過載。
流量控制往往是點對點通信量的控制,是端到端的問題,而擁塞控制是全局性的。
TCP的擁塞控制方法
TCP的擁塞控制是基於窗口的,發送方讓自己的發送窗口(滑動窗口的發送窗口)等於擁塞窗口。
TCP進行擁塞控制的算法有4中:
- 慢開始
- 擁塞避免
- 快重傳
- 快恢復
慢開始
慢開始的思路:當主機開始發送數據時,並不清楚網絡的符合狀況,所以由小到大主鍵增大發送窗口(由小到大逐漸增大擁塞窗口數值)。直到增加到ssthresh慢開始門限,改爲使用擁塞避免算法。
慢開始有三個關鍵點:初始擁塞窗口值,擁塞窗口cwnd的增加量,慢開始門限ssthresh
初始擁塞窗口值:
SMSS是發送方的最大報文段。
若2190 <SMSS字節,則設置初始擁塞窗口cwnd=2 * SMSS字節,且不得超過2個報文段。
若1095 < SMSS < 2190字節,則設置初始擁塞窗口cwnd=3 * SMSS字節,且不得超過3個報文段。
若SMSS<=1095字節,則設置初始擁塞窗口cwnd=4 * SMSS字節,且不得超過4個報文段。
擁塞窗口cwnd每次的增加量:
擁塞窗口cwnd每次的增加量=min(N,SMSS)
N是原先未被確認,但現在被剛收到的確認報文段所確認的字節數。
當一個輪次過後,擁塞窗口會翻倍。
慢開始門限ssthresh:
一開始會有一個慢開始門限(書上默認是16,但我不確定這個是不是正確的)。當慢開始的擁塞窗口>=ssthresh則會使用擁塞避免算法。擁塞避免會把擁塞窗口值+1(看上面的圖),直到網絡發生了擁塞,慢開始門限則會被修改ssthresh=swnd/2,同時將擁塞窗口cwnd=1。
擁塞避免
其實上面講的超不多了。
當慢開始的擁塞窗口>=ssthresh則會使用擁塞避免算法。擁塞避免會把擁塞窗口值+1(看上面的圖),直到網絡發生了擁塞,慢開始門限則會被修改ssthresh=swnd/2,同時將擁塞窗口cwnd=1。
快重傳
有時,個別報文段會在網絡中丟失,但實際上網絡並未發生擁塞。如果發送方遲遲收不到確認,就會產生超時,就會誤認爲網絡發生了擁塞。這就導致發送方錯誤地啓動了慢開始,又將擁塞窗口設置爲1,因而降低了傳輸效率。
快重傳能解決這個問題。當接收方收到了失序的報文就立即發送快重傳報文(對正確順序的最後一個報文的確認報文),接收方每收到一個失序報文就發送快重傳報文。接收方一連接收到3個重複確認,就知道對方沒收到哪一個報文,接下來:
- 立即進行重傳。
- 然後把ssthresh設置爲cwnd的一半。
- 把cwnd再設置爲ssthresh的值(具體實現有些爲ssthresh+3)。
- 重新進入擁塞避免階段。
快恢復
後來的“快速恢復”算法是在上述的“快速重傳”算法後添加的,當收到3個重複ACK時,TCP最後進入的不是擁塞避免階段,而是進入快恢復。
- 當收到3個重複ACK時,把ssthresh設置爲cwnd的一半,把cwnd設置爲ssthresh的值加3,然後重傳丟失的報文段,加3的原因是因爲收到3個重複的ACK,表明有3個“老”的數據包離開了網絡。
- 再收到重複的ACK時,擁塞窗口增加1。
- 當收到新的數據包的ACK時,把cwnd設置爲第一步中的ssthresh的值。原因是因爲該ACK確認了新的數據,說明從重複ACK時的數據都已收到,該恢復過程已經結束,可以回到恢復之前的狀態了,也即再次進入擁塞避免狀態。
連接管理
TCP的連接可以分爲三個階段:
- 連接建立
- 數據傳輸
- 連接釋放
數據的傳輸(確認應答與序列號)其實前面已經講了,還有其他的細節也在上面講過了。
接下來看連接建立釋放,以及TCP的11中狀態。
TCP的連接建立(三握手)
上圖已經很詳細了。
說一下需要注意的地方,主要是和TCP首部的一些聯繫。
SYN=1,ACK=0表示請求連接。SYN=1,ACK=1表示同意連接。(之前講過)
三個報文都不能攜帶數據,只能有首部。
ACK和ack不一樣,一個是標誌位,一個是確認號。(可以翻回TCP首部再看一下)
seq是序號,ack是確認,所以ack的值必然是上一條報文的seq+1。
其實我也有一個比較迷惑的點就是x和y值的選擇,似乎是隨意的。
爲什麼需要三握手,不可以兩握手?
假設出現一種異常情況,即A發出的第一個連接請求報文並沒有丟失,而是在某些網絡節點長時間滯留了,以致延誤到連接釋放以後的某個時間才達到B。本來這是一個早已經失效的報文,但B收到此失效的連接請求報文段後,就誤以爲是A又發出一次新的連接請求。於是就向A發送確認報文段,同意建立連接。假設不採用三握手,那麼B發出確認,新連接就建立了,但實際上出錯了。
TCP的連接釋放(四揮手)
上圖很詳細,基本上的東西也和三握手類似。
半關閉狀態
客戶端發FIN,服務端回ACK後,(也就是前兩個報文發完後)TCP連接處於半關閉狀態,即客戶端已經沒有數據要發送了,但服務器若發送數據,客戶端仍要接收。也就是說從服務器到客戶端這個方向的連接並未關閉,這個狀態可能持續一段時間。
爲什麼需要TIME_WAIT狀態
停留在time_wait會停留2msl,msl就是最長分節生命期,是任何IP能夠在英特網中存活的最長時間。(2msl一般是1分鐘到4分鐘之間)
每個數據報有一個跳限的8位字段,它的最大值是255。儘管這個跳限不是真正的時間限制,我們仍然假設:具有最大跳限的分組在網絡中存在的時間不可能超過msl時間。
分組在網絡中“迷途”通常是路由異常的結果。某個路由器崩潰或兩個路由器之間的某個鏈路斷開時,路由協議需要花數秒甚至數分鐘的時間找到另一條通路(也就是收斂,這裏應該說的是外部網關協議)。因爲某個分組“迷途”了之後,超時重傳了第二個分組,第二個分組通過某條路徑到達目的地。然而不久後,之前崩潰的鏈路恢復了,“迷途”的分組最終也到達了目的地,這樣就有兩個重複的分組到達了目的地。
TIME_WAIT保證了本連接的分組消亡,不會進入下一個連接。可靠地實現了連接的釋放。
TCP的有限狀態機(TCP的11中狀態)
TCP的11個狀態圖需要配合三握手和四揮手圖一起看。之所以放到最後是因爲狀態機貫穿整個TCP連接。