第一部分 网络技术概览

第一章 延迟与带宽

1.1 速度是关键

所有网络通信都有决定性影响的两个方面:延迟和带宽

  • 延迟:分组(Packet)从信息源发送到目的地所需的时间
  • 带宽:逻辑或物理通信路径的最大的吞吐量

在osi七层模型中,不同层传输数据包格式:

层次数据包格式
应用层data 报文
表示层data 报文
会话层data 报文
运输层segments tcp-报文段 udp-用户数据报
网络层packets 分组/数据包
数据链路层frames 帧
物理层bits 比特

详情可见OSI Model

1.2 延迟的构成

任何系统都有许多因素可能影响传送消息的时间。

路由器影响延迟的因素:

  • 传播延迟:消息从发送端到接收到需要的时间,是信号传播距离和速度的函数
  • 传输延迟:把消息中的所有比特传输到链路中需要的时间,是消息长度和链路速率的函数
  • 处理延迟:处理分组首部,检查位错误以及确定分组目标所需要的时间
  • 排队延迟:到来的分组排队等待的时间

形象点理解:

  • 传播延迟:车辆在高速公路上行驶的时间
  • 传输延迟:车辆把要发送的货物装满花费的时间
  • 处理延迟:车辆经过高速收费站需要的时间,涉及目的地确认,检查货物是否安全等
  • 排队延迟:高速收费站车辆太多,车辆排队的时间

1.3 光速与传播延迟

爱因斯坦狭义相对论:光速是所有能量、物质和信息运动所能达到的最高速度。这个结论给网络分组的传播速度设定了上限。

光速是光在真空中传播的速度,而网络分组是通过铜线、光纤等介质传播,这会导致传播速度变慢。于是网速远远达不到光速。

两点之间直线最短,但是一般情况下,从发送端到接收端之间是不存在完美的直线传输路径的。而且消息的传输一般都是两个过程,也就是一个往返(RTT)。

CDN(Content Delivery Network,内容分发网络)服务最重要的就是通过把内容部署在全球各地,让用户从最近的服务器加载内容,大幅降低传播分组的时间。

1.4 延迟时间的最后一公里

延迟中相当大的一部分往往花在了最后几公里,而不是在横跨大洋或大陆时产生的,这就是所谓的“最后一公里”问题。

为让你家或你的办公室接入互联网,本地 ISP[Internet Service Provider 互联网服务供应商,比如中国电信(其他不配有姓名)] 需要在附近安装多个路由收集信号,然后再将信号转发到本地的路由节点。连接类型、路由技术和部署方法五花八门,分组传输中的这前几跳(经过一个路由器叫做一跳)往往要花数十毫秒时间才能到达 ISP 的主路由器。

最后一公里的延迟与提供商、部署方法、网络拓扑,甚至一天中的哪个时段都有很大关系。作为最终用户,如果你想提高自己上网的速度,那选择延迟最短的 ISP 是最关键的。

‘traceroute’测量延迟
traceroute 是一个简单的网络诊断工具,可以列出分组经过的路由节点,以及它在 IP 网络中每一跳的延迟。unix平台:使用traceroute,而windows平台使用tracert。

1.5 网络核心的带宽

  • 光纤:光导管,细如发丝,传输的是光信号。光信号是在光纤内经过一次又一次的全折射传输到终点的,折射之后信号会分散或者衰减,一般需要在几十公里后添加中继器做信号增强。
  • 铜线:金属介质,信号损耗多,电磁干扰大,传输的是电信号

波分复用(WDM,Wavelength-Division Multiplexing)):可在一条光纤内同时传输不同信道的光,具有明显带宽优势。

1.6 网络边缘的带宽

1.7 高带宽和低延迟

  • 高带宽:宽带的需求大部分是因为在线流视频,视频流量占据全部互联网流量的绝大多数。升级带宽可以通过

    1. 技术升级,2007 年到 2011 年太平洋海底光缆新增容量一半是因为,wdm波分复用升级。一样的光缆,传输数据却更多
    2. 若技术停滞,可铺设更多光缆
  • 低延迟:与提高带宽相比,降低延迟往往更加困难。牢记:光速是有限制的。再通过以下方面:

    1. 设计和铺设电缆时,尽可能缩短距离,两点之间直线最短(说是圆弧更贴切)
    2. 提升光纤线路质量,降低光纤折射率,选择速度更快的路由器和中继器
    3. 设计和优化协议
    4. 开发应用是通过技术手段降低延迟

第二章 TCP的构成

因特网有两个核心协议:IP 和 TCP。TCP/IP 也常被称为“因特网协议套件”(Internet Protocol Suite)

  • IP:Internet Protocol(因特网协议),负责联网主机之间的路由选择和寻址;
  • TCP:即Transmission Control Protocol(传输控制协议),负责在不可靠的传输信道之上提供可靠的抽象层。

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

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

2.1 三次握手

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

三次握手

图:三次握手
  • SYN:客户端选择一个随机序列号x,并发送一个 SYN 分组,其中可能还包括其他TCP标志和选项
  • SYN ACK:服务器给 x 加1,并选择自己的一个随机序列号 y,追加自己的标志和选项,然后返回响应
  • ACK:客户端给 x 和 y 加1 并发送握手期间的最后一个 ACK 分组

三次握手完成后,客户端和服务器之间可以进行通信。

  • 客户端可在发送 ACK 分组之后立即发送数据
  • 服务器必须等接收 ACK 分组之后才能发送数据

该启动通信过程适用所有TCP连接,因此对所有适用TCP的应用具有非常大的性能影响。三次握手带来的延迟适用每次创建新的TCP连接都要付出很大代价。这也决定提高TCP应用性能的关键,在于想办法重用连接。

TCP 快速打开
TFO(TCP Fast Open,TCP 快速打开),致力于减少新建 TCP 连接带来的性能损失。谷歌研究人员发现 TFO 平均可以降低 HTTP 事务网络延迟15%、整个页面加载时间10% 以上。在某些延迟很长的情况下,降低幅度甚至可达40%。

2.2 拥塞预防和控制

2.2.1 流量控制

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

流量控制

图:流量控制

第一次建立连接,两端都会使用自身系统的默认设置来发送rwnd。若有一方跟不上数据传输,则它可以向发送端(既可以是客户端也可以是服务器)通告一个较小的窗口。若窗口为0,则必须由应用层先清空缓存区,才可以再接收剩余数据。该过程贯穿于每个TCP 连接的整个生命周期:每个ACK 分组都会携带相应的最新rwnd值,以便两端动态调整数据流速,使之适应发送端和接收端的容量及处理能力。

2.2.2 慢启动

发送端和接收端在连接建立之初,谁也不知道可用带宽是多少,因此需要一个估算机制,然后还要根据网络中不断变化的条件而动态改变速度。根据交换数据来估算客户端与服务器之间的可用带宽是唯一的方法,而且这也是慢启动算法的设计思路。首先,服务器通过TCP 连接初始化一个新的拥塞窗口(cwnd)变量,将其值设置为一个系统设定的保守值(在Linux 中就是initcwnd)。最初,cwnd的值只有1 个TCP 段。1999 年4 月,RFC 2581 将其增加到了4 个TCP 段。2013年4 月,RFC 6928 再次将其提高到10 个TCP 段。

  • 拥塞窗口大小(cwnd):发送端对从客户端接收确认(ACK)之前可以发送数据量的限制

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

这段话看了很多遍才理解,重点:

  1. 初始拥塞窗口数:cwnd=initcwnd=4,服务器可以向客户端发4个TCP段(也有可能起始不是4,根据OS决定)
  2. 服务器停止等待接收确认,也就是说需要接收到4个确认ACK:一次TCP段的发送与接收,都需要一次确认ACK
  3. 每一次收到ACK,则根据TCP拥塞控制算法可以将cwnd窗口数+1,如果4个TCP都成功接收到ACK,则新的cwnd=4+4
  4. 若有TCP段丢失(超时或者收到三个冗余ACK),则将当前的cwnd窗口数据设置为 新cwnd=旧cwnd/2
  5. 若未发生TCP段丢失,则cwnd会增长到设定的一个阈值(ssthresh),此时下一次 新的cwnd = 旧cwnd + initcwnd

拥塞控制和拥塞预防

图:拥塞控制和拥塞预防

为减少增长到拥塞窗口的时间

  • 可以减少客户端与服务器之间的往返时间。比如,把服务器部署到地理上靠近客户端的地方
  • 把初始拥塞窗口大小增加到 RFC9828 规定的10 段
2.2.3 拥塞预防

慢启动以保守的窗口初始化连接,随后的每次往返都会成倍提高传输的数据量,直到超过接收端的流量控制窗口,即系统配置的拥塞阈值(ssthresh)窗口,或者有分组丢失为止,此时拥塞预防算法介入(见上小节图)。

2.3 带宽延迟积

发送端和接收端之间在途未确认的最大数据量,取决于拥塞窗口(cwnd)和接收窗口(rwnd)的最小值。接收窗口会随每次 ACK 一起发送,而拥塞窗口则由发送端根据拥塞控制和预防算法动态调整。

无论发送端发送的数据还是接收端接收的数据超过了未确认的最大数据量,都必须停下来等待另一方ACK 确认某些分组才能继续。要等待多长时间呢?取决于往返时间!

  • BDP(Bandwidth-delay product,带宽延迟积):数据链路的容量与其端到端延迟的乘积。任意时刻处于在途未确认状态的最大数据量。

无论发送端或者接收端谁被迫频繁停止等待之前分组的ACK,都会造成数据缺口,从而限制连接的最大吞吐量。

带宽延迟积

图:带宽延迟积

流量控制窗口(rwnd)和拥塞控制窗口(cwnd)的值多大合适呢?假设rwnd和cwnd的最小值为16KB,往返时间为100ms:

$$ \frac {16KB}{100ms} = \frac{16 × 1024 × 8}{0.1} = 1310 720 bit/s = 1.31Mbit/s $$

不管发送接收端的实际带宽是多大,TCP连接的数据传输速率不会超过1.31Mbit/s,要提高吞吐量,要么增大最小窗口值,要么减少往返时间。

类似地,知道往返时间和两端实际带宽,如何计算最优窗口大小?假设往返时间还是100ms,发送端的带宽为10Mbit/s,接收端的带宽为100Mbit/s+,并且两端之间没有网络拥塞,目的是充分利用客户端的10Mbit/s带宽:

$$ 10Mbit/s × 0.1s = 10 × \frac {1 000 000}{8 × 1024} KB/s × 0.1s = 122.1KB $$

2.4 队首阻塞

每个TCP 分组都会带着一个唯一的序列号被发出,而所有分组必须按顺序传送到接收端(图2-8)。如果中途有一个分组没能到达接收端,那么后续分组必须保存在接收端的TCP 缓冲区,等待丢失的分组重发并到达接收端。该效应成为TCP的队首(HOL,Head of Line)阻塞。

队首阻塞

图:队首阻塞

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

  • 按序交付:每个分组都是独立的消息,按序交付没任何必要
  • 可靠交付:每个消息都会覆盖之前的消息,可靠交付没必要

2.5 TCP优化建议

不同应用程序需求间的复杂关系,以及每个 TCP 算法中的大量因素,使得 TCP 调优成为学术和商业研究的一个“无底洞”!!!

每个算法和反馈机制的具体细节可能会继续发展,但核心原理以及它们的影响是不变的:

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

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

2.5.1 服务器配置调优

TCP 的最佳实践以及影响其性能的底层算法一直在与时俱进,而且大多数变化都只在最新内核中才有实现。一句话,让你的服务器跟上时代是优化发送端和接收端TCP 栈的首要措施。

有最新内核,再遵循如下最佳实践来配置服务器:

  • 增大TCP的初始拥塞窗口
    加大起始拥塞窗口可以让TCP 在第一次往返就传输较多数据,而随后的速度提升也会很明显。对于突发性的短暂连接,这也是特别关键的一个优化。
  • 慢启动重启
    在连接空闲时禁用慢启动可以改善瞬时发送数据的长 TCP 连接的性能
  • 窗口缩放(RFC 1323)
    启用窗口缩放可以增大最大接收窗口大小,可以让高延迟的连接达到更好吞吐量
  • TCP快速打开
    在某些条件下,允许在第一个 SYN 分组中发送应用程序数据。TFO(TCP Fast Open,TCP 快速打开)是一种新的优化选项,需要客户端和服务器共同支持。
2.5.2 应用程序行为调优

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

  • 再快也快不过什么也不用发送,能少发就少发
    减少下载不必要的资源,或者通过压缩算法把要发送的比特数降到最低
  • 我们不能让数据传输得更快,但可以让它们传输的距离更短
    在不同的地区部署服务器(比如,使用CDN),把数据放到接近客户端的地方,可以减少网络往返的延迟
  • 重用 TCP 连接是提升性能的关键
    尽可能重用已经建立的TCP 连接,把慢启动和其他拥塞控制机制的影响降到最低
2.5.3 性能检查清单
  • 把服务器内核升级到最新版本(Linux:3.2+);
  • 确保 cwnd 大小为 10;
  • 禁用空闲后的慢启动;
  • 确保启动窗口缩放;
  • 减少传输冗余数据;
  • 压缩要传输的数据;
  • 把服务器放到离用户近的地方以减少往返时间;
  • 尽最大可能重用已经建立的 TCP 连接。

注:本书写的Linux版本是3.2,截止2020/10/26,Linux最新内核版本是:Kernel: 5.9.1(2020年10月17日,​9天前)

第三章 UDP的构成

UDP(User Datagram Protocol,用户数据报协议) 的主要功能和亮点并不在于它引入了什么特性,而在于它忽略的那些特性。UDP 经常被称为无(Null)协议。

数据报:一个完整、独立的数据实体,携带着从源节点到目的地节点的足够信息,对这些节点间之前的数据交换和传输网络没有任何依赖

