当客户端愉快地发送它们时,为什么TCP数据包始终无法到达服务器?

Rus*_*uss 5 c python sockets tcp tcpdump

我有一个简单的客户端服务器设置,似乎我从客户端发送的TCP数据包没有到达服务器.

通常一切正常,但是当我在客户端上启动50个线程以同时使用相同的小数据包(仅39个字节)"同时"命中服务器时,服务器无法接收所有字节的随机次数.更奇怪的是,它在如何接收它们方面非常一致......只收到5个字节.

我正在使用tcpdumptcpflow捕获两端发生的事情(如果不熟悉tcp流,它会从TCP流中删除大量的TCP SYN/ACK/FIN/etc噪声,只显示发送的数据)任何方向).

在客户端,50个线程触发39字节数据包,它看起来很完美.具体来说,tcpflow(使用libpcap)向我展示了50个相同的数据传输:

07 B6 00 01 | 00 1E 00 00 | <etc>
Run Code Online (Sandbox Code Playgroud)

据我了解,libpcap/tcpdump从相当低的级别(低于TCP堆栈)获取数据,所以我认为这意味着数据发送正常,或者至少没有卡在内核缓冲区中.

但是,在查看服务器端时,一切都不完美.随机数失败,而且百分比很高.例如,在50个套接字连接中,30个工作正常,但是对于其中20个,我有一个协议失败,服务器socket.recv超时等待字节(协议指示确切的数据包长度).

它的失败方式非常一致.对于30/20的情况,30个插座完全接收传输的39个字节.其余20个ALL收到这个部分数据,之后我的socket.recv时间超出:

07 B6 00 01 | 00
Run Code Online (Sandbox Code Playgroud)

对于20个连接中的每一个,只有5个字节到达,并且它似乎处于内核级别,因为tcpdump仅显示5个字节到达.

怎么会发生这种情况?

这个5字节的边界不是100%重合.它是标头的第一部分,接下来是34字节的有效负载,但是没有到达.在客户端,它是这样分开的.

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((HOST, PORT))
sock.sendall(HEADER)  # 5 bytes
sock.sendall(PAYLOAD) #34 bytes
Run Code Online (Sandbox Code Playgroud)

并且两个sock.sendall调用都在每个线程中成功完成,事实证明我的tcp日志记录显示所有50个运行完全发送39个字节"出门".

关于这个根本原因的任何想法?我错过了什么?

Rus*_*uss 5

回答我自己的问题...

简短的回答是,仅使用 TCP,客户端无法知道预期的接收者是否实际收到了发送的字节。

即:它不不论客户“愉快地”发送的字节数...即使TCP他们可能永远不会到来,你绝对没有知识来,他们将获得预期的收件人。无论如何,并非没有在应用程序层中构建一些确认。

对于我的特殊情况,事实证明客户端发送的字节 DID 实际到达服务器,但需要大约 30 秒(!!!)到达,此时客户端和服务器应用程序协议代码都已超时。

客户端和服务器端日志的视图(对于一个失败的连接)在这里:

这些图像是来自tcpdump捕获文件的一个特定 TCP 流的wireshark视图。您可以看到发生了大量的重传。推动这些转播需求的根本原因是什么?我完全不知道(但很想知道!)。

数据在第 2 个最后一个条目 (#974) 中到达服务器,大约在发送后 30 秒,并且中间有大量的重新传输尝试。如果对服务器端#793 感到好奇,这是我的应用层协议尝试将消息发送回客户端,说“超时等待更多数据......它在哪里?”。

除了固有的延迟之外,数据没有出现在tcpdump服务器日志中的原因之一似乎也是我使用tcpdump. 简而言之:tcpdump在查看捕获文件(使用-w开关创建的文件)之前,请确保按 Ctrl-C 退出捕获,因为它似乎对您在文件中看到的内容有很大影响。我希望这是一个刷新/同步问题,但我在猜测。但是,如果没有 Ctrl-C,我肯定会丢失数据。

更多细节以供将来参考...

尽管您经常阅读/听说 TCP 将:

  1. 保证您的数据包会到达(与UDP 相比,不会)
  2. 保证您的包裹按顺序到达

很明显/很明显,第一个实际上根本不是真的。TCP 将尽最大努力将您的字节发送到预期的接收者(包括重试很长时间),但这并不能保证,无论发送手册页是否指示send返回值“成功时,这些调用返回发送的字符数”。后者是正确的,并且具有很强的误导性(见下文)。

其根源主要来自各种套接字调用(send特别是)的行为方式以及它们如何与操作系统的 TCP/IP 堆栈交互......

在 TCP 交换的发送端,进程非常简单。先是你connect(),然后是你send()

connect() 成功返回肯定意味着你能够建立到服务器的连接,所以你至少知道此时服务器在那里并在监听(即:3部分TCP打开握手成功)。

对于“发送”,尽管调用的文档表明返回值(如果为正)是“发送的[字节数]”,但这完全是错误的。返回值告诉您的只是底层操作系统中的 TCP 堆栈接受到其传出缓冲区的字节数。在此之后,操作系统将尽最大努力将这些字节传送给您最初与之建立连接的接收者。但这可能永远不会发生,所以它不会意味着您可以指望发送的这些字节!有点令人惊讶的是,即使 TCP 内置了 ACK 消息,也没有真正的方法来确定这是否发生(或没有发生!),至少在 TCP 套接字层。为了验证您发送的字节是否完全接收,您需要在应用层添加某种确认。nos在另一个问题中有一个很好的答案,该问题讨论了一点。

附录...

一个有趣的困境是我是否需要在我的应用层协议中构建一些重试功能。目前看来,在服务器等待数据超时的情况下,关闭连接并使用相同的请求打开一个新连接将是有益的。之所以这样,是因为低级 TCP 重试不成功,但同时还有其他客户端线程及时通过。但是,这感觉非常错误……您会认为 TCP 重试就足够了。但他们不是。我需要调查 TCP 问题的根本原因来解决这个问题。