HTTP2 详解

原文地址 https://blog.wangriyu.wang/2018/05-HTTP2.html

维基百科关于 HTTP/2 的介绍,可以看下定义和发展历史: wiki

RFC 7540 定义了 HTTP/2 的协议规范和细节,本文的细节主要来自此文档,建议先看一遍本文,再回过头来照着协议大致过一遍 RFC,如果想深入某些细节再仔细翻看 RFC

Why use it ?

HTTP/1.1 存在的问题:

1、TCP 连接数限制

对于同一个域名,浏览器最多只能同时创建 6~8 个 TCP 连接 (不同浏览器不一样)。为了解决数量限制,出现了 域名分片 技术,其实就是资源分域,将资源放在不同域名下 (比如二级子域名下),这样就可以针对不同域名创建连接并请求,以一种讨巧的方式突破限制,但是滥用此技术也会造成很多问题,比如每个 TCP 连接本身需要经过 DNS 查询、三步握手、慢启动等,还占用额外的 CPU 和内存,对于服务器来说过多连接也容易造成网络拥挤、交通阻塞等,对于移动端来说问题更明显,可以参考这篇文章: Why Domain Sharding is Bad News for Mobile Performance and Users

在图中可以看到新建了六个 TCP 连接,每次新建连接 DNS 解析需要时间(几 ms 到几百 ms 不等)、TCP 慢启动也需要时间、TLS 握手又要时间,而且后续请求都要等待队列调度

2、线头阻塞 (Head Of Line Blocking) 问题

每个 TCP 连接同时只能处理一个请求 - 响应,浏览器按 FIFO 原则处理请求,如果上一个响应没返回,后续请求 - 响应都会受阻。为了解决此问题,出现了 管线化 - pipelining 技术,但是管线化存在诸多问题,比如第一个响应慢还是会阻塞后续响应、服务器为了按序返回相应需要缓存多个响应占用更多资源、浏览器中途断连重试服务器可能得重新处理多个请求、还有必须客户端 - 代理 - 服务器都支持管线化

3、Header 内容多,而且每次请求 Header 不会变化太多,没有相应的压缩传输优化方案

4、为了尽可能减少请求数,需要做合并文件、雪碧图、资源内联等优化工作,但是这无疑造成了单个请求内容变大延迟变高的问题,且内嵌的资源不能有效地使用缓存机制

5、明文传输不安全

HTTP2 的优势:

1、二进制分帧层 (Binary Framing Layer)

帧是数据传输的最小单位,以二进制传输代替原本的明文传输,原本的报文消息被划分为更小的数据帧:

h1 和 h2 的报文对比:

图中 h2 的报文是重组解析过后的,可以发现一些头字段发生了变化,而且所有头字段均小写

strict-transport-security: max-age=63072000; includeSubdomains 字段是服务器开启 HSTS 策略 ,让浏览器强制使用 HTTPS 进行通信,可以减少重定向造成的额外请求和会话劫持的风险

服务器开启 HSTS 的方法是: 以 nginx 为例,在相应站点的 server 模块中添加 add_header Strict-Transport-Security "max-age=63072000; includeSubdomains" always; 即可

在 Chrome 中可以打开 chrome://net-internals/#hsts 进入浏览器的 HSTS 管理界面,可以增加 / 删除 / 查询 HSTS 记录,比如下图:

在 HSTS 有效期内且 TLS 证书仍有效,浏览器访问 blog.wangriyu.wang 会自动加上 https:// ,而不需要做一次查询重定向到 https

关于帧详见: How does it work ?- 帧

2、多路复用 (MultiPlexing)

在一个 TCP 连接上,我们可以向对方不断发送帧,每帧的 stream identifier 的标明这一帧属于哪个流,然后在对方接收时,根据 stream identifier 拼接每个流的所有帧组成一整块数据。

把 HTTP/1.1 每个请求都当作一个流,那么多个请求变成多个流,请求响应数据分成多个帧,不同流中的帧交错地发送给对方,这就是 HTTP/2 中的多路复用。

流的概念实现了单连接上多请求 - 响应并行,解决了线头阻塞的问题,减少了 TCP 连接数量和 TCP 连接慢启动造成的问题