分组:用来指代任何格式化的数据块,而数据报则通常只用来描述那些通过不可靠的服务传输的分组,既不保证送达,也不发送失败通知

UDP最广为人知的应用便是 DNS: Domain Name System,域名系统。

3.1 无协议服务

IP 层的主要任务就是按照地址从源主机向目标主机发送数据报。

IPV4首部

图:IPV4首部
![UDP首部](https://cdn.jsdelivr.net/gh/amosnothing/cdn/image/reading-notes-for-high-performance-browser-networking/UDP首部.png)
图:UDP首部

UDP 数据报中的源端口和校验和字段都是可选的,UDP 仅仅是在 IP 层之上通过嵌入应用程序的源端口和目标端口,提供了一个“应用程序多路复用”机制。

  • 不保证消息交付:不确认,不重传,无超时。
  • 不保证交付顺序:不设置包序号,不重排,不会发生队首阻塞。
  • 不跟踪连接状态:不必建立连接或重启状态机。
  • 不需要拥塞控制:不内置客户端或网络反馈机制

3.2 UDP与网络地址转换器

IP 网络地址转换器(NAT,Network Address Translator) 规范解决IPv4 地址即将耗尽的一个方案。在网络边缘加入 NAT 设备,每个 NAT 设备负责维护一个表,表中包含本地 IP 和端口到全球唯一(外网)IP 和端口的映射。

IP网络地址转换器

图:IP网络地址转换器
3.2.1 连接状态超时

NAT 转换的问题(至少对于UDP 而言)在于必须维护一份精确的路由表才能保证数据转发。NAT 设备依赖连接状态,而 UDP 没有状态。转换器必须保存每个 UDP 流的状态。

NAT 设备还被赋予了删除转换记录的责任,但由于UDP 没有连接终止确认环节,任何一端随时都可以停止传输数据报,而不必发送通告。为解决这个问题,UDP 路由记录会定时过期。定时多长?没有规定,完全取决于转换器的制造商、型号、版本和配置。因此,对于较长时间的UDP 通信,有一个事实上的最佳做法,即引入一个双向 keep-alive 分组,周期性地重置传输路径上所有 NAT 设备中转换记录的计时器。

TCP 超时和NAT
从技术角度讲,NAT 设备不需要额外的TCP 超时机制。TCP 协议就遵循一个设计严密的握手与终止过程,通过这个过程就可以确定何时需要添加或删除转换记录。

遗憾的是,实际应用中的NAT 设备给TCP 和UDP 会话应用了类似的超时逻辑。这样就导致TCP 连接有时候也需要双向keep-alive 分组。如果你的TCP 连接突然断开,那很有可能就是中间NAT 超时造成的。

3.2.2 NAT穿透

NAT穿透,原文是:NAT traversal。最初看到这个中文名词的时候,挺懵的。理解意思之后,个人更倾向将其翻译为:NAT遍历,NAT穿越/穿过。为何:

  1. NAT遍历:实际上NAT维护一张映射表,只有遍历这张表成功的分组才能通过NAT,否则会被删掉。
  2. NAT穿越/穿过:直白的理解就是分组的数据可以成功穿过NAT设备。

从上小节,我们已经知道:UDP没有连接状态,这也为NET设备带来了严重的问题。然而更严重的是:很多应用程序根本就无法建立UDP连接。

UDP传输必须:1)知道外网IP地址,内网 -> 外网;2)外网分组必须有目标端口,外网 -> 内网。

下图中,因为外网分组在NAT设备中没有映射记录,会直接被NAT给删除。

NAT穿透

图:NAT穿透
3.2.3 STUN、TURN与ICE

为解决UDP 与NAT 的这种不搭配,人们发明了很多穿透技术(TURN、STUN、ICE),用于在UDP 主机之间建立端到端的连接。

  • STUN:公网架设STUN服务器,内网设备请求STUN服务器绑定,STUN返回给内网设备对应的外网IP和端口号。
  • TURN:发送端和接收端将同一台TURN服务器(中继)发送分配请求建立连接,两端都把数据发送到TURN服务器,再由TURN服务器转发,从而实现通信。
  • ICE:能直连就直连,必要时 STUN 协商,再不行使用 TURN。

3.3 针对UDP的优化建议

如果你想在自己的应用程序中使用UDP,务必要认真研究和学习当下的最佳实践和建议。RFC 5405 就是这么一份文档,它对设计单播UDP 应用程序给出了很多设计建议,简述如下:

  • 应用程序必须容忍各种因特网路径条件;
  • 应用程序应该控制传输速度;
  • 应用程序应该对所有流量进行拥塞控制;
  • 应用程序应该使用与 TCP 相近的带宽;
  • 应用程序应该准备基于丢包的重发计数器;
  • 应用程序应该不发送大于路径 MTU 的数据报;
  • 应用程序应该处理数据报丢失、重复和重排;
  • 应用程序应该足够稳定以支持 2 分钟以上的交付延迟;
  • 应用程序应该支持 IPv4 UDP 校验和,必须支持 IPv6 校验和;
  • 应用程序可以在需要时使用 keep-alive(最小间隔 15 秒)。

第四章 传输层安全

SSL 协议在直接位于TCP 上一层的应用层被实现。SSL 不会影响上层协议(如HTTP、电子邮件、即时通讯),但能够保证上层协议的网络通信安全。

传输层安全

图:传输层安全

IETF(Internet Engineering Task Force,互联网工程任务组)后来在标准化 SSL 协议时, 将其改名为Transport Layer Security(TLS,传输层安全)。很多人会混用TLS 和SSL,但严格来讲它们并不相同,因为它们指代的协议版本不同。

4.1 加密,身份验证和完整性

  • 加密:混淆数据的机制
  • 身份验证:验证身份标识有效性的机制
  • 完整性:检测消息是否被篡改或伪造的机制

TLS协议规定一套严密的握手程序用于交换信息,握手机制使用公钥密码系统(非对称密码加密),通信双方无需事先“认识”即可商定共享的安全密钥,而协商过程还是通过非加密通道完成。握手过程中,TLS 协议还允许通信两端互相验明正身。TLS 协议还提供了自己的消息分帧机制,使用MAC(Message Authentication Code,消息认证码)签署每一条消息。只要发送 TLS 记录,就会生成一个MAC 值并附加到该消息中。接收端通过计算和验证这个 MAC 值来判断消息的完整性和可靠性。

公开密钥密码学(英语:Public-key cryptography)也称非对称式密码学(英语:Asymmetric cryptography)是密码学的一种算法,它需要两个密钥,一个是公开密钥,另一个是私有密钥;公钥用作加密,私钥则用作解密。使用公钥把明文加密后所得的密文,只能用相对应的私钥才能解密并得到原本的明文,最初用来加密的公钥不能用作解密。由于加密和解密需要两个不同的密钥,故被称为非对称加密。

4.2 TLS握手

TLS握手协议

TLS握手协议
  • 0 ms:TLS 在可靠的传输层(TCP)之上运行,这意味着首先必须完成 TCP 的“三次握手”,即一次完整的往返。
  • 56 ms:TCP连接建立之后,客户端再以纯文本形式发送:运行的 TLS 协议的版本、它所支持的加密套件列表,以及它支持或希望使用的另外一些TLS 选项
  • 84 ms:服务器端从客户端提供的加密套件列表中选择一个,再附上自己的证书,将响应发送回客户端。作为可选项,服务器也可以发送一个请求,要求客户端提供证书以及其他TLS 扩展参数。
  • 112 ms:客户端把自己的证书提供给了服务器,然后客户端会生成一个新的对称密钥,用服务器的公钥来加密,加密后发送给服务器,告诉服务器可以开始加密通信了。到目前为止,除了用服务器公钥加密的新对称密钥之外,所有数据都以明文形式发送。
  • 140 ms:最后,服务器解密出客户端发来的对称密钥,通过验证消息的 MAC 检测消息完整性,再返回给客户端一个加密的“Finished”消息。
  • 168 ms:客户端用它之前生成的对称密钥解密这条消息,验证 MAC,如果一切顺利,则建立信道并开始发送应用数据。
4.2.1 应用层协议协商 ALPN

应用层协议协商(ALPN,Application Layer Protocol Negotiation)作为TLS 扩展,让我们能在TLS 握手的同时协商应用协议:

  1. 客户端在 ClientHello 消息中追加一个新的 ProtocolNameList 字段,包含自己支持的应用协议;
  2. 服务器检查 ProtocolNameList 字段,并在 ServerHello 消息中以 ProtocolName 字段返回选中的协议
4.2.2 服务器名称指示 SNI

服务器想在一个 IP 地址为多个站点提供服务,而每个站点都拥有自己的TLS 证书,SNI(Server Name Indication,服务器名称指示)扩展被引入TLS 协议,允许客户端在握手之初就指明要连接的主机。

TLS、HTTP 及专用IP
TLS+SNI 机制与HTTP 中发送 Host 首部是相同的,只不过后者是客户端要在请求中包含站点的主机名。总之,都是相同的IP 地址服务于不同的域名,而区分不同域名的手段就是SNI 或Host。

4.3 TLS会话恢复

完整TLS 握手会带来额外的延迟和计算量,从而给所有依赖安全通信的应用造成严重的性能损失。于是有了会话恢复功能,在多个连接间共享协商后的安全密钥。

4.3.1 会话标识符

服务器保存每个客户端的会话ID和协商后的会话参数,客户端也可保存会话ID信息,于是在三次握手完毕之后(实际上是第三次握手的时候发的,因为第三次握手是可以带消息的),可以将保存的会话ID发给服务器,这样便可以节省一次消息往返。

简短TLS握手协议

图:简短TLS握手协议
4.3.2 会话记录单

“会话记录单”(Session Ticket,RFC 5077)机制,解决了会话标识符需要创建并维护大量TLS连接(占用内存)的问题。

会话记录表不需要服务器记录每个客户端的会话信息,只需要客户端支持会话记录单。在进行四次挥手的最后一次信息交换时,服务器生成新会话记录表记录,而客户端收到后将其保存起来。重新建立连接时,客户端可以直接使用该记录来创建会话。

4.4 信任链接与证书颁发机构

信任链:张三信任李四,李四信任王五, 通过信任的传递,张三信任王五。过程是怎样的呢?

  • 张三和李四分别生成自己的公钥和私钥;
  • 张三和李四分别隐藏自己的私钥;
  • 张三向李四公开自己的公钥,李四也向张三公开自己的公钥;
  • 张三向李四发送一条新消息,并用自己的私钥签名;
  • 李四使用张三的公钥验证收到的消息签名。

目前张三跟李四是相互认识,相互信任的关系,他们是通过安全(物理)的握手确认对方。若现在王五给张三发了消息,并自称是李四的朋友,于是张三需要验证王五的身份。

王五请李四用李四的私钥签署自己的公钥,并在消息中附上签名。张三首先检查王五公钥中李四的签名,验证完后便可以信任王五的消息。

张三李四王五

图:张三李四王五的信任链

WEB以及浏览器中身份验证与上述过程相同,此时此刻你应该问自己:我的浏览器信任谁?我在使用浏览器的时候信任谁?

  • 手工指定证书
    所有浏览器和操作系统都提供了一种手工导入信任证书的机制。至于如何获得证书和验证完整性则完全由你自己来定。
  • 证书颁发机构
    CA(Certificate Authority,证书颁发机构)是被证书接受者(拥有者)和依赖证书的一方共同信任的第三方。
  • 浏览器和操作系统
    每个操作系统和大多数浏览器都会内置一个知名证书颁发机构的名单。因此,你也会信任操作系统及浏览器提供商提供和维护的可信任机构。

现实中最常见的方案就是让证书颁发机构替我们做密钥验证:浏览器指定可信任的证书颁发机构(根CA),他们会审计和验证这些站点的证书没有被滥用或冒充。

证书颁发机构签署数字证书

图:证书颁发机构签署数字证书

4.5 证书撤销

证书撤销情况:证书的私钥不再安全、证书颁发机构本身被冒名顶替,或者其他正常的原因,像以旧换新或所属关系更替等

4.5.1 证书撤销名单 CRL

CRL(Certificate Revocation List,证书撤销名单):每个证书颁发机构维护并定期发布已撤销证书的序列号名单。

效果是好,但是存在问题:1)CRL名单会越来越多,2)刚才撤销的名单,不会立刻被更新

4.5.2 在线证书状态协议 OCSP

为解决CRL的问题,OCSP(Online Certificate Status Protocol,在线证书状态协议),提供了一种实时检查证书状态的机制。

OCSP 查询也带了一些问题:

  • 证书颁发机构必须处理实时查询;
  • 证书颁发机构必须确保随时随地可以访问;
  • 客户端在进一步协商之前阻塞 OCSP 请求;
  • 由于证书颁发机构知道客户端要访问哪个站点,因此实时 OCSP 请求可能会泄露客户端的隐私。

实践中,CRL 和OCSP 机制是互补存在的,大多数证书既提供指令也支持查询

4.6 TLS记录协议

TLS 记录协议负责识别不同的消息类型(握手、警告或数据,通过“内容类型”字段),以及每条消息的安全和完整性验证。

TLS记录结构

图:TLS记录结构

交付应用数据的典型流程如下:

  • 记录协议接收应用数据。
  • 接收到的数据被切分为块:最大为每条记录 214 字节,即 16 KB。
  • 压缩应用数据(可选)。
  • 添加 MAC(Message Authentication Code)或 HMAC。
  • 使用商定的加密套件加密数据。

完成之后,加密数据就交给TCP层传输。

4.7 针对TLS的优化建议

4.7.1 计算成本

建立和维护加密信道给两端带来了额外的计算复杂度

4.7.2 尽早完成握手
  1. TLS会话建立在TCP上,所以2.5节“针对TCP的优化建议”同样适用
  2. 把服务器放到离用户更近的地方:如CDN
  3. 减少握手一次会话:会话缓存
