TCP套接字发送缓冲区大小的效率

4
使用WinSock或POSIX TCP sockets(使用C/C++,因此不需要额外的Java/Python等封装)时,在用户空间中建立一个更大的缓冲区(例如,最多4KB),然后尽可能少地调用send来发送该缓冲区与直接使用数据位(例如1-1000字节)进行多次小的调用相比,是否存在任何效率上的优缺点?除了对于非阻塞/异步sockets而言单个缓冲区可能更容易管理的事实之外。
我知道使用recv时不推荐使用小buffer,但是我找不到有关发送的任何信息。例如,在常见的平台上,每个send呼叫是否进入内核模式?在正常情况下,一个1字节的发送是否会导致发送一个1字节的数据包?

每个发送调用都是一个内核调用。但是,除非你禁用了数据包合并,否则内核调用不对应于单个数据包。我建议你阅读有关Nagle算法的内容。 - Ben Voigt
嗯,Nagle看起来比解决的问题更多...假设维基百科是正确的,“send(1);send(1000);”仍然会最初发送一个1字节数据包,在请求-响应协议中的 send-send-recv 可能会被卡住直到前一个ack(而在另一端延迟的ack可能需要一些时间)...所以实际上的答案是应该忘记Nagle并发送大缓冲区(以避免内核切换和不足大小的数据包)吗? - Fire Lancer
1
事实上,维基百科的文章强烈建议您自己缓冲写入并禁用 Nagle。减少内核调用的成本是另一个好处。 - Ben Voigt
好的,但我能找到的信息仍然不是很清楚。例如,如果没有 Nagle,我想知道连接的 MSS 值是多少,而有了它,我想要一种似乎不存在的刷新方法(MSDN 似乎暗示 MS 在 WS2.x 中已弃用 TCP_NODELAY,但我找不到更多相关信息)…… - Fire Lancer
2
尽可能缓存数据并一次性发送。几千字节是常见值,不用担心MSS。减少send()的调用次数真正的原因是为了减少进入内核的上下文切换。 - user207421
2个回答

4
正如理查德·史蒂文斯在《TCP图解 卷1》中所解释的那样,TCP将发送缓冲区分成接近最优的片段以适应到达其他TCP对等体的路径上的最大数据包大小。这意味着它永远不会尝试发送将在路由到目标时被ip分段的片段(当一个数据包在某个ip路由器上被分段时,它会发送回一个IP分段 ICMP 数据包,TCP会考虑这一点来减少这个连接的 MSS)。也就是说,在路径上沿用链路级接口的最大数据包大小后,没有必要有大于其最大数据包大小两三倍的缓冲区。因为这样做可以确保TCP不会因为其缓冲区未被数据填满而在收到远程对等方的确认后停止发送。

考虑到正常的接口类型是以太网,并且它的最大数据包大小为1500字节,因此通常TCP不会发送大于此大小的片段。并且它通常具有8Kb每个连接的内部缓冲区,因此没有必要增加内核空间的缓冲区大小(如果这是内核空间中缓冲区存在的唯一原因)。

当然,还有其他因素迫使你使用用户空间的缓冲区(例如,你想在某些地方存储要发送给对等进程的数据,因为内核空间中只有8Kb的数据可缓冲,而你需要更多的空间来执行其他进程)。例如:IRC服务器使用高达100Kb写缓冲区,在对面没有接收/确认该数据时会断开连接。如果你只使用 write(2) 连接,一旦内核缓冲区满了,你就会被等待,这可能不是你想要的。

在用户空间拥有缓冲区的原因是TCP还进行流量控制,所以当它无法发送数据时,必须将其放在某个地方以应对此情况。你需要决定是否需要让你的进程将数据保存到一定限制或者你可以阻塞发送数据,直到接收方再次能够接收数据。内核空间中的缓冲区大小受限且通常无法由用户/开发人员控制。用户空间中的缓冲区大小仅受其允许的资源限制。

不推荐在TCP连接中接收/发送小块数据,因为TCP握手和头部的增加会导致额外的开销。假设一个telnet连接,在每个字符发送时都会添加一个TCP头和另一个IP头(TCP最小20字节,IP最小20字节,以太网帧14字节和以太网CRC 4字节),这会导致要发送一个字符就需要60个字节+。通常每个TCP片段都是单独确认的,因此需要一次完整的往返时间来发送一个片段并获得确认(只为释放缓冲区资源并假定该字符已传输)。

那么,最终的限制是什么?这取决于您的应用程序。如果您可以处理内核可用的资源并且不需要更多的缓冲区,则可以在不具有用户空间缓冲区的情况下通过。如果您需要更多,则需要实现缓冲区,并能够在可用时将您的缓冲数据提供给内核缓冲区。


但是如果没有用户空间缓冲区,在每次写入一个字符时,即使使用了Nagle算法,第一个数据包和任何后续的完整确认都将具有1字节的有效负载?所以这显然不是一件好事,而且对于任何写入到当前MSS的情况都适用,不是吗?因此,当我知道我要发送多达200KB的数据时,我仍然需要确保我始终尝试发送()大于最大数据包大小的数据,以便TCP不会发送初始的下限数据包?而且send-send-recv块似乎是一个需要解决或禁用Nagle的破坏性问题? - Fire Lancer
不完全是这样...你必须考虑慢启动和窗口协商。即使如此,你的TCP可能直到下一个时钟周期才会发送任何数据(取决于实现方式),而在此期间,你可能已经向缓冲区添加了大量数据。即使有/没有使用Nagle算法。 - Luis Colorado
实际上,在正常的实现中,TCP 只会开始发送一个几乎为空的数据段(只有 SYN 标志位,没有任何数据在数据包中,即使你已经有一些要发送的数据)。 - Luis Colorado
如果有人正在考虑分配缓冲区以分块发送数据,那么 Nagle 算法是无意义的。Nagle 的作用是在缓冲区中仍然存在未确认的数据时等待发送数据(通常是到下一个时钟滴答),而不是立即发送数据。时钟滴答每秒发生1000次。 - Luis Colorado

0

是的,在非常普通的情况下,一个字节的send 可以导致只有单个字节负载的TCP数据包被发送。在TCP中,发送合并通常通过使用Nagle算法来完成。通过 Nagle 算法,当已经发送但未被确认的数据存在时,将延迟发送数据。

相反,如果没有未确认的数据,则会立即发送数据。这通常适用于以下情况:

  • 连接刚刚建立
  • 连接已经闲置一段时间
  • 连接仅接收数据,但有一段时间没有发送数据

在这种情况下,您的应用程序执行的第一个send调用将立即导致发送一个数据包,无论大小如何。因此,使用两个或多个小的send开始通信通常是一个不好的想法,因为它会增加开销和延迟。

臭名昭著的“send send recv”模式也可能导致非常大的延迟(例如在Windows上通常为200毫秒)。如果本地TCP堆栈使用Nagle算法(通常会延迟第二个发送),并且远程堆栈使用延迟确认(可以延迟第一个数据包的确认),就会发生这种情况。

由于大多数TCP堆栈实现都同时使用Nagle算法和延迟确认,因此最好避免使用此模式。


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