12-服务器的几种异常

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_35733751/article/details/82726661

  一般服务器的几种异常分别为:服务器主机崩溃、服务器主机崩溃后重启、服务器主机关机。

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信号)。


异常处理在网络编程中本就是一个难点

猜你喜欢

转载自blog.csdn.net/qq_35733751/article/details/82726661
今日推荐