HTTP进化史

HTTP/0.9

HTTP/0.9 是个相当简单的协议。它只有一个方法(GET),没有首部,其设计目标也无非 是获取 HTML(也就是说没有图片,只有文本)。

HTTP/1.0

1.0 版本为原有的轻量协议(0.9)新增了大量内容

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

HTTP/1.1

截至目前,1.1 版本的协议已经使用了 20 多年,它修复了之前提到的 1.0 版本的大量问题。因为强制要求客户端提供 Host 首部,让在一个 IP 上提供多个 Web 服务成为可能。

  • 缓存相关首部的扩展
  • OPTIONS 方法
  • Upgrade 首部
  • Range(范围)请求
  • 压缩和传输编码(transfer-encoding)
  • 管道化(pipelining) 管道化这种特性允许客户端一次发送所有的请求

SPDY

2009 年,Google 的工程师 Mike Belshe 和 Roberto Peon 提出了一种 HTTP 的替代方案,SPDY带来了显而易见的性能提升.

HTTP/2

2012 年初,HTTP 工作组(IETF 工作组中负责 HTTP 规范的小组)启动了开发下一个 HTTP 版本的工作.

HTTP/2 协议

HTTP/2分层

HTTP/2 大致可以分为两部分:分帧层,即 h2 多路复用能力的核心部分;数据或 http 层, 其中包含传统上被认为是 HTTP及其关联数据的部分 > 二进制协议

h2 的分帧层是基于帧的二进制协议

首部压缩

仅仅使用二进制协议似乎还不够,h2 的首部还会被深度压缩。这将显著减少传输中的 冗余字节

多路复用

在调试工具里查看基于 h2 传输的连接的时候,你会发现请求和响应交织在一起。

加密传输 线上传输的绝大部分数据是加密过的,所以在中途读取会更加困难。

连接

连接是所有 HTTP/2 会话的基础元素,其定义是客户端初始化的一个 TCP/IP socket,客户端 是指发送 HTTP 请求的实体。 这和 h1 是一样的,不过与完全无状态的 h1 不同的是,h2 把 它所承载的帧(frame)和流(stream)共同依赖的连接层元素捆绑在一起,其中既包含连 接层设置也包含首部表。

为了向服务器双重确认客户端支持 h2,客户端会发送一个叫作 connection preface(连接 前奏)的魔法字节流,作为连接的第一份数据. 如0x505249202a20485454502f322e300d0a0d0a534d0d0a0d0a这个字符串的用处是,如果服务器(或者中间网络设备)不支持 h2,就会产生一个显式错 误。客户端就可以知道不支持h2.

这个魔法字符串会有一个 SETTINGS 帧紧随其后。服务器为了确认它可以支持 h2,会声 明收到客户端的 SETTINGS 帧,并返回一个它自己的 SETTINGS 帧(反过来也需要确认), 然后确认环境正常,可以开始使用 h2.

HTTP/2 是基于帧(frame)的协议。采用分帧是为了将重要信息都封装起来, 让协议的解析方可以轻松阅读、解析并还原信息。 相比之下,h1 不是基于帧的,而是以 文本分隔

1
2
3
4
5
6
7
8
9
 GET / HTTP/1.1 <crlf>
 Host: www.example.com <crlf>
 Connection: keep-alive <crlf>
 Accept: text/html,application/xhtml+xml,application/xml;q=0.9... <crlf>
 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4)... <crlf>
 Accept-Encoding: gzip, deflate, sdch <crlf>
 Accept-Language: en-US,en;q=0.8 <crlf>
 Cookie: pfy_cbc_lb=p-browse-w; customerZipCode=99912|N; ltc=%20;...<crlf>
 <crlf>

解析这种数据用不着什么高科技,但往往速度慢且容易出错。你需要不断读入字节,直到 遇到分隔符为止(这里是指 <crlf>),同时还要考虑一些不太守规矩的客户端,它们会只 发送 <lf>

