跳至主要內容

读书笔记-03-Web性能权威指南

yyshino大约 63 分钟

Web性能权威指南

网络技术概览

本章主要介绍对所有网络通信都有决定性影响的两个方面:延迟和带宽 (图 1-1)。

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

TCP的构成

TCP三次握手

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

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

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

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

2.5针对TCPDF优化建议

UDP的构成

UDP(User Datagram Protocol,用户数据报协议) 被 John Postel 加入了核心网络协议套件。UDP 的主要功能和亮点并不 在于它引入了什么特性,而在于它忽略的那些特性。UDP 经常被称为无(Null)协 议,RFC 768 描述了其运作机制,全文完全可以写在一张餐巾纸上。

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

数据报(datagram)和分组(packet)是两个经常被人混用的词,实际上它们还是 有区别的。

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

关于 UDP 的应用,最广为人知同时也是所有浏览器和因特网应用都赖以运作的,就 是 DNS(Domain Name System,域名系统)。DNS 负责把对人类友好的主机名转换 成 IP 地址。可是,尽管浏览器有赖于 UDP,但这个协议以前从未被看成网页和应 用的关键传输机制。HTTP 并未规定要使用 TCP,但现实中所有 HTTP 实现(以及 构建于其上的所有服务)都使用 TCP。

不 过, 这 都 是 过 去 的 事 了。IETF 和 W3C 工 作 组 共 同 制 定 了 一 套 新 API—— WebRTC(Web Real-Time Communication,Web 实时通信)。WebRTC 着眼于在浏览 器中通过 UDP 实现原生的语音和视频实时通信,以及其他形式的 P2P(Peer-to-Peer, 端到端)通信。正是因为 WebRTC 的出现,UDP 作为浏览器中重要传输机制的地位 才得以突显,而且还有了浏览器 API。

无协议服务

image-20230331132738970
image-20230331132738970
image-20230331132812508
image-20230331132812508

UDP与网络地址装换器

针对UDP的优化建议

单播 UDP 应用程序给出了很多设计建议,简述如下:

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

传输层安全(TLS)

**SSL(Secure Sockets Layer,安全套接字层)**协议最初是网景公司为了保障网上交 易安全而开发的,该协议通过加密来保护客户个人资料,通过认证和完整性检查来 确保交易安全。为达到这个目标,SSL 协议在直接位于 TCP 上一层的应用层被实现 (图 4-1)。SSL 不会影响上层协议(如 HTTP、电子邮件、即时通讯),但能够保证 上层协议的网络通信安全。

image-20230331133901211
image-20230331133901211

加密、身份验证与完整性

TLS 协议的目标是为在它之上运行的应用提供三个基本服务:加密、身份验证和数 据完整性。从技术角度讲,并不是所有情况下都要同时使用这三个服务。比如,可 以接受证书但不验证其真实性,而前提是你非常清楚这样做有什么安全风险且有防 范措施。实践中,安全的 Web 应用都会利用这三个服务

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

TLS握手

客户端与服务器在通过 TLS 交换数据之前,必须协商建立加密信道。协商内容包括 TLS 版本、加密套件,必要时还会验证证书。然而,协商过程的每一步都需要一个 分组在客户端和服务器之间往返一次(图 4-2),因而所有 TLS 连接启动时都要经历 一定的延迟。

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

TLS回话回复

信任链与证书颁发机构

证书撤销

TLS记录协议

TLS优化建议

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

无线网络性能

无线网络概述

无线网络的类型

无线网络的性能基础

测量现实中的无线性能

WIFE

移动网络

移动网络的优化

HTTP

**HTTP(HyperText Transfer Protocol,超文本传输协议)**是互联网上最普遍采用的一 种应用协议,也是客户端与服务器之间的共用语言,是现代 Web 的基础。从最初的 一个关键字和文档路径开始,HTTP 最终不仅成为了浏览器的协议,而且也几乎成 为了所有互联网软件和硬件应用的协议。

本章将简略回顾一下 HTTP 协议的发展史。全面探讨 HTTP 的各种语义不是本书 的意图,但理解 HTTP 在设计上的关键转变,以及每次转变背后的动机——特别是 HTTP 2.0 将带来的很多改进,对我们讨论 HTTP 性能则至关重要。

HTTP简历史

HTTP 0.9:只有一行的协议

1991 年,Tim Berners-Lee 概述了这个新协议的动机,并罗列了几条宏观的设计目 标:支持文件传输、能够请求对超文本文档的索引搜索、格式化协商机制,以及能 够把客户端引导至不同的服务器。为了实际验证这个理论,他构建了一个简单的原 型,实现了建议的部分功能:

  • 客户端请求是一个 ASCII 字符串;
  • 客户端请求由一个回车符(CRLF)结尾;
  • 服务器响应是一个 ASCII 字符流;
  • 服务器响应的是一种超文本标记语言(HTML);
  • 连接在文档传输完毕后断开。

然而,即便这样说也比实际情况复杂。以上规则定义了一个极其简单、可以通过 Telnet 验证的协议,某些 Web 浏览器直到今天仍然支持:

$> telnet google.com 80
Connected to 74.125.xxx.xxx
GET /about/
(超文本响应)
(连接关闭)

请求只有一行,包括 GET 方法和要请求的文档的路径。响应是一个超文本文档,没 有首部,也没有其他元数据,只有 HTML。这实在是简单得不能再简单了!鉴于以 上交互行为只实现了部分预期目标,因此相应的协议也被非官方地称为 HTTP 0.9。 此后发生的事,大家都知道了。

以 1991 年这个低调开端为起点,HTTP 在随后几年中展现了自己的生命力,得到了 迅速发展。下面我们简单总结一下 HTTP 0.9 的功能:

  • 客户端 / 服务器、请求 / 响应协议;
  • ASCII 协议,运行于 TCP/IP 链接之上;
  • 设计用来传输超文本文档(HTML);
  • 服务器与客户端之间的连接在每次请求之后都会关闭。

HTTP 1.0:迅速发展及参考性RFC

1996 年, HTTP 工作组发布了 RFC 1945,解释说明了当时很多 HTTP 1.0 实现的“公共用 法”。不过,据我们所知,这个 RFC 只是参考性的。HTTP 1.0 并不是一个正式的规 范或互联网标准!

不管怎样,HTTP 1.0 的请求对我们而言应该是非常熟悉的:

