TCP的狀態轉化過程(11中狀態)以及TIME_WAIT狀態

TCP中的三次握手,四次揮手是我們所熟知的,可是,我們熟悉裏面的各種狀態嗎???

(SYN_SENT,   ESTABLISHED,    CLOSE_WAIT.............),試問一句,我們瞭解裏面的狀態轉化嗎???



1,大家先看一個簡單的通信圖(圖片轉載與:UNIX網絡編程,page:36,圖2-5)

     

     可以很明顯的看到,在通信雙方,客戶端,服務端的狀態變化過程

     有人可能會說:我們上面不是說,有11中狀態嗎??爲什麼到啦這裏變成了只有10中

     (1,(主動打開:SYN_SENT)        2,ESTABLISHED        3,(主動關閉:FIN_WAIT_1)     4,FIN_WAIT_2

       5,TIME_WAIT        6,SYN_RCVD     7,CLOSE_WAIT(被動關閉)     8,LAST_ACK    9,CLOSED

       10,LISTEN)

       爲什麼不是11個呢???

       哈哈,其實還有一種狀態叫做:CLOSING(這個狀態產生的原因比較特殊,後面分析)

      


      接下來我們分析一下,這些狀態的變化過程,,,

           主動套接口:用來發起連接                            被動套接口:用來接受連接

      1,對於服務器端來說:

           當調用socket函數創建一個套接字時,狀態是CLOSED,它被假設爲一個主動套接字,也就是說,它是一個

           將調用connect發起連接的客戶套接字。listen函數把一個未連接的套接字轉化成一個被動套接字,指示內核

           應接受指向該套接字的連接請求。結合TCP的狀態轉化圖:

           調用listen函數導致套接字從:CLOSED狀態轉化爲:LISTEN狀態

       2,對於客戶端來說:

             調用socket函數創建一個套接口時,狀態也是CLOSED,同樣的,它也被假設爲一個主動套接字,緊接着,調

             用connect主動打開套接口,並且一直阻塞着,等待三次握手的完成,我們把這個狀態稱之爲:主動套接口

             當客戶端發起了三次握手的第一次(SYN   J,MSS = 536)的時候,套接口的狀態變成了:

             SYN_SENT(主動打開)

       3,對於服務器端而言,調用了listen之後,然後狀態就變成了LISTEN狀態,接着調用accept函數,使自身一直

            保持阻塞的狀態,直到三次握手的第一次來到(來自TCP協議棧的TCP的第一個分節),即接收到(SYN  J,

            MSS = 536),此刻狀態由:LISTEN轉變爲SYN_RCVD

       4,對於客戶端來說,剛纔發送了TCP協議棧中TCP三次握手的第一個分節,此刻應該接受來自服務器發送過來的

             TCP三次握手的第二個分節,這時服務器發送過來:(SYN K, ACK J+1, MSS = 1460),此刻,服務器

             的狀態不變,還是SYN_RCVD,然後,客戶端接受服務器發送過來的TCP三次握手的第二次分節,此刻狀態

             由之前的:SYN_SENT轉變爲ESTABLISHED,(客戶端已經建立完成),這時,connect函數返回

       5,然後客戶端保持ESTABLISHED狀態,並且發出TCP協議棧中TCP三次握手的第三個分節(ACK  K+1)

             服務端的狀態由:SYN_RCVD轉變爲:ESTABLISHED,從未完成的隊列中取出隊首的第一個連接放在已完成

             隊列,這樣accept函數就會返回。

             此刻,兩者都建立完成,這個時候可以完成通信了

       6,那麼接下來就是連接終止的四次握手,,,

             當雙方都變成ESTABLISHED狀態之後,雙方就可以通信了,在雙方通信的過程中,由於狀態都沒有變化,

             所以這裏,我們暫且不討論。在通信的時候呢,雙方都可以主動發起關閉,那麼:我們假定客戶端發起一個

             關閉請求(調用close函數):會向服務端發送一個TCP分節(TCP協議棧中四次握手的的第一個分節:

             FIN M)然後客戶端的狀態會變成:FIN_WAIT_1(主動關閉)此刻,服務端接收到這個TCP分節後,

            並且會對剛纔發過來的連接進行確認(ACK M+1),服務端的狀態會變成 CLOSE_WAIT(被動關

             閉)當,客戶端接收到這個確認之後(ACK M+1),客戶端的狀態轉變 

             爲:FIN_WAIT_2   , 只有當服務端的read函數返回爲0的時候,服務端才需要,也是纔可以發起關閉請求(FIN 

             N),發送完成之後,就變成了:

             LAST_ACK,       當客戶端接受到了這個關閉請求之後,狀態會變成了:TIME_WAIT(會經過

             2MSL(TCP報文端最大生存週期的兩倍時間)之後,轉變爲:CLOSED),緊接着客戶端會發送

             最後一次確認:(ACK N+1),等到服務端接收到這個確認後,服務端的狀態會變成:CLOSED

      


             關於CLOSING:

             該狀態產生的原因是:對於客戶端和服務端而言,兩者同時關閉的情況(這種情況並不多見),如下圖:

            

