6

我设置了一个简单的客户端服务器,似乎我从客户端发送的 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>

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

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

它的失败方式非常一致。对于 30/20 的情况,30 个套接字完全接收传输的 39 个字节。剩下的 20 人都收到了这部分数据,之后我的socket.recv超时:

07 B6 00 01 | 00

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

并且两个sock.sendall调用在每个线程中都成功完成,正如我的 tcp 日志记录所证明的那样,所有 50 次运行都完美地“发送”了 39 个字节。

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

4

3 回答 3

5

回答我自己的问题...

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

即:客户端是否“愉快地”发送字节无关紧要......即使使用TCP,它们也可能永远不会到达,而且您绝对不知道它们何时会到达预期的接收者。无论如何,如果没有在应用程序层中构建一些确认,就不是这样了。

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

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

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

数据在最后一个条目 (#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 问题的根本原因来解决这个问题。

于 2012-04-22T16:21:20.110 回答
4

您发送的字节数非常少,因此您可能会违反Nagle 算法,该算法将保留您希望发送的数据,直到已缓冲大量数据并准备好传输。

创建套接字后,在发送任何数据之前尝试添加以下行:

sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True)

但请注意,这样做会带来更多的通信开销。

于 2012-04-22T16:48:03.127 回答
1

您必须非常小心,因为(由于缓冲) send 和 recv 可能不会发送或接收与您预期“应该”可用的数据一样多的数据。您还必须非常小心,任何线程都可以随时阻塞,即使它“应该”能够接收到您认为发送的尽可能多的数据。

于 2012-04-20T17:01:45.610 回答