$> telnet website.org 80
Connected to xxx.xxx.xxx.xxx
GET /rfc/rfc1945.txt HTTP/1.0User-Agent: CERN-LineMode/2.15 libwww/2.17b3
Accept: */*
HTTP/1.0 200 OK ➋
Content-Type: text/plain
Content-Length: 137582
Expires: Thu, 01 Dec 1997 16:00:00 GMT
Last-Modified: Wed, 1 May 1996 12:45:26 GMT
Server: Apache 0.84
(纯文本响应)
(连接关闭)

➊ 请求行中包含 HTTP 版本号,随后是请求首部

➋ 响应状态,后跟响应首部

上面列出的交换信息并未展示出 HTTP 1.0 的所有功能,但却能说明该协议的关键 变化:

  • 请求可以由于多行首部字段构成;
  • 响应对象前面添加了一个响应状态行;
  • 响应对象也有自己的由换行符分隔的首部字段;
  • 响应对象不局限于超文本;
  • 服务器与客户端之间的连接在每次请求之后都会关闭。

请求和响应首部都使用 ASCII 编码,但响应对象本身可以是任何类型:HTML 文 件、纯文本文件、图片,或其他内容类型。事实上,HTTP 中的“HTT”(Hypertext Transfer,超文本传输)在协议出现后不久就已经用词不当了。在实践中,HTTP 迅 速发展为超媒体传输协议,但最初的名字则沿用至今。

除了媒体类型协商,RFC 还解释了很多已经被实现的其他功能:内容编码、字符集 支持、多部分类型、认证、缓存、代理行为、日期格式,等等。

HTTP 1.1:互联网标准

HTTP 1.1 标准厘清了之前版本中很多有歧义的地方,而且还加入了很多重要的性能 优化:持久连接、分块编码传输、字节范围请求、增强的缓存机制、传输编码及请 求管道。

有了这些新功能,我们就可以像任何当前的 HTTP 浏览器和客户端一样检视 HTTP 1.1 会话:

$> telnet website.org 80
Connected to xxx.xxx.xxx.xxx
GET /index.html HTTP/1.1Host: website.org
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_4)... (snip)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
Cookie: __qca=P0-800083390... (snip)
HTTP/1.1 200 OK ➋
Server: nginx/1.0.11
Connection: keep-alive
Content-Type: text/html; charset=utf-8
Via: HTTP/1.1 GWA
Date: Wed, 25 Jul 2012 20:23:35 GMT
Expires: Wed, 25 Jul 2012 20:23:35 GMT
Cache-Control: max-age=0, no-cache
Transfer-Encoding: chunked
100 ➌
<!doctype html>
(snip)
100
(snip)
0 ➍
GET /favicon.ico HTTP/1.1Host: www.website.org
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_4)... (snip)
Accept: */*
Referer: http://website.org/
Connection: close ➏
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
Cookie: __qca=P0-800083390... (snip)
HTTP/1.1 200 OK ➐
Server: nginx/1.0.11
Content-Type: image/x-icon
Content-Length: 3638
Connection: close
Last-Modified: Thu, 19 Jul 2012 17:51:44 GMT
Cache-Control: max-age=315360000
Accept-Ranges: bytes
Via: HTTP/1.1 GWA
Date: Sat, 21 Jul 2012 21:35:22 GMT
Expires: Thu, 31 Dec 2037 23:55:55 GMT
Etag: W/PSA-GAu26oXbDi
(图标数据)
(关闭连接)

➊ 请求 HTML 文件,及其编码、字符集和元数据

➋ 对原始 HTML 请求的分块响应

➌ 以 ASCII 十六进制数字表示的分块数据的字节数(256 字节)

➍ 分块数据流响应结束

➎ 在同一个 TCP 连接上请求图标文件

➏ 通知服务器不再使用连接了

➐ 图标响应,随后关闭连接

最明显的差别是这里发送了两次对象请求,一次请 求 HTML 页面,一次请求图片,这两次请求都是通过一个连接完成的。这个连接是 持久的,因而可以重用 TCP 连接对同一主机发送多次请求,从而实现更快的用户体 验

HTTP 2.0:改进传输性能

HTTP 2.0 的主要目标是改进传输性能,实现低延迟和高吞吐量。主版本号的增加听 起来像是要做大的改进,从性能角度说的确如此。但从另一方面看,HTTP 的高层 协议语义并不会因为这次版本升级而受影响。所有 HTTP 首部、值,以及它们的使 用场景都不会变。

Web性能要点

在任何复杂的系统中,性能优化的很大一部分工作就是把不同层之间的交互过程分 解开来,弄清楚每一层次交互的约束和限制。到目前为止,我们已经比较详细地分 析了一些个别网络组件(不同的物理交付方式和传输协议)。现在,我们把目光转向 更宏观的 Web 性能优化:

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

超文本、网页和Web应用

互联网在过去几十年的发展过程中,至少带给了我们三种体验:超文本文档、富媒 体网页和交互式 Web 应用

  • 超文本文档:万维网就起源于超文本文档,一种只有基本格式,但支持超链接的纯文本文档。

  • 富媒体网页:HTML 工作组和早期的浏览器开发商扩展了超文本,使其支持更多的媒体,如图 片和音频,同时也为丰富布局增加了很多手段。

  • Web应用:JavaScript 及后来 DHTML 和 Ajax 的加入,再一次革命了 Web,把简单的网页 转换成了交互式 Web 应用。Web 应用可以在浏览器中直接响应用户操作。于是, Outlook Web Access(IE5 中的 XMLHTTP 就诞生于这个应用)等最早的、成熟的浏 览器应用出现,也揭开了脚本、样式表和标记文档之间复杂依赖的新时代。

DOM、CSSOM 和 JavaScript

前所说的“脚本、样式表和标记文档之间复杂依赖”到底指什么呢?要回答这个 问题,我们得先回顾一下浏览器架构,了解一下解析、布局和脚本如何相互配合 在屏幕上绘制出像素来(图 10-1)

image-20230331151223506
image-20230331151223506

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

然而,此时不得不提到我们最大的朋友和祸害:JavaScript。脚本执行过程中可能 遇到一个同步的 document.write,从而阻塞 DOM 的解析和构建。类似地,脚本 也可能查询任何对象的计算样式,从而阻塞 CSS 处理。结果,DOM 及 CSSOM 的构建频繁地交织在一起:DOM 构建在 JavaScript 执行完毕前无法进行,而 JavaScript 在 CSSOM 构建完成前也无法进行。

应用的性能,特别是首次加载时的“渲染前时间”,直接取决于标记、样式表和 JavaScript 这三者之间的依赖关系。顺便说一句,还记得流行的“样式在上,脚本 在下”的最佳实践吗?现在你该知道为什么了。渲染和脚本执行都会受样式表的 阻塞,因此必须让 CSS 以最快的速度下载完。

剖析现代Web应用

