确定对等端是否关闭了套接字的读取端

14

我有一个套接字编程的情况,客户端关闭了套接字的写入端口以让服务器知道输入已完成(通过接收EOF),但保持读取端口打开以读取结果(一行文本)。对于服务器来说,知道客户端已成功读取结果并关闭了套接字(或者至少关闭了读取端口)是很有用的。是否有一种好的方法可以检查/等待这种状态?


1
EJP的回答提出了一个合理的问题---服务器为什么需要知道呢?这是一个严格的要求(例如,系统否则会失败,或者是响应已发送的法律证明),还是一个软性要求(监控成功响应率,监控客户端速度)?请澄清您的问题。 - Dima Tisnek
这是一个软性要求。 - R.. GitHub STOP HELPING ICE
1
@大家,我欢迎提供实用的代码来展示解决方案。这就是悬赏的目的所在。 - Dima Tisnek
1
@qarma:唉,现在赏金似乎只会吸引那些希望靠运气得到好处的人提供许多糟糕的答案... - R.. GitHub STOP HELPING ICE
5个回答

6
不行。你只能知道你的发送是否成功,即使在对等方读取关闭后,一些发送仍将成功,这是由于TCP缓冲。
这是设计不良。如果服务器需要知道客户端接收到数据,客户端需要确认它,这意味着它不能关闭其写入端。客户端应该:
1. 作为数据发送一个带内终止消息。 2. 读取并确认所有进一步的响应,直到流结束。 3. 关闭套接字。
服务器应检测带内终止消息,并:
1. 停止从套接字读取请求 2. 发送所有未完成的响应并读取确认 3. 关闭套接字。
或者,如果目标仅是确保客户端和服务器同时结束,则每个端点都应关闭其输出套接字,然后读取输入,直到流结束,然后关闭套接字。这样最终关闭将在两端更或多或少同时发生。

1
你确定吗?在客户端收到响应并关闭套接字后尝试发送更多数据肯定会导致ECONNRESET或类似的错误,不是吗?但仍然有可能没有办法在实际尝试发送更多数据之前探测到这一点。 - R.. GitHub STOP HELPING ICE
是的,send() 会在一些成功的发送后产生 ECONNRESET 错误,因为有缓冲区存在,正如我所说。 - user207421
但是确认也可以通过关闭序列来完成,不是吗?看起来简单而高效。 - usr
@usr 我不明白怎么做。无论客户端是否接收到任何内容,它都将关闭或关闭其套接字。 - user207421
嗯,如果应用程序想要确认,它是否只能优雅地关闭?如果应用程序崩溃,应该会导致重置,如果出于某种原因不想进行确认,则应用程序可以重置。尽管如此,这种方案听起来有些可疑。听起来是一种不好的做法(但有效)。@EJP - usr
@usr 不切实际。如果对等方一直收到垃圾数据,它能做什么呢?如果它的主机操作系统关闭了呢? - user207421

5

getsockopt使用TCP_INFO似乎是最明显的选择,但它不是跨平台的。

这里有一个Linux的示例:

import socket
import time
import struct
import pprint


def tcp_info(s):
    rv = dict(zip("""
            state ca_state retransmits probes backoff options snd_rcv_wscale
            rto ato snd_mss rcv_mss unacked sacked lost retrans fackets
            last_data_sent last_ack_sent last_data_recv last_ack_recv
            pmtu rcv_ssthresh rtt rttvar snd_ssthresh snd_cwnd advmss reordering
            rcv_rtt rcv_space
            total_retrans
            pacing_rate max_pacing_rate bytes_acked bytes_received segs_out segs_in
            notsent_bytes min_rtt data_segs_in data_segs_out""".split(),
                  struct.unpack("BBBBBBBIIIIIIIIIIIIIIIIIIIIIIIILLLLIIIIII",
                                s.getsockopt(socket.IPPROTO_TCP, socket.TCP_INFO, 160))))
    wscale = rv.pop("snd_rcv_wscale")

    # bit field layout is up to compiler
    # FIXME test the order of nibbles
    rv["snd_wscale"] = wscale >> 4
    rv["rcv_wscale"] = wscale & 0xf
    return rv

for i in range(100):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect(("localhost", 7878))
    s.recv(10)
    pprint.pprint(tcp_info(s))

我怀疑不存在真正的跨平台替代品。
基本上有很多状态:
- 你将数据写入套接字,但尚未发送 - 数据已发送,但未接收到 - 数据已发送但丢失(依赖计时器) - 已接收数据,但尚未确认 - 尚未收到确认 - 确认已丢失(依赖计时器) - 远程主机已接收数据,但应用程序尚未读取 - 应用程序已读出数据,但套接字仍然活动 - 数据已读出,并且应用程序崩溃 - 数据已读出,应用程序关闭了套接字 - 数据已读出,并且应用程序调用了shutdown(WR)(与关闭几乎相同) - REMOTE尚未发送FIN - REMOTE已发送FIN但尚未接收到 - FIN已发送并且已丢失 - 您的端点接收到FIN
显然,您的操作系统可以区分这些状态中的许多状态,但不是所有状态。我想不出一个API会如此冗长...
一些系统允许您查询剩余的发送缓冲区空间。也许,如果您这样做,而套接字已经关闭,您会得到一个整洁的错误?
好消息是,仅因为套接字被关闭,就不意味着您无法对其进行审问。我可以在关闭后获取所有TCP_INFO,其中state=7(已关闭)。在某些情况下报告state=8(关闭等待)。 http://lxr.free-electrons.com/source/net/ipv4/tcp.c#L1961具有Linux TCP状态机的所有恶毒细节。

