TCP狀態轉換圖、滑動窗口、半連接狀態、2MSL


一、TCP狀態轉換圖

  下圖對排除和定位網絡或系統故障時大有幫助,也幫助我們更好的編寫Linux程序,對嵌入式開發也有指導意義。 
  這里寫圖片描述 
  先回顧一下TCP建立連接的三次握手過程,以及關閉連接的四次握手過程: 
   
1、建立連接協議(三次握手) 
(1)客戶端發送一個帶SYN標志的TCP報文到服務器。這是三次握手過程中的報文1。 
(2) 服務器端回應客戶端的,這是三次握手中的第2個報文,這個報文同時帶ACK標志和SYN標志。因此它表示對剛才客戶端SYN報文的回應;同時又標志SYN給客戶端,詢問客戶端是否准備好進行數據通訊。 
(3) 客戶必須再次回應服務段一個ACK報文,這是報文段3。 

2、連接終止協議(四次握手) 
  由於TCP連接是全雙工的,因此每個方向都必須單獨進行關閉。這原則是當一方完成它的數據發送任務后就能發送一個FIN來終止這個方向的連接。收到一個 FIN只意味着這一方向上沒有數據流動,一個TCP連接在收到一個FIN后仍能發送數據。首先進行關閉的一方將執行主動關閉,而另一方執行被動關閉。 
(1) TCP客戶端發送一個FIN,用來關閉客戶到服務器的數據傳送(報文段4)。 
(2) 服務器收到這個FIN,它發回一個ACK,確認序號為收到的序號加1(報文段5)。和SYN一樣,一個FIN將占用一個序號。 
(3) 服務器關閉客戶端的連接,發送一個FIN給客戶端(報文段6)。 
(4) 客戶段發回ACK報文確認,並將確認序號設置為收到序號加1(報文段7)。 
  有了以上的回顧,我們就可以很好的理解上圖各狀態以及其對應的轉換條件了:

CLOSED: 這個沒什么好說的了,表示初始狀態。 LISTEN: 這個也是非常容易理解的一個狀態,表示服務器端的某個SOCKET處於監聽狀態,可以接受連接了。 SYN_RCVD: 這個狀態表示接受到了SYN報文,在正常情況下,這個狀態是服務器端的SOCKET在建立TCP連接時的三次握手會話過程中的一個中間狀態,很短暫,基本 上用netstat你是很難看到這種狀態的,除非你特意寫了一個客戶端測試程序,故意將三次TCP握手過程中最后一個ACK報文不予發送。因此這種狀態 時,當收到客戶端的ACK報文后,它會進入到ESTABLISHED狀態。 SYN_SENT: 這個狀態與SYN_RCVD遙想呼應,當客戶端SOCKET執行CONNECT連接時,它首先發送SYN報文,因此也隨即它會進入到了SYN_SENT狀 態,並等待服務端的發送三次握手中的第2個報文。SYN_SENT狀態表示客戶端已發送SYN報文。 ESTABLISHED: 這個容易理解了,表示連接已經建立了。 FIN_WAIT_1(重要): 這個狀態要好好解釋一下,其實FIN_WAIT_1和FIN_WAIT_2狀態的真正含義都是表示等待對方的FIN報文。而這兩種狀態的區別 是:FIN_WAIT_1狀態實際上是當SOCKET在ESTABLISHED狀態時,它想主動關閉連接,向對方發送了FIN報文,此時該SOCKET即 進入到FIN_WAIT_1狀態。而當對方回應ACK報文后,則進入到FIN_WAIT_2狀態,當然在實際的正常情況下,無論對方何種情況下,都應該馬 上回應ACK報文,所以FIN_WAIT_1狀態一般是比較難見到的,而FIN_WAIT_2狀態還有時常常可以用netstat看到。 FIN_WAIT_2(重要): 上面已經詳細解釋了這種狀態,實際上FIN_WAIT_2狀態下的SOCKET,表示半連接,也即有一方要求close連接,但另外還告訴對方,我暫時還有點數據需要傳送給你,稍后再關閉連接。 TIME_WAIT(重要、共詳細的請看下圖的2MSL): 表示收到了對方的FIN報文,並發送出了ACK報文,就等2MSL后即可回到CLOSED可用狀態了。如果FIN_WAIT_1狀態下,收到了對方同時帶 FIN標志和ACK標志的報文時,可以直接進入到TIME_WAIT狀態,而無須經過FIN_WAIT_2狀態。 CLOSING: 這種狀態比較特殊,實際情況中應該是很少見,屬於一種比較罕見的例外狀態。正常情況下,當你發送FIN報文后,按理來說是應該先收到(或同時收到)對方的 ACK報文,再收到對方的FIN報文。但是CLOSING狀態表示你發送FIN報文后,並沒有收到對方的ACK報文,反而卻也收到了對方的FIN報文。什 么情況下會出現此種情況呢?其實細想一下,也不難得出結論:那就是如果雙方幾乎在同時close一個SOCKET的話,那么就出現了雙方同時發送FIN報 文的情況,也即會出現CLOSING狀態,表示雙方都正在關閉SOCKET連接。 CLOSE_WAIT: 這種狀態的含義其實是表示在等待關閉。怎么理解呢?當對方close一個SOCKET后發送FIN報文給自己,你系統毫無疑問地會回應一個ACK報文給對 方,此時則進入到CLOSE_WAIT狀態。接下來呢,實際上你真正需要考慮的事情是察看你是否還有數據發送給對方,如果沒有的話,那么你也就可以 close這個SOCKET,發送FIN報文給對方,也即關閉連接。所以你在CLOSE_WAIT狀態下,需要完成的事情是等待你去關閉連接。 LAST_ACK: 這個狀態還是比較容易好理解的,它是被動關閉一方在發送FIN報文后,最后等待對方的ACK報文。當收到ACK報文后,也即可以進入到CLOSED可用狀態了。

 

 

