2021-07-21 HTTP 2 新特性浅析
<meta charset="utf-8">
HTTP发展简史
HTTP/0.9
- 1991年发布
- 只支持GET命令
- 请求及返回值都是ASCII码
- 请求以换行符(CRLF)结束
- 返回值只支持HTML格式
- 服务器返回值之后立刻关闭连接
GET /index.html
HTTP/1.0
- 1996年发布
- 除了GET命令,还增加了POST和HEAD命令
- 服务器支持返回任意格式
- 请求和返回值除了包含数据部分,还增加头部信息(HTTP header)
- 返回值增加状态码(status code)
GET / HTTP/1.0
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)
Accept: */*
HTTP/1.0 200 OK
Content-Type: text/plain
Content-Length: 137582
Expires: Thu, 05 Dec 1997 16:00:00 GMT
Last-Modified: Wed, 5 August 1996 15:55:28 GMT
Server: Apache 0.84
<html>
<body>Hello World</body>
</html>
HTTP/1.1
- 1997年发布
- 支持持久连接(Connection: keep-alive),即TCP连接默认不关闭,可以被多个请求复用
- 支持管道机制(pipelining),即一个TCP连接内可以同时发起多个请求
- 支持分块传输编码(chunked transfer encoding)
- 支持断点续传(Accept-Ranges)
- 支持更多命令,如PUT, PATCH, OPTIONS, DELETE
- 尽管支持复用TCP连接,但仍然会产生队头阻塞(Head-of-line blocking)问题
HTTP/2
- 2015年发布
- 2009年Google自行研发的SPDY在Chrome上验证成功后,被当作是HTTP/2的基础
- 完全采用二进制协议
- 支持多路复用(multiplexing)
- 支持头部压缩(header compression)
- 支持服务器推送(server push)
二进制协议
HTTP/2之前的协议都是基于ASCII码,好处是可读性好,容易上手。其缺点是可选的空格以及多变的终止符给识别帧造成了一些困难。采用二进制协议可以使得帧的识别更简单,并且传输信息更高效。其缺点是不便于调试,这就需要我们使用相应的工具来理解二进制的内容。
HTTP/2完全采用二进制协议,头信息和数据体都是二进制的,统称为帧(frame)。下图展示了同一个请求在HTTP/1.1和HTTP/2的对应关系,可见请求在HTTP/2中分为了两部分:头部帧和数据帧。
image一个帧的基本格式如下:
image所有帧都由一个9字节的header和可变长的payload组成,各字段定义如下:
- Length: 表示payload的长度,payload的长度默认不能超过214 (16,384) ,除非修改SETTINGS_MAX_FRAME_SIZE为更大的值
- Type: 表示帧的类型,比如0x0表示数据帧,0x1表示头部帧,类型不在规范里定义的帧将会被丢弃
- Flags: 对于不同类型的帧,这个字段有不同的含义,比如在数据帧里,0x1表示这个frame是流的最后一帧(END_STREAM)
- R: 保留位,这一位必须置为0并且需要被忽略
- Stream Identifier: 流ID,客户端发起的流ID必须是奇数的,服务端发起的流ID必须是偶数的
多路复用
为了说明什么是多路复用,我们先需要明确下面几个概念:
- 流(stream): 已建立的连接内的双向字节流,可以承载一条或多条消息
- 消息(message): 与逻辑请求或响应消息对应的完整的一系列帧
- 帧(frame): HTTP/2 通信的最小单位,每个帧都包含帧头,至少也会标识出当前帧所属的数据流
这些概念的关系总结如下:
- 所有通信都在一个 TCP 连接上完成,此连接可以承载任意数量的双向数据流
- 每个数据流都有一个唯一的标识符和可选的优先级信息,用于承载双向消息
- 每条消息都是一条逻辑 HTTP 消息(例如请求或响应),包含一个或多个帧
- 帧是最小的通信单位,承载着特定类型的数据,例如 HTTP Header、消息负载等等。 来自不同数据流的帧可以交错发送,然后再根据每个帧头的数据流标识符重新组装
在HTTP/1.1中,如果客户端为了提高性能想要在一个TCP连接内同时发起多个请求,每个请求必须按顺序被服务器依次响应,如果某一个请求特别耗时,那么后面的请求将会被一直阻塞。
而在HTTP/2中,如果在一个TCP连接内同时发起多个请求,每个消息可以被拆成互不依赖的帧并且各帧之间交错发送,然后在另一端重新把帧组装起来。这个特性就叫做多路复用。
image上图展示了同一个连接内并行的多个数据流。客户端正在向服务器传输一个数据帧(数据流5),与此同时,服务器正向客户端交错发送数据流1和数据流3的一系列帧。因此,一个连接上同时有三个并行数据流。
头部压缩
每个HTTP请求时都会承载一组表头。在HTTP/1.x中表头是以纯文本形式传输,通常需要500~800字节的开销,如果有cookie的话甚至会达到上千字节。为了减少这种开销并且提升性能,HTTP/2使用了HPACK算法进行压缩,具体来说包含了如下两种简单并强大的技术:
- 头部字段使用静态Huffman编码,如果编码后使得字符反而变长了,那么不采用Huffman编码
- 客户端和服务器同时维护并更新一个索引表,如果传输的值在索引表里,那么使用索引值作为传输的值。索引表分为静态表和动态表两部分,静态表在规范里定义,包含了一些常用的字段,动态表初始为空,在连接过程中动态更新
如上图所示,最左边是原始的请求头,第一行的:method GET
通过查找静态索引表得到索引值为2,所以HPACK算法将其编码为2。最后第二行的user-agent Mozilla/5.0 ...
不在静态索引表里,但在动态索引表里查到索引值为62,所以HPACK算法将其编码为62。最后一行的两个字段均未在索引表里查到,所以分别对其进行Huffman编码。
服务器推送
HTTP/2新增的另一个强大的功能是允许服务器除了可以响应客户端请求,还可以向客户端推送额外的资源。
通常当我们请求一个网页时,客户端解析HTML源码,发现有js或css等其他静态资源,然后再发起请求下载静态资源。而实际上,当客户端请求网页后,服务器完全可以预判客户端接下来要请求相关的静态资源,那为什么不让服务器提前推送这些资源,从而减少额外的延迟时间呢?HTTP/2为此提出了服务器推送机制,服务器端可以通过发起PUSH_PROMISE帧告知客户端,客户端收到服务器想要推送资源的意图后,可以决定是否接收推送。
image事实上,如果你在网页中内联CSS或Javascript,那么你已经体验过服务器推送了。使用HTTP/2,我们不仅可以获得相同效果,还可以获得更多的性能优势:
- 客户端可以缓存推送资源
- 推送资源可以被不同页面重用
- 推送资源可以与其他资源复用
- 推送资源可以由服务器设定优先级
- 推送资源可以被客户端拒绝(非强制推送)
服务器推送功能虽然很强大,但在实际使用中还需要考虑一些问题。第一个问题是如果客户端已经有缓存了,那么推送资源就是一种浪费。一种解决方法是只在用户第一次访问的时候推送资源。第二个问题是目前我们一般把静态资源放在CDN上,目前大部分CDN还不支持服务器推送,那么CDN和服务器推送到底哪个效果更好,这个可能还需要一些测试数据来做评判。