现代 Web 应用到底长啥样? HTTP Archive (http://httparchive.org/)可以回答这个问 题。这个网站项目一直在抓取世界上是热门的网站(Alexa 前 100 万名中的 30 多万 名),记录、聚合并分析每个网站使用的资源、内容类型、首部及其他元数据的数量。

数独、性能与用户期望
分析资源瀑布

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

延迟是性能的瓶颈

人造和真实用户性能度量

针对浏览器的优化建议

不得不说,浏览器可远远不止一个网络套接字管理器那么简单。性能可以说是每个 浏览器开发商的核心卖点,既然性能如此重要,那浏览器越来越聪明也就毫不奇怪 158 | 第 10 章 了。预解析可能的 DNS 查询、预连接可能的目标、预取得和优先取得重要资源,这 些都是浏览器变聪明的标志。

可行的优化手段会因浏览器而异,但从核心优化策略来说,可以宽泛地分为两类。

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

好消息是,所有这些优化都由浏览器替我们自动完成,经常可以节省几百 ms 的网 络延迟。既然如此,那理解这些优化背后的原理就至关重要了,这样才能利用浏览 器的这些特性,提升应用性能。大多数浏览器都利用了如下四种技术。

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

HTTP 1.x

改进 HTTP 的性能是 HTTP 1.1 工作组的一个重要目标,后来这个版本也引入了大 量增强性能的重要特性,其中一些大家比较熟知的有:

  • 持久化连接以支持连接重用;
  • 分块传输编码以支持流式响应;
  • 请求管道以支持并行请求处理;
  • 字节服务以支持基于范围的资源请求;
  • 改进的更好的缓存机制。

当然,这些只是其中一部分,要全面讨论 HTTP 1.1 的所有增强特性,非得用一本 书不可。同样,推荐大家买一本《HTTP 权威指南》(David Gourley 和 Brian Totty 合著)放在手边。另外,提到好的参考书,Steve Souder 的《高性能网站建设指南》 中概括了 14 条规则,有一半针对网络优化:

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

持久连接的优点

在我们两个请求的例子中,总共只节约了一次往返时间。但是,更常见的情况是一 次 TCP 连接要发送 N 次 HTTP 请求,这时:

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

在启用持久连接的情况下,N 次请求节省的总延迟时间就是(N-1)×RTT。还记 得吗,前面说过,在当代 Web 应用中,N 的平均值是 90,而且还在继续增加(10.2 节“剖析现代 Web 应用”)。因此,依靠持久连接节约的时间,很快就可以用秒来衡 量了!这充分说明持久化 HTTP 是每个 Web 应用的关键优化手段。

HTTP管道

使用多个TCP连接

域名分区

HTTP 1.x 协议的一项空白强迫浏览器开发商引入并维护着连接池,每个主机最多 6 个 TCP 流。好的一方面是对这些连接的管理工作都由浏览器来处理。作为应用开发 者,你根本不必修改自己的应用。不好的一方面呢,就是 6 个并行的连接对你的应 用来说可能仍然不够用

度量和控制协议开销

连接与拼合

最快的请求是不用请求。不管使用什么协议,也不管是什么类型的应用,减少请求 次数总是最好的性能优化手段。可是,如果你无论如何也无法减少请求,那么对 HTTP 1.x 而言,可以考虑把多个资源捆绑打包到一块,通过一次网络请求获取:

  • 连接:把多个 JavaScript 或 CSS 文件组合为一个文件。
  • 拼合:把多张图片组合为一个更大的复合的图片。

对 JavaScript 和 CSS 来说,只要保持一定的顺序,就可以做到把多个文件连接起来 而不影响代码的行为和执行。类似地,多张图片可以组合为一个“图片精灵”,然后 使用 CSS 选择这张大图中的适当部分,显示在浏览器中。这两种技术都具备两方面 的优点。

  • 减少协议开销:通过把文件组合成一个资源,可以消除与文件相关的协议开销。如前所述,每个 文件很容易招致 KB 级未压缩数据的开销。
  • 应用层管道:说到传输的字节,这两种技术的效果都好像是启用了 HTTP 管道:来自多个响应 的数据前后相继地连接在一起,消除了额外的网络延迟。实际上,就是把管道提 高了一层,置入了应用中。

连接和拼合技术都属于以内容为中心的应用层优化,它们通过减少网络往返开销, 可以获得明显的性能提升。可是,实现这些技术也要求额外的处理、部署和编码 (比如选择图片精灵中子图的 CSS 代码),因而也会给应用带来额外的复杂性。此 外,把多个资源打包到一块,也可能给缓存带来负担,影响页面的执行速度。

要理解为什么这些技术会伤害性能,可以考虑一种并不少见的情况:一个包含十来 个 JavaScript 和 CSS 文件的应用,在产品状态下把所有文件合并为一个 CSS 文件和 一个 JavaScript 文件。

  • 相同类型的资源都位于一个 URL(缓存键)下面。
  • 资源包中可能包含当前页面不需要的内容。
  • 对资源包中任何文件的更新,都要求重新下载整个资源包,导致较高的字节开销。
  • JavaScript 和 CSS 只有在传输完成后才能被解析和执行,因而会拖慢应用的执行 速度。

实践中,大多数 Web 应用都不是只有一个页面,而是由多个视图构成。每个视图都 有自己的资源,同时资源之间还有部分重叠:公用的 CSS、JavaScript 和图片。实 际上,把所有资源都组合到一个文件经常会导致处理和加载不必要的字节。虽然可 以把它看成一种预获取,但代价则是降低了初始启动的速度

对很多应用来说,更新资源带来的问题更大。更新图片精灵或组合 JavaScript 文件 中的某一处,可能就会导致重新传输几百 KB 数据。由于牺牲了模块化和缓存粒度, 假如打包资源变动频率过高,特别是在资源包过大的情况下,很快就会得不偿失。 如果你的应用真到了这种境地,那么可以考虑把“稳定的核心”,比如框架和库,转 移到独立的包中。

image-20230331165654188
image-20230331165654188
image-20230331165715182
image-20230331165715182

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

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

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

image-20230331165836671
image-20230331165836671

嵌入资源

嵌入资源是另一种非常流行的优化方法,把资源嵌入文档可以减少请求的次 数。比如,JavaScript 和 CSS 代码,通过适当的 script 和 style 块可以直接放在 页面中,而图片甚至音频或 PDF 文件,都可以通过数据 URI(data:[mediatype] [;base64],data)的方式嵌入到页面中

虽然 CSS 和 JavaScript 等基于文本的资源很容易直接嵌入页面,也不会带来 多余的开销,但非文本性资源则必须通过 base64 编码,而这会导致开销明显增大: 编码后的资源大小比原大小增大 33%

实践中,常见的一个经验规则是只考虑嵌入 1~2 KB 以下的资源,因为小于这个标 准的资源经常会导致比它自身更高的 HTTP 开销。然而,如果嵌入的资源频繁变更, 又会导致宿主文档的无效缓存率升高。嵌入资源也不是完美的方法。如果你的应用 要使用很小的、个别的文件,在考虑是否嵌入时,可以参照如下建议:

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

HTTP 2.0

HTTP 2.0 的目的就是通过支持请求与响应的多路复用来减少延迟,通过压缩 HTTP 首部字段将协议开销降至最低,同时增加对请求优先级和服务器端推送的支持。为 达成这些目标,HTTP 2.0 还会给我们带来大量其他协议层面的辅助实现,比如新的 流量控制、错误处理和更新机制。上述几种机制虽然不是全部,但却是最重要的, 所有 Web 开发者都应该理解并在自己的应用中利用它们。

HTTP 2.0 不会改动 HTTP 的语义。HTTP 方法、状态码、URI 及首部字段,等等这 些核心概念一如往常。但是,HTTP 2.0 修改了格式化数据(分帧)的方式,以及客 户端与服务器间传输这些数据的方式。这两点统帅全局,通过新的组帧机制向我们 的应用隐藏了所有复杂性。换句话说,所有原来的应用都可以不必修改而在新协议 运行。这当然是好事。

历史及其与SPDY的渊源

SPDY 是谷歌开发的一个实验性协议,于 2009 年年中发布,其主要目标是通过解决 HTTP 1.1 中广为人知的一些性能限制,来减少网页的加载延迟。大致上,这个项目 设定的目标如下:

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

走向HTTP 2.0

SPDY 是 HTTP 2.0 的催化剂,但 SPDY 并非 HTTP 2.0。2012 年初,W3C 向社会征 集 HTTP 2.0 的建议,HTTP-WG 经过内部讨论,决定将 SPDY 规范作为制定标准的 基础。从那时起,SPDY 已经经过了很多变化和改进,而且在 HTTP 2.0 官方标准公 布之前,还将有很多变化和改进。

设计和技术目标

二进制分帧层
流、消息和帧
多向请求与响应
请求优先级
每个来源一个连接
流量控制
服务器推送
首部压缩
有效的HTTP 2.0升级与发现

二进制分帧简介

优化应用的交付

经典的性能优化最佳实践

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

  • 减少DNS查找:每一次主机名解析都需要一次网络往返,从而增加请求的延迟时间,同时还会阻 塞后续请求。
  • 重用TCP连接:尽可能使用持久连接,以消除 TCP 握手和慢启动延迟;参见 2.2.2 节“慢启动”。
  • 减少HTTP重定向:HTTP 重定向极费时间,特别是不同域名之间的重定向,更加费时;这里面既有 额外的 DNS 查询、TCP 握手,还有其他延迟。最佳的重定向次数为零。
  • 使用CDN(内容分发网络):把数据放到离用户地理位置更近的地方,可以显著减少每次 TCP 连接的网络延 迟,增大吞吐量。这一条既适用于静态内容,也适用于动态内容;参见 4.7.2 节 中的“不缓存的原始获取”。
  • 去掉不必要的资源:任何请求都不如没有请求快。

说到这,所有建议都无需解释。延迟是瓶颈,最快的速度莫过于什么也不传输。然 而,HTTP 也提供了很多额外的机制,比如缓存和压缩,还有与其版本对应的一些 性能技巧

  • 在客户端缓存资源:应该缓存应用资源,从而避免每次请求都发送相同的内容。
  • 传输压缩过的内容:传输前应该压缩应用资源,把要传输的字节减至最少:确保对每种要传输的资源 采用最好的压缩手段。
  • 消除不必要的请求开销:减少请求的 HTTP 首部数据(比如 HTTP cookie),节省的时间相当于几次往返 的延迟时间。
  • 并行处理请求和响应:请求和响应的排队都会导致延迟,无论是客户端还是服务器端。这一点经常被忽 视,但却会无谓地导致很长延迟。
  • 针对协议版本采取优化措施:HTTP 1.x 支持有限的并行机制,要求打包资源、跨域分散资源,等等。相对而 言,HTTP 2.0 只要建立一个连接就能实现最优性能,同时无需针对 HTTP 1.x 的 那些优化方法。
在客户端缓存资源

要说最快的网络请求,那就是不用发送请求就能获取资源。将之前下载过的数据缓 存并维护好,就可以做到这一点。对于通过 HTTP 传输的资源,要保证首部包含适 当的缓存字段:

  • Cache-Control 首部用于指定缓存时间;
  • Last-Modified 和 ETag 首部提供验证机制。

只要可能,就给每种资源都指定一个明确的缓存时间。这样客户端就可以直接使用 本地副本,而不必每次都请求相同的内容。类似地,指定验证机制可以让客户端检 查过期的资源是否有更新。没有更新,就没必要重新发送。

最后,还要注意应同时指定缓存时间和验证方法!只指定其中之一是最常见的错误, 于是要么导致每次都在没有更新的情况下重发相同内容(这是没有指定验证),要么 导致每次使用资源时都多余地执行验证检查(这是没有指定缓存时间)。

压缩传输的数据

利用本地缓存可以让客户端避免每次请求都重复取得数据。不过,还是有一些资源 是必须取得的,比如原来的资源过期了,或者有新资源,再或者资源不能缓存。对 于这些资源,应该保证传输的字节数最少。因此要保证对它们进行最有效的压缩。

HTML、CSS 和 JavaScript 等文本资源的大小经过 gzip 压缩平均可以减少 60%~80%。 而图片则需要仔细考量:

  • 图片一般会占到一个网页需要传输的总字节数的一半;
  • 通过去掉不必要的元数据可以把图片文件变小;
  • 要调整大小就在服务器上调整,避免传输不必要的字节;
  • 应该根据图像选择最优的图片格式;
  • 尽可能使用有损压缩。

不同图片格式的压缩率迥然不同,因为不同的格式是分别为不同使用场景设计的。 事实上,如果选错了图片格式(比如,使用了 PNG 而非 JPG 或 WebP),多产生几 百甚至上千 KB 数据是轻而易举的事。建议大家多找一些工具和自动化手段,以确 定最佳图片格式。

选定图片格式后,其次就是不要让图片超过它需要的大小。如果在客户端对超出需 要大小的图片做调整,那么除了额外传输不必要的字节之外,还会浪费 CPU、GPU 和内存资源(参见 11.6 节的“计算图片对内存的需求”)。

最后,选择了正确的格式,确定了必需的大小,接下来就要研究使用哪一种有损图 片格式,比如 JPEG 还是 WebP,以及压缩到哪个级别:较高压缩率可以明显减少字 节数,同时图片品质不会有太大或太明显的损失,尤其是在较小(手机)的屏幕上 看,不容易发现。

image-20230331171744866
image-20230331171744866
消除不必要的请求字节

HTTP 是一种无状态协议,也就是说服务器不必保存每次请求的客户端的信息。然 而,很多应用又依赖于状态信息以实现会话管理、个性化、分析等功能。为了实现 这些功能,HTTP State Management Mechanism(RFC 2965)作为扩展,允许任何 网站针对自身来源关联和更新 cookie 元数据:浏览器保存数据,而在随后发送给来 源的每一个请求的 Cookie 首部中自动附加这些信息。

并行处理请求和响应

浏览器API与协议

浏览器网络概述

XMLHttpRequest

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

XHR 诞生前,网页要获取客户端和服务器的任何状态更新,都必须刷新一次。有了 XHR,这个过程就可以异步实现,而且完全通过应用的 JavaScript 代码完成。XHR 是让我们从制作网页转换为开发交互应用的根本技术。

然而,XHR 的能力不仅仅表现在能实现浏览器的异步通信,还表现在它极大地简化 了这个异步通信过程。XHR 是浏览器提供的应用 API,这就意味着浏览器会自动帮 我们完成所有底层的连接管理、协议协商、HTTP 请求格式化,以及更多工作:

  • 浏览器管理着连接建立、套接字池和连接终止;
  • 浏览器决定最佳的 HTTP(S)传输协议(HTTP 1.0、1.x 和 2.0);
  • 浏览器处理 HTTP 缓存、重定向和内容类型协商;
  • 浏览器保障安全、验证和隐私;
  • 浏览器……

XHR简史

尽管名字里有 XML 的 X,XHR 也不是专门针对 XML 开发的。这只是因为 Internet Explorer 5 当初发布它的时候,把它放到 MSXML 库里,这才“继承”了这个 X:

Mozilla 按照微软的实现也实现了自己的 XHR,并将其命名为 XMLHttpRequest。 Safari、Opera 和其他浏览器也紧随其后,于是 XHR 成为了所有主流浏览器中的事 实标准。W3C 针对 XHR 的官方工作草案发布于 2006 年,而这已经是 XHR 得到广 泛应用以后的事了!

虽然它在 AJAX 革命中扮演了至关重要的角色,但 XHR 的早期版本确实能力有限: 只能传输文本,处理上传的能力不足,而且不能处理跨域请求。为解决这些问题, W3C 于 2008 年发布了“XMLHttpRequest Level 2”草案,新增了如下一些新功能:

  • 支持请求超时;
  • 支持传输二进制和文本数据;
  • 支持应用重写媒体类型和编码响应;
  • 支持监控每个请求的进度事件;
  • 支持有效的文件上传;
  • 支持安全的跨来源请求。

2011 年,“XMLHttpRequest Level 2” 规 范 与 原 来 的 XMLHttpRequest 工 作 草 案 合并。此后,无论人们提及 XHR 时说 Level 1 还是 Level 2,其实已经没有关系 了。今天,只有一个统一的 XHR 规范。而所有新的 XHR2 功能,都是通过同一个 XMLHttpRequest API 提供的:接口不变,功能增强。

跨域资源共享(CORS)

XHR 是一个浏览器层面的 API,向我们隐藏了大量底层处理,包括缓存、重定向、 内容协商、认证,等等。这样做有两个目的。第一,XHR 的 API 因此非常简单,开 发人员可以专注业务逻辑。其次,浏览器可以采用沙箱机制,对应用代码强制施加 一套安全限制。

XHR 接口强制要求每个请求都严格具备 HTTP 语义:应用提供数据和 URL,浏览 器格式化请求并管理每个连接的完整生命周期。类似地,虽然 XHR API 允许应用添 加自定义的 HTTP 首部(通过 setRequestHeader() 方法),同时也有一些首部是应用 代码不能设定的:

  • Accept-Charset、Accept-Encoding、Access-Control-*
  • Host、Upgrade、Connection、Referer、Origin
  • Cookie、Sec-、Proxy- 以及很多其他首部

浏览器会拒绝对不安全首部的重写,以此保证应用不能假扮用户代理、用户或请求 来源。事实上,保护来源(Origin)首部特别重要,因为这是对所有 XHR 请求应用 “同源策略”的关键。

同源策略的出发点很简单:浏览器存储着用户数据,比如认证令牌、cookie 及其 他私有元数据,这些数据不能泄露给其他应用。如果没有同源沙箱,那么 example. com 中的脚本就可以访问并操纵 thirdparty.com 的用户数据!

为解决这个问题,XHR 的早期版本都限制应用只能执行同源请求,即新请求的来 源必须与旧请求的来源一致:来自 example.com 的 XHR 请求,只能从 example.com 请求其他资源。如果后续请求不同源,浏览器就拒绝该 XHR 请求并报错。

可是,在某些必要的情况下,同源策略也会给更好地利用 XHR 带来麻烦:如果服 务器想要给另一个网站中的脚本提供资源怎么办?这就是 Cross-Origin Resource Sharing(跨源资源共享,CORS)的来由! CORS 针对客户端的跨源请求提供了安 全的选择同意机制:

通过XHR下载数据

XHR 既可以传输文本数据,也可以传输二进制数据。事实上,浏览器可以自动为各 种原生数据类型提供编码和解码服务,因此应用在直接将这些数据传给 XHR 时就 已经编码 / 解码好了,反之亦然。浏览器可以自动解码的数据类型如下。

  • ArrayBuffer 固定长度的二进制数据缓冲区。
  • Blob 二进制大对象或不可变数据。
  • Document 解析后得到的 HTML 或 XML 文档。
  • JSON 表示简单数据结构的 JavaScript 对象。
  • Text 简单的文本字符串。

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

var xhr = new XMLHttpRequest();
xhr.open('GET', '/images/photo.webp');
XMLHttpRequest | 231
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();

➊ 将返回数据类型设置为 Blob

➋ 基于返回的对象创建唯一的对象 URI 并设置为图片的源

➌ 图片加载完毕后立即释放对象

这里的二进制大对象接口(Blob)属于 HTML5 的 File API,就像一个不 透明的引用,可以指向任何数据块(二进制或文本)。这个对象本身没有太 多功能,只能查询其大小、MIME 类型,或将它切分成更小的块。这个对 象存在的真正目的,是作为各种 JavaScript API 之间的一种高效的互操作 机制。

通过XHR上传数据

通过 XHR 上传任何类型的数据都很简单,而且高效。事实上,上传不同类型数据 的代码都一样,只不过最后在调用 XHR 请求对象的 send() 方法时,要传入相应的 数据对象。剩下的事就都由浏览器处理了:

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);