二、TCP流量控制(滑動窗口)

  介紹UDP時我們描述了這樣的問題:如果發送端發送的速度較快,接收端接收到數據后處理的速度較慢,而接收緩沖區的大小是固定的,就會丟失數據。TCP協議通過“滑動窗口(Sliding Window)”機制(防止出現丟包)解決這一問題。看下圖的通訊過程。 
  這里寫圖片描述 
  1.發送端發起連接,聲明最大段尺寸是1460,初始序號是0,窗口大小是4K,表示“我的接收緩沖區還有4K字節空閑,你發的數據不要超過4K”。接收端應答連接請求,聲明最大段尺寸是1024,初始序號是8000,窗口大小是6K。發送端應答,三方握手結束。 
  2.發送端發出段4-9,每個段帶1K的數據,發送端根據窗口大小知道接收端的緩沖區滿了,因此停止發送數據。 
  3.接收端的應用程序提走2K數據,接收緩沖區又有了2K空閑,接收端發出段10,在應答已收到6K數據的同時聲明窗口大小為2K。 
  4.接收端的應用程序又提走2K數據,接收緩沖區有4K空閑,接收端發出段11,重新聲明窗口大小為4K。 
  5.發送端發出段12-13,每個段帶2K數據,段13同時還包含FIN位。 
  6.接收端應答接收到的2K數據(6145-8192),再加上FIN位占一個序號8193,因此應答序號是8194,連接處於半關閉狀態,接收端同時聲明窗口大小為2K。 
  7.接收端的應用程序提走2K數據,接收端重新聲明窗口大小為4K。 
  8.接收端的應用程序提走剩下的2K數據,接收緩沖區全空,接收端重新聲明窗口大小為6K。 
  9.接收端的應用程序在提走全部數據后,決定關閉連接,發出段17包含FIN位,發送端應答,連接完全關閉。 
  上圖在接收端用小方塊表示1K數據,實心的小方塊表示已接收到的數據,虛線框表示接收緩沖區,因此套在虛線框中的空心小方塊表示窗口大小,從圖中可以看出,隨着應用程序提走數據,虛線框是向右滑動的,因此稱為滑動窗口。 
  從這個例子還可以看出,發送端是一K一K地發送數據,而接收端的應用程序可以兩K兩K地提走數據,當然也有可能一次提走3K或6K數據,或者一次只提走幾個字節的數據,也就是說,應用程序所看到的數據是一個整體,或說是一個流(stream),在底層通訊中這些數據可能被拆成很多數據包來發送,但是一個數據包有多少字節對應用程序是不可見的,因此TCP協議是面向流的協議。而UDP是面向消息的協議每個UDP段都是一條消息,應用程序必須以消息為單位提取數據,不能一次提取任意字節的數據,這一點和TCP是很不同的。 
  


三、TCP半鏈接狀態

  當TCP鏈接中A發送FIN請求關閉,另一段B回應ACK后,B沒有立即發送FIN給A時,A方處在半鏈接狀態,此時A可以接收B發送的數據,但是A已不能再向B發送數據。

