设计问题 - 使用 Winsock 通过 TCP 发送小型数据段
当需要通过 TCP 发送小型数据包时,Winsock 应用程序的设计尤其重要。 不考虑延迟确认、Nagle 算法和 Winsock 缓冲交互的设计可能会严重影响性能。 本文通过使用几个案例研究来讨论这些问题。 它还派生了一系列建议,用于有效地从 Winsock 应用程序发送小型数据包。
原始产品版本: Winsock
原始 KB 数: 214397
背景
当 Microsoft TCP 堆栈收到数据包时,200 毫秒延迟计时器会关闭。 发送 ACK 时,延迟计时器会重置,并在收到下一个数据包时启动另一个 200 毫秒的延迟。 为了提高 Internet 和 Intranet 应用程序中的效率,TCP 堆栈使用以下条件来决定何时对收到的数据包发送一个 ACK:
- 如果在延迟计时器过期之前收到第二个数据包,则会发送 ACK。
- 如果在接收第二个数据包且延迟计时器过期之前,要以与 ACK 相同的方向发送数据,则 ACK 将随数据段一起备份并立即发送。
- 当延迟计时器过期时,将发送 ACK。
为了避免小型数据包堵塞网络,TCP 堆栈默认启用 Nagle 算法,该算法会合并来自多个发送调用的小型数据缓冲区,并延迟发送该缓冲区,直到从远程主机接收以前发送的数据包的 ACK。 下面是 Nagle 算法的两个例外:
如果堆栈合并了大于最大传输单位 (MTU) 的数据缓冲区,则会立即发送全尺寸数据包,而无需等待来自远程主机的 ACK。 在以太网网络上,用于 TCP/IP 的 MTU 为 1460 字节。
应用
TCP_NODELAY
套接字选项以禁用 Nagle 算法,以便将小数据包毫不延迟地传送到远程主机。
为了优化应用层的性能,Winsock 将数据缓冲区从应用程序发送调用复制到 Winsock 内核缓冲区。 然后,堆栈使用自己的试探 ((如 Nagle 算法) )来确定何时实际将数据包放在网络上。 可以使用 选项更改分配给套接字 SO_SNDBUF
的 Winsock 内核缓冲区量, (默认为 8K) 。 如有必要,Winsock 可以缓冲超过 SO_SNDBUF
缓冲区大小。 在大多数情况下,应用程序中的发送完成仅指示应用程序发送调用中的数据缓冲区已复制到 Winsock 内核缓冲区,并不指示数据已到达网络介质。 唯一的例外是通过将 设置为 SO_SNDBUF
0 来禁用 Winsock 缓冲。
Winsock 使用以下规则指示应用程序的发送完成 (具体取决于发送的调用方式、完成通知可能是从阻止调用返回的函数、发出事件信号或调用通知函数等) :
如果套接字仍在SO_SNDBUF配额内,Winsock 会从应用程序发送复制数据,并指示应用程序的发送完成。
如果套接字超出
SO_SNDBUF
配额,并且堆栈内核缓冲区中只有一个以前缓冲的发送,Winsock 会从应用程序发送复制数据,并指示应用程序的发送完成。如果套接字超出
SO_SNDBUF
配额,并且堆栈内核缓冲区中存在多个以前缓冲的发送,则 Winsock 会从应用程序发送复制数据。 Winsock 不会指示向应用程序发送完成,直到堆栈完成足够的发送以将套接字放回配额或SO_SNDBUF
仅一个未完成的发送条件。
案例研究 1
Winsock TCP 客户端需要将 10000 条记录发送到 Winsock TCP 服务器以存储在数据库中。 记录的大小从 20 字节到 100 字节不等。 为了简化应用程序逻辑,设计如下所示:
- 客户端仅阻止发送。 服务器仅阻止
recv
。 - 客户端套接字将 设置为
SO_SNDBUF
0,以便每个记录在单个数据段中传出。 - 服务器在循环中调用
recv
。 中recv
发布的缓冲区为 200 字节,因此可以在一次调用中接收每条recv
记录。
性能
在测试期间,开发人员发现客户端每秒只能向服务器发送五条记录。 总共 10000 条记录(最多 976 kb 数据 (10000 * 100 /1024) )需要半个多小时才能发送到服务器。
分析
由于客户端未设置 TCP_NODELAY
选项,因此 Nagle 算法会强制 TCP 堆栈等待 ACK,然后才能在网络上发送另一个数据包。 但是,客户端已通过将 选项设置为 SO_SNDBUF
0 来禁用 Winsock 缓冲。 因此,必须分别发送 10000 个发送呼叫和 ACK。 每个 ACK 延迟 200 毫秒,因为服务器的 TCP 堆栈上出现以下情况:
- 当服务器获取数据包时,其 200 毫秒延迟计时器将关闭。
- 服务器不需要发送回任何内容,因此 ACK 无法进行后备。
- 除非确认了上一个数据包,否则客户端不会发送另一个数据包。
- 服务器上的延迟计时器过期,ACK 将发送回。
如何改进
此设计存在两个问题。 首先,存在延迟计时器问题。 客户端需要在 200 毫秒内将两个数据包发送到服务器。 由于客户端默认使用 Nagle 算法,因此它应仅使用默认 Winsock 缓冲,而不设置为 SO_SNDBUF
0。 一旦 TCP 堆栈合并了大于最大传输单元 (MTU) 的缓冲区,将立即发送全尺寸数据包,而无需等待来自远程主机的 ACK。
其次,此设计为每个如此小的记录调用一个发送。 发送这个较小的大小是不有效的。 在这种情况下,开发人员可能希望将每个记录填充到 100 字节,并从一个客户端发送调用中一次发送 80 条记录。 若要让服务器知道总共将发送多少条记录,客户端可能需要开始与包含要跟踪的记录数的固定大小的标头进行通信。
案例研究 2
Winsock TCP 客户端应用程序与提供股票报价服务的 Winsock TCP 服务器应用程序打开两个连接。 第一个连接用作命令通道,用于将股票符号发送到服务器。 第二个连接用作接收股票报价的数据通道。 建立两个连接后,客户端通过命令通道将股票符号发送到服务器,并等待股票报价通过数据通道返回。 仅在收到第一个股票报价后,它才会向服务器发送下一个股票符号请求。 客户端和服务器未设置 SO_SNDBUF
和 TCP_NODELAY
选项。
性能
在测试期间,开发人员发现客户端每秒只能获得五个报价。
分析
此设计一次只允许一个未完成的股票报价请求。 第一个股票符号通过命令通道 (连接) 发送到服务器,响应会立即通过数据通道 (连接) 从服务器发回客户端。 然后,客户端立即发送第二个股票符号请求,当发送调用中的请求缓冲区复制到 Winsock 内核缓冲区时,发送将立即返回。 但是,客户端 TCP 堆栈无法立即从其内核缓冲区发送请求,因为尚未确认通过命令通道发送的第一个请求。 服务器命令通道上的 200 毫秒延迟计时器过期后,第一个符号请求的 ACK 将返回到客户端。 然后,第二个报价请求在延迟 200 毫秒后成功发送到服务器。 第二个股票符号的报价会立即通过数据通道返回,因为此时客户端数据通道上的延迟计时器已过期。 服务器收到上一个引号响应的 ACK。 (请记住,客户端在 200 毫秒内无法发送第二个股票报价请求,从而为客户端上的延迟计时器提供过期时间,并将 ACK 发送到 server。) 因此,客户端将获得第二个报价响应,并可以发出另一个报价请求,该请求受同一周期的约束。
如何改进
此处不需要两个连接 (通道) 设计。 如果只对股票报价请求和响应使用一个连接,则报价请求的 ACK 可以在报价响应中回退并立即返回。 为了进一步提高性能,客户端可以将多个股票报价请求 复用 到一个发送到服务器的发送调用中,服务器也可以将多个报价响应 多路复用 到客户端的一个发送调用中。 如果出于某种原因需要两个单向通道设计,则双方应设置
TCP_NODELAY
选项,以便可以立即发送小数据包,而无需等待前一个数据包的 ACK。
建议
虽然这两个案例研究是捏造的,但它们有助于说明一些最坏的情况。 设计涉及大量小型数据段发送 的应用程序时 recvs
,应考虑以下准则:
如果数据段不是时间关键型数据段,则应用程序应将它们合并到更大的数据块中,以传递给发送调用。 由于发送缓冲区可能会复制到 Winsock 内核缓冲区,因此缓冲区不应太大。 略低于 8K 有效。 只要 Winsock 内核获得大于 MTU 的块,就会发送多个全尺寸数据包和最后一个包含剩余内容的最后一个数据包。 200 毫秒延迟计时器不会命中发送端(最后一个数据包除外)。 如果最后一个数据包恰好是奇数数据包,则仍受延迟确认算法的约束。 如果发送端堆栈获得另一个大于 MTU 的块,它仍然可以绕过 Nagle 算法。
如果可能,请避免使用单向数据流的套接字连接。 通过单向套接字进行的通信更容易受到 Nagle 和延迟确认算法的影响。 如果通信遵循请求和响应流,则应使用单个套接字同时发送 和
recvs
,以便 ACK 可以背靠响应。如果必须立即发送所有小型数据段,请在发送端设置
TCP_NODELAY
选项。除非你希望保证在 Winsock 指示发送完成时在网络上发送数据包,否则不应将 设置为
SO_SNDBUF
零。 事实上,默认的 8K 缓冲区已经启发式确定适用于大多数情况,除非已测试新的 Winsock 缓冲区设置提供的性能优于默认值,否则不应更改它。 此外,将 设置为SO_SNDBUF
零最有利于执行批量数据传输的应用程序。 即便如此,为了获得最大效率,应将其与双重缓冲结合使用, (在任何给定时间) 和重叠 I/O 的多个未完成发送。如果不必保证数据传输,请使用 UDP。
References
有关延迟确认和 Nagle 算法的详细信息,请参阅以下内容:
Braden,R.[1989],RFC 1122,Internet 主机的要求--通信层,Internet 工程任务组。