HTTP 进化史

HTTP/0.9

它只有一个 GET 方法, 没有首部, 设计目标是获取HTML(没有图片, 只有文本)

HTTP/1.0

1996 年通过 RFC 1945 制定为 HTTP/1.0 规范(在 HTTP/0.9 版本的基础上)

  • 首部
  • 响应码
  • 重定向
  • 错误
  • 条件请求
  • 内容编码(压缩)
  • 更多的请求方法 …

但是 HTTP1.0 规范: - 不能让请求共用一个连接 - 缺少强制的 Host 首部 - 缓存的选择也简陋

HTTP/1.1

HTTP/1.0 版本的基础上添加:

  • 强制要求客户端提供 Host 首部, 这使虚拟主机托管成为可能.
  • 新的连接指令时, Web 服务器不需要在每个响应之后关闭连接
  • 缓存相关首部的扩展
  • OPTIONS 方法
  • Upgrade 首部
  • Range 请求
  • 压缩和传输编码(Transfer-encoding)
  • 管道化(pipelining)

SPDY

2009 年 Google 工程师提出了一种替代 HTTP 的方案: SPDY 不是第一个希望替代 HTTP 的方案, 但它是其中最重要的一个, 因为它带来了显而易见的性能提升. 它是 HTTP/2 的基础.

HTTP/2

RFC 7540 在 2015 年 5 月 14 日发布了HTTP/2的正式协议.

期望:

  • 相比于使用 TCP 的 HTTP/1.1, 最终用户可感知的多数延迟都有能够量化的显著改善
  • 解决 HTTP 中的队头阻塞问题
  • 并行的实现机制不依赖与服务器建立多个连接, 从而提升 TCP 连接的利用率, 特别是在拥塞控制方面
  • 保留 HTTP/1.1 的语义, 可以利用已有的文档资源, 包括(但不限于) HTTP 方法, 状态码, URI 和首部字段
  • 明确定义 HTTP/2.0 和 HTTP/1.x 交互的方法, 特别是通过中介时的方法(双向)
  • 明确指出它们可以被合理使用的新的扩展点和策略

HTTP 快速入门

运行 h2 的服务器前提:

  • 支持 H2 的 Web 服务器
  • 安装 TLS 证书

证书的获取

openssl genrsa -out key.pem 2048
openssl req -new -x509 -sha256 -key key.pem -out cert.pem -days 365 -subj "/CN=fake.example.org"
  • Let’s Encrypt

H2 服务器

nghttp2

在这里, 我以我自己的 mac 来示例:

brew install nghttp2

mkdir /tmp/www
touch /tmp/www/index.html

上面的 key.pem 和 cert.pem 在 /tmp/ssl/ 目录下

启动 h2 服务器

./nghttpd -v -d /tmp/www 8443 /tmp/ssl/key.pem /tmp/ssl/cert.pem

这时, 就可以访问测试了: https://localhost:8443

img

Web 优化

资源请求/获取流程图

img

资源响应/渲染流程图

img

关键性能指标

  • 延时. 指IP数据包从一个网络端点到另一个网络端点所花费的时间. 与之相关的是 RTT(往返时延), 它是延迟的时间的两倍. 它是制约Web性能的主要瓶颈.

来回通信延迟(Round-trip delay time),在通信(Communication)、电脑网络(Computer network)领域中,意指:在双方通信中,发讯方的信号(Signal)传播(Propagation)到收讯方的时间(意即:传播延迟(Propagation delay)),加上收讯方回传讯息到发讯方的时间(如果没有造成双向传播速率差异的因素,此时间与发讯方将信号传播到收讯方的时间一样久). 参考自维基百科.

  • 带宽: 只要带宽没饱和, 两个网络端点之间的连接会一次处理尽可能多的数据量.
  • DNS 查询: 通过DNS系统把主机名转换为IP地址. 一个域名, 只需要转换一次.
  • 建立连接时间: “三次握手”. SYN(Client) -> ACK/SYN(Server) -> ACK(Client)
  • TLS 协商时间

