TTP/2
HTTP/1.1 协议的性能问题:高延迟
- 延迟难以下降;
- 并发连接有限,谷歌浏览器最大并发连接数是 6 个,而且每一个连接都要经过 TCP 和 TLS 握手耗时,以及 TCP 慢启动过程给流量带来的影响;
- 队头阻塞问题,同一连接只能在完成一个 HTTP 事务(请求和响应)后,才能处理下一个事务;
- HTTP 头部巨大且重复,由于 HTTP 协议是无状态的,每一个请求都得携带 HTTP 头部,特别是对于有携带 Cookie 的头部,而 Cookie 的大小通常很大;
- 不支持服务器推送消息,因此当客户端需要获取通知时,只能通过定时器不断地拉取消息,这无疑浪费大量了带宽和服务器资源。
HTTP/1.1 性能问题,几个常见的优化手段:
- 合并请求,但是小的修改都要重新获取;
- 图片压缩;
- 将同一个页面的资源分散到不同域名,提升并发连接上限,因为浏览器通常对同一域名的 HTTP 连接最大只能是 6 个;
无法优化:请求-响应模型、头部巨大且重复、并发连接耗时、服务器不能主动推送等
HTTP/2 兼容性
HTTP/2 做到了兼容 HTTP/1.1:
-
HTTP/2 没有在 URI 里引入新的协议名,仍然用「http://」表示明文协议,用「https://」表示加密协议,于是只需要浏览器和服务器在背后自动升级协议,这样可以让用户意识不到协议的升级,很好的实现了协议的平滑升级。
-
只在应用层做了改变,还是基于 TCP 协议传输,应用层方面为了保持功能上的兼容,HTTP/2 把 HTTP 分解成了「语义」和「语法」两个部分,「语义」层不做改动,与 HTTP/1.1 完全一致,比如请求方法、状态码、头字段等规则保留不变。
-
HTTP/2 在「语法」层面做了很多改造,基本改变了 HTTP 报文的传输格式。
优化
-
头部压缩
- HPACK 算法:在客户端和服务器同时维护一张头信息表,只发送索引号。
-
二进制格式:增加了数据传输的效率
- 头信息和数据体都是二进制,并且统称为帧(frame):头信息帧(Headers Frame)和数据帧(Data Frame)
-
并发传输
- 1 个 TCP 连接包含多个 Stream,Stream 里可以包含 1 个或多个 Message,Message 里包含一条或者多个 Frame,Frame 是 HTTP/2 最小单位,以二进制压缩格式存放 HTTP/1 中的内容(头部和包体)。
- 针对不同的 HTTP 请求用独一无二的 Stream ID 来区分,接收端可以通过 Stream ID 有序组装成 HTTP 消息,不同 Stream 的帧是可以乱序发送的,因此可以并发不同的 Stream ,也就是 HTTP/2 可以并行交错地发送请求和响应。
-
服务器主动推送资源:双方都可以建立 Stream
不能避免的缺点
- 队头阻塞:TCP 这一层
头部压缩
HPACK 算法,三个组成部分:
- 静态字典;
- 动态字典;
- Huffman 编码(压缩算法);
客户端和服务器两端都会建立和维护「字典」,用长度较小的索引号表示重复的字符串,再用 Huffman 编码压缩数据,可达到 50%~90% 的高压缩率。
静态表编码
HTTP/2 为高频出现在头部的字符串和字段建立了一张静态表,它是写入到 HTTP/2 框架里的,不会变化的,静态表里共有 61 组
-
表格结构
- Index:索引(Key)
- Header Value:索引对应的 Value
- Header Name:字段的名字
-
有的 Index 没有对应的 Header Value,这是因为这些 Value 并不是固定的而是变化的,这些 Value 都会经过 Huffman 编码后,才会发送出去。
-
如果头部字段属于静态表范围,并且 Value 是变化,那么它的 HTTP/2 头部前 2 位固定为
01
(否则1开头),所以整个头部格式如下图:
-
头部二进制编码
-
不需要冒号空格和末尾的\r\n作为分隔符,字符串长度(Value Length)来分割 Index 和 Value。
-
第一个字节:确定Index
-
第二个字节:首位确定是否哈夫曼编码,剩余的 7 位表示 Value 的长度
-
静态 Huffman 表:据出现频率将 ASCII 码编码为了 Huffman 编码表,可以在 RFC7541 文档找到
-
动态表编码
- 不在静态表范围内的头部字符串就要自行构建动态表,Index 从 62 起步,会在编码解码的时候随时更新。下一次发送的时候,就不用重复发这个字段的数据了,只用发 1 个字节的 Index 号就好了,因为双方都可以根据自己的动态表获取到字段的数据。
- 前提:必须同一个连接上,重复传输完全相同的 HTTP 头部。免了大量的冗余数据的传输。
二进制帧
两类帧(Frame):
-
HEADERS(首部): 9 个字节
-
3 个字节表示帧数据(Frame Playload)的长度。
-
1 个字节是表示帧的类型,HTTP/2 总共定义了 10 种类型的帧,一般分为数据帧和控制帧两类.
-
1 个字节是标志位,可以保存 8 个标志位,用于携带简单的控制信息,比如:
- END_HEADERS 表示头数据结束标志,相当于 HTTP/1 里头后的空行(“\r\n”);
- END_Stream 表示单方向数据发送结束,后续不会再有数据帧。
- PRIORITY 表示流的优先级;
-
4 个字节是流标识符(Stream ID),最高位被保留不用,最大值是 2^31,用来标识该 Frame 属于哪个 Stream,接收方可以根据这个信息从乱序的帧里找到相同 Stream ID 的帧,从而有序组装信息。
-
-
DATA(消息负载)
并发传输
多个 Stream 复用一条 TCP 连接,达到并发的效果,解决了 HTTP/1.1 队头阻塞的问题,提高了 HTTP 传输的吞吐量。
-
组成
- Stream:1 个 TCP 连接包含一个或者多个 Stream,每个帧的头部会携带 Stream ID 信息,不同 Stream 的帧是可以乱序发送的(因此可以并发不同的 Stream ),同一 Stream 内部的帧必须是严格有序的。
- Message:Stream 里可以包含 1 个或多个 Message
- Frame:Message 里包含一条或者多个 Frame
多个 Stream 跑在一条 TCP 连接,同一个 HTTP 请求与响应是跑在同一个 Stream 中,HTTP 消息可以由多个 Frame 构成, 一个 Frame 可以由多个 TCP 报文构成。
客户端和服务器双方都可以建立 Stream,因为服务端可以主动推送资源给客户端, 客户端建立的 Stream 必须是奇数号,而服务器建立的 Stream 必须是偶数号。
同一个连接中的 Stream ID 是不能复用的,只能顺序递增,所以当 Stream ID 耗尽时,需要发一个控制帧 GOAWAY
,用来关闭 TCP 连接。
当 HTTP/2 实现 100 个并发 Stream 时,只需要建立一次 TCP 连接,而 HTTP/1.1 需要建立 100 个 TCP 连接,每个 TCP 连接都要经过 TCP 握手、慢启动以及 TLS 握手过程,这些都是很耗时的。
HTTP/2 还可以对每个 Stream 设置不同优先级,提高用户体验。
服务器主动推送资源
客户端发起的请求,必须使用的是奇数号 Stream,服务器主动的推送,使用的是偶数号 Stream。服务器在推送资源时,会通过 PUSH_PROMISE
帧传输 HTTP 头部,并通过帧中的 Promised Stream ID
字段告知客户端,接下来会在哪个偶数号 Stream 中发送包体。
# Nginx,如果你希望客户端访问 /test.html 时,服务器直接推送 /test.css,那么可以这么配置:
location /test.html {
http2_push /test.css;
}