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信號中斷。。。。。。。。。。