在socket网络编程中,如果此时客户端忽然由于某种原因断开连接或者崩溃,服务端没有处理好,便会同时崩溃掉,本篇文章将会从崩溃到问题分析,解决,一步步入手。
问题分析可以结合TCP的"四次握手"关闭。
TCP是全双工的信道, 可以看作两条单工信道, TCP连接两端的两个端点各负责一条. 当对端调用close时, 虽然本意是关闭整个两条信道, 但本端只是收到FIN包. 按照TCP协议的语义, 表示对端只是关闭了其所负责的那一条单工信道, 仍然可以继续接收数据. 也就是说, 因为TCP协议的限制, 一个端点无法获知对端的socket是调用了close还是shutdown。
对一个已经收到FIN包的socket调用read方法, 如果接收缓冲已空, 则返回0, 这就是常说的表示连接关闭. 但第一次对其调用write方法时, 如果发送缓冲没问题, 会返回正确写入(发送). 但发送的报文会导致对端发送RST报文, 因为对端的socket已经调用了close, 完全关闭, 既不发送, 也不接收数据. 所以, 第二次调用write方法(假设在收到RST之后), 会生成SIGPIPE信号, 导致进程退出。
上述简化:SIGPIPE
产生的原因是这样的:如果一个 socket 在接收到了 RST packet 之后,程序仍然向这个 socket 写入数据,那么就会产生SIGPIPE
信号。
举例如下:当 client 连接到 server 之后,这时候 server 准备向 client 发送多条消息,但在发送消息之前,client 进程意外奔溃了,那么接下来 server 在发送多条消息的过程中,就会出现SIGPIPE
信号。下面看一下 server 的代码:
#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>
#include <csignal>
#define MAXLINE 120
int main(int argc, char *argv[]) {
signal(SIGPIPE, SIG_IGN); // 忽略 SIGPIPE 信号
//1.创建一个侦听socket
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1) {
std::cout << "create listen socket error." << std::endl;
return -1;
}
//2.初始化服务器地址
struct sockaddr_in bindaddr;
bindaddr.sin_family = AF_INET;
bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bindaddr.sin_port = htons(8088);
if (bind(listenfd, (struct sockaddr *) &bindaddr, sizeof(bindaddr)) == -1) {
std::cout << "bind listen socket error." << std::endl;
return -1;
}
//3.启动侦听
if (listen(listenfd, SOMAXCONN) == -1) {
std::cout << "listen error." << std::endl;
return -1;
}
/**
* 服务端连续写两次数据到客户端
*/
while (true) {
struct sockaddr_in clientaddr;
socklen_t clientaddrlen = sizeof(clientaddr);
//4. 接受客户端连接
int clientfd = accept(listenfd, (struct sockaddr *) &clientaddr, &clientaddrlen);
if (clientfd != -1) {
// 假设此时 client 奔溃, 那么 server 将接收到 client 发送的 FIN
sleep(5);
// 写入第一条消息
char msg1[MAXLINE] = {"first message"};
ssize_t n = write(clientfd, msg1, strlen(msg1));
printf("write %ld bytes\n", n);
// 此时第一条消息发送成功,server 接收到 client 发送的 RST
sleep(1);
// 写入第二条消息,出现 SIGPIPE 信号,导致 server 被杀死
char msg2[MAXLINE] = {"second message"};
n = write(clientfd, msg2, strlen(msg2));
printf("%ld, %s\n", n, strerror(errno));
}
close(clientfd);
}
//关闭侦听socket
close(listenfd);
return 0;
我们可以使用 Linux 的 nc 工具作为 client,当 client 连接到 server 之后,就立即杀死 client (模拟 client 的意外奔溃)。这时可以观察 server 的运行情况:
$ gcc -o server server.c
$ ./server & # 后台运行 server
$ nc localhost 8888 # 运行 nc 连接到 server
^C # Ctrl-C 杀死 nc
write 13 bytes
[1]+ Broken pipe ./server
分析一下整个过程:
SIGPIPE
信号,从而杀死 server。对 server 来说,为了不被SIGPIPE
信号杀死,那就需要忽略SIGPIPE
信号:
int main(){
signal(SIGPIPE, SIG_IGN); // 忽略 SIGPIPE 信号
...
}
重新运行上面的程序,server 在发送第二条消息的时候,write()
会返回-1
,并且此时errno
的值为EPIPE
,所以这时并不会产生SIGPIPE
信号。