➊ 把简单的文本字符串上传到服务器

➋ 通过 FormData API 动态创建表单数据

➌ 向服务器上传 multipart/form-data 对象

➍ 创建无符号、8 字节整型的有类型数组(ArrayBuffer)

➎ 向服务器上传字节块

XHR 对象的 send() 方法可以接受 DOMString、Document、FormData、Blob、File 及 ArrayBuffer 对象,并自动完成相应的编码,设置适当的 HTTP 内容类型 (content-type), 然后再分派请求。需要发送二进制 Blob 或上传用户提交的文件?简单,取得对该对 象的引用,传给 XHR。事实上,多写几行代码,还可以把大文件切成几小块:

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 MB XMLHttpRequest

➌ 以 1 MB 为步长迭代数据块

➍ 告诉服务器上传的数据范围(开始位置 - 结束位置 / 总大小)

➎ 通过 XHR 上传 1 MB 大小的数据片段

XHR 不支持请求流,这意味着在调用 send() 时必须提供完整的文件。不过,前面 的例子示范了一个简单的解决方案:切分文件,然后通过多个 XHR 请求分段上传。 这种实现方案当然不能替代真正的请求流 API,但对某些应用来说却是一个可行的 方案。

切分大文件上传是个不错的技巧,适合连接不稳定或经常中断的场景。此 时,假如某个块由于掉线而上传失败,应用可以随后只重新上传该块,而 不必重新上传整个大文件。