4.7.3 会话缓存与无状态恢复

会话标识符会话记录单

4.7.4 TLS记录大小

小记录会造成浪费,大记录会导致延迟,推荐的做法:每个TCP 分组恰好封装一个TLS 记录,而TLS 记录大小恰好占满TCP 分配的MSS(Maximum Segment Size,最大段大小)。
以下数据可作为确定最优 TLS 记录大小的参考:

  • IPv4 帧需要 20 字节,IPv6 需要 40 字节;
  • TCP 帧需要 20 字节;
  • TCP 选项需要 40 字节(时间戳、SACK 等)。

假设常见的MTU 为1500 字节,则 TLS 记录大小在 IPv4 下是1420 字节,在 IPv6 下是1400 字节。为确保向前兼容,建议使用 IPv6 下的大小:1400 字节。当然,如果MTU 更小,这个值也要相应调小。

4.7.5 TLS压缩

我们知道有TLS压缩这个东西即可,实际上TLS有安全漏洞,并不实用,一般都是禁用的。

4.7.6 证书链的长度
  • 尽量减少中间证书颁发机构的数量
  • 很多站点会在证书链中包含根证书颁发机构的证书,这是完全没有必要的
  • 理想的证书链应该在2 KB 或3 KB 左右,同时还能给浏览器提供所有必要的信息,避免不必要的往返或者对证书本身额外的请
4.7.7 OCSP封套

服务器可以在证书链中包含(封套)证书颁发机构的OCSP 响应,让浏览器跳过在线查询。把查询OCSP 操作转移到服务器可以让服务器缓存签名的OCSP 响应,从而节省很多客户端的请求。

4.7.8 HTTP严格传输安全 HSTS

HSTS 通过把责任转移到客户端,让客户端自动把所有链接重写为HTTPS,消除了从HTTP 到HTTPS的重定向损失。

4.8 性能检查清单

检查清单:

  • 要最大限制提升 TCP 性能,请参考 2.5 节“针对 TCP 的优化建议”;
  • 把 TLS 库升级到最新版本,在此基础上构建(或重新构建)服务器;
  • 启用并配置会话缓存和无状态恢复;
  • 监控会话缓存的使用情况并作出相应调整;
  • 在接近用户的地方完成 TLS 会话,尽量减少往返延迟;
  • 配置 TLS 记录大小,使其恰好能封装在一个 TCP 段内;
  • 确保证书链不会超过拥塞窗口的大小;
  • 从信任链中去掉不必要的证书,减少链条层次;
  • 禁用服务器的 TLS 压缩功能;
  • 启用服务器对 SNI 的支持;
  • 启用服务器的 OCSP 封套功能;
  • 追加 HTTP 严格传输安全首部。

4.9 测试与验证

