我們知道,一個基於TCP/IP的客戶端-服務器的程序中,正常情況下,我會是啟動服務器使其在一個端口上監聽請求,等待客戶端的連接;通過TCP的三次握手,客戶端能夠通過socket建立一個到服務器的連接;然后,兩者就可以基於這個socket連接通信了。連接結束后,客戶端(進程)會退出;在不需要繼續處理客戶請求的情況下,服務器(進程)也將退出。而且,當一個進程退出的時候,內核會關閉所有由這個進程打開的套接字,這里將觸發TCP的四次揮手進而關閉一個socket連接。但是,在一些異常的情況下,譬如:服務器進程終止、服務器主機奔潰/奔潰后重啟、服務器關機的情況下,客戶端向服務器發起請求的時候,將會發生什么呢?下邊,我們來看看這幾種情況。
注意:一下描述的各種情況所使用的示例程序在文章的最后貼出
一、服務器進程終止
我們啟動客戶/服務器對,然后殺死子進程(模擬服務器進程崩潰的情形,我們可從中查看客戶端將發生什么)。
1:在同一個主機上啟動服務器和客戶,並在客戶上輸入一行文本,以驗證一切正常。正常情況下,改行文本將由服務器回射給客戶。
2:找到服務器子進程的ID,通過kill命令殺死它。作為進程終止處理的部分工作,子進程中所有打開着的描述字都被關閉。這就導致向客戶發送一個FIN,而客戶TCP則響應以一個ACK。這就是TCP連接終止的前一半工作。
3:子進程終止時,內核將給父進程遞交SIGCHLD信號。
4:客戶上沒有發生任何特殊之事。客戶TCP接受來自服務器TCP的FIN並響應一個ACK,然后問題是客戶進程此時阻塞在fgets調用上,等待從終端接受一行文本。它是看不到這個FIN的。
5:此時我們如果運行netstat命令,可以看到如下的套接口的狀態:
FIN_WAIT2即為我們殺掉的那個子進程的,因為我們知道主動關閉的那端在發送完fin並接受對端的ack后將進入fin_wait2狀態,此時它在等待對端的fin。
6:現在我們在客戶上在輸入一行文本,我們可以看到如下的輸出:
當我們輸入“after server close”時,客戶TCP接着把數據發送給服務器,TCP允許這么做,因為客戶TCP接受到FIN只是表示服務器進程已關閉了連接的服務端,從而不再往其中發送任何數據而已。FIN的接受並沒有告知客戶TCP服務器進程已經終止(在這個例子中它缺失是終止了)。當服務器TCP接收到來自客戶的數據時,既然先前打開那個套接口的進程已經終止,於是響應一個RST。
7:然而客戶進程看不到這個RST,因為它在調用write后立即調用read,並且由於第2步中接收到FIN,所調用的read立即返回0(表示)EOF。我們的客戶此時並未預期收到EOF,於是以出錯信息“server term prematurely.”(服務器過早終止)退出。
8:當客戶終止時,它所有打開着的描述字都被關閉。
我們的上述討論還取決於程序的時序。客戶調用read既可能發生在服務器的RST被客戶收到之前,也可能發生在收到之后。如果read發生在收到RST之前(如本例子所示),那么結果是客戶得到一個未預期的EOF;否則結果是由readline返回一個ECONNRESET(“connection reset by peer”對方復位連接)錯誤。
本例子的問題在於:當FIN到達套接口時,客戶正阻塞在fgets調用上。客戶實際上在應對兩個描述字——套接口和用戶輸入,它不能單純阻塞在這兩個源中某個特定源的輸入上,而是應該阻塞在其任何一個源的輸入上。(可用select等io復用的函數實現)
二、服務器主機崩潰
我們接着查看當服務器主機崩潰時會發生什么。為了模擬這種情形,我們需要在不同的機器上運行客戶與服務器,在首次確認客戶服務器能正常工作后,我們從網絡上斷開服務器主機,並在客戶上再輸入一行文本。這里同時也模擬了當客戶發送數據時服務器主機不可達的情形(機建立連接后某些中間路由器不工作)
1:當服務器主機崩潰時,已有的網絡連接上發不出任何東西。這里我們假設的是主機崩潰,而不是執行了關機命令。
2:我們在客戶上輸入一行文本,它由write寫入內核,再由客戶TCP作為一個數據分節送出。客戶隨后阻塞於read調用,等待服務器的應答。
3:這種情況下,客戶TCP持續重傳數據分節,試圖從服務器上接受一個ACK。(源自Berkeley的實現重傳該數據分節12次,共等待約9分鍾才放棄重傳。)當客戶TCP最終放棄時(假設這段時間內,服務器主機沒有重新啟動或者如果是服務器主機為崩潰但從網絡上不可達的情況,那么假設主機仍然不可達),返回客戶進程一個錯誤。既然客戶阻塞在readline調用上,該調用將返回一個錯誤。假設服務器已崩潰,從而對客戶的數據分節根本沒有響應,那么所返回的錯誤是ETIMEDOUT。然而如果某個中間路由器判定服務器主機已不可達,從而響應以一個“destination unreachable”,那么所返回的錯誤是EHOSTUNREACH或ENETUNREACH。
盡管我們的客戶最后還是發現對端主機已崩潰或不可達,不過有時候我們需要更快地檢測出這種情況,而不是不得不等待9分鍾。所用的方法就是對read調用設置一個超時。
另外我們剛討論的情形只有在向服務器主機發送數據時,才能檢測出它已經崩潰,如果我們不主動發送主句也想檢測出服務器主機的崩潰,那么就需要用到SO_KEEPALIVE這個套接口選項。
三、服務器主機崩潰后重啟
在前一節的分析中,當我們發送數據時,服務器主機仍然處於崩潰狀態;這節,我們將在發送數據前重新啟動崩潰了的服務器主機。模擬這種情況的簡單方法就是:建立連接,再從網絡上端口服務器主機,將它關機后再重啟,最后把它重新連接到網絡中。
如前一節所述,如果在服務器主機崩潰時客戶不主動給服務器發送數據,那么客戶不會知道服務器主機已經崩潰。所發生的步驟如下:
1:啟動客戶服務器,在客戶上輸入一行文本已確認連接已建立。
2:服務器主機崩潰並重啟。
3:在客戶上輸入一行文本,它將作為一個TCP數據分節發送到服務器主機。
4:當服務器主機崩潰后重啟時,它的TCP丟失了崩潰前的所有連接信息,因此服務器TCP對於所收到的來自客戶的數據分節響應以一個RST。
5:當客戶TCP收到該RST時,客戶正阻塞於read調用,導致該調用返回ECONNRESET錯誤。
四、服務器主機關機
這節我們看看當服務器關機時將會發生什么。
Unix系統關機時,init進程通常先給所有進程發送SIGTERM信號(該信號可被捕獲),再等待一段固定的時間(一般在5~20秒之間),然后給所有仍在運行的進程發送SIGKILL信號(該信號不能被捕獲)。這么做是留給所有運行中的進程一小段時間來清除和終止。如果我們不捕獲SIGTERM信號並終止,我們的服務器將由SIGKILL信號終止。當服務器進程終止時,它的所有打開着的描述字都被關閉,隨后發生的步驟與第一節中討論過的一樣。正如第一節中所述的情形,我們必須在客戶中使用select或poll函數,使得服務器進程的終止已經發生,客戶馬上檢測到。
五、示例程序
//client.c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/socket.h> #include <errno.h> #include <error.h> #include <netinet/in.h> #include <netinet/ip.h> #include <arpa/inet.h> #include <string.h> #include <signal.h> void str_cli(FILE *fp, int sfd ) { char sendline[1024], recvline[2014]; memset(recvline, 0, sizeof(sendline)); memset(sendline, 0, sizeof(recvline)); while( fgets(sendline, 1024, fp) != NULL ) { write(sfd, sendline, strlen(sendline)); if( read(sfd, recvline, 1024) == 0 ) { printf("server term prematurely.\n"); } fputs(recvline, stdout); memset(recvline, 0, sizeof(sendline)); memset(sendline, 0, sizeof(recvline)); } } int main() { int s; if( (s = socket(AF_INET, SOCK_STREAM, 0)) < 0 ) { int e = errno; perror("create socket fail.\n"); exit(0); } struct sockaddr_in server_addr, child_addr; bzero(&server_addr, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(9998); inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr); if( connect(s, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0 ) { perror("connect fail."); exit(0); } str_cli(stdin, s); exit(0); }
//server.c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/socket.h> #include <errno.h> #include <error.h> #include <netinet/in.h> #include <netinet/ip.h> #include <arpa/inet.h> #include <string.h> #include <signal.h> #include <sys/wait.h> //using namespace std; typedef void sigfunc(int); void func_wait(int signo) { pid_t pid; int stat; pid = wait(&stat); printf( "child %d exit\n", pid ); return; } void func_waitpid(int signo) { pid_t pid; int stat; while( (pid = waitpid(-1, &stat, WNOHANG)) > 0 ) { printf( "child %d exit\n", pid ); } return; } sigfunc* signal( int signo, sigfunc *func ) { struct sigaction act, oact; act.sa_handler = func; sigemptyset(&act.sa_mask); act.sa_flags = 0; if ( signo == SIGALRM ) { #ifdef SA_INTERRUPT act.sa_flags |= SA_INTERRUPT; /* SunOS 4.x */ #endif } else { #ifdef SA_RESTART act.sa_flags |= SA_RESTART; /* SVR4, 4.4BSD */ #endif } if ( sigaction(signo, &act, &oact) < 0 ) { return SIG_ERR; } return oact.sa_handler; } void str_echo( int cfd ) { ssize_t n; char buf[1024]; //char t[] = "SERVER ECHO: "; again: memset(buf, 0, sizeof(buf)); while( (n = read(cfd, buf, 1024)) > 0 ) { write(cfd, buf, n); } if( n <0 && errno == EINTR ) { goto again; } else { printf("str_echo: read error\n"); } } int main() { signal(SIGCHLD, &func_waitpid); int s, c; pid_t child; if( (s = socket(AF_INET, SOCK_STREAM, 0)) < 0 ) { int e = errno; perror("create socket fail.\n"); exit(0); } struct sockaddr_in server_addr, child_addr; bzero(&server_addr, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(9998); server_addr.sin_addr.s_addr = htonl(INADDR_ANY); if( bind(s, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0 ) { int e = errno; perror("bind address fail.\n"); exit(0); } if( listen(s, 1024) < 0 ) { int e = errno; perror("listen fail.\n"); exit(0); } while(1) { socklen_t chilen = sizeof(child_addr); if ( (c = accept(s, (struct sockaddr *)&child_addr, &chilen)) < 0 ) { perror("listen fail."); exit(0); } if( (child = fork()) == 0 ) { close(s); str_echo(c); exit(0); } close(c); } }