监控下载和上传进度

网络连接可能会间歇性中断,而延迟和带宽也高度不稳定。因此,我们怎么知道 XHR 请求成功了,超时了,还是失败了? XHR 对象提供了一个方便的 API,用于 监控进度事件(表 15-1),这些事件代表请求的当前状态。

image-20230331174023748
image-20230331174023748

每个 XHR 请求开始时都会触发 loadstart 事件,而结束时都会触发 loadend 事件。 在这两事件之间,还可能触发一或多个其他事件,表示传输状态。因此,要监控进 度,可以在 XHR 对象上注册一系列 JavaScript 事件监听器:

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();

➊ 设置请求的超时时间为 5000 ms(默认无超时限制)

➋ 为请求成功注册回调

➌ 为请求失败注册回调

➍ 计算传输进度

➎ 为上传进度事件注册回调

➏ 为下载进度事件注册回调

无 论 load 和 error 中 的 哪 一 个 被 触 发 了, 都 代 表 XHR 传 输 的 最 终 状 态, 而 progress 事件则可能触发任意多次,这就为监控传输状态提供了便利:我们可以比 较 loaded 与 total 属性,估算传输完成的数据比例。

通过XHR实现流式数据传输

实时通知与交付

通过XHR实现轮询

从服务器取得更新的一个最简单的办法,就是客户端在后台定时发起 XHR 请求, 也就是轮询(polling)。如果服务器有新数据,返回新数据,否则返回空响应。

