socket的关闭检测及处理
检测socket关闭
reference SIGPIPE 信号处理整理
调用write, send, sendto等发送函数时,触发 SIGPIPE 信号,导致程序直接退出。
Program received signal SIGPIPE, Broken pipe.
0x00007ffff7af2224 in write () from /lib/x86_64-linux-gnu/libc.so.6
程序将 errno 设置为 EPIPE 之后,程序接受到内核发送过来的 SIGPIPE 信号退出。
产生 SIGPIPE 的条件:
-
对一个已经收到 FIN 包的 socket 调用 read 方法,如果接收缓冲已空,则返回 0,这就是常说的“连接关闭”表示。
-
对一个已经收到 FIN 包的 socket 第一次调用 write 方法时,如果发送缓冲没问题,则 write 调用会返回写入的数据量,同时进行数据发送。但是发送出去的报文会导致对端发回 RST 报文。因为对端的 socket 已经调用了 close 进行了完全关闭,已经处于既不发送,也不接收数据的状态。所以第二次调用 write 方法时(假设在收到 RST 之后)会生成 SIGPIPE 信号,导致进程退出(这就是为什么第二次 write 才能触发 SIGPIPE 的原因)。 <----- 这个应该是阻塞socket才这样,没有试验过
处理SIGPIPE How to prevent SIGPIPEs (or handle them properly)
-
设置信号处理函数
signal(SIGPIPE, SIG_IGN);
-
设置socket option,忽略SIGPIPE信号,转为处理对应的errno,这样就不用安装信号处理函数
int set = 1; setsockopt(sd, SOL_SOCKET, SO_NOSIGPIPE, (void *)&set, sizeof(int));
注意:在Linux中没有SO_NOSIGPIPE信号: SO_NOSIGPIPE was not declared,但是我们可以在调用send或者recv的信号带上MSG_NOSIGNAL
send(fd, buf, nBytes, MSG_NOSIGNAL);
服务端关闭socket
做了如下测试,如下代码是客户端代码片段,服务端用nc监听一个端口作为服务端
sleep(10);
char buf[32];
ret = recv(event[i].data.fd, buf, 32, MSG_NOSIGNAL);
LOG_DEBUG("recv ret: %d, errno: %d, error: %s", ret, errno, strerror(errno));
ret = send(event[i].data.fd, "hello", 5, MSG_NOSIGNAL);
LOG_DEBUG("send ret: %d, errno: %d, error: %s", ret, errno, strerror(errno));
在sleep的10s时间内关闭服务端,打印的日志如下
2021-10-23 Sat 11:38:02 [debug] epolltestconnect.c:163 In function 'server_run' --> recv ret: 0, errno: 115, error: Operation now in progress
2021-10-23 Sat 11:38:02 [debug] epolltestconnect.c:165 In function 'server_run' --> send ret: 5, errno: 115, error: Operation now in progress
wireshark抓包如下 关闭服务端之后抓包
服务端:192.168.92.1:2233
客户端:192.168.92.139:port
图片的前三个包是三次握手包,在三次握手之后,按下Ctrl + C终止服务端,服务端向客户端发送FIN,客户端回复ACK。
在客户端调用recv直接出错,调用send的时候,服务端会收到客户端发过来的数据(这里是5个字节:hello),接着服务端回复RST重置连接。
如果是在服务端先发送数据,在客户端调用recv之前关闭连接,那么客户端还是能收到数据,因为数据已经到达客户端内核缓冲区
服务端发送数据之后关闭连接
对应的日志
2021-10-23 Sat 12:02:25 [debug] epolltestconnect.c:163 In function 'server_run' --> recv ret: 6, errno: 115, error: Operation now in progress
2021-10-23 Sat 12:02:25 [debug] epolltestconnect.c:165 In function 'server_run' --> send ret: 5, errno: 115, error: Operation now in progress
如果连续调用两次recv和send,回怎么样呢?
2021-10-23 Sat 12:08:56 [debug] epolltestconnect.c:163 In function 'server_run' --> recv ret: 0, errno: 115, error: Operation now in progress
2021-10-23 Sat 12:08:56 [debug] epolltestconnect.c:165 In function 'server_run' --> send ret: 5, errno: 115, error: Operation now in progress
2021-10-23 Sat 12:08:56 [debug] epolltestconnect.c:167 In function 'server_run' --> recv ret: 0, errno: 115, error: Operation now in progress
2021-10-23 Sat 12:08:56 [debug] epolltestconnect.c:169 In function 'server_run' --> send ret: 5, errno: 115, error: Operation now in progress
可以看到send的话是会把数据发出去的(应该是发到内核缓冲区),但是errno = EINPROGRESS(Operation now in progress)并不能对资源进行操作.
也就是说作为客服端,如果服务端关闭,再调用recv从服务网读取消息,会返回0,且errno = EINPROGRESS; 调用send像服务端发送信息会返回n(n>0),且errno = EINPROGRESS
ret = recv(server_fd, buf, bufsize, MSG_NOSIGNAL);
if (ret == 0 && errno = EINPROGRESS) {
// server side closed, close client side here.
close(sever_fd);
}
ret = send(server_fd, buf, bufsize, MSG_NOSIGNAL);
if (ret > 0 && errno = EINPROGRESS) {
// server side closed, close client side here.
close(sever_fd);
}
客户端关闭socket
测试方法:nc作为客户端去连接服务端,三次握手之后就断开。
以下是服务端的代码
client_fd = accept(fd, (struct sockaddr *)&client_addr, &client_addr_len);
sleep(10);
ret = recv(client_fd, buf, 32, MSG_NOSIGNAL);
LOG_DEBUG("recv ret: %d, errno: %d, error: %s", ret, errno, strerror(errno));
ret = send(client_fd, "hello", 5, MSG_NOSIGNAL);
LOG_DEBUG("send ret: %d, errno: %d, error: %s", ret, errno, strerror(errno));
close(client_fd);
关闭客户端后再服务端读写, 对应的日志
2021-10-23 Sat 12:55:40 [debug] epolltestconnect.c:219 In function 'main' --> recv ret: 0, errno: 11, error: Resource temporarily unavailable
2021-10-23 Sat 12:55:40 [debug] epolltestconnect.c:221 In function 'main' --> send ret: 5, errno: 11, error: Resource temporarily unavailable
client:192.168.92.1
server:192.168.92.139:2233
在服务端收到的错误不一样
尝试多次读写,发现得到的错误还是一样的。
client_fd = accept(fd, (struct sockaddr *)&client_addr, &client_addr_len);
sleep(10);
ret = recv(client_fd, buf, 32, MSG_NOSIGNAL);
LOG_DEBUG("recv ret: %d, errno: %d, error: %s", ret, errno, strerror(errno));
ret = send(client_fd, "hello", 5, MSG_NOSIGNAL);
LOG_DEBUG("send ret: %d, errno: %d, error: %s", ret, errno, strerror(errno));
ret = recv(client_fd, buf, 32, MSG_NOSIGNAL);
LOG_DEBUG("recv ret: %d, errno: %d, error: %s", ret, errno, strerror(errno));
ret = send(client_fd, "hello", 5, MSG_NOSIGNAL);
LOG_DEBUG("send ret: %d, errno: %d, error: %s", ret, errno, strerror(errno));
close(client_fd);
对应的日志如下
2021-10-23 Sat 13:04:18 [debug] epolltestconnect.c:219 In function 'main' --> recv ret: 0, errno: 11, error: Resource temporarily unavailable
2021-10-23 Sat 13:04:18 [debug] epolltestconnect.c:221 In function 'main' --> send ret: 5, errno: 11, error: Resource temporarily unavailable
2021-10-23 Sat 13:04:18 [debug] epolltestconnect.c:223 In function 'main' --> recv ret: 0, errno: 11, error: Resource temporarily unavailable
2021-10-23 Sat 13:04:18 [debug] epolltestconnect.c:225 In function 'main' --> send ret: 5, errno: 11, error: Resource temporarily unavailable
也就是说在服务端调用recv读客户端,如果recv返回0,且errno = EAGAIN (Resource temporarily unavailable), 表示对端socket关闭;如果调用write返回n(n>0),且errno = EAGAIN (Resource temporarily unavailable), 表示对端socket关闭。
// MSG_NOSIGNAL表示忽略信号,如果不忽略,会产生SIGPIPE信号
// 导致程序退出,并设置errno = EPIPE
ret = recv(client_fd, buf, bufsize, MSG_NOSIGNAL);
if (ret == 0 && errno == EAGAIN) {
// client side closed, close server side here
close(client_fd);
}
ret = send(client_fd, buf, bufsize, MSG_NOSIGNAL);
if (ret > 0 && errno == EAGAIN) {
// client side closed, close server side here
close(client_fd);
}
如果是recv返回-1,且errno = EAGAIN,表示客户端无数据到达,socket是正常的;如果调用send返回-1,且errno = EAGAIN,socket也是正常的。