博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
11-服务端进程终止和SIGPIPE信号
阅读量:2043 次
发布时间:2019-04-28

本文共 5201 字,大约阅读时间需要 17 分钟。

1. 服务端进程终止

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

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

服务器程序:

#include 
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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 
#include
#include
#include
#include
#include
#include
#define SERV_IP "192.168.0.107"#define SERV_PORT 10001int 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 
#include
#include
#include
#include
#include
#include
#include
#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;}

程序执行结果:

这里写图片描述
图1

这里写图片描述
图2

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

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

你可能感兴趣的文章
Tomcat 7优化前及优化后的性能对比
查看>>
VisualVM 提示 tomcat 不受此jvm支持解决办法
查看>>
Java Guava中的函数式编程讲解
查看>>
Eclipse Memory Analyzer 使用技巧
查看>>
tomcat连接超时
查看>>
谈谈编程思想
查看>>
iOS MapKit导航及地理转码辅助类
查看>>
检测iOS的网络可用性并打开网络设置
查看>>
简单封装FMDB操作sqlite的模板
查看>>
iOS开发中Instruments的用法
查看>>
iOS常用宏定义
查看>>
被废弃的dispatch_get_current_queue
查看>>
什么是ActiveRecord
查看>>
有道词典for mac在Mac OS X 10.9不能取词
查看>>
关于“团队建设”的反思
查看>>
利用jekyll在github中搭建博客
查看>>
Windows7中IIS简单安装与配置(详细图解)
查看>>
linux基本命令
查看>>
BlockQueue 生产消费 不需要判断阻塞唤醒条件
查看>>
ExecutorService 线程池 newFixedThreadPool newSingleThreadExecutor newCachedThreadPool
查看>>