一般服务器的几种异常分别为:服务器主机崩溃、服务器主机崩溃后重启、服务器主机关机。
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信号)。
异常处理在网络编程中本就是一个难点