【问题标题】:Configure socket ACK timeout?配置套接字 ACK 超时?
【发布时间】:2011-12-01 19:55:36
【问题描述】:

有没有办法配置套接字在确定连接失败之前期望接收已发送数据的 ACK 的超时时间?

我知道这也可以在应用程序级别完成,但由于我发送的每个数据包都是 ACK 的,我只想知道我的数据是否被接收,使用应用程序级别的附加数据完成同样的事情似乎很浪费。 (更不用说,我的特定应用程序使用按字节收费的蜂窝链接。)

注意:根据我之前的问题—— What conditions cause NetworkStream.Write to block? -- 你不能依靠 .Write 抛出异常来确定数据没有正确发送。

【问题讨论】:

  • 您需要从 promise 开始工作,即 TCP 是一种可靠的协议,可确保您编写的内容确实在另一端结束。它周围的一切都是围绕这一保证而设计的。如果地震将加利福尼亚倾倒到太平洋中,那么当你关闭() 时你会发现它。
  • 这是一个很好的观点 w.r.t. Close()。然而,ACK 的目的是让系统自己决定连接是否死亡,如果它认为它还没有长到足以死亡,则重新传输。
  • 就像汉斯说的,假设你的数据包确实成功了。如果发生异常,然后确定您停止的位置。没有做到这一点的数据是极端情况,应该总是导致异常。只要确保你有办法从你离开的地方继续。
  • 哦,让我澄清一下。我不在乎“从上次停下的地方继续”。我只需要知道连接是否尽快失败,而不是在操作系统最终决定它死亡的 X 分钟内。因此,我想在 30 秒后将缺少 ACK 作为连接失败的标志来处理。
  • 很好的低级问题。 :) 我可以建议每 3 秒来回发送一个小型 UDP 数据包,然后您可以假设 10 秒内没有响应 = 连接丢失(或高流量)? (将与您的主 TCP 连接并行发生,非常适合 LAN 或可配置的互联网。防火墙、代理、商业环境,算了)。

标签: c# sockets network-programming tcpclient


【解决方案1】:

您可以使用TcpClient.SendTimeout 来执行此操作。如果在操作成功完成之前指定的超时到期,它会导致写入操作抛出SocketException

http://msdn.microsoft.com/en-us/library/system.net.sockets.tcpclient.sendtimeout.aspx

此外,有关如何设置具有更多可定制和可靠超时的套接字的更多信息,请参阅此页面:

http://msdn.microsoft.com/en-us/library/bbx2eya8.aspx

【讨论】:

  • .SendTimeout 只会设置Write 调用的超时时间。如问题中所述,您不能依靠它来确定数据包是否已确认。
【解决方案2】:

在一些 IETF RFC (5482793) 中提到了“用户超时”,它可以满足要求。

其他一些操作系统支持此作为套接字选项,但不幸的是 Windows 不支持。

如果没有这个选项,在这种情况下减少直到中止的时间的唯一方法是减少重新传输尝试的次数,或减少初始 RTT。

在 Windows 上,前者可以通过 netsh/registry 控制(机器范围......):Tcp Max Data Retransmissions

是否可以通过自己的超时时间放弃当前连接,并在需要时建立另一个连接?

  • 应用程序必须确定何时放弃连接 - 可能在 TCP 对话开始时根据不活动时间或有效数据速率建立一些“生存时间”
  • 由于从旧连接重新传输,会产生一些数据开销
  • 可能需要更改服务器应用程序以接受多个并发连接
  • 客户端不应无限期重复此过程,以防网络无法达到足够的超时速度

【讨论】:

  • 虽然这不是我想听到的答案(“无法完成/只能在系统范围内完成”),但感谢您的研究工作并提供更多建议。
  • 可以做到...but not on Windows.
【解决方案3】:

我不是 C# 专家,但我想我可以提供帮助。 您正在尝试从应用程序获取 TCP 层控制数据。这并不容易,与任何应用层协议一样,您需要某种应用层响应,例如 HTTP 中的 Request-Response。

知道你所有的写入数据实际上是由另一端接收的问题在于 TCP 是面向流的。这意味着您可以通过套接字发送 1KB 的数据,该 KB 存储在 TCP snd 缓冲区中,并且该 KB 可能与 3 个可能被完全或单独确认的 TCP 段(TCP ACK)一起发送。它是异步的。因此,在某些时候,TCP 可能只发送了 1,000 KB 数据中的 300 字节,这只是一个示例。