、              兩者同時關閉,後狀態同時變成了FIN_WAIT_1,然後當另外一端接收到關閉分節後,狀態同時變成CLOSING,然後都對剛纔那個分節進行確認,當對端收到之後,兩者又都變成了TIME_WAIT,

                  所以說:在關閉的過程中,不一定可以必須要經過FIN_WAIT_2這個狀態。。。。。。。。。。。。


                 



關於TIME_WAIT:

            1,我們可以從上面的狀態分析中得知,對於TIME_WAIT狀態而言,是執行主動關閉的那端經歷了這個狀態。

                   該端點停留在這個狀態的持續時間是最長分節生命期(MAXIMUM  SEGMENT  LIFETIME, msl)的兩

                    倍,有時候稱之爲:2MSL

                

                    任何TCP實現都必須爲MSL選擇一個值,RFC1122的建議值是2分鐘,而源自Berkeley的實現傳統上改用

                     30秒這個值,又因爲:信息的傳送是需要一個來回,着也就說明,TIME_WAIT狀態的持續時間是1分鐘

                     到4分鐘之間。而MSL是任何IP數據報能夠在因特網中存活的最長時間。我們也知道這個時間是有限的,

                     因爲每個數據報含有一個跳限(hop limit)的8位字段,它的最大值是255。儘管這是一個跳數限制而不是

                     真正的時間限制,我們仍然假設:

                     具有最大跳限(255)的分組在網絡中存在的時間不可能超過MSL秒。。。。。

                    

                     分組在網絡中“迷途”通常是路由異路的結果。某個路由器崩潰或某兩個路由器之間的某個鏈路斷開時,路由

                     協議需要花數秒鐘到數分鐘的時間才能穩定並找出另一條通路。在這段時間內可能發生路由循環(

                     路由器A把分組發送給路由器B,而B再把它們發送給A),我們關心的分組可能就此陷入這樣的循環。

                     假設迷途的分組是一個TCP分節,在它迷途期間,發送端TCP超時重傳該分組,而重傳的分組卻通過某條

                     候選路徑到達最終目的。然而不久後(自迷途的分組開始其旅程起最多MSL秒以內)路由循環修復,早先

                     迷失在這個循環中的分組最終也被送到目的地。TCP必須正確處理這些重複的分組。


                     TIME_WAIT狀態存在的兩個理由:

                     1,可靠的實現TCP全雙工連接的終止(更好的完善TCP的可靠性)

                     2,允許老的重複分節在網絡中消逝


                     關於第一點:假設最終的ACK丟失了來解釋(並不能保證傳輸的可靠行)。服務器將重新發送它的最終的

                     那個FIN, 因此客戶必須維護狀態信息,以允許它重新發送那個ACK。要是客戶不維護狀態信息,它將

                     響應以一個RST(另外一種類型的TCP分節),該分節將被服務器解釋成一個錯誤。如果TCP打算執行所

                     有必要的工作以徹底終止某個連接上兩個方向的數據流(即全雙工關閉),那麼它必須正確處理連接終止

                     序列4個分節中任何一個分節丟失的情況。本例子也說明了爲什麼執行主動關閉的那一端是處於

                     TIME_WAIT的那一端;因爲可能不得不重傳最終的那個ACK的就是那一端。


                     關於第二點:我們假設在12.106.32.254的1500端口和206.168.112.219的21端口之間有一個TCP連接。我

                     們關閉這個連接,過一段時間後在相同的IP地址和端口之間建立另一個連接。後一個連接稱爲前一個連接

                     的化身,因爲他們的IP地址和端口號相同。TCP必須防止來自某個連接的老的重複分組在該連接已終止後

                    再現,從而被誤解成屬於同一個連接的某個新的化身。爲做到這一點,TCP將不給處於TIME_WAIT狀態

                    的連接發起新的化身。既然TIME_WAIT狀態的持續時間是MSL的2倍,這就足矣讓某個方向上的分組最多

                    存活MSL秒即被丟棄,另一個方向上的應答最多存活MSL秒也被丟棄。通過實施這個規則,我們就能保證

                    每成功建立一個TCP連接時,來自該連接先前化身的老的重複分組都已在網絡中消逝了。。。。

                     

            

                    大家可以過來看看!!!

                    當我們僅僅打開服務端之後(端口號爲5188),我們來看看所處的狀態。

                    打開服務端:

                   

                    調用命令查看所有的網絡狀態:netstat

                    

                     然後,我們通過命令:摘取有關tcp的狀態:netstat  -an |grep tcp

                    

                     緊接着爲了刪減出有效的信息,我們只需要tcp協議,5188這個端口,我們可以這樣做:

                    

 netstat -an|grep tcp|grep 5188
                     

                     嗯嗯,此刻,可以看到,我們這裏的狀態是處於LISTEN,調用的accept函數還是在阻塞着,等待着返回。



                     這時,我們再次打開客戶端,繼續觀察一下狀態:

                    

                     然後,我們繼續調用之前的命令:


