TCP 负责在不可靠的传输信道之上提供可靠的抽象层,向应用层隐藏了大多数网络通信的复杂性能,比如丢包重发、按需发送、拥塞控制及避免、数据完整,等等。采用 TCP 数据流可以确保发送的所有字节能够完整地被接收到,而且客户端的顺序也一样。
但是 TCP 设计并未过多顾及时间,由此给浏览器 Web 性能带来了挑战。
三次握手
所有 TCP 连接一开始都必须经过三次握手。客户端与服务器在交换应用数据之前,必须就起始分组序列号,以及其他一些连接相关的细节达成一致。处于安全考虑,序列号由两端随机生成。
-
SYN
客户端选择一个随机序列号 x,并发送一个 SYN 分组,其中可能还包含 TCP 标志和选项。
-
SYN ACK
服务器给 x 加 1,并选择自己的一个随机序列号 y,追加自己的标志和选项,然后返回响应。
-
ACK
客户端给 x 和 y 加 1,并发送握手期间的最后一个 ACK 分组。
客户端可以在发送 ACK 分组之后立即发送数据,而服务器必须等接收到 ACK 分组之后发送数据。
每个 TCP 连接都要经过三次握手,倘若客户端与服务器距离过长,会造成非常大的性能影响。因而,提升 TCP 性能关键在于想办法重用连接。
为解决这个问题,人们在积极寻找各种方案,其中长链接(Keep-Alive)、负载均衡、TFO(tcp fast open)便是其中的一些解决办法。
长链接
Keep-Alive,HTTP 1.1 之后默认开启,指在一个 TCP 连接中可以持续发送多份数据而不会断开连接。
负载均衡
基本原理:客户端(如:ClientA)与负载均衡设备之间进行三次握手并发送 HTTP 请求。负载均衡设备收到请求后,会检测服务器是否存在空闲的长链接,如果不存在,服务器将建立一个新连接。当 HTTP 请求响应完成后,客户端与负载均衡设备协商关闭连接,而负载均衡则保持与服务器之间的这个连接。当有其他客户端(如:ClientB)需要发送 HTTP 请求时,负载均衡设备会直接向服务器之间保持的这个空闲连接发送 HTTP 请求,避免来由于新建 TCP 连接造成的延时和服务器资源耗费。
TFO(tcp fast open)
尽管开启了长链接,可是依然有35%的请求是重新发起一条连接,而握手会造成一定的延迟,TFO 的目标就是为了去除这个延迟,在三次握手期间也能交换数据。
基本原理:
- 客户端发送 SYN 包,包尾加一个 FOC 请求,只有4个字节。
- 服务端收到 FOC 请求,验证后根据来源 ip 地址生成 Cookie(8个字节),将这个 cookie 加载到 SYN + ACK 包的末尾,发送至客户端。
- 客户端缓存获取到的 Cookie 可以给下一次使用。
- 下一次请求开始,客户端发送 SYN 包,这时候后面带上缓存的 Cookie,然后就开始正式发送数据。
- 服务器验证 Cookie 正确,将数据交给上层应用处理得到相应结果,然后在发送 SYN + ACK,不再等待客户端的 ACK 确认,即开始发送相应数据。
网络拥塞
拥塞:即对供不应求,对资源的需求超过了可用的资源,网络性能下降,整个网络的吞吐量随之负荷的增大而减小,甚至会发生拥塞崩溃的现象。
为了减缓网络拥塞现象,TCP 加入许多机制用来控制双向发送数据的速度。如流量监控、拥塞控制、拥塞预防机制等。
流量控制
流量控制是一种预防发送端过多向接收端发送数据的机制。
滑动窗口是实现流量控制的一种方法,一个简单例子:
设 A 向 B 发送数据。在建立连接时,B 告诉了 A:“我的接收窗口值 rwnd = 400“ (rwnd: receiver window),因此,发送方的发送窗口不能超过接收方给出的接收窗口的数值。TCP 窗口的单位时字节,并不是报文段。设每个报文段的字节长 100,而数据报文段序号的初始值为 1,大写 ACK 表示首部中的确认位 ACK,小写 ack 表示确认字段的值 ack。
从图中可以看出,B 进行了三次流量控制。第一次把窗口减少到 rwnd = 300,第二次又减少到了 rwnd = 100,最后减到 rwnd = 0,即不允许发送数据了。
当 rwnd = 0 时,则意味着必须由应用层先清空缓存区,才能接收剩余数据。这个过程贯穿于每个 TCP 连接的整个生命周期:每个 ACK 分组都会携带相应的最新的 rwnd 值,以便两端动态调整数据流,使之适应发送端和接收段的容量及处理能力。
慢启动
尽管流量监控可以防止发送端向接收端过多发送数据,但是发送端和接收端在连接建立之初,并不知道可用带宽是多少,因此需要一个估算机制,然后还可以根据网络中不断变化的条件而动态改变速度。
拥塞控制:防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致于过载。
慢启动是实现拥塞控制的一种方法,此外还有拥塞预防、快速重发和快速恢复。
慢启动,即是在分组被确定以后,增大窗口大小,慢慢启动。
具体实现如下:
- 发送方通过 TCP 连接初始化并维护一个拥塞窗口变量(cwnd)。并规定发送端与接收端之间最大可以传输的数据量为接收窗口(rwnd)与拥塞窗口(rwnd)的最小值。
cwnd 最初的值只有一个 TCP 段,1999年提升至 4 个 TCP 段,2013年,提升至 10 个 TCP 段。
发送端向接受端发送 TCP 段后,停下来,等待确认。
此后,每收到一个 ACK,慢启动算法都会告诉发送端,cwnd 窗口增加一个 TCP 段,并可以多发送两个新的分组。这个阶段称为指数增长阶段。
由于慢启动的设计,限制了可用的吞吐量。对于大型流式下载服务的影响倒不显著,但是对于小文件的传输却非常不利,常常会出现还没有达到最大窗口请求就被终止的情况。
简单演示三次握手与慢启动对简单 HTTP 传输的影响。
连接参数:
- 往返事件:56ms。
- 客户端到服务端带宽:5 Mbit/s。
- 客户端和服务端接收窗口:65 535字节。
- 初始的拥塞窗口:4 段(4 x 1460 字节 = 5.7 KB)。
- 服务器生成响应的处理时间:40 ms。
- 没有分组丢失,每个分组都要确认,GET 请求只占 1 段。
- 0 ms:客户端发送 SYN 分组开始 TCP 握手。
- 28 ms:服务端响应 SYN - ACK 并指定其 rwnd 大小。
- 56 ms:客户端确认 SYN - ACK,并指定其 rwnd 大小。并立即发送 HTTP GET 请求。
- 84 ms:服务端接收到 HTTP 请求。
- 124 ms:服务器生成 20 KB 的响应,并发送 4 个 TCP 段(假设初始 cwnd 为 4),然后等待 ACK。
- 152 ms:客户端收到 4 个段,并分别发送 ACK 确认。
- 180 ms:服务器针对每个 ACK 递增 cwnd,然后发送 8 个 TCP 段。
- 208 ms:客户端接收到 8 个段,并分别发送 ACK 确认。
- 236 ms:服务器针对每个 ACK 递增 cwnd,然后发送剩余 TCP 段。
- 264 ms:客户端收到剩余的 TCP 段,并发送 ACK 确认。
当再次发送相同请求时:
- 0 ms:客户端发送 HTTP 请求。
- 28 ms:服务器收到 HTTP 请求。
- 68 ms:服务器生成 20 KB 响应,此时 cwnd 已经大于发送文件所需的 15 段,因此可以一次性发送所有数据段。
- 96 ms:客户端收到所有 15 个段,分别发送 ACK 确认。
拥塞预防
慢启用使用 cwnd 作为起始值发送数据量,随后成倍增长。直到超过接收系统配置的拥塞阈值(ssthresh)窗口,或者发生分组丢失现象,此时拥塞预防算法介入。
由于已经发生拥堵,必须采取删包措施。需要重新调整 cwnd 大小,此后拥塞预防按照自己的算法来增大 cwnd 以避免丢包。若再次丢包,则从头开始。