下面的指标则严重依赖于页面内容本身或服务器性能:

  • TTFB: 首字节时间. 指客户端从开始定位到Web页面, 到接收到 body response 的第一个字节所耗费的时间. 它包含了上面提到的各种耗时, 还要加上服务器处理时间.
  • TTLB: 被请求资源的最后字节到达时间
  • 开始渲染时间: 客户端的屏幕上什么时候开始显示内容? 这指标是用户看到空白页面的时长.
  • 文档加载完成时间: 客户端浏览器认为页面加载完毕的时间.

各种增加:

  • 更多的字节
  • 更多的资源
  • 更高的复杂度
  • 更多的域名
  • 更多的TCP socket

HTTP/1.x 的问题

队头阻塞

在请求响答过程中, 如果出现任何状况, 剩下所有的工作都会被阻塞在那次请求响答之后, 这就是 队头阻塞. 它会阻碍网络传输和Web页面渲染, 直至失去响应.为了防止这种问题, 现代浏览器会针对单个域名开启6个连接, 通过各个连接分别发送请求, 它实现了某种程序上的并行, 但是每一连接仍然会受到 队头阻塞 的影响.

低效的 TCP 利用

拥塞窗口(congestion window): 在接收方确认数据包之前, 发送方可以发出的TCP包的数量. 例如如果拥塞窗口指定为1, 那么发送方发出一个数据包之后, 只有接收方确认了那个数据包, 才能发送下一个.

慢启动(slow start): 它用来探索当前连接对应拥塞窗口的合适大小.目标是为了让新的连接搞清楚当前网络状况, 避免给已经拥堵的网络继续添乱. 刚开始时, 收到 1 个确认回复后, 可以发送 2 个数据包; 收到 2 个确认回复后, 可以发送 4 个数据包; 以此类推, 这种几何级增长, 很快就会达到协议规定的发送数据上限. 这时连接将进入 拥塞避免阶段

h1 并不支持多路复用, 所以浏览器一般会针对指定域名开启6个并发连接. 这意味着拥塞窗口波动也会并行发生6次.

臃肿的消息首部

虽然 h1 提供了压缩被请求内容的机制, 但是消息首部却无法压缩.

受限的优先级设置

  • 浏览器为了先请求优先级高的资源, 会推迟请求其他资源. 但优先级高的资源获取后, 在处理的过程中, 浏览器并不会发起新的资源请求, 所以服务器无法利用这段时间发送优先级低的资源, 总的页面下载时间因此延长了.
  • 一个高优先级资源被浏览器发现后, 但受制于浏览器处理的方式, 它被排在了一个正在获取的低优先级资源之后

第三方资源

Web 性能的最佳实践

DNS 查询优化

  • 限制不同域名的数量
  • 保证低限度的解析延迟
  • 在HTML或响应用利用DNS预取指令
<link rel="dns-prefetch" href="//ajax.googleapis.com">

优化 TCP 连接

  • 利用 preconnect 指令, 在连接被使用之前就已经建立好了
<link rel="preconnect" href="//fonts.example.com" crossorigin>

避免重定向

重定向通常触发与额外域名建立连接. 实在不行的话则可以:

  • 利用 CDN 代替客户端在云端实现重定向
  • 如果是同一域名的重定向, 使用 Web 服务器上的 rewrite 规则, 避免重定向

客户端缓存

设置客户端缓存TTL. 可通过 HTTP 首部:

  • cache control 和 max-age (秒)
  • 或 expires 首部

网络边缘的缓存

CDN

条件缓存

如果内容变了, 请返回内容本身; 否则直接告诉我内容没变.

  • Last-Modified-Since 首部. 返回 304 则表示没更新过.
  • ETag. 返回 304 则表示没更新过.

压缩和代码极简化

