首页 > 解决方案 > 为什么在 ~16370 个套接字连接后会有长时间的停顿?

问题描述

我一直在使用 sockets API 来了解它是如何工作的。

我写了两个小程序:

  1. 服务器在 8080 上侦听流连接。它向任何连接到它的人发送一条简单的消息。
  2. 客户端连接到 127.0.0.1:8080,并将接收到的内容转储到标准输出。它按顺序重复此 20000 次。

消息以极快的速度流向大约 16370 次,然后暂停数十秒,然后再次开始快速移动以完成 20000 个连接。

我把这个实验重复了几次,我在 16370、16371 和 16372 抓到了它。重复实验的结果出奇的一致。

我的问题是:为什么它需要在 ~16370 次迭代后暂停?这里的瓶颈是什么?

FWIW,我在 macOS Sierra 上。

我像这样运行服务器代码:

clang -Wall -Werror -Wpedantic server.c -o server.out && ./server.out

和这样的客户端代码:

clang -Wall -Werror -Wpedantic client.c -o client.out && time ./client.out

这是两个程序:

服务器.c

#include <errno.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

#define PORT 8080
#define MAXMSG 512

int make_socket(int port) {
  int sock;
  struct sockaddr_in name;

  sock = socket(PF_INET, SOCK_STREAM, 0);
  if (sock < 0) {
    perror("socket");
    exit(1);
  }

  name.sin_family = AF_INET;
  name.sin_port = htons(port);
  name.sin_addr.s_addr = htonl(INADDR_ANY);
  if (bind(sock, (struct sockaddr*) &name, sizeof(name)) < 0) {
    perror("bind");
    exit(1);
  }
  return sock;
}

int main(int argc, char** argv) {
  const char hello[] = "Hello visitor ";
  char buffer[MAXMSG];
  int sk;
  unsigned long count = 0;
  strcpy(buffer, hello);

  sk = make_socket(PORT);
  listen(sk, 10);

  printf("ready\n");
  for (;;) {
    count++;
    sprintf(buffer + strlen(hello), "%lu", count);
    int s = accept(sk, NULL, NULL);
    if (send(s, buffer, strlen(buffer) + 1, 0) < 0) {
      perror("send");
      exit(1);
    }
    close(s);
    printf("data socket (%d) message sent (%s)\n", s, buffer);
  }
}

客户端.c

#include <arpa/inet.h>
#include <errno.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

#define PORT 8080
#define MAXMSG 512

int make_socket() {
  int sock;

  sock = socket(PF_INET, SOCK_STREAM, 0);
  if (sock < 0) {
    perror("socket");
    exit(1);
  }

  return sock;
}

int main(int argc, char** argv) {
  char buffer[MAXMSG];
  int sk;
  size_t i;
  struct sockaddr_in addr;
  strcpy(buffer, "Hello world!");

  for (i = 0; i < 20000; i++) {
    sk = make_socket();
    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    connect(sk, (struct sockaddr*) &addr, sizeof(addr));
    recv(sk, buffer, strlen(buffer) + 1, 0);
    close(sk);
    printf("socket (%d) message = %s\n", sk, buffer);
  }
}

这是我得到客户端的最后一个标准输出:

socket (3) message = Hello visitor 16369
socket (3) message = Hello visitor 16370
socket (3) message = Hello visitor 16371
socket (3) message = Hello visitor 16372

标签: csocketsnetworking

解决方案


您很可能在操作系统上遇到了一个称为临时端口范围的限制。相同的原则适用于所有基于 IP 的操作系统。

每当建立套接字连接时,都会为请求分配一个端口,并与建立连接的接口相关联。一旦此套接字关闭,端口就会进入称为 TIME_WAIT 的状态。有效地将端口放在工作台上一段时间,以确保不会过早重复使用。这是为了防止 Internet 中的潜在数据包迟到并导致问题。

临时端口范围在 Linux 上指定为/proc/sys/net/ipv4/ip_local_port_range.

您可以使用以下命令在 MacOS 上显示这些:

sysctl net.inet.ip.portrange.first net.inet.ip.portrange.last

net.inet.ip.portrange.first: 49152
net.inet.ip.portrange.last: 65535

这是临时范围内的16,383 个可用端口。

要查看所有网络参数,您可以执行:

sysctl net.inet.tcp

您可以更改 TIME_WAIT 值,但对于您的高压力应用程序,它只会降低您的减速等待的阈值。

您可以使用 netstat -an 查看打开的连接数。如果您打开和关闭大量连接,套接字可能会卡在 TIME_WAIT 状态。在某些地方这是不可避免的,但如果是这种情况,您可能需要考虑是否需要连接池。

如果 TIME_WAIT 是问题,您可以调整系统设置。您可以设置net.ipv4.tcp_tw_reuse / net.ipv4.tcp_tw_recycle以加快连接周转速度。

一个快速测试是切换到另一个界面并重试。如果您使用的是 localhost,然后遇到速度变慢的情况,您可以在另一个接口上切换到您的外部 IP,并且您应该像 gangbusters 一样运行,直到再次达到限制。

这不是您使用的语言的问题,而是基于套接字的网络使用的问题。


推荐阅读