1. 程式人生 > >針對TCP連線異常斷開的分析

針對TCP連線異常斷開的分析

        我們知道,一個基於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命令,可以看到如下的套介面的狀態:

        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複用的函式實現)

   二、伺服器主機崩潰

        我們接著檢視當伺服器主機崩潰時會發生什麼。為了模擬這種情形,我們需要在不同的機器上執行客戶與伺服器,在首次確認客戶伺服器能正常工作後,我們從網路上斷開伺服器主機,並在客戶上再輸入一行文字。這裡同時也模擬了當客戶傳送資料時伺服器主機不可達的情形(機建立連線後某些中間路由器不工作)

         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);
    }
}

相關推薦

針對TCP連線異常斷開分析

        我們知道,一個基於TCP/IP的客戶端-伺服器的程式中,正常情況下,我會是啟動伺服器使其在一個埠上監聽請求,等待客戶端的連線;通過TCP的三次握手,客戶端能夠通過socket建立一個到伺服器的連線;然後,兩者就可以基於這個socket連線通訊了。連線結束後,客戶端(程序)會退出;在不需要繼續處

TCP連線異常終止(RST包)

轉自:http://blog.csdn.net/ixidof/article/details/8049667 TCP異常終止(reset報文) TCP的異常終止是相對於正常釋放TCP連線的過程而言的,我們都知道,TCP連線的建立是通過三次握手完成的,而TCP正常釋放

對伺服器上出現大量的SYN_RECV狀態的TCP連線的問題分析

首先我們需要弄清楚SYN_RCVD狀態是怎樣產生的,通過TCP狀態轉換圖(如下圖)我們可以清楚的看到,SYN_RCVD是TCP三次握手的中間狀態,是服務埠(監聽埠,如應用伺服器的80埠)收到SYN包併發送[SYN,ACK]包後所處的狀態。這時如果再收到ACK的包,就

TCP連線斷開詳解(socket通訊)

一、TCP資料報結構以及三次握手 TCP(Transmission Control Protocol,傳輸控制協議)是一種面向連線的、可靠的、基於位元組流的通訊協議,資料在傳輸前要建立連線,傳輸完畢後還要斷開連線。 客戶端在收發資料前要使用 connect() 函式和伺服

TCP/IP詳解--TCP異常關閉連線的意義 異常關閉的情況

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

TCP建立連線斷開連線過程詳解

最近在看TCP這塊知識的時候對TCP連線三次握手斷開四次斷開不是很瞭解,找了下面一片文章講的很詳細,收藏下! 原文地址:http://blog.sina.com.cn/s/blog_60a4fcef0101e813.html TCP是一個面向連線的服務,面向連線的服務是電話系統服務模式的抽象

Cron連線正常工作5次後異常原因分析

目錄 目錄 1 問題描述 1 分析定位 1 解決方法 2 附1:Cron工作流 3 附2:SIGPIPE發生的位置 3   如果一個shell命令的“$?”值為141,則它是收到了SIGPIPE訊號。一些shell指令碼中的sleep或ps、wc等命令無效,

以太坊p2p網路(五):P2P模組TCP連線池網路通訊機制原始碼分析

上節中通過設定靜態節點BootstrapNodes節點來發現更多全網的其他節點,這部分只是發現節點並找出其中可以ping通的節點,但是還沒有進行使用,還沒建立TCP連線進行資料傳輸,協議處理等。 這裡主要分析P2P系統的TCP連線池的建立,以及是怎麼跟其他節點通

檢查非正常斷開tcp連線