轮询实现起来简单,但也经常效率很低。其中关键在于选择轮询间隔:长轮询间隔 意味着延迟交付,而短轮询间隔会导致客户端与服务器间不必要的流量和协议开销。 下面看一个简单的例子:

function checkUpdates(url) {
 var xhr = new XMLHttpRequest();
 xhr.open('GET', url);
 xhr.onload = function() { ... }; ➊
 xhr.send();
}
setInterval("checkUpdates('/updates'), 60000");

➊ 处理从服务器收到的更新

➋ 每 60 秒发送一个 XHR 请求

  • 每个 XHR 请求都是一次独立的 HTTP 请求,平均算下来,每个 HTTP 会带有大 约 800 字节的请求 / 响应首部(不算 HTTP cookie)。
  • 定时检查很有效,前提是数据能按时到达。可惜的是,按时到达只是例外,而非 常规情形。于是,定时轮询也会导致服务器端消息可用与交付给客户端之间的额 外延迟。
  • 除非考虑周到,否则轮询对于无线网络来说常常会变成性能杀手(参见 8.2 节“消 除周期性及无效的数据传输”)。唤醒无线电模块会消耗很多电量!
image-20230331175934551
image-20230331175934551
通过XHR实现长轮询

定时轮询的一个大问题就是很可能造成大量没必要的空检查。记住这一点之后,让 我们来对这个轮询过程做一改进(图 15-1):在没有更新的时候不再返回空响应, 而是把连接保持到有更新的时候。

image-20230331180027013
image-20230331180027013

XHR使用场景及性能

XMLHttpRequest是我们从在浏览器中做网页转向开发 Web 应用的关键。首先,它 让我们在浏览器中实现了异步通信,但同样重要的是,它还把这个过程变得非常简 XMLHttpRequest。分派和控制 HTTP 请求只要几行 JavaScript 代码,而其他复杂的任务浏览器都 包办了:

  • 浏览器格式化 HTTP 请求并解析响应;
  • 浏览器强制施加相关的安全(同源)策略;
  • 浏览器处理内容协商(如 gzip 压缩);
  • 浏览器处理请求和响应的缓存;
  • 浏览器处理认证、重定向……

服务器发送事件

Server-Sent Events(SSE)让服务器可以向客户端流式发送文本消息,比如服务器 上生成的实时通知或更新。为达到这个目标,SSE 设计了两个组件:浏览器中的 EventSource 和新的“事件流”数据格式。其中,EventSource 可以让客户端以 DOM 事件的形式接收到服务器推送的通知,而新数据格式则用于交付每一次更新。

WebSocket

WebSocket 可以实现客户端与服务器间双向、基于消息的文本或二进制数据传输。它 是浏览器中最靠近套接字的 API。但 WebSocket 连接远远不是一个网络套接字,因为 浏览器在这个简单的 API 之后隐藏了所有的复杂性,而且还提供了更多服务:

  • 连接协商和同源策略;
  • 与既有 HTTP 基础设施的互操作;
  • 基于消息的通信和高效消息分帧;
  • 子协议协商及可扩展能力。

WebSocket 是浏览器中最通用最灵活的一个传输机制,其极简的 API 可以让我们在客 户端和服务器之间以数据流的形式实现各种应用数据交换(包括 JSON 及自定义的 二进制消息格式),而且两端都可以随时向另一端发送数据。

不过,自定义数据交换协议的问题通常也在于自定义。因为应用必须考虑状态管理、 压缩、缓存及其他原来由浏览器提供的服务。设计限制和性能权衡始终会有,利用 WebSocket 也不例外。简单来说,WebSocket 并不能取代 HTTP、XHR 或 SSE,而为 了追求最佳性能,关键还是要利用这些机制的长处。

WebSocket API

浏览器提供的 WebSocket API 可谓简约。当然,简约背后隐藏着连接管理和消息处 理等底层细节。发起新连接,需要 WebSocket 资源的 URL 和一些应用回调:

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);
 }
}

➊ 打开新的安全 WebSocket 连接(wss)

➋ 可选的回调,在连接出错时调用

➌ 可选的回调,在连接终止时调用

➍ 可选的回调,在 WebSocket 连接建立时调用

➎ 客户端先向服务器发送一条消息

➏ 回调函数,服务器每发回一条消息就调用一次

➐ 根据接收到的消息,决定调用二进制还是文本处理逻辑

这个 API 非常直观。事实上,应该说它与上一章介绍的 EventSource API 很像。这 是故意这么设计的,因为 WebSocket 也提供类似和扩展的功能。当然,除了相似性 之外,还有很多重要的差别。下面我们就逐个介绍。

WS与WSS

WebSocket 资源 URL 采用了自定义模式:ws 表示纯文本通信(如 ws://example. com/socket),wss 表示使用加密信道通信(TCP+TLS)。为什么不使用 http 而要自 定义呢?

WebSocket 的主要目的,是在浏览器中的应用与服务器之间提供优化的、双向通信机 制。可是,WebSocket 的连接协议也可以用于浏览器之外的场景,可以通过非 HTTP 协商机制交换数据。考虑到这一点,HyBi Working Group 就选择采用了自定义的 URL 模式。

接收文本和二进制数据

WebSocket 通信只涉及消息,应用代码无需担心缓冲、解析、重建接收到的数据。比 如,服务器发来了一个 1 MB 的净荷,应用的 onmessage 回调只会在客户端接收到全 部数据时才会被调用。

此外,WebSocket 协议不作格式假设,对应用的净荷也没有限制:文本或者二进制数 据都没问题。从内部看,协议只关注消息的两个信息:净荷长度和类型(前者是一 个可变长度字段),据以区别 UTF-8 数据和二进制数据。

浏览器接收到新消息后,如果是文本数据,会自动将其转换成 DOMString 对象,如 果是二进制数据或 Blob 对象,会直接将其转交给应用。唯一可以(作为性能暗示和 优化措施)多余设置的,就是告诉浏览器把接收到的二进制数据转换成 ArrayBuffer 而非 Blob:

var ws = new WebSocket('wss://example.com/socket');
ws.binaryType = "arraybuffer"; ➊
ws.onmessage = function(msg) {
 if(msg.data instanceof ArrayBuffer) {
 processArrayBuffer(msg.data);
 } else {
 processText(msg.data);
 }
}

➊ 如果接收到二进制数据,将其强制转换成 ArrayBuffer

Blob 对象一般代表一个不可变的文件对象或原始数据。如果你不需要修改它或者不 需要把它切分成更小的块,那这种格式是理想的(比如,可以把一个完整的 Blob 对 象传给 img 标签,参见 15.3 节“通过 XHR 下载数据”)。而如果你还需要再处理接 收到的二进制数据,那么选择 ArrayBuffer 应该更合适。

image-20230331200223842
image-20230331200223842
发送文本和二进制数据

建立了 WebSocket 连接后,客户端就可以随时发送或接收 UTF-8 或二进制消息。 WebSocket 提供的是一条双向通信的信道,也就是说,在同一个 TCP 连接上,可以 双向传输数据:

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);}