要验证和测试你的配置, 可以使用Qualys SSL Server Test(https://www.ssllabs.com/ssltest/)等在线服务来扫描你的服务器,以发现常见的配置和安全漏洞。

第二部分 无线网络性能

第五章 无线网络概览

5.1 无所不在的连接

过去十年来,最具颠覆性的技术趋势非随时随地上网莫属,人们对随时随地上网的需求也与日俱增。

5.2 无线网络的类型

从地理范围角度对无线网络技术加以分类:

类型范围应用标准
个人局域网(PAN)个人活动范围内替代周边设备的线缆蓝牙、ZigBee、NFC
局域网(LAN)一栋楼或校园内有线网络的无线扩展IEEE 802.11(Wi-Fi)
城域网(MAN)一座城市内无线内联网IEEE 802.15(WiMax)
广域网(WAN)世界范围内无线网络蜂窝(UMTS、LTE 等)

5.3 无线网络的性能基础

无论使用哪种无线技术,所有通信方法都有一个最大的信道容量,这个容量是由相同的底层原理决定的。信息论之父克劳德·香农给出了一个确切的数学模型:

信道容量即最大信息速率
$$ C = BW × log_2(1 + \frac SN) $$

  • C:信道容量,单位是bit/s
  • BW是可用带宽,单位是Hz
  • S是信号,N是噪声,单位是W
5.3.1 带宽

有线网络通过线缆将网络各个节点连接起来,而无线网络本质则是共享网络,靠的是无线电波,专业点叫:电磁辐射。为实现通信,双方事先就通信频率范围达成共识。

频率范围由谁分配呢?查了一下资料,应该是:负责履行国际电信联盟组织法、国际电信联盟公约和行政规则中所规定的义务的任何政府部门或政府的业务机构。

无线网络的性能,根据香农公式,信道的总体比特率与分配的带宽,也就是信号频率成正比。低频信号传输距离远、覆盖范围广(大蜂窝),但要求天线更大,而且竞争激烈。另一方面,高频信号能够传输更多数据,但传输距离不远,因此覆盖范围小(小蜂窝),需要较多的基础设施投入。

5.3.2 信号强度

影响无线通信性能的另外一个因素就是收发两端的信号强度,也叫信噪比(SNR,Signal Noise Ratio)。

在存在干扰的情况,当然这更接近现实,想要达到预期的数据传输速率,要么增大发射功率,也就是提高信号强度,要么缩短收发两端的距离。举个形象的例子,同样的小房间里面有2个人,轻声说话可以清晰听到,若是房间里面有20个人说话,之前的这两个人想听清对方,要么提高音量,要么两者缩短距离。

5.3.3 调制

用于编码信号的算法对无线性能同样有显著影响。调制:数字信号(1和0)转换为模拟信号(无线电波)。

5.4 测量现实中的无线性能

万变不离其宗,任何无线网络,都会受制于分配给它的带宽大小和收发两端信噪比。另外,所有利用无线电的通信都:

  • 通过共享的通信媒体(无线电波)实现;
  • 在管制下使用特定频率范围;
  • 在管制下使用特定的发射功率;
  • 受限于不断变化的背景噪声和干扰;
  • 受限于所选无线技术的技术约束;
  • 受限于设备本身的限制,比如形状、电源,等等。

列举几个影响无线网络性能的因素:

  • 收发端的距离;
  • 当前位置的背景噪声大小;
  • 来自同一网络(小区)其他用户的干扰大小;
  • 来自相邻网络(小区)其他用户的干扰大小;
  • 两端发射功率大小;
  • 处理能力及调制算法。

想要得到最大的吞吐量,尽力减少可控的噪声和干扰,尽量缩短两端距离,给它们必要的功率,确保两端选择最有调制算法。

度量无线网络的性能不易,接收端与发送端距离移动十几厘米,传输速率翻倍,由或者其他设备被唤醒并竞争同一信道,则速率可能减半。无线网络本质上极度不稳定。

第六章 Wi-Fi

6.1 从以太网到无线局域网

802.11 无线标准主要是作为既有以太网标准(802.3)的扩展来设计的。事实上,以太网通常被称作局域网(LAN)标准,而802.11 标准族则相应地被称作无线局域网(WLAN,Wireless LAN )标准。

以太网和WIFI

图:以太网和WIFI

以太网标准过去依赖概率访问的CSMA(Carrier Sense Multiple Access,载波监听多路访问),实际是先听后说的一种算法:

  • 检查是否有人正在发送;
  • 如果信道忙,监听并等待信道空闲;
  • 信道空闲后,立即发送数据。

为解决信号传播冲突问题,增加了冲突检测机制(CSMA/CD,Collision Detection):如果检测到冲突,则双方都立即停止发送数据并小睡一段随机的时间(后续时间以指数级增长),保证发生冲突的发送端不会同步,且不会同时重新开始发送数据。

而Wi-Fi处理冲突稍有不同:冲突避免(CSMA/CA,Collision Avoidance)机制,即每个发送方都会在自己认为信道空闲时发送数据,以避免冲突。

6.2 Wi-Fi标准及功能

表:Wi-Fi发布历史及路线图

802.11协议发布时间频率(GHz)带宽(MHz)流速率(Mbit/s)
b1999-092.4201、2、5.5、11
g2003-062.4206、9、12、18、24、36、48、54
n2009-102.4207.2、14.4、21.7、28.9、43.3、57.8、65、72.2
n2009-1054015、30、45、60、90、120、135、150
ac2014年前后520、40、80、160最高866.7

现在大部分路由器都是支持两个频段:2.4G和5G,但是两者各有优缺点。

频段2.4G5G
优点信号频率低,在空气或障碍物中传播时衰减较小,传播距离更远信号频宽较宽,无线环境比较干净,干扰少,网速稳定,且5G可以支持更高的无线速率。
缺点信号频宽较窄,家电、无线设备大多使用2.4G频段,无线环境更加拥挤,干扰较大。信号频率较高,在空气或障碍物中传播时衰减较大,覆盖距离一般比2.4G信号小。

有几种比较好的实用方案供选择(一个路由器):

  1. 设置SSID(wifi名)与频段对应,比如:wifi-2.4g 和 wifi-5g,然后根据具体的场景选择不同的wifi。
  2. 设置两个频段为同名同密码,实际上这是无线桥接技术,它会优先选择信号更好的那一个wifi。因5g近距离传输速率更好,而2.4g更远,所以近距离情况下会自动连接5g,而远距离由于5g信号变弱,则又会自动连接到2.4g。

多个路由器的情况:

  • 路由器A与路由器B放置到常用的区域,比如A放置在卧室,B放置在客厅,两者设置相同SSID,相同密码,则(理想情况下)设备在卧室会自动连接到路由器A,而在客厅时设备又会切换到路由器B。

6.3 测量和优化Wi-Fi性能

关于Wi-Fi 性能的哪些重要因素呢?

  • Wi-Fi 不保证用户的带宽和延迟时间。
  • Wi-Fi 的信噪比不同,带宽也随之不同。
  • Wi-Fi 的发射功率被限制在 200 mW 以内。
  • Wi-Fi 在 2.4 GHz 和较新的 5 GHz 频段中的频谱有限。
  • Wi-Fi 信道分配决定了接入点信号会重叠。
  • Wi-Fi 接入点与客户端争用同一个无线信道。
6.3.1 Wi-Fi中的丢包

除了直观的丢包问题,Wi-Fi 网络更突出的问题则是分组到达时间差异极大,这一切都要归咎于数据链路层和物理层的冲突及重发。

6.4 针对Wi-Fi的优化建议

  • 利用不计流量的带宽:比如有些宽带是流量计费的,又或者通过手机共享出去的热点(也是通过Wi-Fi连接),这些使用场景都会有流量的顾虑
  • 适应可变带宽:好比观看视频网站的视频时,根据网络情况智能切换视频分辨率
  • 适应可变的延迟时间:使用提供不可靠 UDP 传输的WebRTC,有助于降低协议和应用导致的延迟时间

第七章 移动网络

7.1 G字号移动网络简介

表:四代移动网络

峰值数据速率说明
1G无数据模拟系统
2GKbit/s第一代数字系统,作为对模拟系统的替代或与之并存
3GMbit/s专用数字网络,与模拟系统并行部署
4GGbit/s数字及分组网络
7.1.1 最早提供数据服务的2G
年份地点速率标准
1979日本1G-
1991芬兰2G9.6Kbit/sGSM
1990年代中期-2.5G172Kbit/sGPRS
2003美国2.75G384Kbit/sEDGE
7.1.2 3GPP与3GPP2
  • 3GPP(3rd Generation Partnership Project,第三代合作伙伴项目)负责制定UMTS(Universal Mobile Telecommunication System,通用移动通信系统)
  • 3GPP2(3rd Generation Partnership Project 2)负责基于CDMA2000,也就是高通制定的IS-95 标准的后续技术制定3G 规范
7.1.3 3G技术的演进

3G 网络存在两个主导且互不兼容的标准:UMTS 和CDMA, 分别由 3GPP 和 3GPP2 制定。然而,就像早期的蜂窝标准一样,这两种标准各自都有自己的过渡性版本,通常被称为3.5G、3.75G 和3.9G。

为什么不直接跳到4G 呢?因为制定一个新标准需要很长时间,而且更重要的是,建设新网络需要巨额投资。

7.1.4 IMT-Advanced的4G要求

达到这些要求的技术,都可以看作是4G 技术,IMT-Advanced 的部分要求举例如下(实际的要求比这些多得多):

  • 以 IP 分组交换网络为基础;
  • 与之前的无线标准(3G 和 2G)兼容;
  • 移动客户端的速率达到 100 Mbit/s,静止时的速率达到 Gbit/s 以上;
  • 100 ms 控制面延迟,10 ms 用户面延迟;
  • 资源在用户间动态分配和共享;
  • 可变带宽分配,5~20 Mhz。
7.1.5 长期演进 LTE

LTE 网络可不是对已有3G 基础设备的简单升级。相反,LTE 网络必须与现有3G 基础设备并行部署,并采用不同的频率范围。LTE 用户可以无缝切换到3G 网络。

7.1.6 HSPA+推进世界范围内的4G普及

大家都认同 LTE 是未来移动网络的标准,那为什么还要继续改进和在 HSPA+ 上投入呢?答案很简单:成本。世界各地的运营商在这方面已经投入了巨资在3GPP和3GPP2上,迁移到 LTE 必须建设全新的无线网络,而相对而言,HSPA+ 的投入产出比更可取。

7.1.7 为多代并存的未来规划

无线标准发展很快,但这些网络的物理设施建设则既要花钱又得花时间。将来,如果这些网络部署完成,那势必还要投入很多时间维护以收回成本,保证服务品质。

在面向移动网络构建应用时,不能只考虑一种网络,更不能寄希望于特定的吞吐量或延迟时间。

7.2 设备特性及能力

不同厂商设备,上市时间,各有各的特点,比如CPU速度,核心数量,内存大小,存储能力,有无GPU等,这些因素都会影响设备以及运行于其上的应用的整体性能。

运营商部署新的网络,而旧设备就根本不支持。反之:新上市的设备可能不支持旧的网络。

7.3 无线电资源控制器(RRC)

无线电资源控制器(RRC,Radio Resource Controller )。RRC 负责调度协调移动设备与无线电基站之间所有的通信连接。RRC 直接影响延迟、吞吐量和设备电池的使用时间。

7.4 端到端的运营商架构

7.5 移动网络中的分组流

7.5.1 初始化请求

LTE请求流的延迟时间

图:LTE请求流的延迟时间

手机打开浏览器,输入URL,点击前往,整个过程是怎样的呢?

  1. 手机处于空闲RRC 状态,因此无线电模块必须与附近的信号塔同步,然后发送一个请求,以便建立无线通信环境
  2. 建立了无线通信环境后,设备就会从信号塔得到相应资源
  3. 分组通过核心网络从SGW 传输到PGW
  4. 再向外传到公共互联网

用户发出的一次新请求总会导致一些不同的延迟

  • 控制面延迟: 由RRC 协商和状态切换导致的固定的、一次性的延迟时间,从空闲到活动少于 100 ms,从休眠到活动少于50 ms。
  • 用户面延迟:应用的每个数据分组从设备到无线电信号塔之间都要花的固定的时间,少于5 ms。
  • 核心网络延迟:分组从无线电信号塔传输到分组网关的时间,因运营商而不同,一般为30~100 ms。
  • 互联网路由延迟:从运营商分组网关到公共互联网上的目标地址所花的时间,可变。
入站数据流

LTE入站数据流延迟

图:LTE入站数据流延迟

下面再看一看相反的过程:

  1. PGW 会把入站分组路由到SGW,SGW 进一步查询MME
  2. MME 会向当前跟踪区内的所有信号塔发送一条寻呼消息
  3. 收到消息的信号塔接着通过共享的无线信道广播一条通知
  4. 设备周期性地唤醒以监听寻呼消息,如果在寻呼列表中发现了自己,它就会向无线电信号塔发送一条协商请求,请求重建无线通信环境
  5. 无线通信环境重建之后,负责协商的信号塔向MME 回发一条消息
  6. MME 向服务网关返回一个应答,服务网关于是就会把数据路由到该信号塔
  7. 该信号塔再把数据转发给目标设备

7.6 异质网络 HetNet

通过广泛部署多层异质网络(heterogeneous networks,HetNets),可以促进小区内协调、转移和干扰管理等多方面的改进。HetNet 背后的核心思想非常简单:覆盖较大地理区域的无线网络容易导致用户竞争,因此不如用更小的小区来覆盖这些区域.

异质网络信息图(爱立信)

图:异质网络信息图(爱立信)

7.7 真实的3G、4G和Wi-Fi性能

第八章 移动网络的优化建议

文章中将优化细节分章节列出,我在此简单列出

  • 节约用电
  • 消除周期性及无效的数据传输
  • 预测网络延迟上限
    • 考虑RRC状态切换:
    • 解耦用户交互与网络通信:设计得好的应用,即便底层连接慢或者请求时间长,通过在UI 中提供即时反馈也能让人觉得速度快
  • 面对多网络接口并存的现实
  • 爆发传输数据并转为空闲
  • 把负载转移到Wi-Fi网络
  • 遵从协议和应用最佳实践

第三部分:HTTP

第九章 HTTP简史

HTTP(HyperText Transfer Protocol,超文本传输协议)是互联网上最普遍采用的一种应用协议,也是客户端与服务器之间的共用语言,是现代Web 的基础。

  • HTTP 0.9 只有一行的协议
  • HTTP 1.0 迅速发展及参考性RFC:不仅能访问超文本文档,还能提供有关请求和响应的各种元数据,而且要支持内容协商
  • HTTP 1.1 互联网标准:厘清了之前版本中很多有歧义的地方,而且还加入了很多重要的性能优化:持久连接、分块编码传输、字节范围请求、增强的缓存机制、传输编码及请求管道。
  • HTTP 2.0 改进传输性能

第十章 Web性能要点

宏观的Web 性能优化:

  • 延迟和带宽对 Web 性能的影响;
  • 传输协议(TCP)对 HTTP 的限制;
  • HTTP 协议自身的功能和缺陷;
  • Web 应用的发展趋势及性能需求;
  • 浏览器局限性和优化思路。

10.1 超文本、网页和Web应用互

  • 超文本文档:万维网就起源于超文本文档,一种只有基本格式,但支持超链接的纯文本文档。
  • 富媒体网页:HTML 工作组和早期的浏览器开发商扩展了超文本,使其支持更多的媒体,如图片和音频,同时也为丰富布局增加了很多手段。
  • Web应用:JavaScript 及后来 DHTML 和 Ajax 的加入,再一次革命了Web,把简单的网页转换成了交互式 Web 应用

浏览器处理流水线

图:浏览器处理流水线

浏览器在解析HTML 文档的基础上构建DOM(Document Object Model,文档对象模型)。与此同时,CSSOM(CSS ObjectModel,CSS 对象模型),也会基于特定的样式表规则和资源构建而成。这两个模型共同创建“渲染树”,之后浏览器就有了足够的信息去进行布局,并在屏幕上绘制图形。

10.2 剖析现代Web应用

一个普通Web应用由下列内容构成:

  • HTML
  • 图片
  • JavaScript
  • CSS
  • 其他资源

与桌面应用相比,Web 应用不需要单独安装,只要输入URL,按下回车键,就可以正常运行。可是,桌面应用只需要安装一次,而Web 应用每次访问都需要走一遍“安装过程”——下载资源、构建DOM 和CSSOM、运行JavaScript。

10.2.1 速度、性能与用户期望

表:时间和用户感觉

时间感觉
0~100 ms很快
100~300 ms有一点点慢
300~1000 ms机器在工作呢
>1000 ms先干点别的吧
>10000 ms不能用了

把性能变成钞票

谷歌、微软和亚马逊的研究都表明,性能可以直接转换成收入。比如,Bing 搜索网页时延迟 2000 ms 会导致每用户收入减少4.3%

类似地,Aberdeen 一项覆盖 160 多家组织的研究表明,页面加载时间增加1秒,会导致转化率损失7%,页面浏览量减少11%,客户满意度降低16%

网络越快,PV 越多,黏性越强,转化率越高。

10.2.2 分析资源瀑布

谈到Web 性能,必然要谈资源瀑布。事实上,资源瀑布很可能是我们可以用来分析网络性能,诊断网络问题的一个最有价值的工具。

原书测试采用的网址是 yahoo.com,由于yahoo的测试结果太长,下面的内容除了第一张图,其余采用的是 google.com 的测试结果。

HTTP请求的构成(WebPageTest)

图:HTTP请求的构成(WebPageTest)

google.com资源瀑布图(WebPageTest)

图:google.com资源瀑布图(WebPageTest)

资源的瀑布图能够揭示出页面的结构和浏览器处理顺序。

首先,取得www.google.com对应的文档,同时分派新的 HTTP 请求:HTTP 解析是递增执行的,这样浏览器可以及早发现必要的资源,然后并行发送请求。实际上,何时获得什么资源很大程度上取决于标记结构。浏览器可以变更某些请求的优先顺序,但递增地发现文档中的每一个资源,最终造就了不同资源间的“瀑布效果”。

其次,“Start Render”(绿色的竖线)会在所有资源下载完成前开始,以便用户在页面构建期间就能与之交互。其实,“Document Complete”事件(蓝色的竖线) 也会在剩余资源下载完成前触发。换句话说,浏览器的加载旋转图标此时停止旋转,用户可以继续与页面交互,但 Google 主页会渐进地在后台填充后续内容,比如广告和社交部件。

最早渲染时间、文档完成时间和最后资源获取时间,这三个时间说明我们讨论Web性能时有三个不同测量指标。我们应该关注哪一个时间呢?答案并不唯一,因应用而不同! 工程师们选择了利用浏览器递增加载机制,让用户能够尽早与重要内容交互。而这样一来,他们必须根据应用不同,确定哪些内容重要(须先加载),哪些内容不重要(可以后填充)。

资源瀑布图记录的是HTTP 请求,而连接视图展示了每个TCP 连接的生命期,这些连接用于获取 Google 主页的资源。

GooleConnectionView

图:Goole Connection View

最后,也是最重要的,连接视图的底部显示了带宽利用率曲线。除了少量数据爆发外,可用连接的带宽利用率很低。这说明性能的限制并不在带宽!难道这是个异常现象,或者浏览器问题?都不是。对大多数应用而言,带宽的的确确不是性能的限制因素。限制 Web 性能的主要因素是客户端与服务器之间的网络往返延迟

10.3 性能来源:计算、渲染和网络访问

Web 应用的执行主要涉及三个任务:取得资源页面布局和渲染JavaScript执行。其中,渲染和脚本执行在一个线程上交错进行,不可能并发修改生成的DOM。实际上,优化运行时的渲染脚本执行是至关重要的。

10.3.1 更多带宽其实不(太)重要

能接入更高带宽固然好,特别是传输大块数据时更是如此,比如在线听音乐、看视频,或者下载大文件。可日常上网浏览需要的是从数十台主机获取较小的资源,这时候往返时间就成了瓶颈:

  • 在 Youtube 主页上看视频受限于带宽;
  • 加载和渲染 Youtube 主页受限于延迟。
10.3.2 延迟是性能瓶颈

通过图片定量的感受不同带宽和延迟时间对页面加载时间的影响

页面加载时间与带宽和延迟的关系

图:页面加载时间与带宽和延迟的关系

这正是TCP 握手机制、流量和拥塞控制、由丢包导致的队首拥塞等底层协议特点影响性能的直接后果。大多数HTTP 数据流都是小型突发性数据流,而TCP 则是为持久连接和大块数据传输而进行过优化的。网络往返时间在大多数情况下都是TCP 吞吐量和性能的限制因素。于是,延迟自然也就成了HTTP 及大多数基于HTTP 交付的应用的性能瓶颈。

10.4 人造和真实用户性能度量

宽泛地说,在受控度量环境下完成的任何测试都可称为人造测试。首先,本地构建过程运行性能套件,针对基础设施加载测试,或者针对一组分散在各地的监控服务器加载测试,这些服务器定时运行脚本并记录输出。这些测试中的任何一个都可能测试不同的基础设施(如应用服务器的吞吐量、数据库性能、DNS 时间,等等),并作为稳定的基准辅助检测性能衰退或聚焦于系统的某个特定组件。

人造环境下收集到的性能数据缺乏现实当中的多样性,难以据之确定应用带给用户的最终体验。

  • 场景及页面选择:很难重复真实用户的导航模式;
  • 浏览器缓存:用户缓存不同,性能差别很大;
  • 中介设施:中间代理和缓存对性能影响很大;
  • 硬件多样化:不同的 CPU、GPU 和内存比比皆是;
  • 浏览器多样化:各种浏览器版本,有新有旧;
  • 上网方式:真实连接的带宽和延迟可能不断变化。

我们必须通过真实用户度量(RUM,Real-User Measurement)来获取用户使用我们应用的真实性能数据,从而确保性能度量的有效性。

W3C Web Performance Working Group 通过引入Navigation Timing API(下图)为我们做真实用户测试提供了便利,这个API 目前已得到很多现代桌面和移动浏览器的支持。

NavigationTiming监测到的特定于用户的计时器

图:NavigationTiming监测到的特定于用户的计时器

W3C Performance Group 还标准化了另外两个API:User TimingResource Timing。Navigation Timing 只针对根文档提供性能计时器,Resource Timing 则针对页面中的每个资源都提供类似的性能数据,可以让我们收集到关于页面的完整性能数据。User Timing 可以标记和度量特定应用的性能指标,提供高精确度的计时结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function init() {
performance.mark("startTask1"); ➊
applicationCode1(); ➋
performance.mark("endTask1");

logPerformance();
}

function logPerformance() {
var perfEntries = performance.getEntriesByType("mark");
for (var i = 0; i < perfEntries.length; i++) { ➌
console.log("Name: " + perfEntries[i].name +
" Entry Type: " + perfEntries[i].entryType +
" Start Time: " + perfEntries[i].startTime +
" Duration: " + perfEntries[i].duration + "\n");
}
console.log(performance.timing); ➍
}
  • ➊ 存储(标记)时间戳,并命名(startTask1)
  • ➋ 执行应用代码
  • ➌ 迭代和记录用户计时数据
  • ➍ 记录当前页面的Navigation Timing 对象

优化要以度量为依据,RUM 和人造测试是互为补充的手段,可以帮我们发现回归现象和真正的瓶颈,提升应用的用户体验。

10.5 针对浏览器的优化建议

浏览器核心优化策略来说,可以宽泛地分为两类:

  • 基于文档的优化
    熟悉网络协议,了解文档、CSS 和JavaScript 解析管道,发现和优先安排关键网络资源,尽早分派请求并取得页面,使其尽快达到可交互的状态。主要方法是优先获取资源、提前解析等。
  • 推测性优化
    浏览器可以学习用户的导航模式,执行推测性优化,尝试预测用户的下一次操作。然后,预先解析DNS、预先连接可能的目标。

这些优化都由浏览器替我们自动完成,优化背后的原理,大多都是利用如下四种技术:

  • 资源预取和排定优先次序:文档、CSS 和JavaScript 解析器可以与网络协议层沟通,声明每种资源的优先级:初始渲染必需的阻塞资源具有最高优先级,而低优先级的请求可能会被临时保存在队列中。
  • DNS预解析:对可能的域名进行提前解析,避免将来HTTP 请求时的DNS 延迟。预解析可以通过学习导航历史、用户的鼠标悬停,或其他页面信号来触发。
  • TCP预连接:DNS 解析之后,浏览器可以根据预测的HTTP 请求,推测性地打开TCP 连接。如果猜对的话,则可以节省一次完整的往返(TCP 握手)时间。
  • 页面预渲染:某些浏览器可以让我们提示下一个可能的目标,从而在隐藏的标签页中预先渲染整个页面。这样,当用户真的触发导航时,就能立即切换过来。

要密切关注每个页面的结构和交付:

  • CSS 和JavaScript 等重要资源应该尽早在文档中出现;
  • 应该尽早交付 CSS,从而解除渲染阻塞并让 JavaScript 执行;
  • 非关键性 JavaScript 应该推迟,以避免阻塞 DOM 和 CSSOM 构建;
  • HTML 文档由解析器递增解析,从而保证文档可以间隙性发送,以求得最佳性能。

除了优化页面结构,还可以在文档中嵌入提示,以触发浏览器为我们采用其他优化机制:

1
2
3
4
<link rel="dns-prefetch" href="//hostname_to_resolve.com">
<link rel="subresource" href="/javascript/myapp.js">
<link rel="prefetch" href="/images/big.jpeg">
<link rel="prerender" href="//example.org/next_page.html">
  • ➊ 预解析特定的域名
  • ➋ 预取得页面后面要用到的关键性资源
  • ➌ 预取得将来导航要用的资源
  • ➍ 根据对用户下一个目标的预测,预渲染特定页面

第十一章 HTTP 1.x

Steve Souder 的《高性能网站建设指南》中概括了14 条规则,有一半针对网络优化:

  • 减少DNS查询:每次域名解析都需要一次网络往返,增加请求的延迟,在查询期间会阻塞请求。
  • 减少HTTP请求:任何请求都不如没有请求更快,因此要去掉页面上没有必要的资源。
  • 使用CDN:从地理上把数据放到接近客户端的地方,可以显著减少每次TCP 连接的网络延迟,增加吞吐量。
  • 添加Expires首部并配置ETag标签:相关资源应该缓存,以避免重复请求每个页面中相同的资源。Expires 首部可用于指定缓存时间,在这个时间内可以直接从缓存取得资源,完全避免HTTP 请求。ETag 及Last-Modified 首部提供了一个与缓存相关的机制,相当于最后一次更新的指纹或时间戳。
  • Gzip资源:所有文本资源都应该使用Gzip 压缩,然后再在客户端与服务器间传输。一般来说,Gzip 可以减少60%~80% 的文件大小,也是一个相对简单(只要在服务器上配置一个选项),但优化效果较好的举措。
  • 避免HTTP重定向:HTTP 重定向极其耗时,特别是把客户端定向到一个完全不同的域名的情况下,还会导致额外的DNS 查询、TCP 连接延迟,等等。

11.1 持久连接的优点

为简单起见,我们限定最多只有一个TCP 连接,并且只取得两个小文件(每个<4 KB):一个HTML 文档,一个CSS 文件,服务器响应需要不同的时间(分别为40 ms 和20 ms)。

通过单独的TCP连接取得HTML和CSS文件

图:通过单独的TCP连接取得HTML和CSS文件

添加对HTTP 持久连接的支持,就可以避免第二次TCP 连接时的三次握手、消除另一次TCP 慢启动的往返,节约整整一次网络延迟。更常见的情况是一次TCP 连接要发送N 次HTTP 请求,这时:

  • 没有持久连接,每次请求都会导致两次往返延迟;
  • 有持久连接,只有第一次请求会导致两次往返延迟,后续请求只会导致一次往返延迟。

11.2 HTTP管道

HTTP管道发送请求

图:HTTP管道发送请求

如果客户端需要请求两个资源

  1. 以前的做法是在同一个TCP连接里面,先发送A请求,然后等待服务器做出回应,收到后再发出B请求;
  2. 管道机制则允许浏览器同时发出A请求和B请求,但是服务器还是按照顺序,先回应A请求,完成后再回应B请求。

所以存在队首阻塞问题:假如A请求的响应被无期限挂起,那么后续的请求都将被阻塞。

HTTP1.1的管道技术优点毋庸置疑,但是应用非常有限。目前大多数浏览器都会默认禁用它。

11.3 使用多个TCP连接

大多数现代浏览器,包括桌面和移动浏览器,都支持每个主机打开 6 个连接:

  • 客户端可以并行分派最多 6 个请求;
  • 服务器可以并行处理最多 6 个请求;
  • 第一次往返可以发送的累计分组数量(TCP cwnd)增长为原来的 6 倍。

这似乎是比较方便的解决方案,而实际这么做的代价:

  1. 更多的套接字会占用客户端、服务器以及代理的资源,包括内存缓冲区和 CPU 时钟周期;
  2. 并行 TCP 流之间竞争共享的带宽;
  3. 由于处理多个套接字,实现复杂性更高;
  4. 即使并行 TCP 流,应用的并行能力也受限制。

并非长期方案,第一点会导致运维成本提高,而复杂性提高又使得开发成本提高,应用的并行性提供的好处非常有限。依然采用的原因:

  1. 作为绕过应用协议(HTTP)限制的一个权宜之计;
  2. 作为绕过 TCP 中低起始拥塞窗口的一个权宜之计;
  3. 作为让客户端绕过不能使用TCP 窗口缩放的一个权宜之计。

11.4 域名分区

将同一个域名下的资源分到不同子域名,比如:www.example.com 提供所有资源, 手工分散到多个子域名:{shard1,shard2…shardn}.example.com。

  • 优点:突破浏览器的连接限制(上小节),限制是针对同域名的
  • 缺点:
    1. 多域名会导致更多的dns查询
    2. 手工分离资源比较复杂

实践中,域名分区经常会被滥用,导致几十个 TCP 流都得不到充分利用,其中很多永远也避免不了TCP 慢启动,最坏的情况下还会降低性能。此外,如果使用的是HTTPS,那么由于 TLS 握手导致的额外网络往返,会使得上述代价更高。此时,请大家注意如下几条:

  • 首先,把TCP 利用好,参见 2.5 节“针对 TCP 的优化建议”;
  • 浏览器会自动为你打开 6 个连接;
  • 资源的数量、大小和响应时间都会影响最优的分区数目;
  • 客户端延迟和带宽会影响最优的分区数目;
  • 域名分区会因为额外的 DNS 查询和 TCP 慢启动而影响性能。

域名分区是一种合理但又不完美的优化手段。请大家一定先从最小分区数目(不分区)开始,然后逐个增加分区并度量分区后对应用的影响。现实当中,真正因同时打开十几个连接而提升性能的站点并不多,如果你最终使用了很多分区,那么你会发现减少资源数量或者将它们合并为更少的请求,反而能带来更大的好处。

11.5 度量和控制协议开销

浏览器发起的 HTTP 请求,都会携带额外 500~800 字节的 HTTP 元数据:用户代理字符串、很少改变的接收和传输首部、缓存指令,等等。还没有包含最大的一块:HTTP cookie。现代应用经常通过 cookie 进行会话管理、记录个性选项或者完成分析。综合到一起,所有这些未经压缩的HTTP 元数据经常会给每个 HTTP 请求增加几千字节的协议开销。

减少要传输的首部数据(高度重复且未压缩),可以节省相当于一次往返的延迟时间,显著提升很多Web 应用的性能。

11.6 连接与拼合

  • 连接
    把多个 JavaScript 或 CSS 文件组合为一个文件
  • 拼合
    把多张图片组合为一个更大的复合的图片。多张图片可以组合为一个“图片精灵”,然后使用 CSS 选择这张大图中的适当部分,显示在浏览器中

连接和拼合是在HTTP 1.x 协议限制(管道没有得到普遍支持,多请求开销大)的现实之下可行的应用层优化。但代价则是增加应用的复杂度,以及导致缓存、更新、执行速度,甚至渲染页面的问题。应用这两种优化时,要注意度量结果,根据实际情况考虑如下问题。

  • 你的应用在下载很多小型的资源时是否会被阻塞?
  • 有选择地组合一些请求对你的应用有没有好处?
  • 放弃缓存粒度对用户有没有负面影响?
  • 组合图片是否会占用过多内存?
  • 首次渲染时是否会遭遇延迟执行?

在上述问题的答案间求得平衡是一种艺术。

11.7 嵌入资源

资源嵌入文档可以减少请求的次数。比如,JavaScript代码,CSS 代码, 图片甚至音频或 PDF 文件,都可以通过数据URI(data:[mediatype][;base64],data)的方式嵌入到页面中:

1
2
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAAAAACH5BAAAAAAALAAAAAABAAEAAAICTAEAOw=="
alt="1x1 transparent (GIF) pixel" />

考虑是否嵌入时,可以参照如下建议:

  • 如果文件很小,而且只有个别页面使用,可以考虑嵌入;
  • 如果文件很小,但需要在多个页面中重用,应该考虑集中打包;
  • 如果小文件经常需要更新,就不要嵌入了;
  • 通过减少 HTTP cookie 的大小将协议开销最小化。

第十二章 HTTP 2.0

HTTP 2.0 不会改动 HTTP 的语义,修改了格式化数据(分帧)的方式,以及客户端与服务器间传输这些数据的方式。这两点统领全局,通过新的组帧机制向我们的应用隐藏了所有复杂性。

12.1 历史及其与SPDY的渊源

SPDY 是谷歌开发的一个实验性协议,项目设定的目标如下:

  • 页面加载时间(PLT,Page Load Time)降低 50%;
  • 无需网站作者修改任何内容;
  • 把部署复杂性降至最低,无需变更网络基础设施;
  • 与开源社区合作开发这个新协议;
  • 收集真实性能数据,验证这个实验性协议是否有效。

SPDY 在被行业采用并证明能够大幅提升性能之后,已经具备了成为一个标准的条件。最终,HTTP-WG(HTTP Working Group)在2012 年初把HTTP 2.0提到了议事日程,吸取SPDY 的经验教训,并在此基础上制定官方标准。

12.2 走向HTTP 2.0

HTTP/2.0 应该满足如下条件:

  • 相对于使用TCP 的HTTP 1.1,用户在大多数情况下的感知延迟要有实质上、可度量的改进;
  • 解决 HTTP 中的“队首阻塞”问题;
  • 并行操作无需与服务器建立多个连接,从而改进 TCP 的利用率,特别是拥塞控制方面;
  • 保持 HTTP 1.1 的语义,利用现有文档,包括(但不限于)HTTP 方法、状态码、URI,以及首部字段;
  • 明确规定 HTTP 2.0 如何与 HTTP 1.x 互操作,特别是在中间介质上;
  • 明确指出所有新的可扩展机制以及适当的扩展策略。

对现有的HTTP 部署——特别是Web 浏览器(桌面及移动)、非浏览器(“HTTPAPI”)、Web 服务(各种规模),以及中间介质(代理、公司防火墙、“反向”代理及CDN)而言,最终规范应该满足上述这些目标。类似地,当前和未来对HTTP/1.x 的语义扩展(如首部、方法、状态码、缓存指令)也应该得到新协议的支持。

HTTPbis WG 宣言 HTTP 2.0

之所以要递增一个大版本到2.0,主要是因为它改变了客户端与服务器之间交换数据的方式。为实现宏伟的性能改进目标,HTTP 2.0 增加了新的二进制分帧数据层,而这一层并不兼容之前的HTTP 1.x 服务器及客户端——是谓2.0。

12.3 设计和技术目标

12.3.1 二进制分帧层

HTTP2.0二进制分帧层

图:HTTP2.0二进制分帧层

这里所谓的“层”,指的是位于套接字接口与应用可见的高层HTTP API 之间的一个新机制:HTTP 的语义,包括各种动词、方法、首部,都不受影响,不同的是传输期间对它们的编码方式变了。HTTP 1.x 以换行符作为纯文本的分隔符,而HTTP2.0 将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码。

12.3.2 流、消息和帧
  • 流:已建立的连接上的双向字节流。
  • 消息:与逻辑消息对应的完整的一系列数据帧。
  • 帧:HTTP 2.0 通信的最小单位,每个帧包含帧首部,至少也会标识出当前帧所属的流。

HTTP2.0流、消息和帧

图:HTTP2.0流、消息和帧

所有HTTP 2.0 通信都在一个连接上完成,这个连接可以承载任意数量的双向数据流。相应地,每个数据流以消息的形式发送,而消息由一或多个帧组成,这些帧可以乱序发送,然后再根据每个帧首部的流标识符重新组装。

要理解HTTP 2.0,就必须理解流、消息和帧这几个基本概念。

  • 所有通信都在一个 TCP 连接上完成。
  • 流是连接中的一个虚拟信道,可以承载双向的消息;每个流都有一个唯一的整数标识符(1、2…N)。
  • 消息是指逻辑上的 HTTP 消息,比如请求、响应等,由一或多个帧组成。
  • 帧是最小的通信单位,承载着特定类型的数据,如 HTTP 首部、负荷,等等。
12.3.3 多向请求与响应

HTTP 1.x 交付模型,保证每个连接每次只交付一个响应(多个响应必须排队),并且会导致队首阻塞。HTTP 2.0 中新的二进制分帧层突破了这些限制,实现了多向请求和响应:客户端和服务器可以把 HTTP 消息分解为互不依赖的帧(下图),然后乱序发送,最后再在另一端把它们重新组合起来。

HTTP2.0在共享的连接上同时发送请求和响应

图:HTTP2.0在共享的连接上同时发送请求和响应

客户端正在向服务器传输一个 DATA 帧(stream 5),与此同时,服务器正向客户端乱序发送stream 1 和stream 3的一系列帧。此时,一个连接上有3 个请求/响应并行交换!

12.3.4 请求优先级

把HTTP 消息分解为很多独立的帧之后,就可以通过优化这些帧的交错和传输顺序,进一步提升性能。每个流都可以带有一个 31 比特的优先值:

  • 0 表示最高优先级;
  • 231-1 表示最低优先级。

但这也会存在问题:

  1. 服务器不理会优先级,本应先处理css和js,但却在发送图片
  2. 高优先级的请求被阻塞,仍然会存在队首阻塞问题
12.3.5 每个来源一个连接

所有 HTTP 2.0 连接都是持久化的,而且客户端与服务器之间也只需要一个连接即可。

每个来源一个连接显著减少了相关的资源占用:连接路径上的套接字管理工作量少了,内存占用少了,连接吞吐量大了。此外,从上到下所有层面上也都获得了相应的好处:

  1. 所有数据流的优先次序始终如一;
  2. 压缩上下文单一使得压缩效果更好;
  3. 由于 TCP 连接减少而使网络拥塞状况得以改观;
  4. 慢启动时间减少,拥塞和丢包恢复速度更快。
12.3.6 流量控制

标定数据流的优先级有助于按序交付,但只有优先级还不足以确定多个数据流或多个连接间的资源分配。为解决这个问题,HTTP 2.0 为数据流和连接的流量控制提供了一个简单的机制:

  • 流量控制基于每一跳进行,而非端到端的控制;
  • 流量控制基于窗口更新帧进行,即接收方广播自己准备接收某个数据流的多少字节,以及对整个连接要接收多少字节;
  • 流量控制窗口大小通过 WINDOW_UPDATE 帧更新,这个字段指定了流 ID 和窗口大小递增值;
  • 流量控制有方向性,即接收方可能根据自己的情况为每个流乃至整个连接设置任意窗口大小;
  • 流量控制可以由接收方禁用,包括针对个别的流和针对整个连接。

HTTP 2.0 标准没有规定任何特定的算法、值,或者什么时候发送WINDOW_UPDATE 帧。因此,实现可以选择自己的算法以匹配自己的应用场景,从而求得最佳性能。

12.3.7 服务器推送

HTTP 2.0 新增的一个强大的新功能,就是服务器可以对一个客户端请求发送多个响应。换句话说,除了对最初请求的响应外,服务器还可以额外向客户端推送资源(下图),而无需客户端明确地请求。

服务器发起推送资源的新流(要约)

图:服务器发起推送资源的新流(要约)

在HTTP 2.0 中,可以把资源直接推送给客户端的过程从应用中拿出来,放到HTTP 协议本身来实现,带来了如下好处:

  • 客户端可以缓存推送过来的资源;
  • 客户端可以拒绝推送过来的资源;
  • 推送资源可以由不同的页面共享;
  • 服务器可以按照优先级推送资源。

实现HTTP 2.0服务器推送

  • 应用可以在自身的代码中明确发起服务器推送
  • 应用可以通过额外的 HTTP 首部向服务器发送信号,列出它希望推送的资源。这样可以将应用与HTTP 服务器API 分离
  • 服务器可以不依赖应用而自动学习相关资源。服务器可以解析文档,推断出要推送的资源,或者可以分析流量,然后作出适当的决定
12.3.8 首部压缩

为减少这些开销并提升性能,HTTP 2.0 会压缩首部元数据:

  • HTTP 2.0 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送;
  • 首部表在HTTP 2.0的连接存续期内始终存在,由客户端和服务器共同渐进地更新;
  • 每个新的首部键-值对要么被追加到当前表的末尾,要么替换表中之前的值。(可以简单的理解为一个HashMap

HTTP2.0首部的差异化编码

图:HTTP2.0首部的差异化编码

第二个请求只需要发送变化了的路径首部(:path),其他首部没有变化,不用再发送了,这样就可以避免传输冗余的首部。

12.3.9 有效的HTTP2.0升级与发现

HTTPS加密信道协商升级,可以通过使用ALPN(Application Layer Protocol Negotiation)发现和协商HTTP 2.0 的支持情况。

而非加密信道,在不确定服务器是否支持HTTP2.0时,客户端需要使用HTTP Upgrade机制通过协调确定适当的协议:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GET /page HTTP/1.1
Host: server.example.com
Connection: Upgrade, HTTP2-Settings
Upgrade: HTTP/2.0 ➊
HTTP2-Settings: (SETTINGS payload) ➋
--------------------------------------
HTTP/1.1 200 OK ➌
Content-length: 243
Content-type: text/html
(... HTTP 1.1 response ...)
--------------------------------------
(or)
HTTP/1.1 101 Switching Protocols ➍
Connection: Upgrade
Upgrade: HTTP/2.0
(... HTTP 2.0 response ...)
  1. 发起带有 HTTP 2.0 Upgrade 首部的HTTP 1.1 请求
  2. HTTP/2.0 SETTINGS 净荷的 Base64 URL 编码
  3. 服务器拒绝升级,通过HTTP 1.1 返回响应
  4. 服务器接受升级,切换到新分帧

若客户端已经确认服务器支持HTTP2.0,它可以直接发送HTTP2.0分帧,而无需Upgrade机制。最坏的情况是无法建立连接,需要回退一步:

  1. HTTP - 使用Upgrade首部协商
  2. HTTPS - 切换为带ALPN 协商的TLS 信道

12.4 二进制分帧简介

建立了HTTP 2.0 连接后,客户端与服务器会通过交换帧来通信,帧是基于这个新协议通信的最小单位。所有帧都共享一个8 字节的首部:

8字节帧首部

图:8字节帧首部
  • 16 位的长度前缀意味着一帧大约可以携带 64 KB 数据,不包括 8 字节首部。
  • 8 位的类型字段决定如何解释帧其余部分的内容。
  • 8 位的标志字段允许不同的帧类型定义特定于帧的消息标志。
  • 1 位的保留字段始终置为 0。
  • 31 位的流标识符唯一标识 HTTP 2.0 的流。

HTTP 2.0 规定了如下帧类型

  • DATA:用于传输 HTTP 消息体。
  • HEADERS:用于传输关于流的额外的首部字段。
  • PRIORITY:用于指定或重新指定引用资源的优先级。
  • RST_STREAM:用于通知流的非正常终止。
  • SETTINGS:用于通知两端通信方式的配置数据。
  • PUSH_PROMISE:用于发出创建流和服务器引用资源的要约。
  • PING:用于计算往返时间,执行“活性”检查。
  • GOAWAY:用于通知对端停止在当前连接中创建流。
  • WINDOW_UPDATE:用于针对个别流或个别连接实现流量控制。
  • CONTINUATION:用于继续一系列首部块片段。
12.4.1 发起新流

在发送应用数据之前,必须创建一个新流并随之发送相应的元数据,比如流优先级、HTTP 首部等。而HTTP2.0协议规定客户端和服务器都可以发起:

  • 客户端:通过发送 HEADERS 帧来发起新流(下图),这个帧里包含带有新流 ID 的公用首部、可选的31 位优先值,以及一组HTTP 键-值对首部;
  • 服务器:通过发送 PUSH_PROMISE 帧来发起推送流,这个帧与 HEADERS 帧等效,但它包含“要约流ID”,没有优先值。

带优先值的HEADERS帧

图:带优先值的HEADERS帧

两端的流 ID 不会冲突,各自持有一个简单的计数器,每次发起新流时递增ID即可,客户端发起的流具有奇数ID,服务器发起的流具有偶数ID。(这里与原书不同,见下文!)

根据RFC 75405.1.1 Stream Identifiers 的内容:

Streams are identified with an unsigned 31-bit integer. Streams initiated by a client MUST use odd-numbered stream identifiers; those initiated by the server MUST use even-numbered stream identifiers.

该书英文原文:

Also, because both sides can initiate new streams,the stream counters are offset: client-initiated streams have even-numbered stream IDs and server-initiated streams have odd-numbered stream IDs.

中文是根据英文翻译来的:

同样,由于两端都可以发起新流,流计数器偏置:客户端发起的流具有偶数ID,服务器发起的流具有奇数ID

所以,这里的内容我们不取原书的内容,按照RFC 7540的内容来。

12.4.2 发送应用数据

应用数据可以分为多个DATA 帧,最后一帧要翻转帧首部的END_STREAM 字段。

DATA帧

图:DATA帧

数据净荷不会被另行编码或压缩。编码方式取决于应用或服务器,纯文本、gzip 压缩、图片或视频压缩格式都可以。

12.4.3 HTTP2.0帧数据流分析

HTTP2.0在共享的连接上同时发送请求和响应

图:HTTP2.0在共享的连接上同时发送请求和响应
  • 有3 个活动的流:stream 1 、stream 3 和 stream 5。
  • 3 个流的 ID 都是奇数,说明都是客户端发起的。(这里的原文又是没有问题的,我愕然!)
  • 这里没有服务器发起的流。
  • 服务器发送的 stream 1 包含多个 DATA 帧,这是对客户端之前请求的响应数据。这也说明在此之前已经发送过HEADERS 帧了。
  • 服务器在交错发送 stream 1 的 DATA 帧和 stream 3 的 HEADERS 帧,这就是响应的多路复用!
  • 客户端正在发送 stream 5 的 DATA 帧,表明 HEADERS 帧之前已经发送过了。

第十三章 优化应用的交付

13.1 经典的性能优化最佳实践

所有应用都应该致力于消除或减少不必要的网络延迟将需要传输的数据压缩至最少。这两条标准是经典的性能优化最佳实践,是其他数十条性能准则的出发点。

  • 减少dns查找
  • 重用tcp连接
  • 减少HTTP重定向
  • 使用cdn
  • 去掉不必要的资源
  • 在客户端缓存资源
    • Cache-Control首部用于指定缓存时间
    • 同时指定 Last-Modified 和 ETag 首部提供验证机制
  • 传输压缩过的内容:HTML/CSS/JS/图片等
  • 消除不必要的请求开销
    • 尽可能的减小cookie大小,甚至不用cookie
  • 并行处理请求和响应
    • 使用持久连接
    • 利用多个HTTP1.1实现并行下载
    • 使用HTTP1.1管道技术
    • 升级到HTTP2.0

13.2 针对HTTP1.x的优化建议

  • 利用HTTP管道
  • 采用域名分区
  • 打包资源以减少HTTP请求
  • 嵌入小资源

13.3 针对HTTP2.0的优化建议

13.3.1 去掉对1.x的优化
  • 每个来源使用一个连接
  • 去掉不必必要的文件合并和图片拼接
    • HTTP2.0可以并行发送小资源,导致打包资源的效率反而更低
  • 利用服务器推送
  • 不要使用域名分区
13.3.2 双协议应用策略

升级到2.0不可能一蹴而就,可以考虑以下几种可能性:

  1. 相同的应用代码,双协议部署
  2. 分离应用代码,双协议部署
  3. 动态HTTP1.x和HTTP2.0优化:选用适当Web优化框架或者产品(比如google的PageSpeed),在响应请求时动态重写交付的应用代码(包括连接、拼合、分区等等)
  4. HTTP2.0单协议部署
13.3.3 1.x与2.0的相互转换

重用原有HTTP1.x基础设施,应用HTTP2.0更新的最简单方式:在客户端和服务器之间添加一台中间转换服务器:接收HTTP2.0会话处理后分派给HTTP1.x服务器,收到HTTP1.x服务器响应后转为HTTP2.0的数据流再返回给客户端。但这中简单策略并非长久之计,长久之计是升级到2.0。

HTTP2.0到1.x的转换

图:HTTP2.0到1.x的转换
13.3.4 评估服务器质量与性能
  • HTTP 2.0 服务器必须理解流优先级;
  • HTTP 2.0 服务器必须根据优先级处理响应和交付资源;
  • HTTP 2.0 服务器必须支持服务器推送;
  • HTTP 2.0 服务器应该提供不同推送策略的实现。
13.3.5 2.0与TLS

由于存在很多不兼容的中间代理,早期的HTTP 2.0 部署必然依赖加密信道。这样我们就面临两种可能出现ALPN 协商和TLS 终止的情况:

  • TLS 连接可能会在 HTTP 2.0 服务器上终止
  • TLS 连接可能会在上游(如负载均衡器)上终止

支持TLS+ALPN的负载均衡器

图:支持TLS+ALPN的负载均衡器
13.3.6 负载均衡器、代理及应用服务器

负载均衡器与TLS终止策略

图:负载均衡器与TLS终止策略

简单分析一下三种情况:

  1. 服务器与客户端直接对话,并负责完成TLS连接,进行ALPN协商,处理所有请求
  2. 服务器与客户端之间有TCP负载均衡,但中间代理是无法完成ALPN协商的,需要服务器支持ALPN
  3. 服务器与客户端之间有TLS+ALPN负载均衡,中间代理可以完成TLS+ALPN协商,然后建立加密通道,又或者直接将非加密HTTP2.0数据流发送到服务器

到底由哪个组件负责终止TLS连接,以及是否能否执行必要的ALPN协商:

  • 要在TLS 之上实现 HTTP 2.0 通信,终端服务器必须支持 ALPN;
  • 尽可能在接近用户的地方终止 TLS,参见 4.7.2 节“尽早完成(握手)”;
  • 如果无法支持 ALPN,那么选择 TCP 负载均衡模式;
  • 如果无法支持 ALPN 且 TCP 负载均衡也做不到,那么就退而求其次,在非加密信道上使用HTTP 的Upgrade 流,参见12.3.9 节“有效的HTTP 2.0 升级与发现”。

第四部分 浏览器API与协议

第十四章 浏览器网络概述

现代浏览器完全是一个囊括数百个组件的操作系统,包括进程管理、安全沙箱、分层的优化缓存、JavaScript 虚拟机、图形渲染和 GPU 管道、存储系统、传感器、音频与视频、网络机制,等等。浏览器乃至运行在其中的应用的性能, 取决于若干组件: 解析、布局、HTML 与CSS 的样式计算、JavaScript 执行速度、渲染管道,当然还有网络相关各层协议的配合。

高层浏览器网络API、协议和服务

图:高层浏览器网络API、协议和服务

14.1 连接管理和优化

运行在浏览器上的Web应用并不管理个别网络套接字的生命周期,而是委托给浏览器,可以自动化许多性能优化任务:包括套接字重用、请求优先级排定,晚绑定、协议协商、施加连接数限制等等。浏览器有意将请求管理生命周期与套接字管理分开,这很微妙也至关重要。

套接字以的形式进行管理,按照来源,每个池都有自己的连接限制和安全约束。

自动管理的套接字池在所有浏览器进程间共享

图:自动管理的套接字池在所有浏览器进程间共享
  • 来源:由应用协议、域名和端口三个要素构成
  • 套接字池:属于同一个来源的一组套接字。实践中,所有主流浏览器的最大池规模都是6个套接字

自动化的套接字池管理会自动重用TCP连接,从而有效保障性能。另外:

  • 浏览器可以按照优先级次序发送排队的请求
  • 浏览器可以重用套接字以最小化延迟并提升吞吐量
  • 浏览器可以预测请求提前打开套接字
  • 浏览器可以优化何时关闭空闲套接字
  • 浏览器可以优化分配给所有套接字的带宽

14.2 网络安全和沙箱

将个别套接字的管理任务委托给浏览器,还可以让浏览器运用沙箱机制,对不受忍心的应用代码采取一致的安全与策略限制。

  • 连接限制:浏览器管理所有打开的套接字池并强制施加连接数限制,保护客户端和服务器的资源不会被耗尽
  • 请求格式化与响应处理:格式化所有外发请求和响应,保证格式一致和符合协议的语义
  • TLS协商:浏览器执行TLS握手和必要的证书检查,有问题的证书用户会收到通知
  • 同源策略:浏览器会限制应用只能向哪个来源发送请求

14.3 资源和客户端状态缓存

  • 浏览器针对每个资源自动执行缓存指令。
  • 浏览器会尽可能恢复失效资源的有效性。
  • 浏览器会自动管理缓存大小及资源回收。

浏览器为每个来源维护着独立的cookie容器,为读写新cookie、会话和认证数据提供必要的应用及服务器API,还会为我们自动追加和处理HTTP首部。

直观的例子,说明浏览器管理会话状态的好处:

  1. 认证的会话可以在多个标签页或者浏览器窗口间共享
  2. 反之,用户在某个标签页退出,则其他所有打开的窗口中的会话都将失效

14.4 应用API与协议

浏览器提供的网络服务的最上层,就是应用API 和协议。

表:XHR、SSE和WebSocket的高级特性

-XMLHttpRequestServer-Sent EventWebSocket
请求流
响应流受限
分帧机制HTTP事件流二进制分帧
二进制数据传输否(base64)
压缩受限
应用传输协议HTTPHTTPWebSocket
网络传输协议TCPTCPTCP

在这个表中有意忽略了WebRTC,因为那是一种端到端的交付模型,与XHR、SSE 和WebSocket 协议有着根本的不同。

第十五章 XMLHttpRequest

XMLHttpRequest(XHR)是浏览器层面的API, 可以让开发人员通过JavaScript实现数据传输。XHR 是在Internet Explorer 5 中首次亮相的, 后来成为AJAX(Asynchronous JavaScript and XML)革命的核心技术,是今天几乎所有Web 应用必不可少的基本构件。

15.1 XHR简史

15.2 跨域资源共享 CORS

CORS 针对客户端的跨源请求提供了安全的选择同意机制:

1
2
3
4
5
6
7
8
9
10
// 脚本来源:(http, example.com, 80)
var xhr = new XMLHttpRequest();
xhr.open('GET', '/resource.js'); ➊
xhr.onload = function() { ... };
xhr.send();

var cors_xhr = new XMLHttpRequest();
cors_xhr.open('GET', 'http://thirdparty.com/resource.js'); ➋
cors_xhr.onload = function() { ... };
cors_xhr.send();
  1. 同源XHR 请求
  2. 跨源XHR 请求

CORS 请求也使用相同的XHR API,区别仅在于请求资源用的 URL 与当前脚本并不同源。

CORS的请求的选择同意机制由底层处理:请求发出后,浏览器自动追加受保护的 Origin HTTP 首部。而远程服务器可以检查Origin首部,决定是否接收该请求,接收便返回 Access-Control-Allow-Origin 首部,否则不返回即可。

1
2
3
4
5
6
7
8
9
=> 请求
GET /resource.js HTTP/1.1
Host: thirdparty.com
Origin: http://example.com ➊
...
<= 响应
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://example.com ➋
...
  1. Origin 首部由浏览器自动设置
  2. 选择同意首部由服务器设置

因为CORS会提前采取一系列安全措施:

  1. CORS请求会省略cookie和HTTP认证等用户凭据
  2. 客户端被限制只能发送“简单的跨域请求”,包括只能使用特定的方法(GET/POST/HEAD),以及只能访问可以通过XHR发送并读取的HTTP首部

为解决上述的难题,相应的:

  1. 客户端在发送请求时通过XHR对象发送额外属性(withCredentials),由服务器是否返回首部(Access-Control-Allow-Credentials)来表示接收与否
  2. 客户端需要发送一个预备(preflight)请求,来获得第三方服务器的许可:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
=> 预备请求
OPTIONS /resource.js HTTP/1.1 ➊
Host: thirdparty.com
Origin: http://example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: My-Custom-Header
...
<= 预备响应
HTTP/1.1 200 OK ➋
Access-Control-Allow-Origin: http://example.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: My-Custom-Header
...
(正式的HTTP 请求) ➌
  1. 验证许可的预备 OPTIONS 请求
  2. 第三方源的成功预备响应
  3. 实际的 CORS 请求

15.3 通过XHR下载数据

浏览器可以依靠HTTP 的content-type 首部来推断适当的数据类型( 比如把application/json 响应解析为JSON 对象),应用也可以在发起XHR 请求时显式重写数据类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var xhr = new XMLHttpRequest();
xhr.open('GET', '/images/photo.webp');
xhr.responseType = 'blob'; ➊
xhr.onload = function() {
if (this.status == 200) {
var img = document.createElement('img');
img.src = window.URL.createObjectURL(this.response); ➋
img.onload = function() {
window.URL.revokeObjectURL(this.src); ➌
}
document.body.appendChild(img);
}
};
xhr.send();
  1. 将返回数据类型设置为Blob
  2. 基于返回的对象创建唯一的对象URI 并设置为图片的源
  3. 图片加载完毕后立即释放对象

15.4 通过XHR上传数据

上传不同类型数据的代码都一样,只不过最后在调用XHR 请求对象的send() 方法时,要传入相应的数据对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var xhr = new XMLHttpRequest();
xhr.open('POST','/upload');
xhr.onload = function() { ... };
xhr.send("text string"); ➊

var formData = new FormData(); ➋
formData.append('id', 123456);
formData.append('topic', 'performance');

var xhr = new XMLHttpRequest();
xhr.open('POST', '/upload');
xhr.onload = function() { ... };
xhr.send(formData); ➌

var xhr = new XMLHttpRequest();
xhr.open('POST', '/upload');
xhr.onload = function() { ... };
var uInt8Array = new Uint8Array([1, 2, 3]); ➍
xhr.send(uInt8Array.buffer); ➎
  1. 把简单的文本字符串上传到服务器
  2. 通过 FormData API 动态创建表单数据
  3. 向服务器上传 multipart/form-data 对象
  4. 创建无符号、8 字节整型的有类型数组(ArrayBuffer)
  5. 向服务器上传字节块

也可以将大文件切分成几个小块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var blob = ...; ➊

const BYTES_PER_CHUNK = 1024 * 1024; ➋
const SIZE = blob.size;

var start = 0;
var end = BYTES_PER_CHUNK;
while(start < SIZE) { ➌
var xhr = new XMLHttpRequest();
xhr.open('POST', '/upload');
xhr.onload = function() { ... };

xhr.setRequestHeader('Content-Range', start+'-'+end+'/'+SIZE); ➍
xhr.send(blob.slice(start, end)); ➎

start = end;
end = start + BYTES_PER_CHUNK;
}
  1. 任意数据(二进制或文本)的二进制对象
  2. 将块大小设置为 1 MB
  3. 以 1 MB 为步长迭代数据块
  4. 告诉服务器上传的数据范围(开始位置- 结束位置/ 总大小)
  5. 通过XHR 上传 1 MB 大小的数据片段

XHR 不支持请求流,这意味着在调用send() 时必须提供完整的文件。简单的解决方案:切分文件,然后通过多个 XHR 请求分段上传。

15.5 监控下载和上传进度

XHR对象提供一个方便的API用于监控进度事件:

表:XHR的进度相关事件

事件类型说明触发次数
loadstart传输已开始一次
progress正在传输零或多次
error传输出错零或多次
abort传输终止零或多次
load传输成功零或多次
loadend传输完成一次

要监控进度,可以在 XHR 对象上注册一系列 JavaScript 事件监听器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var xhr = new XMLHttpRequest();
xhr.open('GET','/resource');
xhr.timeout = 5000; ➊

xhr.addEventListener('load', function() { ... }); ➋
xhr.addEventListener('error', function() { ... }); ➌

var onProgressHandler = function(event) {
if(event.lengthComputable) {
var progress = (event.loaded / event.total) * 100; ➍
...
}
}

xhr.upload.addEventListener('progress', onProgressHandler); ➎
xhr.addEventListener('progress', onProgressHandler); ➏
xhr.send();
  1. 设置请求的超时时间为5000 ms(默认无超时限制)
  2. 为请求成功注册回调
  3. 为请求失败注册回调
  4. 计算传输进度
  5. 为上传进度事件注册回调
  6. 为下载进度事件注册回调

15.6 通过XHR实现流式数据传输

目前基于 XHR 的流实现起来既麻烦,效率又低。更糟糕的是,由于缺乏规范,浏览器实现一家一个样。结论就是,在 Streams API 规范正式推出之前,XHR 并不适合用来实现流式数据处理。

15.7 实时通知与交付

客户端与服务器同步:

  1. 客户端向服务器发送XHR请求,更新服务器上的数据
  2. 服务器数据更新,如何通知客户端呢? 那就是XHR轮询
15.7.1 轮询

从服务器取得更新的一个最简单的办法,就是客户端在后台定时发起 XHR 请求,也就是轮询(polling)。如果服务器有新数据,返回新数据,否则返回空响应。最佳的轮询间隔,没有唯一答案,轮询频率取决于应用的需要,权衡效率和消息延迟。轮询选择合适的轮询间隔:

  1. 长轮询间隔意味延迟交付
  2. 短轮询间隔导致客户端与服务器不必要的流量和协议开销

简单的示例:

1
2
3
4
5
6
7
8
function checkUpdates(url) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = function() { ... }; ➊
xhr.send();
}

setInterval("checkUpdates('/updates'), 60000"); ➋
  1. 处理服务器收到的更新
  2. 每60s发送一个XHR请求
15.7.2 长轮询

定时轮询会造成大量的空检查,如果没有更新不再返回空响应,而是把连接保持到有更新的时候,然后再返回,这便是长轮询。

轮询(左)与长轮询(右)的迟延

图:轮询(左)与长轮询(右)的迟延
1
2
3
4
5
6
7
8
9
10
11
function checkUpdates(url) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = function() { ➊
...
checkUpdates('/updates'); ➋
};
xhr.send();
}

