在前文中講述了Linux服務端TCP通訊出現CLOSE_WAIT狀態的原因,這篇文章主要通過一個執行個體示範它個一個“惡劣”影響:直接使服務端進程Down掉。
CentOS服務端建立監聽連接埠
1 CentOS服務端建立監聽連接埠
如上圖所示,在虛擬機器CentOS7伺服器(192.168.1.178)中開啟一個終端介面,建立8000連接埠的監聽服務(PID:13035)。所用代碼如下,和上一篇文章中的程式大體一樣,只是多了一個SIGPIPE訊號處理以及自動回複(Auto response from server.)部分。
代碼如下 |
複製代碼 |
#include <stdio.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <errno.h> #include <signal.h> //Whether add a signal handle. #define SIGNAL_HANDLE 0 void sig_handle( int signal ) { printf( "Receive a signal=[%d].\n", signal ); return; } int main( int argc, char **argv ) { int server_sockfd; int client_sockfd; int len; int llOpt = 1; struct sockaddr_in my_addr; struct sockaddr_in remote_addr; int sin_size; char buf[BUFSIZ]; memset( &my_addr, 0, sizeof(my_addr) ); my_addr.sin_family = AF_INET; my_addr.sin_addr.s_addr = INADDR_ANY; my_addr.sin_port = htons(8000); #if SIGNAL_HANDLE struct sigaction new_act, old_act; new_act.sa_handler = sig_handle; new_act.sa_flags = 0; sigemptyset( &new_act.sa_mask ); sigaction( SIGPIPE, &new_act, &old_act ); sigaction( SIGINT, &new_act, &old_act ); #endif if( ( server_sockfd = socket( AF_INET, SOCK_STREAM, 0 ) ) < 0 ) { perror("socket"); return 1; } if( setsockopt( server_sockfd, SOL_SOCKET, SO_REUSEADDR, &llOpt, sizeof(llOpt) ) ) { close(server_sockfd); return errno; } if( bind( server_sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr) ) < 0 ) { perror( "bind" ); return 1; } listen( server_sockfd, 5 ); sin_size = sizeof( struct sockaddr_in ); printf( "Server is listening with pid=[%d].\n", getpid() ); while(1) { if( ( client_sockfd = accept( server_sockfd, (struct sockaddr *)&remote_addr, &sin_size ) ) < 0 ) { perror( "accept" ); return 1; } //Print the ip address and port of client. printf( "Accept client[%s:%u].\n", inet_ntoa(remote_addr.sin_addr), ntohs(remote_addr.sin_port) ); send( client_sockfd, "Auto response from server.", strlen("Auto response from server."), 0 ); memset( buf, 0x00, BUFSIZ ); while( ( len = recv( client_sockfd ,buf, BUFSIZ, 0) ) > 0 ) { buf[len]='\0'; printf( "Message from client=[%s]\n", buf ); } close( client_sockfd ); } close( server_sockfd ); return 0; } |
2 在Linux中利用telnet命令建立一個用戶端
建立一個shell指令碼netstat_nap.sh,裡面只包含一條有效命令netstat -nap|head -n 2;netstat -nap|grep 8000。
再開啟一個Linux終端介面,然後輸入命令telnet 192.168.1.177 8000作為用戶端建立與服務端的TCP串連。這時執行指令碼./netstat_nap.sh可以看到Linux用戶端(PID:13045)和服務端(PID:13035)的TCP通訊已經變成ESTABLISHED狀態,效果如下圖所示:
3 在Windows中利用telnet命令建立一個用戶端
在Windows主機(192.168.1.110)中開啟一個PowerShell終端介面,然後輸入命令telnet 192.168.1.177 8000作為用戶端建立與Linux服務端的TCP串連。
這時執行指令碼./netstat_nap.sh,可以看到Windows用戶端(連接埠:64012)和服務端(PID:13035)的TCP通訊已經變成ESTABLISHED狀態。同時使用命令lsof -i:8000,可以看到進程開啟的檔案。
4 直接關閉Windows telnet用戶端介面並使用Wireshark抓包
在直接關閉telnet介面後,繼續使用netstat_nap.sh指令碼和lsof命令發現剛才建立的TCP通訊出現了CLOSE_WAIT的狀態。
在等待2分鐘後,在Windows中使用Wireshark抓包發現由於用戶端發送了RST+ACK報文給Linux服務端,所以二者的TCP鏈路已經被複位了:
在Windows中使用Wireshark抓包
這時在Linux中再次使用netstat_nap.sh指令碼和lsof命令發現CLOSE_WAIT的狀態已經不存在了。
5 關閉Linux telnet用戶端
在Windows關閉telnet用戶端介面並發送RST+ACK報文後,關閉小節2中在Linux中開啟的telnet用戶端。這時Linux服務端進程會執行第90行處的close()函數,也即執行正常四次揮手關閉TCP串連。
接著Linux服務端進程繼續從核心中已完成串連隊列中取出已完成串連,這樣之前小節3中Windows telnet建立的用戶端串連被讀取。如下圖所示,服務端進程列印了第80行出的資料(Accept client[192.168.1.110:64012].),但是服務端進程卻掛掉了。
CentOS服務端建立監聽連接埠
這時在Linux中再次使用netstat_nap.sh指令碼和lsof命令:
6 原因分析
由於Windows用戶端的TCP鏈路在小節4中由於RST的緣故而關閉了,沒有讀端。那麼當Linux服務端執行82行的send()函數時,向之前的socket描述符發送26位元組的報文資料時,會收到核心發送過來的SIGPIPE訊號,導致服務端進程預設關閉。
因此,如果想捕捉到這個SIGPIPE訊號的話,可以將程式17行的SIGNAL_HANDLE宏定義值改成1,那麼就會得到如下圖所示的情況(進程能正常運行了)。
7 問題延伸
如果在第4小節中關閉Windows用戶端介面後,又直接如第5小節所示關閉Linux telnet用戶端介面,那麼又會出現什麼情況呢?於是又重新做了一遍測試,流程同上,下面是測試結果以及分析。
先用netstat和lsof命令查看TCP服務狀態,發現監聽服務正常:
然後分別用TCPDUMP和Wireshark抓取TCP通訊包,截取如下所示。可以發現在Linux telnet用戶端完成四次揮手後,服務端進程繼續向之前Windows telnet用戶端建立的socket描述符發送26位元組的報文資料。
因為Windows用戶端此時處於FIN_WAIT2狀態(Linux服務端處於CLOSE_WAIT狀態),所以服務端能繼續發其發送資料(即圖中的PUSH+ACK報文),接著Windows用戶端回應RST+ACK報文,從而兩者的TCP鏈路複位。
在Linux中使用TCPDUMP抓包
在Windows中使用Wireshark抓包
這樣Linux服務端進程還是能夠正常執行監聽任務:
8 其它
網上有人把這種用戶端或者服務端異常關閉的串連叫做TCP半關閉(Half-Close),例如網線拔掉、突然斷電等,此時對端串連仍認為雙方串連處於開啟中。