解析 h1 的请求或响应可能出现下列问题。

  • 一次只能处理一个请求或响应,完成之前不能停止解析。
  • 无法预判解析需要多少内存。这会带来一系列问题:你要把一行读到多大的缓冲区里; 如果行太长会发生什么;应该增加并重新分配内存,还是返回 400 错误。为了解决这些 问题,保持内存处理的效率和速度可不简单。

有了帧,处理协议的程序就能预先知道会收到什么。基于帧的协议,特 别是 h2,开始有固定长度的字节,其中包含表示整帧长度的字段.

HTTP/2 帧结构

名称 长度 描述
Length 3 字节 表示帧负载的长度(取值范围为 214~224-1 字节)。 请注意,214 字节是默认
Type 1 字节 当前帧类型
Flags 1 字节 具体帧类型的标识
R 1 字节 保留位,不要设置,否则可能带来严重后果
Stream Identifier 31 位 每个流的唯一 ID
Frame Payload 长度可变 真实的帧内容,长度是在 Length 字段中设置的

HTTP/2帧类型

名称 ID 描述
DATA 0x0 传输流的核心内容
HEADERS 0x1 包含 HTTP 首部,和可选的优先级参数
PRIORITY 0x2 指示或者更改流的优先级和依赖
RST_STREAM 0x3 允许一端停止流(通常由于错误导致的)
SETTINGS 0x4 协商连接级参数
PUSH_PROMISE 0x5 提示客户端,服务器要推送些东西
PING 0x6 测试连接可用性和往返时延(RTT)
GOAWAY 0x7 告诉另一端,当前端已结束
WINDOW_UPDATE 0x8 协商一端将要接收多少字节(用于流量控制)
CONTINUATION 0x9 用以扩展 HEADER 数据块

HTTP/2 规范对流(stream)的定义是:“HTTP/2 连接上独立的、双向的帧序列交换。”你 可以将流看作在连接上的一系列帧,它们构成了单独的 HTTP 请求和响应。如果客户端想 要发出请求,它会开启一个新的流。然后,服务器将在这个流上回复。这与 h1 的请求 / 响 应流程类似,重要的区别在于,因为有分帧,所以多个请求和响应可以交错,而不会互相 阻塞。流 ID(帧首部的第 6~9 字节)用来标识帧所属的流。

客户端到服务器的 h2 连接建立之后,通过发送 HEADERS 帧来启动新的流,如果首 部需要跨多个帧,可能还发会送 CONTINUATION 帧(更多信息参见下面的附注栏 “CONTINUATIONS 帧”)。该 HEADERS 帧可能来自 HTTP 请求,也可能来自响应,具体 取决于发送方。后续流启动的时候,会发送一个带有递增流 ID 的新 HEADERS 帧。

消息

HTTP 消息泛指 HTTP 请求或响应。上一节已经讲过,流是用来传输一对请求 / 响 应消息的。一个消息至少由 HEADERS 帧(它初始化流)组成,并且可以另外包含 CONTINUATIONDATA 帧,以及其他的 HEADERS

流量控制

h2 的新特性之一是基于流的流量控制。不同于 h1 的世界,只要客户端可以处理,服务端 就会尽可能快地发送数据,h2 提供了客户端调整传输速度的能力。(并且,由于在 h2 中, 一切几乎都是对称的,服务端也可以调整传输的速度。)WINDOW_UPDATE 帧用来指示 流量控制信息。每个帧告诉对方,发送方想要接收多少字节。当一端接收并消费被发送的 数据时,它将发出一个 WINDOW_UPDATE 帧以指示其更新后的处理字节的能力。(许多 早期的 HTTP/2 实现者花了大量时间调试窗口更新机制,来回答“为什么我没有取到数据” 的问题。)发送方有责任遵守这些限制。

优先级