checkUpdates('/updates'); ➌
  1. 处理更新并打开新的轮询XHR
  2. 发送长轮询请求并等待下次更新(不停循环)
  3. 发送第一次长轮询XHR请求

15.8 XHR使用场景及性能

尽管XHR 是一种“实时”交付的流行机制,但从性能角度说,它或许并不是最佳选择。现代浏览器支持比它更简单也更高效的API,比如Server-Sent EventsWebSocket。事实上,除非你有特别的理由要使用XHR 轮询,否则应该使用这些新技术。

第十六章 服务器发送事件

Server-Sent Events(SSE)设计两个组件,完成服务器上生成的实时通知或更新:

  1. EventSource:让客户端以DOM事件的形式接收到服务器推送的通知
  2. 新的“事件流”数据格式:用于交付每一次更新

16.1 EventSource API

EventSource接口隐藏所有底层细节,只需要指定SSE事件流资源的URL,并在对象上注册响应的JS事件监听器即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var source = new EventSource("/path/to/stream-url"); ➊

source.onopen = function () { ... }; ➋
source.onerror = function () { ... }; ➌

source.addEventListener("foo", function (event) { ➍
processFoo(event.data);
});

source.onmessage = function (event) { ➎
log_message(event.id, event.data);
if (event.id== "CLOSE") {
source.close(); ➏
}
}
  1. 打开到流重点的SSE连接
  2. 可选的回调,建立连接时调用
  3. 可选的回调,连接失败时调用
  4. 监听foo事件,调用自定义代码
  5. 监听所有事件,不明确指定事件类型
  6. 如果服务器发送“CLOSE”消息ID,关闭SSE连接