netstat -an|grep tcp|grep 5188

                    

                     當客戶端一打開,那麼就完成了TCP的建立,這裏,我們可以看到有兩個是:ESTABLISHED

                     其中第二行的42555表示的是客戶端所打開的端口,5188是服務端所打開的端口,客戶端連向了服務器端


                     由於我們上面的測試是在同一臺主機上的,所以會出現上面的三種信息


                     而對於其他的狀態而言,只是因爲狀態的轉化時間非常短(三次握手,四次揮手完成的特別快),我們不

                     去探究具體的狀態,,,



  1,查找服務器進程:

       

ps  -ef | grep echoserv


分析其pid號,知道了我們此刻打開的是中間的這個服務端(21858,21849)

所以,此刻,我們殺死這個進程:


kill -9  21858



到啦這裏,我們再次查看一下狀態:


至於爲什麼會產生一個FIN_WAIT2,   而不是TIME_WAIT狀態呢,,,,這是因爲:我們程序中是這樣處理的,我們

的服務端關閉之後,然後客戶端接收到啦這個分節,並向服務端發送了當前的分節確認,然後自己阻塞在了從鍵盤獲

取字符的這個位置,並不能運行到函數read處去,也就是說,

read函數壓根就不會返回0,所以客戶端就不會重新向服務端重新發送關閉連接的分節,也就停留在此刻了,同樣的,

服務端接受到啦確認分節,那麼自己的狀態就變成了FIN_WAIT_2,這樣就解釋的通了,哈哈哈

以下是:我們的客戶端處理程序:

