---
TCP 的构成
---

#### 目录

1. 前言
2. 三次握手
3. 拥塞预防及控制
   * 流量控制
   * 慢启动
   * 拥塞预防
4. 队首阻塞
5. 针对 TCP 的优化建议
   * 服务器配置调优
   * 应用程序行为调优
   * 性能检查清单

#### 前言

因特网有两个核心协议：TCP 和 IP。IP，即因特网协议，负责联网主机之间的路由选择和寻址；TCP，即传输控制协议，负责在不可靠的传输信道之上提供可靠地抽象层。

TCP 负责在不可靠的传输信道之上提供可靠的抽象层，向应用层隐藏了大多数网络通信的复杂细节，比如丢包重发、按序发送、拥塞控制及避免、数据完整，等等。采用 TCP 数据流可以确保发送的所有字节能够完整的被接收到，而且到达客户端的顺序也一样。也就是说，TCP 专门为精确传送做了优化，但并未过多顾及时间，这一点也给优化浏览器 Web 性能带来了挑战。

HTTP 标准并未规定 TCP 就是唯一的传输协议。如果你愿意，还可以通过 UDP（用户数据报协议）或者其他可用协议来发送 HTTP 消息。但在现实当中，由于 TCP 提供了很多有用的功能，几乎所有 HTTP 流量都是通过 TCP 传送的。

####  三次握手

所有 TCP 连接一开始都要经过三次握手。客户端与服务器在交换应用数据之前，必须就起始分组序列号，以及其他一些连接相关的细节达成一致。出于安全考虑，序列号由两端随机生成。