EventSource 接口还能自动重新连接并跟踪最近接收的消息:如果连接断开了,EventSource 会自动重新连接到服务器,还可以向服务器发送上一次接收到的消息ID,以便服务器重传丢失的消息并恢复流。

16.2 Event Stream协议

SSE 事件流是以流式HTTP 响应形式交付的:客户端发起常规HTTP 请求,服务器以自定义的“text/event-stream”内容类型响应,然后交付UTF-8 编码的事件数据。看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
=> 请求
GET /stream HTTP/1.1 ➊
Host: example.com
Accept: text/event-stream

<= 响应
HTTP/1.1 200 OK ➋
Connection: keep-alive
Content-Type: text/event-stream
Transfer-Encoding: chunked

retry: 15000 ➌
data: First message is a simple string. ➍
data: {"message": "JSON payload"} ➎
event: foo ➏
data: Message of type "foo"
id: 42 ➐
event: bar
data: Multi-line message of
data: type "bar" and id "42"
id: 43 ➑
data: Last message, id "43"
  1. 客户端通过 EventSource 接口发起连接
  2. 服务器以 “text/event-stream” 内容类型响应
  3. 服务器设置连接中断后重新连接的间隔时间(15 s)
  4. 不带消息类型的简单文本事件
  5. 不带消息类型的 JSON 数据载荷
  6. 类型为”foo” 的简单文本事件
  7. 带消息 ID 和类型的多行事件
  8. 带可选 ID 的简单文本事件

