11-服务端进程终止和SIGPIPE信号

1. 服务端进程终止

  这一篇我们将讨论服务器进程终止的问题。

  先启动服务端,再启动客户端,然后杀死服务端子进程,以此来模拟服务器进程终止的情况。如果客户端再向服务端发送数据,这将会发生什么情况?


服务器程序:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
#include <stdlib.h>

#define SERV_PORT 10001

注册SIGCHLD信号回收子进程
void wait_child(int signo)
{
    printf("hello SIGCHILD\n");
    //非阻塞方式回收子进程
    while (waitpid(0, NULL, WNOHANG) > 0);
}

int main(void)
{
    pid_t pid;
    int lfd, cfd;
    struct sockaddr_in serv_addr, clie_addr;
    socklen_t clie_addr_len;
    char buf[BUFSIZ], clie_IP[BUFSIZ];
    int n, i ,n2;

    lfd = socket(AF_INET, SOCK_STREAM, 0);

    bzero(&serv_addr, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(SERV_PORT);
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
    listen(lfd, 128);

    while (1) {
        clie_addr_len = sizeof(clie_addr);
        cfd = accept(lfd, (struct sockaddr *)&clie_addr, &clie_addr_len);
        printf("client IP:%s, port:%d\n", 
                inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_IP, sizeof(clie_IP)), 
                ntohs(clie_addr.sin_port));

        //创建子进程
        pid = fork();
        if (pid < 0) {
            perror("fork error");
            close(cfd);
            exit(1);
        } else if (pid == 0) {
            close(lfd);
            break;
        } else {
            //父进程
            close(cfd);
            //捕捉SIGCHLD信号
            signal(SIGCHLD, wait_child);
        }
    }


    //子进程
    if (pid == 0) {
        while (1) {
            n = read(cfd, buf, sizeof(buf));
            if (n == 0) { 
               printf("peer close\n");
             //如果read返回0,说明对端已关闭,打印退出客户端信息
            printf("client IP:%s, port:%d ----- exit\n",
                    inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_IP, sizeof(clie_IP)) ,
                    ntohs(clie_addr.sin_port));
                close(cfd);
                break;   //子进程退出
            } else if (n == -1) {
                perror("read error");
                close(cfd);
                break;
            } else {
                for (i = 0; i < n; i++){
                    buf[i] = toupper(buf[i]);
                }
                n2 = write(cfd, buf, n);
                if(n2 < n){
                        puts("short write");
                }
            }
        }
    }
    close(lfd);
	return 0;
}

客户端程序:

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

#define SERV_IP "192.168.0.107"
#define SERV_PORT 10001

int main(void) {
	int sfd, len , ret;
	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);                      
	ret = connect(sfd, (struct sockaddr *)&serv_addr ,  sizeof(serv_addr));
	if(ret < 0){
		perror("connect error:");
		close(sfd);
		exit(0);
	}

	//循环读写数据
	while (1) {
		fgets(buf, sizeof(buf), stdin);
		write(sfd, buf, strlen(buf)); 
		len = read(sfd, buf, sizeof(buf));
        //服务端的RST是什么时候到达,如果是在read返回前到达,那么read将返回错误
        //如果是在read之后到达,那么read将返回0,判断为对端已关闭
		if(len == 0){
			puts("peer close");
			break;
		}else if(len < 0){
             perror("read error: ");
}
		write(STDOUT_FILENO, buf, len);
	}
	//关闭链接
	close(sfd);
	return 0;
}

1 . 先启动服务端

2.然后执行./client命令,启动客户端,并在客户端输入hello,然后服务端回射HELLO,验证客户端和服务端之间通信正常
这里写图片描述

3.然后将服务端进程切换到后台执行,并找到子进程,通过kill命令将子进程杀死。
这里写图片描述

  当通过kill命令把服务端子进程终止后,父进程收到SIGCHLD信号后回收了子进程,并且子进程打开的所有套接字都被关闭,这将导致服务端子进程将发送了一个FIN,同时客户端响应一个ACK,这只是代表服务端到客户端的连接关闭了而已,也就是所谓的半关闭,进入了FIN_WAIT2状态。
这里写图片描述

  从tcpdump抓取到的数据包来看,服务端子进程确实发送了FIN,并接收到了客户端响应的ACK。


  对此,客户端并不做任何事情,但问题是客户端将阻塞在fgets调用处,等待从终端读取数据。此时我们在客户端的终端上输入world,因为此时只是半关闭,所以tcp允许这样做。

这里写图片描述

  当服务端子进程的tcp收到客户端的world时,于是就响应一个RST段(这是因为之前打开的套接字服务端子进程已经终止掉了,无法再接收数据了,此时服务端期望对端下一个发送FIN,而不是发送一个数据报文,所以才会以RST响应)。我们通过tcpdump查看发现,确实验证了这一点,重点关注最后4个报文。

这里写图片描述

  但是对于客户端来说,它并不知道这个RST,因为它执行的太快了,在调用了write后又立即调用了read,也就是说在收到对端发送的RST段之前,它所调用的read就已经返回了,所以read返回0得出对端已经关闭,于是打印peer closed,退出循环,执行 close(sockfd)。然后向对端发送 FIN 段,然后服务端以一个RST响应了客户端的FIN。


2. SIGPIPE信号

   继续前面所说的服务器进程终止的情况,从而又引发出了另一个问题:如果客户端忽略了read返回的错误,继续向服务端写入数据,这时又会发生什么情况呢?

  比如说客户端可能在读取任何数据之前,调用了两次write对服务端进行写操作,在第一次writen后,休眠1秒,保证第二次write写操作前收到RST报文。为了模拟这种情况,我们需要对客户端代码做以下的修改,同时注册SIGPIPE信号捕捉函数,具体示例代码如下:

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

#define SERV_IP "192.168.0.107"
#define SERV_PORT 10001

//捕捉SIGPIPE信号函数
void signal_handler(int signo){
        if(signo == SIGPIPE){
                puts("hello SIGPIPE");
        }
}

int main(void) {
        int sfd, nr , nw , ret;
        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);                      
        ret = connect(sfd, (struct sockaddr *)&serv_addr ,  sizeof(serv_addr));
        if(ret < 0){
                perror("connect error:");
                close(sfd);
                exit(0);
        }

        //注册SIGPIPE信号
        signal(SIGPIPE , signal_handler);

        //循环读写数据
        while (1) {
                fgets(buf, sizeof(buf), stdin);
                //把写操作分成两次
                nw = write(sfd, buf, strlen(buf));
                sleep(1);
                //休眠1秒,保证第二次调用writen收到RST,并捕获SIGPIPE信号
                nw = write(sfd, buf, strlen(buf));
                nr = read(sfd, buf, sizeof(buf));

                if(nr == 0){
                        puts("read by peer close");
                        break;
                }else if(nr < 0){
                        perror("read error: ");
                }
                write(STDOUT_FILENO, buf, nr);
        }
        //关闭链接
        close(sfd);
        return 0;
}

程序执行结果:

这里写图片描述

这里写图片描述

  那么根据上一篇服务器进程终止的情况,服务端发送的RST无论是在read前到达还是read后到达,如果客户端忽略了read返回的错误,再次循环调用write对服务端进行写操作。于是客户端进行了两次write操作,在第一次write操作后,sleep了1秒,而这1秒的时间已经足够RST段到达客户端,所以客户端第二次write时就会出现EPIPE错误。

   如果客户端调用write向一个已经收到RST的套接字进行写操作时,内核会向该进程发送一个SIGPIPE信号,以此终止该进程,并返回EPIPE错误。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值