1. 程式人生 > >12-伺服器的幾種異常

12-伺服器的幾種異常

  一般伺服器的幾種異常分別為:伺服器主機崩潰、伺服器主機崩潰後重啟、伺服器主機關機。

1. 伺服器主機崩潰

  所謂伺服器崩潰就是伺服器掛了,導致網路斷開,那麼當伺服器崩潰時會發生什麼?

  為了模擬這種情況我們需要在不同機器上啟動伺服器和客戶端,先啟動伺服器,再啟動客戶端,在客戶端輸入hello以確認連線正常工作,然後再把伺服器的網路斷開,此時客戶端傳送的資料到達不了伺服器,而伺服器傳送的資料也到不了客戶端,並且客戶端和服務端並不知道雙方是否發生異常。

  也就是說客戶端傳送world後,會呼叫read一直阻塞等待讀取伺服器的回射,但由於伺服器已經崩潰,服務端已經收不到客戶端的資料了,那麼客戶端tcp會進行重傳(一般Berkeley實現會重傳12次,然後等待大約9分鐘),並期望收到ACK。假設服務端在此期間網路一直是斷開的,當客戶端放棄等待時,read將返回以下幾個錯誤:

1.ETIMEDOUT錯誤(主機存在,但是主機不響應)

2.ENETUNREACH錯誤(主機不可達,鏈路中間路由存在問題,比如某個路由器無法到達主機)

3.EHOSTUNREACH 錯誤(網路存在,但主機不存在)

2. 示例程式

