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;
}
程式執行結果:
先啟動服務端,再啟動客戶端,並輸入hello驗證客戶端和服務端之間通訊正常,然後再把服務端的網路斷開模擬伺服器主機崩潰,再輸入world,整個程式運行了大約16分鐘左右,最後客戶端的read返回EHOSTUNREACH錯誤(需要注意的是:不同的實驗環境,產生的錯誤可能是不一樣的),程式執行結束。
從tcpdump抓取到的資料包來看,客戶端總共重傳了7次,然後就放棄了:
換句話說,當客戶端阻塞於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訊號)。
異常處理在網路程式設計中本就是一個難點