避免阻塞CSS/JS

  • 定期检验这些资源的使用情况, 不需要的资源, 要去掉它.
  • 如果JS的顺序无关紧要, 并且必须在 onload 事件触发之前, 则可设置 async 属性. <script async src"/js/myfile.js">
  • 如果JS执行顺序很重要, 并且你也能承受脚本在 DOM 加载完成之后运行, 那可使用 defer 属性. <script defer src"/js/myfile.js">

图片优化

  • 图片元信息. 可去掉.
  • 图片过载. 图片最终被浏览器自动缩小. 这个在响应式设计的Web站点上比较常见.

反模式

HTTP/2 对每个域名只会开启一个连接. 所以, HTTP/1.1 下的一些诀窍对它来说只会适得其反, 以下如今流行的做法却不再适用于 h2 的:

生成 spiriting 图片

即把很多小图片拼合成一张大图. 这样,只需要一次请求, 就可以覆盖多个图片元素.

在HTTP/2中, 针对特定资源的请求不再是阻塞式的, 很多请求可以并行处理; 于是就性能而言, 生成 spirit 图就失去了意义了.

与之类似的, 小的文本资源, 例如 JS/CSS , 会依照惯例合并成一份大的资源, 或直接内嵌在 HTML 中, 这也是为了减少客户端-服务端连接数.

不过, khanacademy.org 发表的研究指出, 很多小的 JS 合并成一个大的, 仍旧对 h2 有意义, 因为这样子可以更好地压缩处理并节省CPU

域名拆分

域名拆分是为了利用浏览器针对每个域名开启多个连接的能力来并行下载资源.

在 HTTP/1.x 下, 请求和响应首部从不会被压缩. 因此, 对图片之类不依赖于 cookie 的资源, 设置禁用 cookie 的域名是个合理的建议.

在 HTTP/2 中, 首部是会被压缩的, 并且客户端和服务端都会保留”首部历史”, 避免重复传输已知信息.

迁移到 HTTP/2

浏览器支持的情况

https://caniuse.com/#search=http2

迁移到 TLS

注意, HTTP/2 的规范并不明确要求 TLS, 也支持以明文通信.

目前所有主流浏览器只能访问基于 TLS 的 h2. 并且是要支持 TLS 1.2 或更高版本.

原因是: 从之前对 WebSocket 和 SPDY 的实验来看, 使用 Upgrade 首部, 通过 80 端口(明文的 HTTP 端口)通信时, 通信链路上代理服务器的中断等因素会导致非常高的错误率. 但基于 443 (HTTPS 端口)上的 TLS, 则错误率会显著降低, 并且协议通信也更简洁. 第二: 人们越来越相信, 考虑到安全和隐私, 一切都应该被加密.

注意事项

  • 了解你使用的 web 服务器
  • 获得证书
  • 保护私钥
  • 为服务器的负载做准备
    • 尽可能使用长连接
    • 使用会话凭证
    • 使用内置加解密的芯片(如 Intel 的 AES-NI 指令)
  • 紧跟潮流, 及时修复 HTTPS 的漏洞
  • 定期检测: https://www.ssllabs.com

撤销针对 HTTP/1.1 的 “优化”

img

HTTP/2 协议

分层

分帧层

它是 h2 多路复用能力的核心部分.

它是基于帧的二进制协议.

数据或HTTP层

包含传统上被认为是 HTTP 及其关联数据的部分

首部压缩

多路复用

加密传输

img

img

HTTP/2 对流的定义是: HTTP/2连接上独立的, 双向的帧序列交换.

消息

一个消息至少由 HEADERS 帧(它初始化流) 组成, 并且可以另外包含 CONTINUATION 和 DATA 帧, 以及其他的 HEADERS 帧.

h2 的请求和响应分成 HEADERS 帧和 DATA 帧.

h1 把消息分为: 请求/状态行; 首部. h2 则取消了这种区分, 并把这些行变成了魔法伪首部.

  • h2 中没有分块编码(chuned encoding)
  • 不再在 101 的响应

流量控制

这是 h2 的新特性之一.

优先级