现在另一个问题是,您是每次发送数据块时打开连接并关闭连接(A)还是始终打开连接(B)。

在(A)中它更简单,因为如果连接打开失败,就是这样。超时可能需要超过一分钟,但您不会发送超过几个 20 字节 IP 和(20 字节)TCP 标头(对于 IP 和 TCP 选项,有时超过 20 字节)。

在(B)中,当您要发送数据时,您会意识到成功或失败。我会考虑 3 种情况:

1-套接字的另一端关闭或重置TCP连接。在这种情况下,您应该立即收到错误响应,或者在 C 中,指示管道损坏的信号,我想它会成为 C# 中的异常。

2-另一端变得无法访问并且没有关闭/重置套接字。这很难检测到,因为 TCP 将发送超时的消息,并且在几次重试/超时后,它将决定连接已断开。超时时间和重试次数可以配置,但在操作系统级别(适用于所有应用程序)。我认为您不能通过套接字进行配置。 在这种情况下,您的应用程序在发送数据时不会意识到。

3-数据被对方成功接收并在TCP层确认。

复杂的部分是尽快区分 (2) 和 (3)。我会假设你在问这个。 除非你破解内核,否则我认为不可能完全做到这一点。

无论如何,在应用层从服务器获取 ACK 可能意味着只有 1 或 2 个字节来告知接收到的数据量。除了用于 IP 和 TCP 基本标头的 20+20 字节之外。

如果有可能按照你说的做,我会试试这个,但我从未测试过:

您可以使用发送缓冲区大小和选择功能。您可以使用 setsockopt 和 OS_SNDBUF 套接字选项设置套接字的发送缓冲区大小。 http://msdn.microsoft.com/en-us/library/system.net.sockets.socket_methods(v=vs.110).aspx

如果您知道您总是要发送 2 KB,则将发送缓冲区大小设置为 2 KB。通常您只有在连接后才能更改它。 http://msdn.microsoft.com/en-us/library/system.net.sockets.socket.sendbuffersize(v=vs.110).aspx?cs-save-lang=1&cs-lang=csharp#code-snippet-1

然后调用 Socket 上的 Select 或 Poll 方法来检查它是否可写。

只要确认了一条 TCP 消息,Select 或 Poll 就应该指示套接字是可写的,因为发送的数据已从发送缓冲区中删除。

注意这个算法有局限性:

  1. 操作系统可以定义最小缓冲区大小。
  2. 如果算法可行,当有可用的缓冲区空间但只有一部分数据被另一端实际接收并确认时,Select 和 Poll 将告诉您套接字是可写的。
  3. 如果您发送可变大小的消息,这是不可能的。

如果您无法应用上述算法,您可能需要支付额外的 TCP 消息的额外费用,该消息具有大约 42 字节和应用层简单 ACK。

很抱歉无法提供明确的解决方案。也许操作系统应该实现告诉您可用缓冲区字节的功能,这将解决您的问题。

编辑:我正在添加我的 cmets 的另一个建议。

如果你有可能让其他进程使用 Winpcap,你可以捕获来自另一端的 TCP 响应!!!例如,使用共享内存之类的本地 IPC 或仅使用套接字,一个应用程序可以告诉另一个应用程序有关 socekt 的数据(src IP、src 端口、dst IP、dst 端口)。另一个第二个进程,称为监视进程,可以通过嗅探连接来检测从另一个端点接收到的 ACK。也可以使用 winpcap 链接到本机代码...