➊ 发送 UTF-8 编码的文本消息

➋ 发送 UTF-8 编码的 JSON 净荷

➌ 发送二进制 ArrayBuffer

➍ 发送二进制 ArrayBufferView

➎ 发送二进制 Blob

WebSocket API 可以接收 UTF-8 编码的 DOMString 对象,也可以接收 ArrayBuffer、 ArrayBufferView 或 Blob 等二进制数据。但要注意,所有二进制数据类型只是为了简 化 API:在传输中,只通过一位(bit)即可将 WebSocket 帧标记为二进制或者文本。 假如应用或服务器需要传输其他的内容类型,就必须通过其他机制来沟通这个信息。

这里的 send() 方法是异步的:提供的数据会在客户端排队,而函数则立即返回。特 别是在传输大文件的时候,千万别因为返回快,就错误地以为数据已经发送出去 了!要监控在浏览器中排队的数据量,可以查询套接字的 bufferedAmount 属性:

var ws = new WebSocket('wss://example.com/socket');
ws.onopen = function () {
 subscribeToApplicationUpdates(function(evt) {if (ws.bufferedAmount == 0) ➋
 ws.send(evt.data);});
};

➊ 预订应用更新(如游戏状态更新)

➋ 检查客户端缓冲的数据量

➌ 如果缓冲是空的,发送下一次更新

子协议协商

WebSocket 协议对每条消息的格式事先不作任何假设:仅用一位标记消息是文本还是 二进制,以便客户端和服务器有效地解码数据,而除此之外的消息内容就是未知的。

此外,与 HTTP 或 XHR 请求不同——它们是通过每次请求和响应的 HTTP 首部来 沟通元数据,WebSocket 并没有等价的机制。因此,如果需要沟通关于消息的元数 据,客户端和服务器必须达成沟通这一数据的子协议。

  • 客户端和服务器可以提前确定一种固定的消息格式,比如所有通信都通过 JSON 编码的消息或者某种自定义的二进制格式进行,而必要的元数据作为这种数据结 构的一个部分。
  • 如果客户端和服务器要发送不同的数据类型,那它们可以确定一个双方都知道的 消息首部,利用它来沟通说明信息或有关净荷的其他解码信息。
  • 混合使用文本和二进制消息可以沟通净荷和元数据,比如用文本消息实现 HTTP 首部的功能,后跟包含应用净荷的二进制消息。

WebSocket协议

二进制分帧层
协议扩展

WebSocket 规范允许对协议进行扩展:数据格式和 WebSocket 协议的语义可以通过 新的操作码和数据字段扩展。虽然有些不同寻常,但这却是一个非常强大的特性, 因为它允许客户端和服务器在基本的 WebSocket 分帧层之上实现更多功能,又不需 要应用代码介入或协作。

  • 多路复用扩展(A Multiplexing Extension for WebSockets) 这个扩展可以将 WebSocket 的逻辑连接独立出来,实现共享底层的 TCP 连接。
  • 压缩扩展(Compression Extensions for WebSocket) 给 WebSocket 协议增加了压缩功能。
HTTP升级协商

WebSocket 协议提供了很多强大的特性:基于消息的通信、自定义的二进制分帧层、 子协议协商、可选的协议扩展,等等。换句话说,在交换数据之前,客户端必须与 服务器协商适当的参数以建立连接。

利用 HTTP 完成握手有几个好处。首先,让 WebSockets 与现有 HTTP 基础设施兼 容:WebSocket 服务器可以运行在 80 和 443 端口上,这通常是对客户端唯一开放 的端口。其次,让我们可以重用并扩展 HTTP 的 Upgrade 流,为其添加自定义的 WebSocket 首部,以完成协商。

  • Sec-WebSocket-Version 客户端发送,表示它想使用的 WebSocket 协议版本(“13”表示 RFC 6455)。如 果服务器不支持这个版本,必须回应自己支持的版本。
  • Sec-WebSocket-Key 客户端发送,自动生成的一个键,作为一个对服务器的“挑战”,以验证服务器 支持请求的协议版本。
  • Sec-WebSocket-Accept 服务器响应,包含 Sec-WebSocket-Key 的签名值,证明它支持请求的协议版本。
  • Sec-WebSocket-Protocol 用于协商应用子协议:客户端发送支持的协议列表,服务器必须只回应一个协 议名。
  • Sec-WebSocket-Extensions 用于协商本次连接要使用的 WebSocket 扩展:客户端发送支持的扩展,服务器通 过返回相同的首部确认自己支持一或多个扩展。

有了这些协商字段,就可以在客户端和服务器之间进行 HTTP Upgrade 并协商新的 WebSocket 连接了:

GET /socket HTTP/1.1
Host: thirdparty.com
Origin: http://example.com
Connection: Upgrade
Upgrade: websocket ➊
Sec-WebSocket-Version: 13 ➋
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== ➌
Sec-WebSocket-Protocol: appProtocol, appProtocol-v2 ➍
Sec-WebSocket-Extensions: x-webkit-deflate-message, x-custom-extension ➎

➊ 请求升级到 WebSocket 协议

➋ 客户端使用的 WebSocket 协议版本

➌ 自动生成的键,以验证服务器对协议的支持

➍ 可选的应用指定的子协议列表

➎ 可选的客户端支持的协议扩展列表

与浏览器中客户端发起的任何连接一样,WebSocket 请求也必须遵守同源策略:浏 览器会自动在升级握手请求中追加 Origin 首部,远程服务器可能使用 CORS 判断接 受或拒绝跨源请求 [ 参见 15.2 节“跨源资源共享(CORS)”]。要完成握手,服务 器必须返回一个成功的“Switching Protocols”(切换协议)响应,并确认选择了客 户端发送的哪个选项:

HTTP/1.1 101 Switching Protocols ➊
Upgrade: websocket
Connection: Upgrade
Access-Control-Allow-Origin: http://example.com ➋
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= ➌
Sec-WebSocket-Protocol: appProtocol-v2 ➍
Sec-WebSocket-Extensions: x-custom-extension ➎

➊ 101 响应码确认升级到 WebSocket 协议

➋ CORS 首部表示选择同意跨源连接

➌ 签名的键值验证协议支持

➍ 服务器选择的应用子协议

➎ 服务器选择的 WebSocket 扩展

最低限度,成功的 WebSocket 握手必须是客户端发送协议版本和自动生成的挑战 值,服务器返回 101 HTTP 响应码(Switching Protocols)和散列形式的挑战答案, 确认选择的协议版本:

  • 客户端必须发送 Sec-WebSocket-Version 和 Sec-WebSocket-Key;
  • 服务器必须返回 Sec-WebSocket-Accept 确认协议;
  • 客户端可以通过 Sec-WebSocket-Protocol 发送应用子协议列表;
  • 服务器必须选择一个子协议并通过 Sec-WebSocket-Protocol 返回协议名;如果服 务器不支持任何一个协议,连接断开;
  • 客户端可以通过 Sec-WebSocket-Extensions 发送协议扩展;
  • 服务器可以通过 Sec-WebSocket-Extensions 确认一或多个扩展;如果服务器没有 返回扩展,则连接不支持扩展。