流的最后一个重要特性是依赖关系。现代浏览器都经过了精心设计,首先请求网页上最重 要的元素,以最优的顺序获取资源,由此来优化页面性能。拿到了 HTML 之后,在渲染页 面之前,浏览器通常还需要 CSS 和关键 JavaScript 这样的东西。在没有多路复用的时候, 在它可以发出对新对象的请求之前,需要等待前一个响应完成。有了 h2,客户端就可以 一次发出所有资源的请求,服务端也可以立即着手处理这些请求。由此带来的问题是,浏 览器失去了在 h1 时代默认的资源请求优先级策略。假设服务器同时接收到了 100 个请求, 也没有标识哪个更重要,那么它将几乎同时发送每个资源,次要元素就会影响到关键元素 的传输。

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

  • 依赖关系: 为客户端提供了一种能力,通过指明某些对象对另一些对象有依赖,告知服务 器这些对象应该优先传输。
  • 权重 让客户端告诉服务器如何确定具有共同依赖关系的对象的优先级。

假如在收到主体 HTML 文件之后,客户端会解析它,并生成依赖树,然后给树里的元素分配权 重。客户端表明它最需要的是 style.css,其次是 critical.js。没有这两个文件, 它就不能接着渲染页面。等它收到了 critical.js,就可以给出其余对象的相对权重。权重表 示服务一个对象时所需要花费的对应“努力”程度

服务端推送

提升单个对象性能的最佳方式,就是在它被用到之前就放到浏览器的缓存里面。这正是 HTTP/2 的服务端推送的目的。推送使服务器能够主动将对象发给客户端,这可能是因为 它知道客户端不久将用到该对象

推送对象

如果服务器决定要推送一个对象(RFC 中称为“推送响应”),会构造一个 PUSH_ PROMISE 帧。这个帧有很多重要属性,列举如下。

  • PUSH_PROMISE 帧首部中的流 ID 用来响应相关联的请求。推送的响应一定会对应到 客户端已发送的某个请求。如果浏览器请求一个主体 HTML 页面,如果要推送此页面 使用的某个 JavaScript 对象,服务器将使用请求对应的流 ID 构造 PUSH_PROMISE 帧。
  • PUSH_PROMISE 帧的首部块与客户端请求推送对象时发送的首部块是相似的。所以客 户端有办法放心检查将要发送的请求。
  • 被发送的对象必须确保是可缓存的。
  • :method 首部的值必须确保安全。安全的方法就是幂等的那些方法,这是一种不改变 任何状态的好办法。例如,GET 请求被认为是幂等的,因为它通常只是获取对象,而 POST 请求被认为是非幂等的,因为它可能会改变服务器端的状态。
  • 理想情况下,PUSH_PROMISE 帧应该更早发送,应当早于客户端接收到可能承载着推 送对象的 DATA 帧。假设服务器要在发送 PUSH_PROMISE 之前发送完整的 HTML, 那客户端可能在接收到 PUSH_PROMISE 之前已经发出了对这个资源的请求。h2 足够 健壮,可以优雅地解决这类问题,但还是会有些浪费。
  • PUSH_PROMISE 帧会指示将要发送的响应所使用的流 ID。

如果客户端对 PUSH_PROMISE 的任何元素不满意,就可以按照拒收原因选择重置这个流 (使用 RST_STREAM),或者发送 PROTOCOL_ERROR(在 GOAWAY 帧中)

首部压缩

现代网页平均包含几十个请求,每个HTTP请求平均有400多字节,即使在最好的环境下,这也会造成相当长的延时, 如果考虑到拥挤的 WiFi或连接不畅的蜂窝网络,那可是非常痛苦的. 首部压缩就出现了。但是首部应该 怎么压缩?浏览器的世界刚从 SPDYCRIME 漏洞中恢复过来,该漏洞以创造性的方式 利用 deflate 首部压缩算法来解密早期的加密帧,因此原有的方法肯定不行。我们需要的机 制应当可以抵御 CRIME,同时具备和 GZIP 类似的压缩能力。

经过多次创新性的思考和讨论,人们提出了 HPACKHPACK 是种表查找压缩方案,它利 用霍夫曼编码获得接近 GZIP 的压缩率