这个问题的公认答案解释了:
当您发送数据时,您的 TCP 会对其进行缓冲。所以如果你发送一个字符,它不会立即发送,而是等待看看你是否有更多。
但是如果我没有那么多数据要发送怎么办?尽管缓冲区未满,TCP 如何决定何时发送已缓冲的数据?我确定必须有某种超时,但我找不到关于它的信息。
这个问题的公认答案解释了:
当您发送数据时,您的 TCP 会对其进行缓冲。所以如果你发送一个字符,它不会立即发送,而是等待看看你是否有更多。
但是如果我没有那么多数据要发送怎么办?尽管缓冲区未满,TCP 如何决定何时发送已缓冲的数据?我确定必须有某种超时,但我找不到关于它的信息。
TCP 堆栈实现一般和在某些情况下遵循 RFC 的
存在RFC 896
...(small packet problem)
This classic problem is well-known and was first addressed in the
Tymnet network in the late 1960s. The solution used there was to
impose a limit on the count of datagrams generated per unit time.
This limit was enforced by delaying transmission of small packets
RFC 896 Congestion Control in IP/TCP Internetworks 1/6/84
until a short (200-500ms) time had elapsed, in hope that another
character or two would become available for addition to the same
packet before the timer ran out.
所以我们将数据发送到缓冲区,网络堆栈等待是否还有任何数据,然后发送数据包(从我这边来说非常简短,有很多事件)。
但它是一个 RFC,最好检查 linux 内核中 tcp 堆栈的实现(例如)或其他 tcp 实现。
您已经完美地概述了TCP 中Push标志的需求和用例。
当完整的数据集被发送后,无论接收实体的缓冲区的状态如何,发送方都可以发送一个Push数据包,告诉接收方将缓冲区中的所有内容推送给应用程序。
您可以通过 Telnet 经常看到这一点。每个 telnet 数据包(大部分)只有一个字母。在 ASCII 编码中,这是一个单字节(8 位)。另一端的 TCP 缓冲区肯定没有满,但完整的数据集(信件)已经发送。因此,几乎每个 Telnet 数据包都会打开Push标志,以将数据从接收器的缓冲区推送到应用程序中。
所以最终,TCP 没有决定。发送方通过启用推送标志来决定。
让我扩展一点。
如果您还不熟悉 OSI 模型,我建议您通读这篇文章。至少明白TCP/UDP是L4,并且有L3、L2、L1、L5+(应用)的独立功能。
当应用程序需要通过线路发送某些内容时,它会将其发送到 L4。L4 将其分成小块(基于 MSS 大小)并通过网络发送。为简单起见,我们假设 MSS 为 1000 字节。
为了说明,让我们使用两个例子:
在示例 A 中,发送了单个字母(1 个字节),并且很容易放入一个 TCP 数据包中。在接收端,TCP 保持接收字节并等待下一个字节。当下一个字母被发送时(多 1 个字节),它仍然很容易装入单个 TCP 数据包中,并且也很容易装入(连同第一个字母)在接收方的 TCP 缓冲区中。
但是,用户正在等待这封信出现在他们的应用程序中。TCP 尚未向应用程序“释放”字母,因此应用程序无法向用户显示字母。
从用户的角度来看,它看起来就像滞后/延迟。发送方必须发送足够多的信件来填充接收方的 TCP 缓冲区,然后才能将它们清除并推送到应用程序。
因此,Telnet 会随每个数据包发送 PSH 标志,告诉接收者不要等到缓冲区已满,而是立即将数据包(字母)推送到应用程序。允许应用程序立即向用户显示它们。
但是 TCP 怎么知道发送推送标志呢?继续阅读。
.
在示例 2 中,发送的第一个数据包将包括前 1000 个字节。另一端将接收它,并缓冲它。如果另一端将 1000 字节推送给应用程序,那么应用程序最多只能显示大约四分之一的图像,最坏的情况是应用程序将不知道如何处理四分之一的文件(它甚至可能不知道如何处理)被理解为图像文件,请注意)。
然后发送方将发送下一个数据包,大小为 1000、1000、1000、500。此时,所有 4500 个字节都已发送。但更重要的是,应用程序提供给 TCP 的所有内容都已发送。这会触发 TCP 发送 PSH 标志,以指示“嘿,这是我需要传输的整个数据集,让应用程序随心所欲地处理它”。
.
所以触发 PSH 标志的不是应用程序调用的 API。它只是将应用程序发送的完整数据交给 TCP,当数据集发送完成时,TCP 会自动设置它。
在示例 A 中,每个“数据集”是 8 位(一个字母),并且每 8 位(一个字母)一起发送 PUSH 标志。
在示例 B 中,每个“数据集”都包含 4500 字节图像的一部分。当所有 4500 个字节(整个图像)都已发送时,将发送 PUSH 标志。
如果有些设备不遵循 TCP 规则并尝试缓冲多于或少于应有的缓冲,最终效果将是用户对应用程序的体验会受到影响。想象一个 telnet 会话,您只能看到每 100 个字母后键入的内容。想象一个图像共享应用程序,它持续只显示部分图像或损坏图像文件的警告。
免责声明:上面的链接是我的博客,旨在帮助理解给出的答案。该博客不货币化,不包含广告,并且不需要任何电子邮件订阅。提供它纯粹是为了帮助读者。
TCP 是一种可靠的、有序的字节流的概念,它通过不进行字节的传输,既不有序也不可靠。它既是一个规范,也是一个实际的实现(在不同的系统上可能会有很大的不同!)。如果您愿意,可以使用某种抽象想法的软件模拟。
一般来说,从一个比较幼稚的角度来看,TCP只是简单地抓取你扔给它的任何东西,把它分成段(对于小数据,只有一个段),然后一个接一个地发送出去,等待确认在发送下一个段之前,对于每个段,如果没有确认进来,则在“一段时间”后重新发送一个段。还添加了一些校验和和序列号,等等。
另一端以正确的顺序再次将这些段放在一起,并通过一个模糊的“套接字”(或多或少与文件描述符相同)向应用程序提供“字节”,而没有人真正知道在哪里字节来自,或提供它们所必需的。
现在,如果世界是完美的,如果带宽是无限的,如果路由器没有队列,如果往返时间为零,那么这将是一个完美的工作方法。唉,这不是世界的样子。
出于这个原因,TCP 做一些不同的事情,例如在阻塞之前发送几个段以等待确认(使用各种不同的算法以不同的方式调整窗口以提供最佳可能的吞吐量,而不会在线路上丢弃太多数据包)。
此外,它试图避免发送单字节(或极少字节)的数据报,因为这是一件非常令人讨厌的事情。这是通过所谓的 Nagle 算法完成的,它实际上非常简单。少量数据被缓冲并保留,直到先前发送的数据包被确认(这是 TCP通常会做的,没有延迟的 ACK)。这会自动对微小的发送和批量数据进行速率限制,而无需大量额外的逻辑。除非您明确指定TCP_NODELAY
套接字,否则这就是您通常在每个合理系统上获得的行为。嗯,理论上就是这么简单,实际上,延迟 ACK 的情况要复杂一些,但让我们忽略这一点。
另一方面,TCP 不时地根据未知的、未指定的、模糊的指标,自行决定是否延迟发送数据。数据大小和时间是起作用的参数,你或我,或任何人都不知道。每隔几百毫秒发送一次缓冲区中存在的任何内容是一种常见的方法。虽然这听起来很糟糕,但实际上对于 99.9% 的人来说已经足够了,99.9% 的时间。
请记住,TPC 不是保证实时(或零延迟)的神奇传送系统。它是对可靠的有序字节流的模拟。“可靠”甚至并不意味着您有保证该数据将到达另一端。这是任何协议都无法提供的保证!试想一下,拔掉网线会怎样,怎么保证数据会到?
您只能保证 TCP 会尽最大努力(通过重新发送,如有必要),如果它不起作用,您就会知道。例如,除了原始 IP 数据报或 UDP。在那里你大胆地发送东西,没有人告诉你是否有东西被发送和接收,或者是否丢失,丢失,等等。
有时,TCP 代表自己发送数据并不是您想要的!有时,您希望在一个块中发送几个较小的数据。一个“典型的”服务器回复,以一些头数据开始,后面是实际内容,就是一个例子。果然,你不希望 TCP 单独发送相当无用的标头,然后再发送实际内容,只是因为你需要两三个调用send
,而有人试图在你完成一半时变得更加聪明-大大地。
部分数据报浪费带宽,并且更有可能导致数据包在互联网上的某个地方丢失(线路上的数据包越多意味着丢弃的数据包越多,不可避免)。
这就是诸如TCP_CORK
(或TCP_NOPUSH
在 BSD 中)存在的原因。这基本上与 完全相反TCP_NODELAY
,您告诉网络堆栈您将一个接一个地添加几个数据块,而您的期望是 TCP 同时不会发送一半-MTU 大小或更小的数据报,或其他。
在实际的行为,同样,不同取决于什么操作系统运行上。例如,BSD 将缓冲数据,直到缓冲区中存在最大大小的数据报数据(然后发送 MTU 大小的数据报,并再次缓冲),或者直到您设置TCP_NOPUSH
再次归零,或者直到您关闭套接字(此时它将发送剩余部分)。另一方面,Linux 将严格遵守您的请求 200 毫秒,然后,魔鬼可能会关心,无论如何都会发送部分数据。
作为旁注,TCP 可能会在调用时发送剩余数据这一事实close
是实际检查返回码的一个很好的理由(许多人不这样做,谁在乎close
可能返回什么!)。除非您确实检查,否则无法知道发生了什么,无法确保您的保证与您认为的一样。