所以我已一個一般的嵌入式web伺服器boa為原形進行了從寫,專門適應嵌入式web伺服器這種需要。 伺服器這種需要。 為什麼選用boa? 實際上我參考了很多web伺服器的程式碼和構架,嵌入式應用上,以多執行緒(程序)為構架的主流伺服器(apache類)徹底歇菜了(想想跑這些玩意的大

TCP連線建立只需要三次握手,為什麼斷開連線需要四次揮手?

通常TCP連線是由客戶端向伺服器發起和斷開的。因為只有伺服器在監聽埠,客戶端上沒有監聽埠,所以客戶端無法接收主動來的連線。而客戶端獲得了自己想要的資源或者服務之後,就會斷開連線。下面的三次握手和四次揮手描述了典型情況。 TCP連線建立過程: 1、客戶端向伺服器傳送SYN,其中seq=x。 2、伺服器

FTP協議的粗淺學習--利用wireshark抓包分析相關tcp連線

一、為什麼寫這個 昨天遇到個ftp相關的問題,關於ftp匿名訪問的。花費了大量的腦細胞後,終於搞定了服務端的配置,現在客戶端可以像下圖一樣,直接在瀏覽器輸入url,即可直接訪問。 期間不會彈出輸入使用者名稱密碼來登入的視窗。 今天我主要是有點好奇,在此過程中,究竟是否是用匿名賬戶“anonymo

Linux 建立 TCP 連線的超時時間分析(解惑)

Linux 系統預設的建立 TCP 連線的超時時間為 127 秒,對於許多客戶端來說,這個時間都太長了, 特別是當這個客戶端實際上是一個服務的時候,更希望能夠儘早失敗,以便能夠選擇其它的可用服務重新嘗試。 socket 是 Linux 下實現的傳輸控制層協議,包括 TCP 和 UDP,一個 socket 端

為什麼TCP連線的建立是需要三次,而斷開卻需要四次

TCP的為什麼三次握手就可以建立連線: 我先丟擲一個命題,兩個人想要正常溝通,至少保證雙方都知道自己和對方的傳送和接受功能是正常的。 假設這裡有兩個人,server和client。現在client需要和server建立連線。 ① client->se

TCP建立連線斷開連線流程

一、基本描述 TCP協議為提供面向連線的服務,需要先建立連線,然後才可以通訊,通訊結束時,需要斷開連線。 二、建立連線流程 建立連線的過程也叫做三次握手,流程如下: 1、首先,一定是由客戶端發起連線請求,服務端來接收連線請求。客戶端發

伺服器tcp連線timewait過多優化及詳細分析

【背景說明】 在7層負載均衡上,查詢網路狀態發現timewait太多,於是開始準備優化事宜 整體的拓撲結構,前面是lvs做dr模式的4層負載均衡,後端使用(nginx、or haproxy)做7層負載均衡 【優化效果】 修改前,建立連線的有29個,timewait的就達到了900個,如下圖所示 &n

基於TCP傳送http請求建立連線斷開連線的過程

首先說說TCP是什麼,HTTP又是什麼? TCP:傳輸控制協議,面向連線的,可靠地,安全的,基於位元組流的傳輸層協議 HTTP:超文字傳輸協議,但是這個協議是應用層傳輸協議,它是建立在TCP之上的協議 建立連線(三次握手):                        第

TCP/IP協議的三次握手和四次揮手(建立連線斷開連線

1、TCP/IP協議概述 TCP/IP協議(TransmissionControl Protocol/Internet Protocol)叫做傳輸控制/網際協議,又叫網路通訊協議,這個協議是Internet國際網際網路絡的基礎。TCP/IP是網路中使用的基本的通訊協議。雖然

Redis主從是如何維持TCP連線並且檢測異常斷鏈?

最近專案需要,所以研究了一下Redis主從的TCP連結是如何維持的,發現這一塊還沒有資料說明過,所以就把打算把這個東西記錄一下分享出來。 這個問題可以拆成以下幾個部分來說,包括: Redis如何建立主從關係 Redis如何檢測斷鏈 Redis如何重新建立主

tcp連線的第二次握手的分析

資料如下:15:20:16.315251 00:22:19:4f:7c:f2 > 00:22:19:4f:7a:09, ethertype IPv4 (0x0800), length 62: IP localhost.localdomain.9900 > 192.

TCP異常斷開檢測

1) 在TCP協議中提供了KEEPALIVE檢測。該選項使能後,在一個TCP連線上,若指定的一段時間內沒有資料交換,則自動傳送分節等待對方確認。              SO_KEEPALIVE : 該選項設定是否開啟探測              TCP_KEEPIDLE : 開始傳送探測分節前等待