所以 http2 对于同一域名只需要创建一个连接,而不是像 http/1.1 那样创建 6~8 个连接:

关于流详见: How does it work ?- 流

3、服务端推送 (Server Push)

浏览器发送一个请求,服务器主动向浏览器推送与这个请求相关的资源,这样浏览器就不用发起后续请求。

Server-Push 主要是针对资源内联做出的优化,相较于 http/1.1 资源内联的优势:

  • 客户端可以缓存推送的资源
  • 客户端可以拒收推送过来的资源
  • 推送资源可以由不同页面共享
  • 服务器可以按照优先级推送资源

关于服务端推送详见: How does it work ?- Server-Push

4、Header 压缩 (HPACK)

使用 HPACK 算法来压缩首部内容

关于 HPACK 详见: How does it work ?- HPACK

5、应用层的重置连接

对于 HTTP/1 来说,是通过设置 tcp segment 里的 reset flag 来通知对端关闭连接的。这种方式会直接断开连接,下次再发请求就必须重新建立连接。HTTP/2 引入 RST_STREAM 类型的 frame,可以在不断开连接的前提下取消某个 request 的 stream,表现更好。

6、请求优先级设置

HTTP/2 里的每个 stream 都可以设置依赖 (Dependency) 和权重,可以按依赖树分配优先级,解决了关键请求被阻塞的问题

7、流量控制

每个 http2 流都拥有自己的公示的流量窗口,它可以限制另一端发送数据。对于每个流来说,两端都必须告诉对方自己还有足够的空间来处理新的数据,而在该窗口被扩大前,另一端只被允许发送这么多数据。

关于流量控制详见: How does it work ?- 流量控制

8、HTTP/1 的几种优化可以弃用

合并文件、内联资源、雪碧图、域名分片对于 HTTP/2 来说是不必要的,使用 h2 尽可能将资源细粒化,文件分解地尽可能散,不用担心请求数多

How does it work ?

帧 - Frame

帧的结构

所有帧都是一个固定的 9 字节头部 (payload 之前) 跟一个指定长度的负载 (payload):

+-----------------------------------------------+
|                 Length (24)                   |
+---------------+---------------+---------------+
|   Type (8)    |   Flags (8)   |
+-+-------------+---------------+-------------------------------+
|R|                 Stream Identifier (31)                      |
+=+=============================================================+
|                   Frame Payload (0...)                      ...
+---------------------------------------------------------------+
  • Length 代表整个 frame 的长度,用一个 24 位无符号整数表示。除非接收者在 SETTINGS_MAX_FRAME_SIZE 设置了更大的值 (大小可以是 2^14(16384) 字节到 2^24-1(16777215) 字节之间的任意值),否则数据长度不应超过 2^14(16384) 字节。头部的 9 字节不算在这个长度里
  • Type 定义 frame 的类型,用 8 bits 表示。帧类型决定了帧主体的格式和语义,如果 type 为 unknown 应该忽略或抛弃。
  • Flags 是为帧类型相关而预留的布尔标识。标识对于不同的帧类型赋予了不同的语义。如果该标识对于某种帧类型没有定义语义,则它必须被忽略且发送的时候应该赋值为 (0x0)
  • R 是一个保留的比特位。这个比特的语义没有定义,发送时它必须被设置为 (0x0), 接收时需要忽略。
  • Stream Identifier 用作流控制,用 31 位无符号整数表示。客户端建立的 sid 必须为奇数,服务端建立的 sid 必须为偶数,值 (0x0) 保留给与整个连接相关联的帧 (连接控制消息),而不是单个流
  • Frame Payload 是主体内容,由帧类型决定

