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错误。