h2 中, 客户端可以通过 HEADERS 帧和 PRIORITY 帧, 客户端可以明确地和服务端沟通它需要什么, 以及它需要这些资源的顺序. 这是通过声明依赖关系树和树里的相对权重实现的.

服务端推送

推送对象, 服务端会构造一个 PUSH_PROMISE 帧. 它有很多重要的属性, 如下:

  • PUSH_PROMISE 帧首部的流ID, 用来响应相关联的请求
  • PUSH_PROMISE 帧首部块与客户端请求推送对象的首部块是相似的.
  • 被发送的对象必须确保是可缓存的
  • :method 首部的值必须确保安全.安全的方法, 就是那些幂等的方法(如 GET, 但 POST 则被认为是非幂等的)
  • 理想情况下, PUSH_PROMISE 帧应该更早发送, 应当早于客户端接收到可能承载着推送对象的 DATA 帧.

客户端拒收: - 使用 RST_STREAM 或 发送 PROTOCOL_ERROR (在 GOAWAY 帧中).

首部压缩

HPACK 方案. 它是一种表查找压缩方案.

h2 调试工具

nghttp -v -n --no-dep -w 14 -a -H "Header: Foo" https://host

它会输出 h2 的连接和传输过程的详细信息. 感觉类似 curl -v 一样.

-w 14 表示将窗口大小设置为 16KB (2^14).

-v 打印 debug 信息

-n 丢弃下载的数据, 如 HTML 内容

-s 打印统计信息

Chrome

打开Chrome的设置 chrome://net-internals 选择 HTTP/2 , 然后打开一个新的tab 来访问你的 h2 的URL, 即可看到相应的调试信息了.

用来解析 Chrome 中 h2 日志的输出工具:

https://github.com/rmurphey/chrome-http2-log-parser

在 Chrome 的调试页面的 network tab 中, 在下面的任意一列中, 右键, 然后就可以显示更多的列信息了, 例如使用的协议.

注意事项

TLS 版本问题

http://http2.github.io/http2-spec/#TLSUsage

根据规范可知, 它必须要使用 TLS version 1.2 或更版本

nginx HTTPS 的配置

ssl_protocols 至少要支持 v1.2

server {

        #ssl start --------------------------
        listen 443 ssl http2;

        # 下面两个地址改为你的证书的正确位置
        ssl_certificate /home/username/ssl/xxx.crt;
        ssl_certificate_key /home/username/ssl/xxx.key;


        #enables all versions of TLS, but not SSLv2 or 3 which are weak and now deprecated.
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

        #Disables all weak ciphers
        ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
ssl_prefer_server_ciphers on;
        ## ssl end -----------------------------
        ....
}

Nginx 中使用 http2

Nginx 是从 1.9.5 版本开始支持的.编译时要指定 http2 的模块. (而且编译时 openssl 的版本至少要 1.0.2 或以上版本)

下面是 ubuntu 安装示例

sudo apt-get install -y build-essential libssl-dev libpcre3 libpcre3-dev

下载最新版本的 openssl, 假设压缩在 /path/to/openssl-1.0.2

./config && make -j8

下面是开始编译 nginx
./configure --prefix=/home/username/nginx/nginx-1.12.2 --with-http_stub_status_module --with-http_ssl_module --with-http_realip_module --with-http_v2_module --with-openssl=/path/to/openssl-1.0.2

查看 nginx 编译时的依赖及版本

./sbin/nginx -V
nginx version: nginx/1.12.2
built by gcc 4.8.2 (Ubuntu 4.8.2-19ubuntu1)
built with OpenSSL 1.0.2o  27 Mar 2018
TLS SNI support enabled
configure arguments: --prefix=/home/username/nginx/nginx-1.12.2 --with-http_stub_status_module --with-http_ssl_module --with-http_realip_module --with-http_v2_module --with-openssl=/home/username/nginx/ssl/openssl-1.0.2o/

注意, OpenSSL 的版本至少要 >= 1.0.2