![](https://i.loli.net/2019/06/18/5d087dcb28ff059341.png)

* SYN

  客户端选择一个随机序列号 x，并发送一个 SYN 分组，其中可能还包括其他 TCP 标志和选项。

* SYN ACK

  服务器给 x 加 1，并选择自己的一个随机序列号 y，追加自己的标志和选项，然后返回响应。

* ACK

  客户端给 x 和 y 加 1 并发送握手期间的最后一个 ACK 分组。

三次握手完成后，客户端与服务器之间就可以通信了。客户端可以在发送 ACK 分组之后立即发送数据，而服务器必须等接收到 ACK 分组之后才能发送数据。这个启动通信的过程适用于所有 TCP 连接，因此对所有使用 TCP 的应用具有非常大的性能影响，因为每次传输应用数据之前，都必须经历一次完整的往返。

举个例子，如果客户端在纽约，服务器在伦敦，要通过光纤启动一次新的 TCP 连接，光三次握手至少要花 56 ms；向伦敦发送分组需要 28 ms，响应发回纽约又要 28 ms。在此，连接的带宽对时间没有影响，延迟完全取决于客户端和服务器之间的往返时间，这其中主要是纽约到伦敦之间的传输时间。

三次握手带来的延迟使得每创建一个新的 TCP 连接都要付出很大代价。而这也决定了提高 TCP 应用性能的关键，在于想办法重用连接。

> TCP 快速打开
>
> 遗憾的是，连接不是想重用就可以重用的。事实上，由于非常短的 TCP 连接在互联网上随处可见，握手阶段已经成为影响网络总延迟的一个重要因素。为解决这个问题，人们正在积极寻找各种方案，其中 TFO（TCP Fast Open，TCP 快速打开）就是这样一种机制，致力于减少新建 TCP 连接带来的性能损失。
>
> Linux 3.7 及之后的内核已经在客户端和服务器中支持 TFO，因此成为了客户端和服务器操作系统选型的有力候选方案。即使如此，TFO 并不能解决所有问题。它虽然有助于减少三次握手的往返时间，但却只能在某些情况下有效。比如，随同 SYN 分组一起发送的数据净荷有最大尺寸限制、只能发送某些类型的 HTTP 请求，以及由于依赖加密 cookie，只能应用于重复的连接。

#### 阻塞预防及控制

1984 年初，John Nagle 提到了一个被称为 “拥塞崩溃” 的现象，这个现象会影响节点间带宽容量不对称的任何网络：可能是往返时间超过了所有主机的最大中断间隔，于是相应的主机会在网络中制造越来越多的数据报副本，使得整个网络陷入瘫痪。最终，所有交换节点的缓冲区都将被填满，多出来的分组必须删掉。目前的分组往返时间已经设定为最大值。主机会把每个分组都发送好几次，结果每个分组的某个副本会抵达目标，这就是拥塞崩溃。

为了解决这些问题，TCP 加入了很多机制，以便控制双向发送数据的速度，比如流量控制、阻塞控制和拥塞预防机制。

##### 流量控制

流量控制是一种预防发送端过多向接收端发送数据的机制。否则，接收端可能因为忙碌、负载重或缓冲区既定而无法处理。为实现流量控制，TCP 连接的每一方都要通告自己的接收窗口（rwnd），其中包含能够保存数据的缓冲区空间大小信息。

![](https://i.loli.net/2019/06/18/5d0885677bcb877502.png)

第一次建立连接时，两端都会使用自身系统的默认设置来发送 rwnd。浏览网页通过主要是从服务器向客户端下载数据，因此客户端窗口更可能成为瓶颈。然后，如果是在上传图片或视频，即客户端向服务端传送大量数据时，服务器的接收窗口又可能成为制约因素。

不管怎样，如果其中一端跟不上数据传输，那它可以向发送端通告一个较小的窗口。假如窗口为零，则意味着必须由应用层先清空缓冲区，才能再接收剩余数据，这个过程贯穿于每个 TCP 连接的整个生命周期：每个 ACK 分组都会携带相应的最新的 rwnd 值，以便两端动态调整数据流速，使之适应发送端和接收端的容量及处理能力。

##### 慢启动

尽管 TCP 有了流量控制机制，但网络拥塞崩溃仍然在 1980 年代中后期浮出水面。流量控制确实可以防止发送端向接收端过多发送数据，但却没有机制预防任何一端向潜在网络过多发送数据。换句话说，发送端和接收端在连接建立之初，谁也不知道可用带宽是多少，因此需要一个估算机制，然后还要根据网络中不断变化的条件而动态改变速度。

要说明这种动态适应机制的好处，可以想象你在家观看一个大型的流视频。视频服务器会尽最大努力根据你的下行连接提供最高品质信息。而此时，你家里又有人打开一个新连接下载某个软件的升级包。可供视频流使用的下行带宽一下子少了很多，视频服务器必须调整它的发送速度。否则，如果继续保持同样的速度，那么数据很快就会在某个中间的网关越积越多，最终会导致分组被删除，从而降低网络传输效率。

要理解慢启动，最好看一个例子。同样，假如纽约有一个客户端，尝试从位于伦敦的服务器上取得一个文件。首先，三次握手，而且在此期间双方各自通过 ACK 分组通告自己的接收窗口（rwnd）大小。在发送完最后一次 ACK 分组后，就可以交换应用数据了。

此时，根据交换数据来估算客户端与服务器之间的可用带宽是唯一的方法，而且这也是慢启动算法的设计思路。首先，服务器通过 TCP 连接初始化一个新的拥塞窗口（cwnd）变量，将其值设置为一个系统设定的保守值（在 Linux 中就是 initcwnd）。

拥塞窗口大小（cwnd）：

发送端对从客户端接收确认（ACK）之前可以发送数据量的限制。

发送端不会通过 cwnd 变量，即发送端和接收端不会交换这个值。此时，位于伦敦的服务器只是维护这么一个私有变量。此时又有了一条新规则，即客户端与服务器之间最大可以传输（未经 ACK 确认的）数据量取 rwnd 和 cwnd 变量中的最小值。那服务器和客户端怎么确定拥塞窗口大小的最优值呢？毕竟，网络状况随时都在变化，即使相同的两个网络节点之间也一样。如果能通过算法来确定每个连接的窗口大小，而不用手工调整就最好了。

解决方案就是慢启动，即在分组被确认后增大窗口大小，慢慢的启动！

新 TCP 连接传输的最大数据量取 rwnd 和 cwnd 中的最小值，而服务器实际上可以向客户端发送 4 个 TCP 段，然后就必须停下来等待确认。此后，每收到一个 ACK，慢启动算法就会告诉服务器可以将它的 cwnd 窗口增加 1 个 TCP 段。每次收到 ACK 后，都可以多发送两个新的分组。TCP 连接的这个阶段通常被称为 “指数增长” 阶段，因为客户端和服务器都在向两者之间网络路径的有效带宽迅速靠拢。

![](https://i.loli.net/2019/06/20/5d0af8e0edfb940685.png)

为什么知道有个慢启动对我们构建浏览器应用这么重要呢？因为包括 HTTP 在内的很多应用层协议都运行在 TCP 之上，无论带宽多大，每个 TCP 连接都必须经过慢启动阶段。换句话说，我们不可能一上来就完全利用连接的最大带宽！

慢启动导致客户端与服务器之间经过几百 ms 才能达到接近最大速度的问题，对于大型流式下载服务的影响倒不显著，因为慢启动的时间可以分摊到整个传输周期内消化掉。

可是，对于很多 HTTP 连接，特别是一些短暂、突发的连接而言，常常会出现还没有达到最大窗口请求就被终止的情况。换句话说，很多 Web 应用的性能经常受到服务器与客户端之间往返时间的制约。因为慢启动限制了可用的吞吐量，而这对于小文件传输非常不利。

> 慢启动重启
>
> 除了调节新连接的传输速度，TCP 还实现了 SSR（Slow-Start Restart，慢启动重启）机制。这种机制会在连接空闲一定时间后重置连接的拥塞窗口。道理很简单，在连接空闲的同时，网络状况也可能发生了变化，为了避免拥塞，理应将拥塞窗口重置回 “安全的” 默认值。
>
> 毫无疑问，SSR 对于那些会出现突发空闲的长周期 TCP 连接（比如 HTTP 的 keep-alive 连接）有很大的影响。因此，我们建议在服务器上禁用 SSR。

##### 拥塞预防

认识到 TCP 调节性能主要依赖丢包反馈机制非常重要。换句话说，这不是一个假设命题，而是一个具体何时发生的命题。慢启动以保守的窗口初始化连接，随后的每次往返都会成倍提高传输的数据量，直到超过接收端的流量控制窗口，即系统配置的拥塞阈值窗口，或者有分组丢失为止，此时拥塞预防算法介入。

拥塞预防算法把丢包作为网络拥塞的标志，即路径中某个连接或路由器已经拥堵了，以至于必须采取删包措施。因此，必须调整窗口大小，以避免造成更多的包丢失，从而保证网络畅通。

重置拥塞窗口后，拥塞预防机制按照自己的算法来增大窗口以尽量避免丢包。某个时刻，可能又会有包丢失，于是这个过程再从头开始。如果你看过 TCP 连接的吞吐量跟踪曲线，发现该曲线呈锯齿状，到现在就该明白为什么了。这是拥塞控制和预防算法在调整拥塞窗口，进而消除网络中的丢包问题。

#### 队首阻塞

TCP 在不可靠的信道上实现了可靠的网络传输。基本的分组错误检测与纠正、按序交互、丢包重发，以及保证网络最高效率的流量控制、拥塞控制和预防机制，让 TCP 成为大多数网络应用中最常见的传输协议。

虽然 TCP 很流行，但它并不是唯一的选择，而且在某些情况下也不是最佳选择。特别是按序交互和可靠交互有时候并不必要，反而会导致额外的延迟，对性能造成负面影响。

要理解为什么，可以想一想，每个 TCP 分组都会带着一个唯一的序列号被发出，而所有分组必须按顺序传送到接收端（如下图）。如果中途有一个分组没能到达接收端，那么后续分组必须保存到接收端的 TCP 缓冲区，等待丢失的分组重发并到达接收端。这一切都发生在 TCP 层，应用程序对 TCP 重发和缓冲区中排队的分组一无所知，必须等待分组全部到达才能访问数据。在此之前，应用程序只能在通过套接字读数据时感觉到延迟交互。这种效应称为 TCP 的队首阻塞。

![](https://i.loli.net/2019/06/20/5d0b1e1f372ee20894.png)



队首阻塞造成的延迟可以让我们的应用程序不用关心重排和重组，从而让代码保持简洁。然后，代码简洁也要付出代价，那就是分组到达时间会存在无法预知的延迟变化。这个时间变化通常被称为抖动，也是影响应用程序性能的一个主要因素。

另外，有些应用程序可能并不需要可靠的交付或者不需要按顺序交付。比如，每个分组都是独立的消息，那么按顺序交付就没有任何必要。而且，如果每个消息都会覆盖之前的消息，那么可靠交付同样也没有必要了。可惜的是，TCP 不支持这种情况，所有分组必须按顺序交付。

无需按序交付数据或能够处理分组丢失的应用程序，以及对延迟或抖动要求很高的应用程序，最好选择 UDP 等协议。

>丢包就丢包
>
>事实上，丢包是让 TCP 达到最佳性能的关键。被删除的包恰恰是一种反馈机制，能够让接收端和发送端各自调整速度，以避免网络拥堵，同时保持延迟最短。另外，有些应用程序可以容忍丢失一定数量的包，比如语言和游戏状态通信，就不需要可靠传输或按序交付。

#### 针对 TCP 的优化建议

TCP 是一个自适应的，对所有网络节点一视同仁的、最大限制利用底层网络的协议。因此，优化 TCP 的最佳途径就是调整它感知当前网络状况的方式，根据它之上或之下的抽象层的类型和需求来改变它的行为。

不同应用程序需求间的复杂关系，以及每个 TCP 算法中的大量因素，使得 TCP 调优成为学术和商业研究的一个无底洞。本章只蜻蜓点水般地介绍了影响 TCP 性能的几个典型因素，而没有探讨的选择性应答、延迟应答、快速转发等，随便一个都能让你领略到 TCP 的复杂性，感受到理解、分析和调优之难。

尽管如此，而且每个算法和反馈机制的具体细节可能会继续发展，但核心原理以及它们的影响是不变的：

* TCP 三次握手增加了整整一次往返时间
* TCP 慢启动将被应用到每个新连接
* TCP 流量及拥塞控制会影响所有连接的吞吐量
* TCP 的吞吐量由当前拥塞窗口大小控制

结果，现代高速网络中 TCP 连接的数据传输速度，往往会受到接收端和发送端之间往返时间的限制。另外，尽管带宽不断增长，但延迟依旧受限于光速，而且已经限定在了其最大值的一个很小的常数因子之内。大多数情况下，TCP 的瓶颈都是延迟，而非带宽。

##### 服务器配置调优

一句话，让你的服务器跟上时代是优化发送端和接收端 TCP 栈的首要措施。

有了最新的内核，我们推荐你遵循如下最佳实践来配置自己的服务器。

1. 增大 TCP 的初始拥塞窗口

   加大起始拥塞窗口可以让 TCP 在第一次往返就传输较多数据，而随后的速度提升也会很明显。对于突发性的短暂连接，这也是特别关键的一个优化。

2. 慢启动重启

   在连接空闲时禁用慢启动可以改善瞬时发送数据的长 TCP 连接的性能。

3. 窗口缩放

   启用窗口缩放可以增大最大接收窗口大小，可以让高延迟的连接达到更好吞吐量。

4. TCP 快速打开

   在某些条件下，允许在第一个 SYN 分组中发送应用程序数据。TFO（TCP Fast Open，TCP 快速打开）是一种新的优化选项，需要客户端和服务器共同支持。为此，首先要搞清楚你的应用程序是否可以利用这个特性。

以上几个设置再加上最新的内核，可以确保最佳性能：每个 TCP 连接都会具有较低的延迟和较高的吞吐量。

##### 应用程序行为调优

调优 TCP 性能可以让服务器和客户端之间达到最大吞吐量和最小延迟。而应用程序如何使用新的或已经建立的 TCP 连接同样也有很大的关系。

* 在快也快不过什么也不用发送，能少发就少发
* 我们不能让数据传输的更快，但可以让它们传输的距离更短
* 重用 TCP 连接是提升性能的关键

当然，消除不必要的数据传输本身就是很大的优化。比如，减少下载不必要的资源，或者通过压缩算法把要发送的比特数降到最低。然后，通过在不同的地区部署服务器（比如，使用 CDN），把数据放到接近客户端的地方，可以减少网络往返的延迟，从而显著提升 TCP 性能。最后，尽可能重用已经建立的 TCP 连接，把慢启动和其他拥塞控制机制的影响降到最低。

##### 性能检查清单

优化 TCP 性能的回报是丰厚的，无论什么应用，性能提升可以在服务器的每个连接中体现出来。下面几条请大家务必记在自己的日程表里：

* 把服务器内核升级到最新版本
* 确保 cwnd 大小为 10
* 禁用空闲后的慢启动
* 确保启动窗口缩放
* 减少传输冗余数据
* 压缩要传输的数据
* 把服务器放到离用户近的地方以减少往返时间
* 尽最大可能重用已经建立的 TCP 连接