void echo_cli(int sock)
{
                                                                                                                                                
        char sendbuf[1024] = {0};
        char recvbuf[1024] = {0};

        while(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
        {
                writen(sock, sendbuf, strlen(sendbuf));

                int ret = readline(sock, recvbuf, sizeof(recvbuf));
                
                if(ret == -1)
                        ERR_EXIT("READline");
                else if(ret == 0)
                {
                        printf("client close \n");      
                        break;
                }
                
                fputs(recvbuf, stdout);      //fgets接受到的數據,默認說明是存在換行符的
                memset(sendbuf, 0 , sizeof(sendbuf));
                memset(recvbuf, 0 , sizeof(recvbuf));
        }

        close(sock);
}

此刻,如果我們再重新輸入字符,然後就會執行到read函數處,由於對方已經關閉,對端會接收到(四次揮手)的

第一個分節(FIN),然後read返回0,從上面函數可以看出,程序執行break,然後繼續執行close(sock)



而對於客戶端先關閉的情況,,,則是這個樣子的,,,

同理,先打開服務端,再打開客戶端,,,

進去之後,直接按:CTRL + C,使客戶端退出,我們查看一下狀態:


可以知道,出現了TIME_WAIT狀態,,,

同樣的,這裏,我們也需要查看一下echoserv具體的實現:

void echo_serv(int conn)
{
        char recvbuf[1024];
        while(1)
        {
                memset(recvbuf, 0, sizeof(recvbuf));
                int ret = readline(conn, recvbuf, 1024);
                if(ret == -1)
                        ERR_EXIT("READLine");
                
                if(ret == 0)
                {
                        printf("client close\n");       
                        break;
                }
                        
                fputs(recvbuf, stdout);
                writen(conn, recvbuf, strlen(recvbuf));  
        }
}

出現這個狀態也是比較簡單,因爲:客戶端結束了之後,服務端開始執行readline(裏面封裝了read),read 返回爲0

不會阻塞,緊接着就執行close,會繼續發送一個fin分節,,所以會出現後面的TIME_WAIT狀態啦,,,


我們的服務器端會處於TIME_WAIT狀態,這時如果我們繼續打開服務器會出現:地址佔用,

bind:address already in use

如果,我們不使用REUSEADDR的話,如果我們使用這個REUSEADDR,並且設置選項的話,setsockopt的話,那麼

我們可以隨時打開服務器,不用等待2MSL個時間






關於RST分節,

1,對於RST分節,其實是這個樣子的,我們打開服務端,客戶端,然後關閉服務端(會向客戶端發送一個FIN 分節)

      ,但是這個時候,我們的客戶端是阻塞在fgets函數的,我們從鍵盤給一個字符串,讓其滿足fgets函數,執行到write

      函數,將剛纔的字符串輸出給服務端,由於剛纔的服務端已經終止了並且發送了一個FIN,說明不能在發送

      新的段,並且也不能接受對端的數據,由於此時服務端已經終止,所以上面客戶端發送給服務端的信息,也就找不

      到歸宿這個時候(對方進程不存在了),TCP協議棧就會發送一個RST的tcp分節過去。如果這個時候,我們在調用

      write() 函數去讀取的話,那麼就會產生SIGPIPE,

       程序如下:

       

 while(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
        {

        //      writen(sock, sendbuf, strlen(sendbuf));
                write(sock , sendbuf, 1);      //分兩次發送,先發送1個,然後在發送剩餘的
                write(sock , sendbuf + 1, strlen(sendbuf) - 1);

                int ret = readline(sock, recvbuf, sizeof(recvbuf));
                
                if(ret == -1)
                        ERR_EXIT("READline");
                else if(ret == 0)
                {
                        printf("client close \n");      
                        break;
                }
                
                fputs(recvbuf, stdout);      //fgets接受到的數據,默認說明是存在換行符的
                memset(sendbuf, 0 , sizeof(sendbuf));
                memset(recvbuf, 0 , sizeof(recvbuf));
        }

可以看到,上面我們調用了兩次的write函數,第一次write函數(發送字符的時候),對面的進程已經不在了,TCP

協議棧會發送一個RST分節,緊接着我們再次調用了write函數,此刻就產生了一個SIGPIPE的信號中斷,直接終止當

前進程,倘使不退出程序的話,那麼read會返回0(readline中封裝着read),所以ret等於0,應該會打印client  close

,但是我們的程序並沒有打印。。。。。


(打開相應的客戶端,服務端)


觀察狀態:

服務端關閉:


觀察狀態:


給客戶端一個字符串,滿足fgets函數

程序直接退出了,所以看得出來,並沒有打印client  close


所以說,我們上面的分析是合理的。。。。。。


接下來我們修改一下程序:

<span style="color:#000000;">void handle_sigpipe(int sig)
{
        printf("recv is a sig = %d\n", sig);    
}


int main()
{                                                                                                                                               

        signal(SIGPIPE, handle_sigpipe);
        int sock;
        if((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
                ERR_EXIT("socket");</span>
同樣的道理,我們來運行一下程序:

這裏還能輸出:client  close,爲什麼呢???這是因爲產生了sigpipe中斷信號後,我們對中斷信號進行了處理了,所以不會退出程序了

同樣的,我們來查看一下這個:sig = 13

可以看到,這裏的正是sigpipe信號



上面看啦這麼多,我們貌似好像看到了用kill殺死一個進程和CTRL + C,我們來看看區別!!!

同理,打開客戶端,服務端

查看狀態:


調用CTRL + C,關閉服務器

接着我們繼續查看狀態


如果我們:調用kill殺死相應的服務端進程的話!!!

緊接着,我們再來看看狀態:



CTRL+C:發送SIGINT信號給前臺進程組中的所有進程。常用於終止正在運行的程序,強制中斷程序的執行

CTRL+Z:發送SIGTSTP信號給前臺進程組中的所有進程,常用於掛起一個進程,是將任務中斷,但是此任務並沒有結束,它仍然在進程中他只是維持掛起的狀態,用戶可以使用fg/bg操作繼續前臺或後臺的任務,fg命令重新啓動前臺被中斷的任務,bg命令把被中斷的任務放在後臺執行


可知,如果我們調用kill的話,那麼我們還能觀察到對等的狀態,如果我們調用CTRL + C的話,那麼我們的整個服務端

程序都被中斷



總之:上面說了這麼多的原因,就是說,一端A調用close退出的話,會發送FIN分節給

對端B,但是對於B接收到A的分節之後,並不能保證A端的進程是不是已經消失,,,

因爲對方調用close,並不意味着對方的進程會消失,,,當然,上面我們是通過kill或

者CTRL + C來確保的,如果這時B端再調用write,發現A端不存在,那麼TCP協議棧會

發送一個RST分節(連接重置的TCP端),對於當前的全雙工管道而言,如果再次調

用write函數的話,那麼就會

產生SIGPIPE信號中斷。。。。。。。。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章