服务器还可以给每条消息关联任意 ID 字符串。浏览器会自动记录最后一次收到的消息ID,并在发送重连请求时自动在 HTTP 首部追加“Last-Event-ID”值。下面看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(既有SSE 连接)
retry: 4500 ➊
id: 43 ➋
data: Lorem ipsum

(连接断开)
(4500 ms 后)

=> 请求
GET /stream HTTP/1.1 ➌
Host: example.com
Accept: text/event-stream
Last-Event-ID: 43

<= 响应
HTTP/1.1 200 OK ➍
Content-Type: text/event-stream
Connection: keep-alive
Transfer-Encoding: chunked
id: 44 ➎
data: dolor sit amet
  1. 服务器将客户端的重连间隔设置为4.5 s
  2. 简单文本事件,ID:43
  3. 带最后一次事件ID 的客户端重连请求
  4. 服务器以 ‘text/event-stream’ 内容类型响应
  5. 简单文本事件,ID:44

16.3 SSE使用场景及性能

SSE是服务器向客户端发送实时文本消息的高性能机制:服务器可以在消息刚刚生成就将其推送给客户端(低延迟),使用长连接的事件流协议,而且可以gzip压缩(低开销),浏览器负责解析消息,也没有无限缓冲。

但SSE有两个局限:

  1. 只能从服务器向客户端发送数据,不能满足需要请求流的场景,比如向服务器流式上传大文件
  2. 事件流协议设计只能传输UTF-8数据,即使可以传输二进制流,效率也不高