【讨论】:

  • 我不确定您为什么要关注缓冲区中的字节。你能解释更多吗? TCP 发送缓冲区预计会立即传输(假设 Nagle 的算法被禁用),它实际上主要是作为缓冲,以防网络连接的带宽不堪重负,也可以重新传输丢弃的数据包。这里的问题不在于我们不知道数据包已发送——它已发送。这里的问题是我们不知道对方是否收到了字节,或者是否发生了其他坏事。
  • 我还假设 Nagle 已禁用。说了这么多,让我不同意你的看法。数据不一定立即发送。当 write/send 函数返回时,仅表示应用程序数据已写入 TCP 发送缓冲区。也有可能只有一部分数据在一个 TCP 段中发送,而其余数据在其他 TCP 段中发送。这是很常见的。之后,TCP 期望所有(1 或 n)段都被确认。只有在收到一个段中发送的一大块数据的 ACK 后,这些字节才会从 TCP snd 缓冲区中删除。
  • 继续我的回复:在收到相应的 ACK 后,TCP 会从 snd 缓冲区中删除字节,因为如果超时,TCP 必须重新发送。您在应用层无法知道收到的 ACK。因此,我能想到的唯一解决方法是使用 snd 缓冲区中的数据。
  • 继续:也许还有另一种解决方法。您是否有可能使用 Winpcap 进行其他进程?在这种情况下,您可以从另一端捕获 TCP 响应!!!例如,使用本地 IPC(如共享内存)或仅使用套接字,一个应用程序可以告诉另一个应用程序有关 socekt 的数据(src IP、src 端口、dst、IP、dst 端口)。另一个第二个进程,称为监视进程,可以通过嗅探连接来检测从另一个端点接收到的 ACK。
【解决方案4】:

这是一个老问题,但它打动了我......正如你原来的问题所提到的,这应该在应用程序层完成。

我希望我的经验可能会有所帮助,因为我的想法与您完全相同(甚至与我团队中的其他开发人员争论过,坚持认为 TCP 应该完成工作)。实际上,很容易将 TCP 与无线连接、冲突的网络 MTU 以及有时实施不当的路由器/接入点弄乱,这些路由器/接入点会过早地或在故障条件下进行 ACK。但也因为 TCP 旨在从一个源流向一个目的地,而不是真正确保全双工事务通信。

我在一家嵌入式设备制造商工作了多年,并为仓库中的无线条码终端编写了完整的客户端-服务器系统。在这种情况下不是蜂窝网络,但 wifi 可能同样糟糕(但即使 WiFi 也会证明所需的任务无用)。仅供参考,我的系统在将近 7 年后的今天仍然在生产中可靠运行,所以我认为我的实施相当稳健(它经常受到工业制造机器/焊机/空气压缩机/老鼠咀嚼网络线等的干扰)。

了解问题

@rodolk 发布了一些很好的信息。 TCP 级别的 ACK 不一定与您的每个应用程序网络传输对应 1-1(如果您发送的数据超过网络的 MTU 或最大数据包大小,即使 Nagle 被禁用,也总是不会是 1-1)。

最终,TCP 和 IP (Transport and Network layers) 的机制是确保在一个方向(从源到目的地)传送您的流量,并对最大重试次数等进行一些限制。应用程序通信最终是关于位于 TCP/IP 之上的全双工(双向)Application layer 通信。混合这些层不是一个好策略。想想 TCP/IP 之上的 HTTP 请求-响应。 HTTP 不依赖 TCP ACKS 来实现自己的超时等。如果您有兴趣,HTTP 将是一个很好的研究规范。

但我们甚至可以假装它正在做你想做的事。您总是在 1 次传输中发送少于 1 个 MTU(或最大数据包大小)并恰好接收 1 个 ACK​​。介绍您的无线环境,一切都会变得更加复杂。在成功传输和对应的ACK之间可以有失败!

问题在于无线通信流的每个方向不一定具有相同的质量或可靠性,并且会根据本地环境因素和无线设备的移动随时间而变化。

设备的接收效果通常比它们传输的效果好。设备完美接收您的传输,回复某种已传输的“ACK”是很常见的,但由于信号质量、传输距离、射频干扰、信号衰减、信号反射等原因,无线 ACK 永远不会到达目的地. 在工业应用中,这可能是重型机械的开启、焊接机、冰箱/冰柜、荧光灯等。在城市环境中,它可能是结构、停车场、钢结构建筑等内的移动性。

在这种情况下,客户端在什么时候采取行动(保存/提交数据或更改状态)以及服务器在什么时候认为该操作成功(保存/提交数据或更改状态)?如果没有在您的应用程序层进行额外的通信检查,这很难可靠地解决(有时包括事务的 2 路 ACK,即:客户端传输、服务器 ACKS、客户端 ACKS ACK :-) 您不应该在这里依赖 TCP 级别的 ACK,因为它们不会可靠地等同于成功的全双工通信,也不会为您的应用程序提供可靠的重试机制。

