TCP 套接字上的 send() 是不是可以返回 >=0 和 <length?

Posted

技术标签:

【中文标题】TCP 套接字上的 send() 是不是可以返回 >=0 和 <length?【英文标题】:Can send() on a TCP socket return >=0 and <length?TCP 套接字上的 send() 是否可以返回 >=0 和 <length? 【发布时间】:2013-11-10 21:50:20 【问题描述】:

我看到很多关于send() 的问题都在讨论底层协议。我完全清楚,对于 TCP,任何消息都可能在发送时被分解成多个部分,并且不能保证接收者会在一次原子操作中获得消息。在这个问题中,我只讨论send() 系统调用在与本地系统的网络层交互时的行为。

根据 POSIX 标准和我阅读过的send() 文档,要发送的消息的长度由长度参数指定。请注意:send() 发送 一个 消息,长度为 length。进一步:

如果发送套接字上没有可用空间来保存消息 被传输,并且套接字文件描述符没有 O_NONBLOCK 设置,send() 将阻塞直到空间可用。如果空间 无法在发送套接字处保存消息 传输,并且套接字文件描述符确实设置了O_NONBLOCKsend() 会失败。

我认为在这个定义中 send() 没有任何可能返回除 -1 之外的任何值(这意味着内核中没有数据排队等待传输)或 length ,这意味着整个消息在内核中排队等待传输。即,在我看来,send() 必须是原子的相对于在内核中对消息进行本地排队

    如果内核中的套接字队列中有足够的空间容纳整个消息并且没有信号出现(正常情况),则会复制它并返回 length。 如果在send() 期间出现信号,则它必须返回-1。显然,在这种情况下,我们不能将消息的一部分排队,因为我们不知道发送了多少。因此,在这种情况下无法发送任何内容。 如果内核中的套接字队列没有足够的空间容纳整个消息并且套接字正在阻塞,那么根据上面的语句send()必须阻塞直到空间可用。然后消息将被排队,send() 返回 length。 如果内核中的套接字队列中没有足够的空间容纳整个消息并且套接字是非阻塞的,那么send() 必须失败(返回-1)并且errno 将被设置为@987654338 @ 或 EWOULDBLOCK。同样,由于我们返回 -1,很明显在这种情况下,消息的任何部分都不能排队。

我错过了什么吗? send() 是否可以返回 &gt;=0 &amp;&amp; &lt;length 的值?在什么情况下?非 POSIX/UNIX 系统呢? Windows send() 的实现是否符合这个?

【问题讨论】:

似乎有些模棱两可。虽然send() 应该等价于sendto() 如果套接字指的是连接模式套接字,POSIX 也说send() 等价于write() 如果flags 为0。非阻塞或中断阻塞write() 允许返回一个短值。 【参考方案1】:

您的第 2 点过于简单化了。 send 返回大于零但小于长度的值的正常情况(请注意,正如其他人所说,它永远不会返回零,除非长度参数可能为零)是消息足够长导致阻塞,并且在某些内容已经发送后会出现中断信号。在这种情况下,send 不能以EINTR 失败(因为这会阻止应用程序知道它已经成功发送了一些数据)并且它不能重新阻塞(因为信号正在中断,而这一切的重点是摆脱阻塞),因此它必须返回已发送的字节数,该字节数小于请求的总长度。

【讨论】:

我非常感谢 EJP 知识渊博的 cmets 关于实现以及它们的行为方式,但我认为这个答案最直接、最具体地解决了我的问题。【参考方案2】:

    根据 Posix 规范和我 30 年来见过的所有 man 2 send 页面,是的,send() 可以返回任何值 > 0 和 length .请注意,它不能返回零。

    根据几年前关于所有 TCP 实现者所在的 news:comp.protocols.tcp-ip 的讨论,阻塞 send() 在将所有数据传输到套接字发送缓冲区:换句话说,返回值为 -1 或 length. 一致认为所有已知实现都是如此,write(), 也是如此 writev(), sendmsg(), writev(),

【讨论】:

对于#2,信号中断可能会触发短写返回值。 @jxh 很抱歉,但我不相信任何人说他们见过send() 返回零。 30 年来我从未见过它,也从未针对它编写过代码,包括一些非常广泛使用的 Cobol 运行时系统,除了长度参数为零的情况。 @MadScientist: errnonot 设置(设置为EINTR 或其他任何有意义的值),如果在发送某些数据后信号中断传输。 EINTR 错误条件仅适用于信号中断操作在传输任何数据之前。在所有其他情况下,这不是“错误”,而只是“短发送”(比全长短)。 @EJP 你是说Beej 著名的“网络编程指南”中的section on sendall 是错误的,或者至少它们被误导了?他们说有必要确保发送所有数据,并可能重复调用send,直到发送所有数据,这似乎与您在第 2 点中概述的内容相矛盾。或者我错过了什么? @amn 我正在报告我引用的新组中 TCP/IP 实施者的共识。我认为这有一定的分量。注意你的链接坏了:目前here。【参考方案3】:

我知道这个东西在 Linux 上是如何工作的,使用 GNU C 库。在这种情况下,您问题的第 4 点会有所不同。如果您为文件描述符设置标志O_NONBLOCK,并且如果无法在内核中原子地对整个消息进行排队,send() 返回实际发送的字节数(它可以在 1 和长度之间),并且errno 设置为 EWOULDBLOCK

(如果文件描述符在阻塞模式下工作,send() 会阻塞。)

【讨论】:

【参考方案4】:

send() 可以返回 value &gt;= 0 &amp;&amp; &lt; length。如果发送缓冲区的空间小于调用send() 时的消息长度,则可能会发生这种情况。类似地,如果发送方已知的当前接收窗口大小小于消息的长度,则可能只发送部分消息。有趣的是,我在 Linux 上通过 localhost 连接看到了这种情况,当时接收进程从接收缓冲区卸载它正在接收的数据很慢。

我的感觉是,一个人的实际经验会因实施而有所不同。从this Microsoft link 可以看出,可以出现小于长度的非错误返回值。

如果发送长度为零的消息,也可以获得零返回值(同样,至少在某些实现中)。

此答案基于我的经验,并特别借鉴了this SO answer。

编辑:从this answer 及其 cmets 来看,显然只有在中断发生在任何数据发送之前,才会导致 EINTR 失败,这将是获得此类返回值的另一种可能方式。

【讨论】:

糟糕,我的 SO 链接有点晚了,我看到 jxh 在评论中注明。但这也是我找到的最好的链接。 我说的是 TCP 套接字 (AF_INET/SOCK_STREAM):接收方的窗口大小如何影响本地 send()?也许如果您使用的是 AF_UNIX 或其他东西,它会有所不同。如果发送缓冲区没有足够的空间,那么 send() 必须阻塞(用于阻塞套接字)。我同意 EJP 的回答:如果实施者弄错了,我会感到非常惊讶。 接收方的窗口大小是 TCP 标头的一部分,因此该信息会返回给发送方。我假设这与 send() 相关,但是......我实际上并不确切知道该信息是如何使用的。我看到你的问题是如何为非阻塞和阻塞套接字定义行为,但我认为在发送过程中可能会发生一些事情,例如中断。对不起,好问题;也许有人会提出更好的答案。 我了解 TCP 标头,但那是“协议层的底层”。我说的是更远的堆栈,在用户空间和内核之间的发送者系统调用接口。据我所知,接收方的 TCP 接收窗口不会直接影响该接口。【参考方案5】:

在 64 位 Linux 系统上:

sendto(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 4294967296, 0, NULL, 0) = 2147479552

因此,即使尝试发送低 4GB 的数据,Linux 也会退缩并发送不到 2GB。所以,如果你认为你会要求它发送 1TB 并且它会耐心地坐在那里,请继续希望。

类似地,在只有几 KB 空闲的嵌入式系统上,不要认为它会失败或等待某些东西 - 它会尽可能多地发送,并告诉你那是多少,让您可以重试其余部分(或同时做其他事情)。

每个人都同意,在 EINTR 的情况下,可以进行短发送。但是 EINTR 可以随时发生,所以总是有一个短发送。

最后,POSIX 说返回发送的字节数,句号。整个 Unix 和将其形式化的 POSIX 建立在短读/写的概念之上,这允许 POSIX 系统的实现从最小的嵌入式扩展到具有众所周知的“大数据”的超级计算机。因此,无需尝试在字里行间阅读并找到对您手头的特定临时实现的放纵。还有更多的实现,只要您遵循标准,您的应用就可以在其中移植。

【讨论】:

“最后,POSIX 说返回发送的字节数,句号。”从 man 2 写:“根据 POSIX.1,如果计数大于 SSIZE_MAX,则结果是实现定义的;” ...“在 Linux 上,write()(和类似的系统调用)将最多传输 0x7ffff000 (2,147,479,552) 个字节,返回实际传输的字节数。(这在 32 位和 64 位系统上都是如此。) "【参考方案6】:

澄清一点,它说:

将阻塞直到空间可用。

有几种方法可以从该块/睡眠中唤醒:

有足够的空间可用。 一个信号中断了当前的阻塞操作。 为套接字设置了SO_SNDTIMEO,超时到期。 其他,例如套接字在另一个线程中关闭。

所以事情就这样结束了:

    如果内核中的套接字队列中有足够的空间容纳整个消息并且没有信号发生(正常情况),则会复制它并返回长度。 如果在 send() 期间出现信号,则它必须返回 -1。显然,在这种情况下,我们不能将消息的一部分排队,因为我们不知道发送了多少。所以在这种情况下什么都不能发送。 如果内核中的套接字队列中没有足够的空间容纳整个消息并且套接字处于阻塞状态,那么根据上述语句,send() 必须阻塞直到空间可用。 然后消息将被排队并且send()返回长度。 然后send()可以被信号中断,发送超时可以过去,...导致短发送/部分写入.如果没有将任何内容复制到发送缓冲区,则合理的实现将返回 -1 并将 errno 设置为适当的值。 如果内核中的套接字队列中没有足够空间容纳整个消息并且套接字是非阻塞的,则 send() 必须失败(返回 -1)并且 errno 将设置为 EAGAIN 或 EWOULDBLOCK。同样,由于我们返回 -1,很明显在这种情况下,消息的任何部分都不能排队。

【讨论】:

以上是关于TCP 套接字上的 send() 是不是可以返回 >=0 和 <length?的主要内容,如果未能解决你的问题,请参考以下文章

当 TCP 连接中的链接断开时的 send() 函数行为

在一次 send() 调用(tcp 套接字)中保证发送的最小数据大小是多少? [复制]

为啥我不能激发 TCP 将 send() 拆分为多个 recv()

TCP 套接字上的 read() 何时返回

Linux:在 TCP 套接字上发送整个消息或不发送任何消息

使用 gen_tcp:send/2 通过套接字发送消息