針對TCP鏈接異常斷開的分析

        咱們知道,一個基於TCP/IP的客戶端-服務器的程序中,正常狀況下,我會是啓動服務器使其在一個端口上監聽請求,等待客戶端的鏈接;經過TCP的三次握手,客戶端可以經過socket創建一個到服務器的鏈接;而後,二者就能夠基於這個socket鏈接通訊了。鏈接結束後,客戶端(進程)會退出;在不須要繼續處理客戶請求的狀況下,服務器(進程)也將退出。並且,當一個進程退出的時候,內核會關閉全部由這個進程打開的套接字,這裏將觸發TCP的四次揮手進而關閉一個socket鏈接。可是,在一些異常的狀況下,譬如:服務器進程終止、服務器主機奔潰/奔潰後重啓、服務器關機的狀況下,客戶端向服務器發起請求的時候,將會發生什麼呢?下邊,咱們來看看這幾種狀況。服務器

        注意:一下描述的各類狀況所使用的示例程序在文章的最後貼出 網絡

     1、服務器進程終止socket

        咱們啓動客戶/服務器對,而後殺死子進程(模擬服務器進程崩潰的情形,咱們可從中查看客戶端將發生什麼)。函數

        1:在同一個主機上啓動服務器和客戶,並在客戶上輸入一行文本,以驗證一切正常。正常狀況下,改行文本將由服務器回射給客戶。spa

        2:找到服務器子進程的ID,經過kill命令殺死它。做爲進程終止處理的部分工做,子進程中全部打開着的描述字都被關閉。這就致使向客戶發送一個FIN,而客戶TCP則響應以一個ACK。這就是TCP鏈接終止的前一半工做。3d

        3:子進程終止時,內核將給父進程遞交SIGCHLD信號。code

        4:客戶上沒有發生任何特殊之事。客戶TCP接受來自服務器TCP的FIN並響應一個ACK,而後問題是客戶進程此時阻塞在fgets調用上,等待從終端接受一行文本。它是看不到這個FIN的。server

        5:此時咱們若是運行netstat命令,能夠看到以下的套接口的狀態:blog

        qj1

        FIN_WAIT2即爲咱們殺掉的那個子進程的,由於咱們知道主動關閉的那端在發送完fin並接受對端的ack後將進入fin_wait2狀態,此時它在等待對端的fin。接口

        6:如今咱們在客戶上在輸入一行文本,咱們能夠看到以下的輸出:

        qj2

        當咱們輸入「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複用的函數實現)

   2、服務器主機崩潰

 

        咱們接着查看當服務器主機崩潰時會發生什麼。爲了模擬這種情形,咱們須要在不一樣的機器上運行客戶與服務器,在首次確認客戶服務器能正常工做後,咱們從網絡上斷開服務器主機,並在客戶上再輸入一行文本。這裏同時也模擬了當客戶發送數據時服務器主機不可達的情形(機創建鏈接後某些中間路由器不工做)

         1:當服務器主機崩潰時,已有的網絡鏈接上發不出任何東西。這裏咱們假設的是主機崩潰,而不是執行了關機命令。

         2:咱們在客戶上輸入一行文本,它由write寫入內核,再由客戶TCP做爲一個數據分節送出。客戶隨後阻塞於read調用,等待服務器的應答。

         3:這種狀況下,客戶TCP持續重傳數據分節,試圖從服務器上接受一個ACK。(源自Berkeley的實現重傳該數據分節12次,共等待約9分鐘才放棄重傳。)當客戶TCP最終放棄時(假設這段時間內,服務器主機沒有從新啓動或者若是是服務器主機爲崩潰但從網絡上不可達的狀況,那麼假設主機仍然不可達),返回客戶進程一個錯誤。既然客戶阻塞在readline調用上,該調用將返回一個錯誤。假設服務器已崩潰,從而對客戶的數據分節根本沒有響應,那麼所返回的錯誤是ETIMEDOUT。然而若是某個中間路由器斷定服務器主機已不可達,從而響應以一個「destination unreachable」,那麼所返回的錯誤是EHOSTUNREACH或ENETUNREACH。

         儘管咱們的客戶最後仍是發現對端主機已崩潰或不可達,不過有時候咱們須要更快地檢測出這種狀況,而不是不得不等待9分鐘。所用的方法就是對read調用設置一個超時。

         另外咱們剛討論的情形只有在向服務器主機發送數據時,才能檢測出它已經崩潰,若是咱們不主動發送主句也想檢測出服務器主機的崩潰,那麼就須要用到SO_KEEPALIVE這個套接口選項。        

     3、服務器主機崩潰後重啓

        在前一節的分析中,當咱們發送數據時,服務器主機仍然處於崩潰狀態;這節,咱們將在發送數據前從新啓動崩潰了的服務器主機。模擬這種狀況的簡單方法就是:創建鏈接,再從網絡上端口服務器主機,將它關機後再重啓,最後把它從新鏈接到網絡中。

        如前一節所述,若是在服務器主機崩潰時客戶不主動給服務器發送數據,那麼客戶不會知道服務器主機已經崩潰。所發生的步驟以下:

        1:啓動客戶服務器,在客戶上輸入一行文本已確認鏈接已創建。

        2:服務器主機崩潰並重啓。

        3:在客戶上輸入一行文本,它將做爲一個TCP數據分節發送到服務器主機。

        4:當服務器主機崩潰後重啓時,它的TCP丟失了崩潰前的全部鏈接信息,所以服務器TCP對於所收到的來自客戶的數據分節響應以一個RST。

        5:當客戶TCP收到該RST時,客戶正阻塞於read調用,致使該調用返回ECONNRESET錯誤。

    4、服務器主機關機

        這節咱們看看當服務器關機時將會發生什麼。

        Unix系統關機時,init進程一般先給全部進程發送SIGTERM信號(該信號可被捕獲),再等待一段固定的時間(通常在5~20秒之間),而後給全部仍在運行的進程發送SIGKILL信號(該信號不能被捕獲)。這麼作是留給全部運行中的進程一小段時間來清除和終止。若是咱們不捕獲SIGTERM信號並終止,咱們的服務器將由SIGKILL信號終止。當服務器進程終止時,它的全部打開着的描述字都被關閉,隨後發生的步驟與第一節中討論過的同樣。正如第一節中所述的情形,咱們必須在客戶中使用select或poll函數,使得服務器進程的終止已經發生,客戶立刻檢測到。

 

    5、示例程序

//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);
    }
}
相關文章
相關標籤/搜索