设计问题-通过使用 Winsock 的 TCP 发送小数据段

当您需要通过 TCP 发送小型数据包时,您的 Winsock 应用程序的设计尤其重要。 不考虑延迟确认、Nagle 算法和 Winsock 缓冲的交互的设计可能会显著影响性能。 本文通过几个案例研究讨论了这些问题。 此外,它还派生了一系列从 Winsock 应用程序有效发送小型数据包的建议。

原始产品版本:  Winsock
原始 KB 数:  214397

背景

当 Microsoft TCP 堆栈收到数据包时,200-ms 延迟计时器将熄灭。 当发送 ACK 时,延迟计时器将被重置,并将在收到下一个数据包时启动另一个200毫秒的延迟。 为了提高 Internet 和 intranet 应用程序的效率,TCP 堆栈使用以下条件来决定何时在收到的数据数据包上发送一条 ACK:

  • 如果在延迟计时器过期之前收到第二个数据包,则发送 ACK。
  • 如果在收到第二个数据包且延迟计时器过期之前,有与 ACK 相同的方向发送的数据,则会将该 ACK 与数据段 piggybacked,并立即发送。
  • 当延迟计时器过期时,将发送 ACK。

为了避免在网络中 congest 较小的数据数据包,TCP 堆栈默认启用 Nagle 算法,这会将一个小数据缓冲区从多个发送调用中进行,并在发送来自远程主机的先前数据包发送的 ACK 时延迟发送该数据缓冲区。 以下是 Nagle 算法的两个例外:

  • 如果堆栈合并了大于最大传输单位(MTU)的数据缓冲区,则会立即发送全尺寸数据包,而无需等待来自远程主机的 ACK。 在以太网网络中,TCP/IP 的 MTU 为1460字节。

  • TCP_NODELAY套接字选项用于禁用 Nagle 算法,以便将小型数据数据包传递到远程主机,而不会发生延迟。

为了在应用程序层优化性能,Winsock 将从应用程序发送调用的数据缓冲区复制到 Winsock 内核缓冲区。 然后,堆栈使用自己的试探法(如 Nagle 算法)来确定何时将数据包实际放在网络上。 您可以使用选项更改分配给套接字的 Winsock 内核缓冲区的数量 SO_SNDBUF (默认情况下为8k)。 如有必要,Winsock 可以缓冲大于 SO_SNDBUF 缓冲区大小。 在大多数情况下,应用程序中的发送完成仅指示将向 Winsock 内核缓冲区中的应用程序发送调用中的数据缓冲区复制到 Winsock 内核缓冲区中,而不表示数据已命中网络媒体。 唯一的例外是当您禁用 Winsock 缓冲时,设置 SO_SNDBUF 为0。

Winsock 使用以下规则来指示向应用程序发送完成(具体取决于调用 send 的方式、完成通知可能是从阻止调用返回的函数、发出事件的信号或调用通知函数等):

  • 如果套接字仍在 SO_SNDBUF 配额内,Winsock 将从应用程序发送中复制数据,并指示向应用程序发送完成。

  • 如果套接字超出 SO_SNDBUF 配额,并且之前只有一个缓冲的发送仍在堆栈内核缓冲区中,Winsock 将从应用程序发送中复制数据,并指示向应用程序发送完成。

  • 如果套接字超出 SO_SNDBUF 配额,并且堆栈内核缓冲区中的以前缓冲的发送超过一个,Winsock 将从应用程序发送中复制数据。 Winsock 并不指示向应用程序发送完成,直到堆栈完成足够的发送以将套接字放在 SO_SNDBUF 配额或仅一个未完成的发送条件中。

案例研究1

Winsock TCP 客户端需要向 Winsock TCP 服务器发送10000条记录以存储在数据库中。 记录大小的长短从20个字节到100字节。 为简化应用程序逻辑,设计如下所示:

  • 客户端只阻止发送。 服务器只会阻止 recv
  • 客户端套接字将设置 SO_SNDBUF 为0,以便在单个数据段中输出每个记录。
  • 服务器 recv 在循环中调用。 在中投递的缓冲区 recv 为200个字节,以便可以在一个调用中接收每个记录 recv

性能

在测试过程中,开发人员会发现客户端每秒只能向服务器发送五条记录。 10000个记录(最大值为 976 kb 的数据(10000 * 100/1024))需要的时间超过半个小时才能发送到服务器。

分析

由于客户端未设置该 TCP_NODELAY 选项,因此 Nagle 算法强制 TCP 堆栈等待 ACK,然后它才能在网络上发送其他数据包。 但是,客户端已通过将选项设置为0来禁用 Winsock 缓冲 SO_SNDBUF 。 因此10000,必须单独发送和 ACK'ed 发送呼叫。 每个 ACK 延迟200毫秒,因为服务器的 TCP 堆栈上发生以下情况:

  • 当服务器收到数据包时,其 200-ms 延迟计时器将熄灭。
  • 服务器不需要发送回任何内容,因此 ACK 不能为 piggybacked。
  • 客户端将不会发送其他数据包,除非已确认以前的数据包。
  • 服务器上的延迟计时器已过期,且确认发送回来。

如何改进

此设计有两个问题。 首先,存在延迟计时器问题。 客户端需要能够在200毫秒内向服务器发送两个数据包。 由于客户端在默认情况下使用 Nagle 算法,因此应仅使用默认 Winsock 缓冲,而不会设置 SO_SNDBUF 为0。 TCP 堆栈合并大于最大传输单位(MTU)的缓冲区后,会立即发送全尺寸数据包,而无需等待来自远程主机的 ACK。

其次,此设计为此类小尺寸的每条记录调用一次发送。 发送这一小部分的大小不会提高效率。 在这种情况下,开发人员可能需要将每条记录填充为100字节,并从一个客户端发送呼叫一次发送80条记录。 若要让服务器知道总共要发送多少条记录,客户端可能希望从包含要关注的记录数的固定大小的标头开始通信。

案例研究2

Winsock TCP 客户端应用程序打开两个与 Winsock TCP 服务器应用程序(提供股票报价服务)的连接。 第一个连接用作将股票代号发送到服务器的命令通道。 第二个连接用作接收股票报价的数据频道。 建立两个连接后,客户端通过命令通道向服务器发送股票符号,并等待股票报价通过数据通道返回。 仅在收到第一个股票报价后,才会将下一个股票代号请求发送到服务器。 客户端和服务器不设置 SO_SNDBUFTCP_NODELAY 选项。

  • 性能

    在测试过程中,开发人员会发现客户端每秒只能获得五个引号。

  • 分析

    此设计仅允许一次未完成的股票报价请求。 第一个股票符号通过命令通道(连接)发送到服务器,并通过数据通道(连接)立即从服务器向客户端发送响应。 然后,客户端会立即发送第二个股票代号请求,并在发送呼叫中的请求缓冲区被复制到 Winsock 内核缓冲区中时立即发送该请求。 但是,客户端 TCP 堆栈无法立即从其内核缓冲区发送请求,因为尚未确认第一次通过命令通道发送。 在服务器命令通道上的 200-ms 延迟计时器过期后,第一个符号请求的 ACK 将返回到客户端。 然后,第二个报价请求将在延迟200毫秒后成功发送到服务器。 第二个股票符号的引号立即返回到数据通道中,因为目前,客户端数据通道中的延迟计时器已过期。 服务器收到上一个报价响应的 ACK。 (请记住,客户端无法将第二个股票报价请求发送到200毫秒,从而给客户端的延迟计时器提供时间过期并向服务器发送 ACK。)因此,客户端将获得第二个报价响应,并且可以发出另一个报价请求,该请求将受到同一周期的制约。

  • 如何改进

    此处不需要这两个连接(通道)设计。 如果只为股票报价请求和响应使用一个连接,则报价请求的 ACK 可以在报价响应中 piggybacked,并立即返回。 若要进一步提高性能,客户端可以将多个股票报价请求复用到服务器的一次发送调用中,并且服务器还可以将多个报价响应多路复用到对客户端的一个 send 调用。 如果由于某种原因,这两个单向通道设计是必需的,则双方都应设置此 TCP_NODELAY 选项,以便可以立即发送小数据包,而无需等待对以前的数据包的 ACK。

建议

虽然这两个案例研究都是虚构的,但它们有助于说明一些最坏的情形。 当您设计涉及大量较大数据段的发送的应用程序时 recvs ,应考虑以下准则:

  • 如果数据段不是时间关键的,则应用程序应将它们合并到一个较大的数据块中,以传递给 send 调用。 由于可能会将发送缓冲区复制到 Winsock 内核缓冲区,因此缓冲区不应太大。 略微小于8K 是有效的。 只要 Winsock 内核获取的块大于 MTU,它将发送多个全大小的数据包,并使用任何留下的内容发送最后一个数据包。 200-ms 延迟计时器不会命中发送端(最后一个数据包除外)。 最后一个数据包(如果是奇数数据包)仍受延迟的确认算法的影响。 如果发送端堆栈获取的另一个块大于 MTU,它仍然可以绕过 Nagle 算法。

  • 如果可能,请避免使用单向数据流的套接字连接。 通过单向套接字的通信更容易受到 Nagle 和延迟的确认算法的影响。 如果通信遵循请求和响应流,则应使用单个套接字执行发送, recvs 以便可以在响应中 PIGGYBACKED ACK。

  • 如果必须立即发送所有小数据段,请 TCP_NODELAY 在发送端上设置选项。

  • 除非您希望在由 Winsock 指示发送完成时,在线路上发送数据包,否则不应将设置 SO_SNDBUF 为零。 实际上,默认的8K 缓冲区已 heuristically 确定为在大多数情况下都能正常运行,除非您已测试新 Winsock 缓冲区设置的性能比默认值更好,否则不应进行更改。 此外,设置 SO_SNDBUF 为零对于执行批量数据传输的应用程序来说是非常有益的。 尽管如此,为了获得最大效率,应将其与双缓冲(在任意给定时间有多个未完成的发送)和重叠的 i/o 结合使用。

  • 如果无需保证数据传递,请使用 UDP。

参考

有关延迟确认和 Nagle 算法的详细信息,请参阅以下内容:

Braden、R. [1989]、RFC 1122、Internet 主机的要求(通信层、Internet 工程任务组)。