透视 HTTP 协议(四)

原本不安全的 HTTP 协议,通过引入 SSL/TLS 在安全上达到了 “极致”,但在性能提升方面却是乏善可陈,只优化了握手加密的环节,对于整体的数据传输没有提出更好的改进方案,还只能依赖于 “长连接” 这种 “落后” 的技术。

所以,在 HTTPS 逐渐成熟之后,HTTP 就向着 性能方面 开始发力。由 Google 率先发明了 SPDY 协议,随后互联网工程组 IETF 以 SPDY 为基础,综合其他多方的意见,终于推出了 HTTP/1 的继任者 HTTP/2,在性能方面有了一个大的飞跃。

兼容 HTTP/1

由于 HTTPS 已经在安全方面做的非常好了,所以 HTTP/2 的唯一目标就是改进性能。

但它不仅背负着众多的期待,同时还背负着 HTTP/1 庞大的历史包袱。对于协议的修改必须小心谨慎,兼容性是首要考虑的目标,否则就会破坏互联网上无数现有的资产,这方面 TLS 已经有了先例。

因为必须要保持功能上的兼容,所以 HTTP/2 把 HTTP 分解成了 “语义”“语法” 两个部分,语义层不做改动,与 HTTP/1 完全一致。比如请求方法、URI、状态码、头字段等概念都保留不变,这样就消除了再学习的成本,基于 HTTP 的上层应用也不需要做任何修改,可以无缝转换到 HTTP/2。

特别要说的是,与 HTTPS 不同,HTTP/2 没有在 URI 里引入新的协议名,仍然用 “http” 表示明文协议,用 “https” 表示加密协议。这是一个非常了不起的决定,可以让浏览器或者服务器去自动升级或降级协议,免去了选择的麻烦,让用户在上网的时候都意识不到协议的切换,实现平滑过渡。

在语义保持稳定之后,HTTP/2 在语法层做了天翻地覆的改造,完全变更了 HTTP 报文的传输格式。

头部压缩

我们知道,在 HTTP/1 中可以用头字段 Content-Encoding 指定 Body 的编码方式,比如用 gzip 压缩来节约带宽,但报文的另一个组成部分——Header 却被忽略了,没有针对它的优化手段。

由于报文 Header 一般会携带 User Agent、Cookie、Accept、Server 等许多固定的头字段,多达几百字节甚至上千字节,但 Body 却经常只有几十字节(比如 GET 请求、204/301/304 响应),成了不折不扣的 “大头儿子”。更要命的是,成千上万的请求响应报文里有很多字段值都是重复的,非常浪费,大量带宽消耗在了这些冗余度极高的数据上。

所以,HTTP/2 把 头部压缩 作为了性能改进的一个重点。

不过 HTTP/2 并没有使用传统的压缩算法,而是开发了专门的 HPACK 算法,在客户端和服务器两端建立 “字典”,用索引号表示重复的字符串,还釆用哈夫曼编码来压缩整数和字符串,可以达到 50%~90% 的高压缩率。

二进制“帧”

HTTP/2 在 HTTP/1 用纯文本形式的报文上没有妥协,不再使用肉眼可见的 ASCII 码,而是向下层的 TCP/IP 协议靠拢,全面采用 二进制格式

这种方式虽然对人不友好,但却大大方便了计算机的解析。原来使用纯文本的时候容易出现多义性,比如大小写、空白字符、回车换行等等,程序在处理时必须用复杂的状态机,效率低还麻烦。而二进制里只有 “0” 和 “1”,可以严格规定字段大小、顺序、标志位等格式,“对就是对,错就是错”,解析起来没有歧义,实现简单,而且体积小、速度快,做到 内部提效

以二进制格式为基础,HTTP/2 就开始了大刀阔斧的改革。

它把 TCP 协议的部分特性挪到了应用层,把原来的 Header+Body 的消息打散为数个小片的 二进制帧(Frame),用 HEADERS 帧存放头数据、DATA 帧存放实体数据。

这种做法有点像是 Chunked 分块编码的方式,也是 “化整为零” 的思路,但 HTTP/2 数据分帧后 Header+Body 的报文结构就完全消失了,协议看到的只是一个个的 “碎片”。

虚拟的“流”

“消息碎片” 到达目的地后应该怎么组装起来呢?

HTTP/2 为此定义了一个 流(Stream)的概念,它是二进制帧的双向传输序列,在同一个消息往返的帧会分配一个唯一的 Stream ID。你可以想象把它成是一个虚拟的 “数据流”,在里面流动的是一串有先后顺序的数据帧,这些数据帧按照次序组装起来就是 HTTP/1 里的请求报文和响应报文。

