【问题标题】:Why is there a long pause after ~16370 socket connections?为什么在 ~16370 个套接字连接后会有长时间的停顿?
【发布时间】:2018-10-30 08:06:57
【问题描述】:

我一直在使用套接字 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);
  }
}

client.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

【问题讨论】:

  • 16.384 是 2^14,所以我认为它已经达到了极限,必须及时处理。也许您没有正确清理套接字,它必须等待连接超时才能进行更多操作。但这只是一个猜测,我对这个话题了解不够。
  • 试试shutdown
  • @KamilCuk 我尝试在调用close(s) 之前将shutdown(s, SHUT_RDWR); 添加到客户端和服务器,但我仍然得到类似的行为
  • 它是特定于操作系统的。

标签: c sockets networking


【解决方案1】:

您很可能在您的操作系统上达到了称为Ephemeral Port Range 的限制。同样的原则适用于所有基于 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 一样运行,直到再次达到限制。

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

【讨论】:

    【解决方案2】:

    您可能正在处理套接字的 TIME_WAIT 状态。在主动关闭服务器中的每个连接套接字后,套接字会在很长一段时间(数十秒)内保持该状态。因此,正如@Blaze 建议的那样,您的程序已达到资源限制,必须等待此套接字最终关闭。

    此功能用于防止两种情况,第一种是在一个连接中发送的延迟数据包被解释为稍后连接的一部分的机会。第二个是连接关闭的最后一个ACK没有被连接的被动关闭端接收到,导致它重新传输FIN/ACK的机会。如果发生这种情况并且主动关闭方已经关闭了套接字,它将以 RST 响应,导致另一方收到错误,尽管所有信息都已正确发送。

    看看这个:http://www.serverframework.com/asynchronousevents/2011/01/time-wait-and-its-design-implications-for-protocols-and-scalable-servers.html

    如果您真的想完全关闭套接字(有发生之前事件的风险),您必须在尝试关闭它之前调整 SO_LINGER 选项。

    struct linger immediate_linger;
    immediate_linger.l_onoff = 1; /* Do linger on closing */
    immediate_linger.l_linger = 0;  /* Wait 0 seconds to linger after closing */
    if (setsockopt(comm_socket, SOL_SOCKET, SO_LINGER, &immediate_linger, sizeof (immediate_linger))) {
      err = errno;
      printf("Error setting immediate linger for socket %d: %s.", comm_socket, strerror(err));
    }
    else {
      printf("Set immediate linger after close for the socket %d.", comm_socket);
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-06-19
      • 1970-01-01
      • 1970-01-01
      • 2021-01-09
      • 1970-01-01
      • 2011-01-27
      • 1970-01-01
      相关资源
      最近更新 更多