TCP之CLOSE_WAIT

之所以会出现CLOSE_WAIT,是因为Linux实现TCP的设计缺陷。毕竟我们都不是那种走过TCP的发明到广泛使用的年代的人,这种错误确实是可能犯的。

close和shutdown

首先Linux关闭一个socket,有两个系统调用closeshutdown。之所以会这样,是因为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状态的通道。

c 复制代码
#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问题

  1. 使用长时间未通讯自动关闭机制

这种方案在Redis中也有。cluster communicate bus就有这种方案的。

  1. send + sigpipe + close,虽然可行,但估计没人这么干。当个玩具自己玩玩还可以

进行了send,会触发sigpipe,让套接字处于不可用状态,但是你还是要手动close。这个图就是一个例子。看得出用这种方案确实会去掉两个通道,但是fd还是没有关闭。

  1. read + close

如果read返回0,那意味着你该close了。(估计没人会试着往read的size参数里写0吧......)

  1. 多路复用 + read + close

客户端close之后,服务端会触发可读事件。此时read这个socket,会返回0,这意味着服务端该close了。

  1. 非阻塞read + close

非阻塞read在没有数据的时候,会返回EAGAIN/EWOULDBLOCK,但是如果返回0,依然还是该close了。

不过都用上了多路复用了,应该没有人还会想着用非阻塞read吧......非阻塞write确实有必要,因为阻塞的write会等至少一个RTT,但read不会。