關於客戶端和服務端的程式實現如下 服務端程式:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <ctype.h> #include <arpa/inet.h> #define SERV_PORT 10001 #define SERV_IP "192.168.0.107" int main(void) { int sfd, cfd; int len, i; //BUFSIZ是系統內嵌的一個巨集,用來指定buf大小 char buf[BUFSIZ], clie_IP[BUFSIZ]; struct sockaddr_in serv_addr, clie_addr; socklen_t clie_addr_len; sfd = socket(AF_INET, SOCK_STREAM, 0
); bzero(&serv_addr, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; inet_pton(AF_INET , SERV_IP , &serv_addr.sin_addr.s_addr); serv_addr.sin_port = htons(SERV_PORT); //繫結套接字 bind(sfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); listen(sfd, 64); printf("wait for client connect ...\n"); clie_addr_len = sizeof(clie_addr); //阻塞等待客戶端發起連線 cfd = accept(sfd, (struct sockaddr *)&clie_addr, &clie_addr_len); //列印客戶端的ip地址和埠號 printf("client IP:%s\tport:%d\n", inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_IP, sizeof(clie_IP)), ntohs(clie_addr.sin_port)); //迴圈處理客戶端的資料請求 while (1) { len = read(cfd, buf, sizeof(buf)); //read返回0說明對端已經關閉 if(len == 0){ break; } write(STDOUT_FILENO, buf, len); //處理客戶端資料,小寫轉大寫 for (i = 0; i < len; i++){ buf[i] = toupper(buf[i]); } //處理完資料,回寫給客戶端 write(cfd, buf, len); } //關閉連線 close(sfd); close(cfd); return 0; }

客戶端程式:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <errno.h>

#define SERV_IP "192.168.0.107"
#define SERV_PORT 10001

int main(void) {
        int sfd, len;
        struct sockaddr_in serv_addr;
        char buf[BUFSIZ];
        sfd = socket(AF_INET, SOCK_STREAM, 0);
        bzero(&serv_addr, sizeof(serv_addr));                       
        serv_addr.sin_family = AF_INET;                         
        inet_pton(AF_INET, SERV_IP, &serv_addr.sin_addr.s_addr); 
        serv_addr.sin_port = htons(SERV_PORT);                      
        connect(sfd, (struct sockaddr *)&serv_addr ,  sizeof(serv_addr));
        //迴圈讀寫資料
        while (1) {
                fgets(buf, sizeof(buf), stdin);
                //將資料寫給伺服器
                write(sfd, buf, strlen(buf)); 
                //從伺服器讀取轉換後資料
                len = read(sfd, buf, sizeof(buf));

                //判斷read返回什麼錯誤
                if(len < 0){
                        if(errno == ETIMEDOUT){
                                puts("主機存在,但是主機不響應");
                        }else if(errno == EHOSTUNREACH){
                                puts("網路存在,但主機不存在");
                        }else if(errno == ENETUNREACH){
                                puts("主機不可達");
                        }
                        break;
                }
                write(STDOUT_FILENO, buf, len);

        }
        //關閉連結
        close(sfd);
        return 0;
}

程式執行結果:

這裡寫圖片描述 圖1

  先啟動服務端,再啟動客戶端,並輸入hello驗證客戶端和服務端之間通訊正常,然後再把服務端的網路斷開模擬伺服器主機崩潰,再輸入world,整個程式運行了大約16分鐘左右,最後客戶端的read返回EHOSTUNREACH錯誤(需要注意的是:不同的實驗環境,產生的錯誤可能是不一樣的),程式執行結束。

  從tcpdump抓取到的資料包來看,客戶端總共重傳了7次,然後就放棄了:

這裡寫圖片描述 圖2

  換句話說,當客戶端阻塞於read呼叫處,一直重傳直到超時,客戶端才發現服務端主機已崩潰或主機不可達,然後返回一個狀態碼,而這個過程是很長的(在這個試驗中重傳超時時間為16分鐘)。如果客戶端希望能及時知道服務端是否崩潰時,一般我們可以自己實現一個超時的read函式,呼叫read並設定超時時間;又或者設定SO_KEEPALIVE套接字選項,也可以通過套接字選項設定tcp重傳超時時間。

3. 伺服器主機崩潰重啟

  客戶端和服務端建立連線後,再斷開伺服器主機連線的網路,把服務端程序關閉掉,通過netstat -ant命令檢視服務端程序的狀態,如果是FIN_WAIT1狀態的話,那麼等待FIN_WAIT1狀態消失為止再恢復伺服器主機的網路。

  在伺服器主機崩潰後重啟的情況下,如果客戶端在主機崩潰重啟前不主動傳送資料,那麼客戶是不會知道伺服器已經崩潰重啟的,客戶端會一直阻塞與read呼叫。如果客戶端向伺服器傳送了資料,伺服器依然能收到這個報文,但是伺服器崩潰重啟後丟失了之前的連線資訊(之前的連線已經不存在了),所以伺服器主機會以RST響應客戶端,當客戶端收到RST時,又因為客戶端正阻塞於read呼叫處,這會導致read返回ECONNRESET錯誤。

修改客戶端部分程式碼:

len = read(sfd, buf, sizeof(buf));
if(len < 0){
    if(errno == ECONNRESET){
        perror("read error: ");
    }
}

程式執行結果:

這裡寫圖片描述

結果分析:   通過程式的執行結果來看,客戶端在向服務端傳送hello後,服務端會立即傳送了RST迴應,按理來說應該是write收到這個RST並返回ECONNRESET錯誤。但是要知道程式執行速度非常的快,write函式極有可能收不到這個RST並返回成功,但是客戶端又接著呼叫read阻塞等待在此處,所以read一定會收到這個RST,然後導致read返回ECONNRESET錯誤,列印Connection reset by peer,這句話大概的意思是:對端連線已重置。

而tcpdump抓取到的資料包正好驗證了這一點:

這裡寫圖片描述

  同樣的,如果客戶端需要檢測伺服器主機是否崩潰重啟,也需要設定SO_KEEPALIVE套接字選項或者其他心跳檢測函式,以此來檢測伺服器的狀態。

4. 伺服器關機

  這一小節將介紹伺服器程式正在執行時,伺服器正常關機的情況。一般來說,當unix系統關機時,init程序會發送SIGTERM訊號,並等待一段時間(5 — 20秒左右),然後給所有正在執行的程序傳送SIGKILL訊號,也就是說所有正在執行的程式會在這段時間內清理,終止。

  換句話說,如果忽略SIGTERM訊號的話,那麼伺服器程式會被SIGKILL訊號終止掉,這將會發生和伺服器程序終止時一樣的情況(11-服務端程序終止和SIGPIPE訊號)。

異常處理在網路程式設計中本就是一個難點