TCP内核缓冲过大导致应用程序在FIN上失败



我想重新打开以前的一个问题,它被错误地归类为网络工程问题,经过更多的测试,我认为这对程序员来说是一个真正的问题。

因此,我的应用程序从服务器流式传输mp3文件。我无法修改服务器。客户端根据需要从服务器读取160kbit/s的数据,并将其提供给DAC。让我们使用一个3.5 MB的文件。

当服务器发送完最后一个字节后,它会关闭连接,因此它会发送一个FIN,这似乎是正常的做法。

问题是内核,尤其是在Windows上,似乎存储了1到3 MB的数据,我认为TCP窗口大小已经完全打开。

几秒钟后,服务器发送了整个3.5 MB,大约3 MB位于内核缓冲区内。在这一点上,服务器已经在适当的时候发送了作为ACK的FIN。

从客户端的角度来看,它继续按20kB的块读取数据,并将在看到EOF之前的3MB/20~=150s内这样做。

同时,服务器处于FIN_WAIT_2(而不是我最初写的TIME_WAIT,感谢Steffen纠正我的错误。现在,像Windows这样的操作系统似乎有一个半封闭的套接字计时器,它从发送FIN开始,小到120秒,而不管实际的TCPWindows大小BTW如何)。当然,在120秒之后,它认为它应该已经接收到客户端的FIN,所以它发送RST。该RST导致所有客户端的内核缓冲区被丢弃,应用程序失败。

由于代码是必需的,这里是:

int sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
addr.sin_family = AF_INET;
addr.sin_port = htons(80);
int res = connect(sock, (const struct sockaddr*) & addr, sizeof(addr));
char* get = "GET /data-3 HTTP/1.0nr"
"User-Agent: minenr"
"Host: localhostnr"
"Connection: closenr"
"nrnr";
bytes = send(sock, get, strlen(get), 0);
printf("send %dn", bytes);
char *buf = malloc(20000);
while (1) {
int n = recv(sock, buf, 20000, 0);
if (n == 0) {
printf(“normal eof at %d”, bytes);
close(sock);
break;
}
if (n < 0) {
printf(“error at %d”, bytes);
exit(1);
}
bytes += n;
Sleep(n*1000/(160000/8));
}
free(buf);
closesocket(sock);

它可以在任何HTTP服务器上进行测试。

我知道有一些解决方案是在服务器关闭套接字之前与服务器握手(但服务器只是一个HTTP服务器),但内核级别的缓冲区会在缓冲区大于消耗缓冲区的时间时导致系统性故障。

客户端完全实时地吸收数据。拥有更大的客户端缓冲区或根本没有缓冲区并不能改变这个问题,这在我看来是一个系统设计缺陷,除非有可能在应用程序级别(而不是整个操作系统)控制内核缓冲区,或者在recv()的EOF之前在客户端级别检测到FIN接收。我已经尝试过更改SO_RCVBUF,但它似乎不会从逻辑上影响内核缓冲级别。

以下是一个成功和一个失败的交换的捕获

success
3684    381.383533  192.168.6.15    192.168.6.194   TCP 54  [TCP Retransmission] 9000 → 52422 [FIN, ACK] Seq=9305427 Ack=54 Win=262656 Len=0
3685    381.387417  192.168.6.194   192.168.6.15    TCP 60  52422 → 9000 [ACK] Seq=54 Ack=9305428 Win=131328 Len=0
3686    381.387417  192.168.6.194   192.168.6.15    TCP 60  52422 → 9000 [FIN, ACK] Seq=54 Ack=9305428 Win=131328 Len=0
3687    381.387526  192.168.6.15    192.168.6.194   TCP 54  9000 → 52422 [ACK] Seq=9305428 Ack=55 Win=262656 Len=0
failed
5375    508.721495  192.168.6.15    192.168.6.194   TCP 54  [TCP Retransmission] 9000 → 52436 [FIN, ACK] Seq=5584802 Ack=54 Win=262656 Len=0
5376    508.724054  192.168.6.194   192.168.6.15    TCP 60  52436 → 9000 [ACK] Seq=54 Ack=5584803 Win=961024 Len=0
6039    628.728483  192.168.6.15    192.168.6.194   TCP 54  9000 → 52436 [RST, ACK] Seq=5584803 Ack=54 Win=0 Len=0

以下是我认为的原因,非常感谢Steffen让我走上了正轨。

  • mp3文件在160 kbits/s=20 kB/s时为3.5 MB
  • 客户端以20kB/秒的精确速度读取它,比如说每秒20kB的recv(),为了简单起见,没有预缓冲
  • 一些操作系统,如Windows,可以有非常大的TCP内核缓冲区(大约3MB或更多),并且通过快速连接,TCP窗口的大小是广泛开放的
  • 在几秒钟内,整个文件被发送到客户端,假设内核缓冲区中有大约3MB
  • 就服务器而言,所有内容都已发送并确认,因此它执行close()
  • close()向客户端发送FIN,客户端通过ACK进行响应,服务器进入FIN_WAIT_2状态
  • 但是,从客户端的角度来看,在看到eof之前,所有recv()在接下来的150秒内都会有大量的读取
  • 因此客户端不会执行关闭()操作,因此不会发送FIN
  • 服务器处于FIN_WAIT_2状态,根据TCP规范,它应该永远保持这种状态
  • 现在,各种操作系统(至少是Windows)在启动close()时,或在收到FIN的ACK时,启动一个类似于TIME_WAIT(120s)的计时器,我不知道(事实上Windows有一个特定的注册表项,AFAIK)。这是为了更积极地处理半封闭套接字
  • 当然,在120秒之后,服务器没有看到客户端的FIN,并发送RST
  • 客户端接收到RST并导致出现错误,TCP缓冲区中的所有剩余数据将被丢弃和丢失
  • 当然,高比特率格式不会发生这种情况,因为客户端消耗数据的速度足够快,因此内核TCP缓冲区永远不会空闲120秒,而当应用程序缓冲系统读取所有数据时,低比特率格式可能不会发生这种情况。它必须是比特率、文件大小和内核缓冲区的糟糕组合。。。因此,这种情况并非总是发生

就是这样。只需几行代码和每个HTTP服务器就可以复制。这是可以争论的,但我认为这是一个系统性的操作系统问题。现在,似乎有效的解决方案是将客户端的接收缓冲区(SO_RCVBUF)强制到较低的级别,这样服务器几乎没有机会发送所有数据,并且数据在客户端的内核缓冲区中停留的时间太长。请注意,如果缓冲区是20kB,并且客户端以1B/s的速度消耗它,那么这种情况仍然可能发生。。。因此我称之为系统性失败。现在我同意,有些人会将其视为的应用问题

相关内容

  • 没有找到相关文章

最新更新