第十七章 WebSocket

WebSocket实现客户端和服务器间双向、基于消息的文本或二进制数据传输,位于OSI模型的应用层。它是浏览器中最靠近套接字,但远远不是一个套接字,还提供更多服务:

  1. 连接协商和同源策略
  2. 与即有HTTP基础设施的互操作
  3. 基于消息的通信和高效消息分帧
  4. 子协议协商以及可扩展能力

17.1 WebSocket API

发起新连接,需要WebSocket资源的URL和应用回调:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var ws = new WebSocket('wss://example.com/socket'); ➊

ws.onerror = function (error) { ... } ➋
ws.onclose = function () { ... } ➌

ws.onopen = function () { ➍
ws.send("Connection established. Hello server!"); ➎
}

ws.onmessage = function(msg) { ➏
if(msg.data instanceof Blob) { ➐
processBlob(msg.data);
} else {
processText(msg.data);
}
}
  1. 打开新的安全WebSocket 连接(wss)
  2. 可选的回调,在连接出错时调用
  3. 可选的回调,在连接终止时调用
  4. 可选的回调,在WebSocket 连接建立时调用
  5. 客户端先向服务器发送一条消息
  6. 回调函数,服务器每发回一条消息就调用一次
  7. 根据接收到的消息,决定调用二进制还是文本处理逻辑
17.1.1 WS与WSS
  • ws:表示纯文本通信
  • wss:表示使用加密信道通信, TCP + TLS
17.1.2 接收文本和二进制数据

服务器发来了一个 1MB 的净荷,应用的 onmessage 回调只会在客户端接收到全部数据时才会被调用。WebSocket 协议不作格式假设,对应用的净荷也没有限制:文本或者二进制数据都没问题。从内部看,协议只关注消息的两个信息:净荷长度和类型(前者是一个可变长度字段),据以区别 UTF-8 数据和二进制数据。

浏览器接收到新消息后:

  1. 文本数据,会自动将其转换成DOMString 对象
  2. 二进制数据或Blob 对象,会直接将其转交给应用。或者作为性能优化,把接收到的二进制数据转换成ArrayBuffer而非Blob
17.1.3 发送文本和二进制数据

WebSocket 提供的是一条双向通信的信道,在同一个TCP 连接上,可以双向传输数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var ws = new WebSocket('wss://example.com/socket');

ws.onopen = function () {
socket.send("Hello server!"); ➊
socket.send(JSON.stringify({'msg': 'payload'})); ➋

var buffer = new ArrayBuffer(128);
socket.send(buffer); ➌

var intview = new Uint32Array(buffer);
socket.send(intview); ➍

var blob = new Blob([buffer]);
socket.send(blob); ➎
}
  1. 发送 UTF-8 编码的文本消息
  2. 发送 UTF-8 编码的 JSON 净荷
  3. 发送二进制ArrayBuffer
  4. 发送二进制ArrayBufferView
  5. 发送二进制Blob

send()方法是异步的,提供的数据会在客户端排队,而函数会立即返回。在传送大文件的时候,可以通过查询套接字的bufferedAmount属性监控浏览器中排队的数据量。也可以尝试将大文件切分多个小块,以避免队首阻塞。又或者实现优先队列,而非盲目的把数据推送到套接字上排队。

17.1.4 子协议协商

WebSocket 提供简单便捷的子协议协商API。客户端可以在初次连接握手时,告诉服务器自己支持哪种协议:

1
2
3
4
5
6
7
8
9
10
var ws = new WebSocket('wss://example.com/socket',
['appProtocol', 'appProtocol-v2']); ➊

ws.onopen = function () {
if (ws.protocol == 'appProtocol-v2') { ➋
...
} else {
...
}
}
  1. 在WebSocket 握手期间发送子协议数组
  2. 检查服务器选择了哪个子协议

那么可能存在两种情况:

  1. 从协商的协议中选择一个,那么客户端以对应协商的协议处理数据,触发客户端onopen回调
  2. 协商失败,WebSocket握手不完整,触发onerror回调,连接断开

17.2 WebSocket协议

WebSocket 通信协议包含两个高层组件:

  1. 开放性 HTTP 握手用于协商连接参数
  2. 二进制消息分帧机制用于支持低开销的基于消息的文本和二进制数据传输
17.2.1 二进制分帧

请区分文中的与数据传输层的:文中的帧是消息的最小通信单位,而数据传输层的帧是数据包格式,实际由上往下(应用层 -> 物理层)传输的过程,数据包会层层封装带上对应层协议的Header,也就是说数据传输层的比文中WebSocket应用层的帧的容量要大。

WebSocket把每个应用消息分成一个或者多个帧,发送到目的地后再组装起来,等待接收到完整的消息后再通知接收端

WebSocket帧格式:2~14字节+净荷

图:WebSocket帧格式:2~14字节+净荷
  • 帧:最小的通信单位,包括可变长度的帧首部和净荷,净荷可能包含完整或部分应用消息
  • 消息:一系列帧,与应用消息对等

WebSocket 帧消息格式:

  • 第一位(FIN)表示当前帧是不是消息的最后一帧。1:是最后一帧,0:非最后一帧
  • 操作码(4 位)表示被传输帧的类型:传输应用数据时,是文本(1)还是二进制(2);连接有效性检查时,是关闭(8)、呼叫(ping,9)还是回应(pong,10)。
  • 掩码位表示净荷是否有掩码(只适用于客户端发送给服务器的消息)。
  • 净荷长度由可变长度字段表示:
    • 如果是 0~125,就是净荷长度;
    • 如果是 126,则接下来 2 字节表示的 16 位无符号整数才是这一帧的长度;
    • 如果是 127,则接下来 8 字节表示的 64 位无符号整数才是这一帧的长度。
  • 掩码键包含 32 位值,用于给净荷加掩护。
  • 净荷包含应用数据,如果客户端和服务器在建立连接时协商过,也可以包含自定义的扩展数据。
17.2.2 协议扩展
  1. 多路复用扩展(A Multiplexing Extension for WebSockets):将WebSocket 的逻辑连接独立出来,使用“信道ID”扩展每个 WebSocket 帧,从而实现多个虚拟的WebSocket 信道共享一个TCP 连接
  2. 压缩扩展(Compression Extensions for WebSocket):WebSocket 规范没有压缩数据的机制或建议,除非应用实现自己的压缩和解压缩逻辑,否则很多情况下都会造成传输载荷过大的问题。实际上,压缩扩展就相当于HTTP 的传输编码协商。
17.2.3 HTTP升级协商
  • Sec-WebSocket-Version:客户端发送,表示它想使用的 WebSocket 协议版本(“13”表示RFC 6455)。如果服务器不支持这个版本,必须回应自己支持的版本。
  • Sec-WebSocket-Key:客户端发送,自动生成的一个键,作为一个对服务器的“挑战”,以验证服务器支持请求的协议版本。
  • Sec-WebSocket-Accept:服务器响应,包含 Sec-WebSocket-Key 的签名值,证明它支持请求的协议版本。
  • Sec-WebSocket-Protocol:用于协商应用子协议:客户端发送支持的协议列表,服务器必须只回应一个协议名。
  • Sec-WebSocket-Extensions:用于协商本次连接要使用的 WebSocket 扩展:客户端发送支持的扩展,服务器通过返回相同的首部确认自己支持一或多个扩展。

17.3 WebSocket使用场景及性能

17.3.1 请求和响应流

WebSocket 是唯一一个能通过同一个TCP 连接实现双向通信的机制(下图),客户端和服务器随时可以交换数据。因此,WebSocket 在两个方向上都能保证文本和二进制应用数据的低延迟交付。

XHR、SSE和WebSocket的通信比较

图:XHR、SSE和WebSocket的通信比较
17.3.2 消息开销

建立了 WebSocket 连接后,应用消息会被拆分为一或多个帧,每个帧会添加2~14 字节的开销。与XHR和SSE比较:

  • SSE 会给每个消息添加 5 字节,但仅限于 UTF-8 内容,参见 16.2 节“Event Stream协议”。
  • HTTP 1.x 请求(XHR 及其他常规请求)会携带 500~800 字节的 HTTP 元数据,加上cookie,参见 11.5 节“度量和控制协议开销”。
  • HTTP 2.0 压缩 HTTP 元数据,这样可以显著减少开销,参见 12.3.8 节“首部压缩”。事实上,如果请求都不修改首部,那么开销可以低至 8 字节!
17.3.3 数据效率及压缩
  • XHR:文本数据采用gzip压缩
  • SSE:utf-8文本数据,在整个会话期间使用gzip压缩
  • WebScoket:自定义压缩机制,并针对每个消息选择应用
17.3.4 自定义应用协议

浏览器是为HTTP 数据传输而优化的,它理解HTTP 协议,提供各种服务,比如认证、缓存、压缩,等等。而WebScoket协议错过浏览器提供的服务,由应用必须实现自己的逻辑来填充某些功能空白:比如缓存、状态管理、元数据交付,等等。

17.3.5 部署WebSocket基础设施

某些网络会完全屏蔽WebSocket通信,可以通过TLS建立端到端的加密信道,绕过所有的中间代理。

通信路径上的每一台负载均衡器、路由器、和Web服务器都必须正对长时连接进行调优。

17.4 性能检查表

无论是客户端还是服务器,都应该考虑下列要点:

  • 使用安全WebSocket(基于 TLS 的 WSS)实现可靠的部署。
  • 密切关注腻子脚本的性能(如果使用腻子脚本)。
  • 利用子协议协商确定应用协议。
  • 优化二进制净荷以最小化传输数据。
  • 考虑压缩 UTF-8 内容以最小化传输数据。
  • 设置正确的二进制类型以接收二进制净荷。
  • 监控客户端缓冲数据的量。
  • 切分应用消息以避免队首阻塞。
  • 合用的情况下利用其他传输机制。

对于移动应用而言,实时推送会造成电量的浪费。注意一下要点:

  1. 节约用电
  2. 消除周期性及无效的数据传输
  3. 内格尔(Nagle)及有效的服务器推送
  4. 消除不必要的长连接

第十八章 WebRTC

Web Real-Time Communication(Web 实时通信,WebRTC)由一组标准、协议和 JavaScript API 组成,用于实现浏览器之间(端到端)的音频、视频及数据共享。

关于封面

封面

封面

本书封面上的动物是马达加斯加鹞鹰(鹞属小鹰)。这种鹞鹰主要生活在科摩罗群岛(位于马达加斯加北面和非洲大陆东南面之间的印度洋中)和马达加斯加岛,由于生存环境缩小或遭受破坏,总量不断减少。最近发现这种鹞鹰的数量比原来想象得还要少,它们小群散居于岛上各处,每群大约250~500 只成年个体。

在马达加斯加岛上的湿地附近,植被环绕的湖泊、沼泽、滨海湿地和水田,是这种鹞鹰最常出没的捕食场所。它们主要猎食小型无脊椎动物和昆虫,包括小型鸟、蛇、蜥蜴、鼠类和家禽。捕食家禽的习性(数量只占总捕食量的1%),导致当地居民对它们大开杀戒。

每年的枯水季节(8 月末及9 月)是这种鹞鹰的繁殖季节。雨季开始的时候,孵化(约3234 天)已经结束,接下来雏鹰的丰羽期大约为4245 天。然而,这种鹞鹰的繁殖率始终不高,平均每巢繁殖丰羽幼鹰0.9 只,而只有四分之三的巢可以安然度过繁殖期。这么低的繁殖率缘于年复一年大范围的草原和沼泽烧荒(本地居民掏岛蛋及捅鸟窝也是一方面原因),目的是让大地长出新鲜的牧草和开荒,而且经常发生在鹞鹰繁殖的季节。鹞鹰繁衍需要安定的环境,而马达加斯加岛上的居民不断对土地的开发利用对此形成了威胁。

为保护这种鹞鹰,人们提出了一些建议,包括进一步调查摸清这种鹞鹰的具体数量,研究它们的种群动态,取得关于出巢率的准确信息,控制重点地带的烧荒行为——特别是在繁殖季节,以及围绕重点建巢区域划定和建立保护区。