共分为十种类型的帧:

  • HEADERS: 报头帧 (type=0x1),用来打开一个流或者携带一个首部块片段
  • DATA: 数据帧 (type=0x0),装填主体信息,可以用一个或多个 DATA 帧来返回一个请求的响应主体
  • PRIORITY: 优先级帧 (type=0x2),指定发送者建议的流优先级,可以在任何流状态下发送 PRIORITY 帧,包括空闲 (idle) 和关闭 (closed) 的流
  • RST_STREAM: 流终止帧 (type=0x3),用来请求取消一个流,或者表示发生了一个错误,payload 带有一个 32 位无符号整数的错误码 (Error Codes),不能在处于空闲 (idle) 状态的流上发送 RST_STREAM 帧
  • SETTINGS: 设置帧 (type=0x4),设置此 连接 的参数,作用于整个连接
  • PUSH_PROMISE: 推送帧 (type=0x5),服务端推送,客户端可以返回一个 RST_STREAM 帧来选择拒绝推送的流
  • PING: PING 帧 (type=0x6),判断一个空闲的连接是否仍然可用,也可以测量最小往返时间 (RTT)
  • GOAWAY: GOWAY 帧 (type=0x7),用于发起关闭连接的请求,或者警示严重错误。GOAWAY 会停止接收新流,并且关闭连接前会处理完先前建立的流
  • WINDOW_UPDATE: 窗口更新帧 (type=0x8),用于执行流量控制功能,可以作用在单独某个流上 (指定具体 Stream Identifier) 也可以作用整个连接 (Stream Identifier 为 0x0),只有 DATA 帧受流量控制影响。初始化流量窗口后,发送多少负载,流量窗口就减少多少,如果流量窗口不足就无法发送,WINDOW_UPDATE 帧可以增加流量窗口大小
  • CONTINUATION: 延续帧 (type=0x9),用于继续传送首部块片段序列,见 首部的压缩与解压缩

DATA 帧格式

+---------------+
 |Pad Length? (8)|
 +---------------+-----------------------------------------------+
 |                            Data (*)                         ...
 +---------------------------------------------------------------+
 |                           Padding (*)                       ...
 +---------------------------------------------------------------+
  • Pad Length: ? 表示此字段的出现时有条件的,需要设置相应标识 (set flag),指定 Padding 长度,存在则代表 PADDING flag 被设置
  • Data: 传递的数据,其长度上限等于帧的 payload 长度减去其他出现的字段长度
  • Padding: 填充字节,没有具体语义,发送时必须设为 0,作用是混淆报文长度,与 TLS 中 CBC 块加密类似,详见 https://httpwg.org/specs/rfc7540.html#padding

DATA 帧有如下标识 (flags):

  • END_STREAM: bit 0 设为 1 代表当前流的最后一帧
  • PADDED: bit 3 设为 1 代表存在 Padding

例子:

HEADERS 帧格式

+---------------+
 |Pad Length? (8)|
 +-+-------------+-----------------------------------------------+
 |E|                 Stream Dependency? (31)                     |
 +-+-------------+-----------------------------------------------+
 |  Weight? (8)  |
 +-+-------------+-----------------------------------------------+
 |                   Header Block Fragment (*)                 ...
 +---------------------------------------------------------------+
 |                           Padding (*)                       ...
 +---------------------------------------------------------------+
  • Pad Length: 指定 Padding 长度,存在则代表 PADDING flag 被设置
  • E: 一个比特位声明流的依赖性是否是排他的,存在则代表 PRIORITY flag 被设置
  • Stream Dependency: 指定一个 stream identifier,代表当前流所依赖的流的 id,存在则代表 PRIORITY flag 被设置
  • Weight: 一个无符号 8 为整数,代表当前流的优先级权重值 (1~256),存在则代表 PRIORITY flag 被设置
  • Header Block Fragment: header 块片段
  • Padding: 填充字节,没有具体语义,作用与 DATA 的 Padding 一样,存在则代表 PADDING flag 被设置

HEADERS 帧有以下标识 (flags):

  • END_STREAM: bit 0 设为 1 代表当前 header 块是发送的最后一块,但是带有 END_STREAM 标识的 HEADERS 帧后面还可以跟 CONTINUATION 帧 (这里可以把 CONTINUATION 看作 HEADERS 的一部分)
  • END_HEADERS: bit 2 设为 1 代表 header 块结束
  • PADDED: bit 3 设为 1 代表 Pad 被设置,存在 Pad Length 和 Padding
  • PRIORITY: bit 5 设为 1 表示存在 Exclusive Flag (E), Stream Dependency, 和 Weight

例子:

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章