#include <sys/socket.h> int shutdown(int sockfd, int how); sockfd: 需要關閉的socket的描述符 how:允許為shutdown操作選擇以下幾種方式: SHUT_RD:關閉連接的讀端。也就是該套接字不再接受數據,任何當前在套接字接受緩沖區的數據將被丟棄。       進程將不能對該套接字發出任何讀操作。       對TCP套接字該調用之后接受到的任何數據將被確認然后無聲的丟棄掉。 SHUT_WR:關閉連接的寫端,進程不能在對此套接字發出寫操作 SHUT_RDWR:相當於調用shutdown兩次:首先是以SHUT_RD,然后以SHUT_WR

  使用close中止一 個連接,但它只是減少描述符的參考數,並不直接關閉連接,只有當描述符的參考數為0時才關閉連接。 shutdown可直接關閉描述符,不考慮描述 符的參考數,可選擇中止一個方向的連接。 
  注意: 
  1、 如果有多個進程共享一個套接字,close每被調用一次,計數減1,直到計數為0時,也就是所用進程都調用了close,套接字將被釋放。 
  2、 在多進程中如果一個進程中shutdown(sfd, SHUT_RDWR)后其它的進程將無法進行通信. 如果一個進程close(sfd)將不會影響到其它進程. 得自己理解引用計數的用法了 
  


四、2MSL

2MSL TIME_WAIT狀態存在的理由: 
  TIME_WAIT狀態的存在有兩個理由: 
  (1)讓4次握手關閉流程更加可靠;4次握手的最后一個ACK是是由主動關閉方發送出去的,若這個ACK丟失,被動關閉方會再次發一個FIN過來。若主動關閉方能夠保持一個2MSL的TIME_WAIT狀態,則有更大的機會讓丟失的ACK被再次發送出去。 
  (2)防止lost duplicate對后續新建正常鏈接的傳輸造成破壞。 
  lost duplicate在實際的網絡中非常常見,經常是由於路由器產生故障,路徑無法收斂,導致一個packet在路由器A,B,C之間做類似死循環的跳轉。IP頭部有個TTL,限制了一個包在網絡中的最大跳數,因此這個包有兩種命運,要么最后TTL變為0,在網絡中消失;要么TTL在變為0之前路由器路徑收斂,它憑借剩余的TTL跳數終於到達目的地。但非常可惜的是TCP通過超時重傳機制在早些時候發送了一個跟它一模一樣的包,並先於它達到了目的地,因此它的命運也就注定被TCP協議棧拋棄。另外一個概念叫做incarnation connection,指跟上次的socket pair一摸一樣的新連接,叫做incarnation of previous connection。lostduplicate加上incarnation connection,則會對我們的傳輸造成致命的錯誤。大家都知道TCP是流式的,所有包到達的順序是不一致的,依靠序列號由TCP協議棧做順序的拼接;假設一個incarnation connection這時收到的seq=1000, 來了一個lost duplicate為seq=1000,len=1000, 則tcp認為這個lost duplicate合法,並存放入了receive buffer,導致傳輸出現錯誤。通過一個2MSL TIME_WAIT狀態,確保所有的lost duplicate都會消失掉,避免對新連接造成錯誤。 
  該狀態為什么設計在主動關閉這一方: 
  (1)發最后ack的是主動關閉一方 
  (2)只要有一方保持TIME_WAIT狀態,就能起到避免incarnation connection在2MSL內的重新建立,不需要兩方都有 
  如何正確對待2MSL TIME_WAIT?

RFC要求socket pair在處於TIME_WAIT時,不能再起一個incarnation connection。 但絕大部分TCP實現,強加了更為嚴格的限制。在2MSL等待期間,socket中使用的本地端口在默認情況下不能再被使用。若A 10.234.5.5:1234和B 10.55.55.60:6666建立了連接,A主動關閉,那么在A端只要port為1234,無論對方的port和ip是什么,都不允許再起服務。 顯而易見這是比RFC更為嚴格的限制,RFC僅僅是要求socket pair不一致,而實現當中只要這個port處於TIME_WAIT,就不允許起連接。這個限制對主動打開方來說是無所謂的,因為一般用的是臨時端口;但對於被動打開方,一般是server,就悲劇了,因為server一般是熟知端口。比如http,一般端口是80,不可能允許這個服務在2MSL內不能起來。 解決方案是給服務器的socket設置SO_REUSEADDR選項,這樣的話就算熟知端口處於TIME_WAIT狀態,在這個端口上依舊可以將服務啟動。當然,雖然有了SO_REUSEADDR選項,但sockt pair這個限制依舊存在。比如上面的例子,A通過SO_REUSEADDR選項依舊在1234端口上起了監聽,但這時我們若是從B通過6666端口去連它,TCP協議會告訴我們連接失敗,原因為Address already in use.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM