前面我们所用的 ​​unp/protocol/tools/winclient/echo_cli.cpp​​ 程序的特别之处是它总会发送一个小分组(TCP 段,只有 41 字节)到服务器。这样的小分组在英文中称为 tinygram,在网络状态好的情况下,比如局域网中,通常不会引起什么麻烦。但是在广域网中,这样的小分组会增加网络拥塞的可能。

为了能够减少这样的 tinygram 在网络中的数量,在 TCP 协议栈中,默认使用了 Nagle 算法。

1. Nagle 算法

Nagle 算法要求:

  • 一个 TCP 连接上最多只能有一个未被确认的未完成的小分组,在它到达目的地前,不能发送其它分组。
  • 在上一个小分组未到达目的地前,即还未收到它的 ack 前,TCP 会收集后来的小分组。当上一个小分组的 ack 收到后,TCP 就将收集的小分组合并成一个大分组发送出去。

在广域网中,一般网络延时都比较大,小分组发送出去后,可能要等很久才会收到 ack,因此,在收到 ack 前,发送方可能会累积好多好多未发送的小分组。


19- TCP 协议(Nagle)_unp


图1 Nagle 算法如何处理小分组


在图 1 中,客户端首先发送了一个字符 ​​l​​ 给服务器,在收到服务器的回应前,客户端又发送了三个分组,根据 Nagle 算法规则,在未收到 ack 前,这些小分组都不能发出去。收到 ack 后,tcp 将这三个小分组合并成一个,一次性发出去。

2. 实验 1(开启 Nagle 算法)

默认情况下 Nagle 算法就是开启的。

在图 1 中,假设往返时间是 16ms,要想在这 16ms 里连续发送 4 个字符 ​​l​​​, ​​o​​​, ​​v​​​, ​​e​​ 几乎是不可能的,这意味着我们每秒打字速度要超过 250 个左右,16 ms 里发送至少 2 个字符,打字速度也得要超过每秒 60 个。那么如何在实验中模拟快速发送字符呢?

客户端 echo_client.cpp 提供了一个选项,它能帮我们在键入一个字符时,连续将此字符发送多次。比如键入字符 ​​x​​​,echo_client.cpp 会在极短时间内将 ​​x​​ 发送很多次。

  • 客户端路径:​​unp/protocol/tools/winclient/echo_client.cpp​​,部署在 windows 上。
  • 服务器路径:​​unp/protocol/tools/tcpserver/echo_serv.c​​,部署在 Linux 上。

2.1 实验步骤

  • 在 Linux 上启动服务器 echo_serv
$ ./echo_serv 192.168.80.130 8000
  • 在 Windows 上打开 OmniPeek 抓包
  • Windows 上启动 echo_client.exe
// 注意 echo_client 后面又加了个参数 5,表示将输入的字符连续发送 5 次
echo_client.exe 192.168.80.130 8000 5

接下来,我在 client 中键入了一个字符 ​​x​​​,然后按 ​​q​​ 键结束。

2.2 抓包结果


19- TCP 协议(Nagle)_unp_02


图2 OmniPeek 抓的数据


图 2 中,客户端首先发送了一个字符 ​​x​​​ 过去,接下来等待 ack,在此期间,客户端又请求发送了四个 ​​x​​​,TCP 将这些后来的小分组收集,当收到 ack 后,将这 4 个 ​​x​​ 合并成了一个分组一次发送出去。

3. 实验 2(关闭 Nagle 算法)

这一次我们关闭 Nagle 算法。

和实验 1 不同的地方在于,客户端启动的时候,再加一个参数,NONAGLE.

// 启动客户端

echo_client.exe 192.168.80.130 8000 5

同样的在客户端中输入一个字符 ​​x​​​,此时客户端会帮我们连续发送 5 次 ​​x​​​,然后按 ​​q​​ 退出。抓包结果如下:


19- TCP 协议(Nagle)_nagle_03


图3 没有 Nagle 算法的情况下,客户端发数据的结果


从图 3 中可以看到,红色框框包含的那 5 个数据包,在极短的时间内被发送出去,TCP 在发送第一个包后,并没有等待对方回送的确认。

4. 总结

  • 掌握 Nagle 算法的规则
  • 有 Nagle 算法和无 Nagle 有什么不同