嵌入式设备上不可靠无线通信的应用层技术

我们的技术是,每条应用程序级消息都带有几个字节的应用程序级标头,其中包括数据包 ID #(只是一个递增的整数)、整个消息的长度(以字节为单位)和整个消息的 CRC32 校验和。我不记得了,但我相信我们用 8 个字节完成了这个,2 | 2 | 4.(取决于您要支持的最大消息长度)。

假设您正在盘点仓库中的库存,您盘点一件物品并计算 5 个单位,条码终端向服务器发送一条消息,说“Ben 计算了 5 个单位的 Item 1234”。当服务器收到消息时,它会等到收到完整的消息,首先验证消息长度,然后是 CRC32 校验和(如果长度匹配)。如果这一切都通过了,我们会向此消息发送回应用程序响应(类似于应用程序的 ACK)。在此期间,条码终端正在等待来自服务器的 ACK,如果它没有收到服务器的回复,它将重新传输。如果服务器接收到相同数据包 ID 的多个副本,它可以通过放弃未提交的事务来进行重复数据删除。然而,如果条码扫描器确实从服务器接收到它的 ACK,它会再用一个最终的“COMMIT”命令回复服务器。因为前 2 条消息刚刚验证了一个有效的全双工连接,所以提交在这几毫秒的时间范围内不太可能失败。仅供参考,这种故障情况很容易在您的 WiFi 覆盖范围边缘复制,因此请带上您的笔记本电脑/设备去散步,直到 wifi 仅为“1 bar”或最低连接速度通常为 1 mbps。

因此,您将在消息的开头添加 8 个字节的标头,并且如果您在只有一侧无线通信可能失败的情况下需要已处理的请求/响应,则可以选择添加一个额外的最终 COMMIT 消息传输。

使用复杂的应用层到传输层挂钩系统(例如挂钩到 winpcap),很难证明每条消息节省 8 个字节是合理的。此外,您可能会或可能无法在其他设备上复制此传输层挂钩(也许您的系统将来会在其他设备上运行?Android、iOS、Windows Phone、Linux,您能否为所有这些实现相同的应用层通信平台?我认为无论 TCP 堆栈是如何实现的,您都应该能够在每个设备上实现您的应用程序。)

我建议您将应用层与传输层和网络层分开,以便很好地分离关注点,并严格控制重试条件、超时和潜在的事务处理应用程序状态更改。

【讨论】:

  • 回复:“很难证明使用复杂的应用层为传输层挂钩系统(例如挂钩到 winpcap)每条消息节省 8 个字节是合理的。” -- 在我的情况下,总有效负载为 14 字节,这意味着这将使有效负载增加 50% 以上。此外,鉴于有效载荷很小,这不太可能导致碎片。
  • @BenSwayne:让我添加一些 cmets。 1) 不仅对于分段的 IP 数据报,一条消息可以在 N 个数据报中发送。其他情况可能是因为对应的 TCP 堆栈正在通告的 TCP 窗口大小小于您的应用程序尝试发送的数据。 (顺便说一句,我假设只有一个写入/发送函数调用将完整的消息传递给内核)。 2)关于 CRC32,它应该是没有必要的,因为你也有 TCP 校验和,用于应用程序到应用程序的通信,以及 802.11 的帧校验序列(FCS),用于 Wi-Fi 链接,完成这项工作。跨度>
  • @DavidPfeffer:我相信你有理由试图避免应用层 ACK,但根据应用程序的性质,你可以只用 1 个字节来实现它(如果你有 stop-and-等待类型的应用程序,或 2/3 字节发送 ACK 和服务器正在确认的序列/消息号)在响应消息中。现在,如果您不关心可移植性,我会尝试在 Windows 中使用 winpcap 或在 Linux 中使用 pcap。
  • 感谢您提供如此详细的答案。我本来想给你赏金的,但不幸的是,在它到期的那一天,它无法上线。不幸的是,如果不添加额外的硬件,我无法控制应用层,因为无线调制解调器会将它接收到的数据直接转储到我无法控制的固件中。
猜你喜欢
  • 1970-01-01
  • 2012-12-06
  • 2017-10-15
  • 2011-01-11
  • 1970-01-01
  • 2016-12-13
  • 2012-04-20
  • 2012-11-12
相关资源
最近更新 更多