之所以会出现CLOSE_WAIT,是TCP自己的问题的现实表现。毕竟我们都不是那种走过TCP的发明到广泛使用的年代的人,这种错误确实是可能犯的。
close和shutdown
首先Linux关闭一个socket,有两个系统调用close
和shutdown
。之所以会这样,是因为TCP是由两个单向通道组成的双向通讯协议。close
会关闭这两个通道,而shutdown
可以选择关闭其中一个,或者两个都关闭。用一个表来分辨。
功能 | shutdown | close |
---|---|---|
关闭方式 | 可以选择关闭读端、写端或同时关闭 | 同时关闭读写端 |
发送FIN报文 | 是 | 是 |
释放资源 | 否,直到四次挥手完成 | 是,立即释放 |
发送缓冲区 | 如果关闭写端,则发送缓冲区中的数据会继续发送 | 如果shutdown函数没有关闭写端,close函数会强制关闭写端,将发送缓冲区中剩余的数据丢弃 |
而TCP在关闭读端的时候,是没有通知socket另一端的方法的,也就说,客户端关闭读端,调用shutdown(sock, SHUT_RD)
的时候,只是在内核里标记了一个状态,不会告知服务端“你再也不能给我发数据了”。
复现CLOSE_WAIT
使用以下代码作为服务端,然后用nc localhost 12345
,再对nc Ctrl+C关闭它。此时netstat -anp | grep 12345
,你会发现至少有一个处于CLOSE_WAIT状态的通道,还有一个处于FIN_WAIT2的(哪怕客户端被杀死了都会有)。
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define PORT 12345
#define BUFFER_SIZE 1024
#include <signal.h>
void sigpipe_handler(int sig) {
printf("SIGPIPE received!\n");
}
int main() {
// 在程序开始时设置信号处理函数
signal(SIGPIPE, sigpipe_handler);
int server_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_addr_len = sizeof(client_addr);
char buffer[BUFFER_SIZE];
// 创建 socket
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
perror("socket");
exit(EXIT_FAILURE);
}
// 绑定地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("bind");
close(server_fd);
exit(EXIT_FAILURE);
}
// 监听连接
if (listen(server_fd, 1) < 0) {
perror("listen");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("Server is listening on port %d\n", PORT);
// 接受连接
client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_addr_len);
if (client_fd < 0) {
perror("accept");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("Client connected, %d\n", client_fd);
// 读取客户端数据(保持连接)
while (1) {
ssize_t bytes_received = recv(client_fd, buffer, BUFFER_SIZE - 1, 0);
if (bytes_received < 0) {
perror("recv");
break;
}
sleep(1);
buffer[bytes_received] = '\0';
printf("Received: %ld, %s\n", bytes_received, buffer);
// send(client_fd, "pong", 4, 0);
}
// 关闭连接(模拟 CLOSE_WAIT 状态)
// 注意:不关闭 socket,将导致进入 CLOSE_WAIT 状态
// close(client_fd);
// 关闭 server socket
close(server_fd);
return 0;
}
避免CLOSE_WAIT问题
- 使用长时间未通讯自动关闭机制
这种方案在Redis中也有,虽然它的设计目的不是为了CLOSE_WAIT,但也确实是一个方案。cluster communicate bus就有这种方案的。
- send + sigpipe + close,虽然可行,但估计没人这么干。当个玩具自己玩玩还可以
进行了send,会触发sigpipe,让套接字处于不可用状态,但是你还是要手动close。这个图就是一个例子。看得出用这种方案确实会去掉两个通道,但是fd还是没有关闭。
- read + close
如果read返回0,那意味着你该close了。(估计没人会试着往read的size参数里写0吧……)
- 多路复用 + read + close
客户端close之后,服务端会触发可读事件。此时read这个socket,会返回0,这意味着服务端该close了。
- 非阻塞read + close
非阻塞read在没有数据的时候,会返回EAGAIN/EWOULDBLOCK
,但是如果返回0,依然还是该close了。
不过都用上了多路复用了,应该没有人还会想着用非阻塞read吧……非阻塞write确实有必要,因为阻塞的write会等至少一个RTT,但read不会。