因为 “流” 是虚拟的,实际上并不存在,所以 HTTP/2 就可以在一个 TCP 连接上用 “流” 同时发送多个 “碎片化” 的消息,这就是常说的 多路复用(Multiplexing)——多个往返通信都复用一个连接来处理。

在 “流” 的层面上看,消息是一些有序的 “帧” 序列,而在 “连接” 的层面上看,消息却是乱序收发的 “帧”。多个请求、响应之间没有了顺序关系,不需要排队等待,也就 不会再出现 “队头阻塞” 问题,降低了延迟,大幅度提高了连接的利用率。

为了更好地利用连接,加大吞吐量,HTTP/2 还添加了一些控制帧来管理虚拟的 “流”,实现了优先级和流量控制,这些特性也和 TCP 协议非常相似。

HTTP/2 还在一定程度上改变了传统的 “请求-应答” 工作模式,服务器不再是完全被动地响应请求,也可以新建 “流” 主动向客户端发送消息。比如,在浏览器刚请求 HTML 的时候就提前把可能会用到的 JS、CSS 文件发给客户端,减少等待的延迟,这被称为 服务器推送(Server Push)

强化安全

出于兼容的考虑,HTTP/2 延续了 HTTP/1 的 “明文” 特点,可以像以前一样使用明文传输数据(二进制),不强制使用加密通信。

但由于 HTTPS 已经是大势所趋,而且主流的浏览器 Chrome、Firefox 等都公开宣布只支持加密的 HTTP/2,所以 事实上的 HTTP/2 是加密的。也就是说,我们通常所能见到的 HTTP/2 都是使用 https 协议名,跑在 TLS 上面。

为了区分 “加密” 和 “明文” 这两个不同的版本,HTTP/2 协议定义了两个字符串标识符:“h2” 表示加密的 HTTP/2,“h2c” 表示明文的 HTTP/2。

在 HTTP/2 标准制定的时候(2015 年)已经发现了很多 SSL/TLS 的弱点,而新的 TLSv1.3 还未发布,所以加密版本的 HTTP/2 在安全方面做了强化,要求下层的通信协议必须是 TLSv1.2 以上,还要支持前向安全和 SNI,并且把几百个弱密码套件列入了 “黑名单”,比如 DES、RC4、CBC、SHA-1 都不能在 HTTP/2 里使用,相当于底层用的是 “TLSv1.25”。

ALPN

在 URI 里用的都是 https,也没有版本标记,那么浏览器怎么知道服务器支持 HTTP/2 呢?

答案在 TLS 的扩展里,有一个 ALPN(Application Layer Protocol Negotiation,应用层协议协商)字段,用来与服务器协商在 TLS 上使用的应用协议。

客户端在发起 “Client Hello” 握手的时候,后面会带上一个 ALPN 扩展,里面按照优先顺序列出客户端支持的应用协议。就像下面这样,最优先的是 h2,其次是 http/1.1,以后还可能会有 h3。

1
2
3
4
Extension: application_layer_protocol_negotiation (len=14)
ALPN Protocol
ALPN Next Protocol: h2
ALPN Next Protocol: http/1.1

服务器看到 ALPN 扩展以后就可以从列表里选择一种应用协议,在 “Server Hello” 里也带上 “ALPN” 扩展,告诉客户端服务器决定使用的是哪一种。因为我们在 Nginx 配置里使用了 HTTP/2 协议,所以在这里它选择的就是 “h2”。

1
2
3
4
Extension: application_layer_protocol_negotiation (len=5)
ALPN Protocol
ALPN string length: 2
ALPN Next Protocol: h2

这样在 TLS 握手结束后,客户端和服务器就通过 “ALPN” 完成了应用层的协议协商,后面就可以使用 HTTP/2 通信了。

对于明文的 HTTP/2(h2c),由于其不使用 TLS,也就无法使用 ALPN 进行协议协商,所以需要客户端使用头字段 Connection: upgrade 升级到 HTTP/2,服务器返回状态码 101 表示同意切换协议。

协议栈

下面的这张图对比了 HTTP/1、HTTPS 和 HTTP/2 的协议栈,可以清晰地看到,HTTP/2 是建立在 HPack、Stream、TLS1.2 基础之上的,比 HTTP/1 和 HTTPS 更复杂了一些。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!