WebSocker使用场景及性能

WebSocket API 提供一个简单的接口,能够在客户端与服务器之间实现基于消息 的双向通信,可以是文本数据,可以是二进制数据:把 WebSocket URL 传递给构 造函数,设置几个 JavaScript 回调函数,就好了——剩下的就全都由浏览器负责 了。再加上 WebSocket 协议提供的二进制分帧、可扩展性以及子协议协商,使得 WebSocket 成为在浏览器中采用自定义应用协议的最佳选择。

不过,就跟以前关于性能的讨论一样,虽然 WebSocket 协议的实现复杂性对应 用隐藏了,但何时以及如何使用 WebSocket,毋庸置疑会对性能产生巨大影响。 WebSocket 不能取代 XHR 或 SSE,而要获得最佳性能,我们必须善于利用它的 长处!

请求和响应流

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

  • XHR 是专门为“事务型”请求 / 响应通信而优化的:客户端向服务器发送完整 的、格式良好的 HTTP 请求,服务器返回完整的响应。这里不支持请求流,在 Streams API 可用之前,没有可靠的跨浏览器响应流 API。
  • SSE 可以实现服务器到客户端的高效、低延迟的文本数据流:客户端发起 SSE 连 接,服务器使用事件源协议将更新流式发送给客户端。客户端在初次握手后,不 能向服务器发送任何数据。

消息开销

建立了 WebSocket 连接后,客户端和服务器通过 WebSocket 协议交换数据:应用消 息会被拆分为一或多个帧,每个帧会添加 2~14 字节的开销。而且,由于分帧是按 照自定义的二进制格式完成的,UTF-8 和二进制应用数据可以有效地通过相同的机 制编码。这一点与 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 字节!

数据效率及压缩

通过常规的 HTTP 协商,每个 XHR 请求都可以协商最优的传输编码格式(如对文 本数据采用 gzip 压缩)。类似地,SSE 局限于 UTF-8 文本数据,因此事件流数据可 以在整个会话期间使用 gzip 压缩。

而使用 WebSocket 时,情况要复杂一些:WebSocket 可以传输文本和二进制数据, 因此压缩整个会话行不通。二进制的净荷也可能已经压缩过了!为此,WebSocket 必须实现自己的压缩机制,并针对每个消息选择应用。

自定义应用协议

浏览器是为 HTTP 数据传输而优化的,它理解 HTTP 协议,提供各种服务,比如认 证、缓存、压缩,等等。于是,XHR 请求自然而然就继承了所有这些功能。

部署WebSocket基础设施

HTTP 是专为短时突发性传输设计的。于是,很多服务器、代理和其他中间设备的 HTTP 连接空闲超时设置都很激进。而这显然是我们在持久的 WebSocket 会话中所 不愿意看到的。为解决这个问题,要考虑三个方面:

  • 位于各自网络中的路由器、负载均衡器和代理;
  • 外部网络中透明、确定的代理服务器(如 ISP 和运营商的代理);
  • 客户网络中的路由器、防火墙和代理。

性能检查表

部署高性能的 WebSocket 服务要求细致地调优和考量,无论在客户端还是在服务器 上。可以参考下列要点。

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

最后但同样重要的是,为移动应用而优化!实时推送对于手持设备而言,反倒可能 造成负面影响,因为手持设备的电池始终很宝贵。这并不代表不能在移动应用中使 用 WebSocket。相反,WebSocket 其实是一个高效的传输机制,但一定要确保注意 以下问题:

  • 8.1 节“节约用电”;
  • 8.2 节“消除周期性及无效的数据传输”;
  • 8.2 节中的“内格尔及有效的服务器推送”;
  • 以及 8.2 节之后的“消除不必要的长连接”

WebRTC

Web Real-Time Communication(Web 实时通信,WebRTC)由一组标准、协议和 JavaScript API 组成,用于实现浏览器之间(端到端)的音频、视频及数据共享。 WebRTC 使得实时通信变成一种标准功能,任何 Web 应用都无需借助第三方插件和 专有软件,而是通过简单的 JavaScript API 即可利用。

要实现涵盖音频和视频的电话会议等完善、高品质的 RTC 应用,以及端到端的数据 交换,需要浏览器具备很多新功能:音频和视频处理能力、支持新应用 API、支持 好几种新网络协议。好在,浏览器把大多数复杂性抽象成了三个主要 API:

  • MediaStream:获取音频和视频流;
  • RTCPeerConnection:音频和视频数据通信;
  • RTCDataChannel:任意应用数据通信。

全部功能实现起来只需十几行 JavaScript 代码,任何 Web 应用都可以立即支持全功 能的电话会议,以及端到端的数据交换。这是 WebRTC 的愿景和能力!可是,上述 API 只是冰山一角:发信号、成员发现、连接协商、安全性,以及多层协议,这些 还有更多组件的协同也是关键。

毫无疑问,WebRTC 的架构和协议也决定了其性能特点:连接准备延迟、协议 开销,还有交付语义,只是其中一部分。事实上,与其他浏览器通信机制不同, WebRTC 通过 UDP 传输数据。不过,UDP 只是个起点,为了实现浏览器间实时通 信,还有很多超出原始 UDP 之外的技术。本章我们就详细地加以讨论。

标准和WebRTC的发展

在浏览器中实现实时通信不可谓不雄心勃勃,毕竟,这是 Web 被发明以来最重大的 一次功能增补。WebRTC 脱开我们熟悉的 C/S 通信模型,重新设计了浏览器的网络 层,同时引入了全新的媒体机制,而这对于有效处理音频和视频是必需的。

正因为如此,WebRTC 架构由十余个标准组成,涵盖了应用和浏览器 API,以及很 多必要的协议和数据格式:

  • W3C 的 Web Real-Time Communications(WEBRTC) Working Group 负责制定浏 览器 API;
  • IETF 的 Real-Time Communication in Web-browsers(RTCWEB)工作组负责定 义协议、数据格式、安全及其他在浏览器中实现端到端通信必需的内容。

WebRTC 并不是一个孤立的标准。虽然它的目的主要是实现浏览器间的实时通信, 但设计它的时候也会考虑已有通信系统:VOIP(Voice Over IP)、各种 SIP 客户端, 甚至 PSTN(Public Switched Telephone Network,公共交换电话网),等等。这些 WebRTC 标准不规定任何特定的互操作需求或 API,但它们会尽可能重用相同的概 念和协议。

音频和视频引擎

要实现电话会议功能,浏览器必须访问系统硬件采集音频和视频。为此,无需第三 方插件或自定义的驱动,只要使用一套简单统一的 API 即可。可是,只捕获原始音 频和视频流还不够,还需要对它们分别加以处理以增强品质,保证同步,而且要适 应不断变化的带宽和客户端之间的网络延迟调整输出的比特率。

接收端的处理过程相反,必须实时解码音频和视频流,并适应网络抖动和时延。总 之,采集及处理音频和视频很复杂。不过,好消息是 WebRTC 会让浏览器具备功能 完备的音频和视频引擎(图 18-1),由它们替我们完成处理信号等琐碎的工作。

image-20230331202343839
image-20230331202343839