在我的情况下,代码不需要可移植性(它是为特定的嵌入式应用程序而设计的),因此这可能是一个非常好的解决方案。谢谢! - R.. GitHub STOP HELPING ICE

1

简而言之:

不要依赖套接字状态来做这个事情,因为在很多错误情况下它会给你带来麻烦。你需要将确认/收据功能融入到你的通信协议中。对于基于文本的协议,每行的第一个字符用于状态/确认非常有效。


在许多Unix-like/POSIXy系统上,可以使用TIOCOUTQ(也是SIOCOUTQioctl来确定传出缓冲区中剩余的数据量。
对于TCP套接字,即使另一端关闭了其写入端(因此不会向此端发送更多数据),所有传输都将得到确认。只有在收到接收方内核的确认后,才会删除传出缓冲区中的数据。因此,当传出缓冲区中没有更多数据时,我们知道另一端的内核已经接收到数据。
不幸的是,这并不意味着应用程序已经接收和处理了数据。所有依赖套接字状态的方法都存在同样的限制;这也是为什么从其他应用程序接收/接受最终状态行的确认必须来自其他应用程序,而不能自动检测的原因。
这意味着,任何一端都不能在最后一个接收/确认消息之前关闭它们的发送端。你不能依赖于TCP或其他协议的自动套接字状态管理。你必须把重要的接收/确认信息嵌入到流协议本身中。
在OP的情况下,流协议似乎是基于简单的基于行的文本。这非常有用且易于解析。扩展这样一个协议的一个稳健方法是将每行的第一个字符保留为状态码(或者选择某些单字符行作为确认)。
对于大型正在飞行的二进制协议(即,发送方和接收方真正不同步的协议),有用的是为每个数据帧标记一个递增(循环)整数,并让另一端偶尔以更新方式响应,以让发送方知道哪些帧已经完全处理,哪些已经接收,以及是否很快会收到更多帧。这对于网络设备非常有用,它们消耗大量数据,数据提供者希望随时了解进度和期望数据速率(例如3D打印机,CNC机床等,其中数据的内容动态更改最大可接受数据速率)。

-2

好的,我记得在90年代后期为解决这个问题而烦恼了很久。最终我找到了一份不太常见的文档,指出从一个已断开的套接字读取数据会返回 0。我至今仍在利用这个事实。


你找到了 man 2 recv 或者 man 2 read。这些内容并不含糊,或者说你所“找到”的信息是正确的。 - user207421
这是针对相反方向问题的,这个问题很简单。 - R.. GitHub STOP HELPING ICE

-3

你最好使用ZeroMQ。它可以发送整个消息,或者根本不发送消息。如果将其发送缓冲区长度设置为1(最短长度),则可以测试发送缓冲区是否已满。如果没有,那么消息可能已成功传输。如果您的系统中存在不可靠或间歇性的网络连接,则ZeroMQ也非常好用。

但这仍然不完全令人满意。你最好在ZeroMQ之上实现自己的发送确认机制。这样,你就有绝对的证据表明消息已被接收。你无法证明消息未被接收(在发出和接收确认之间可能会出现问题,而你无法解决“两个将军的问题”)。但这是可以实现的最佳方案。这样做的结果是,在ZeroMQ的Actor模型之上实现了一个通信顺序进程体系结构,该模型本身是基于TCP流实现的。最终速度会稍慢,但您的应用程序可以更加确定发生了什么。


1
这个问题并不是要求重新设计通信协议的建议,而是要找到一些方法来获取现有协议中更有用的状态信息。 - R.. GitHub STOP HELPING ICE
@R.. 随你便。很多人,包括我自己,已经决定尽可能避免使用原始套接字来工作。如果您控制整个系统并且有自由选择自己的协议(从您的问题中并不完全清楚),并且以前没有使用过它,那么ZeroMQ或类似的东西(例如nanomsg)非常值得一看。 - bazza
@bazza 而且有更多的人成功地实现了他们的需求,而不必部署第三方产品。 - user207421
@EJP,最终很多人都通过利用免费、方便且极好的东西来更快地完成他们的软件。这是一个选择的问题。R..似乎不像我一样特别感兴趣做出选择,但那是他们的选择而不是我的。我所能做的就是指出有一个很好的替代品。顺便说一句,套接字库与ZeroMQ一样都是第三方产品,即使它内置于诸如glibc之类的东西中。哦,大多数网络应用程序并没有直接从其源代码使用原始套接字;我相信你听说过某个叫做http的东西... - bazza

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接