HTTP2 详解

原文地址: blog.wangriyu.wang/2018/05-HTT…javascript

维基百科关于 HTTP/2 的介绍,能够看下定义和发展历史:php

Wikicss

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

RFC7540java

Why use it ?

HTTP/1.1 存在的问题:

一、TCP 链接数限制nginx

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

image

image

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

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

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

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

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

五、明文传输不安全

HTTP2 的优点:

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

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

image

h1 和 h2 的报文对比:

image
image

图中 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 记录,好比下图:

image

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

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

二、多路复用 (MultiPlexing)

在一个 TCP 链接上,咱们能够向对方不断发送帧,每帧的 stream identifier 的标明这一帧属于哪一个流,而后在对方接收时,根据 stream identifier 拼接每一个流的全部帧组成一整块数据。 把 HTTP/1.1 每一个请求都看成一个流,那么多个请求变成多个流,请求响应数据分红多个帧,不一样流中的帧交错地发送给对方,这就是 HTTP/2 中的多路复用。

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

因此 http2 对于同一域名只须要建立一个链接,而不是像 http/1.1 那样建立 6~8 个链接:

image
image

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

三、服务端推送 (Server Push)

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

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

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

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

四、Header 压缩 (HPACK)

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

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

五、应用层的重置链接

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

六、请求优先级设置

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

七、流量控制

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

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

八、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 块加密相似,详见 httpwg.org/specs/rfc75…

DATA 帧有以下标识 (flags):

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

例子:

image

image

image

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

例子:

image

image

首部的压缩与解压缩

HTTP/2 里的首部字段也是一个键具备一个或多个值。这些首部字段用于 HTTP 请求和响应消息,也用于服务端推送操做。

首部列表 (Header List) 是零个或多个首部字段 (Header Field) 的集合。当经过链接传送时,首部列表经过压缩算法(即下文 HPACK) 序列化成首部块 (Header Block)。而后,序列化的首部块又被划分红一个或多个叫作首部块片断 (Header Block Fragment) 的字节序列,并经过 HEADERS、PUSH_PROMISE,或者 CONTINUATION 帧进行有效负载传送。

Cookie 首部字段须要 HTTP 映射特殊对待,见 8.1.2.5. Compressing the Cookie Header Field

一个完整的首部块有两种可能

  • 一个 HEADERS 帧或 PUSH_PROMISE 帧加上设置 END_HEADERS flag
  • 一个未设置 END_HEADERS flag 的 HEADERS 帧或 PUSH_PROMISE 帧,加上多个 CONTINUATION 帧,其中最后一个 CONTINUATION 帧设置 END_HEADERS flag

必须将首部块做为连续的帧序列传送,不能插入任何其余类型或其余流的帧。尾帧设置 END_HEADERS 标识表明首部块结束,这让首部块在逻辑上等价于一个单独的帧。接收端链接片断重组首部块,而后解压首部块重建首部列表。

image

SETTINGS 帧格式

httpwg.org/specs/rfc75…

一个 SETTINGS 帧的 payload 由零个或多个参数组成,每一个参数的形式以下:

+-------------------------------+
 |       Identifier (16)         |
 +-------------------------------+-------------------------------+
 |                        Value (32)                             |
 +---------------------------------------------------------------+
复制代码
  • Identifier: 表明参数类型,好比 SETTINGS_HEADER_TABLE_SIZE 是 0x1
  • Value: 相应参数的值

在创建链接开始时双方都要发送 SETTINGS 帧以代表本身期许对方应作的配置,对方接收后赞成配置参数便返回带有 ACK 标识的空 SETTINGS 帧表示确认,并且链接后任意时刻任意一方也均可能再发送 SETTINGS 帧调整,SETTINGS 帧中的参数会被最新接收到的参数覆盖

SETTINGS 帧做用于整个链接,而不是某个流,并且 SETTINGS 帧的 stream identifier 必须是 0x0,不然接收方会认为错误 (PROTOCOL_ERROR)。

SETTINGS 帧包含如下参数:

  • SETTINGS_HEADER_TABLE_SIZE (0x1): 用于解析 Header block 的 Header 压缩表的大小,初始值是 4096 字节
  • SETTINGS_ENABLE_PUSH (0x2): 能够关闭 Server Push,该值初始为 1,表示容许服务端推送功能
  • SETTINGS_MAX_CONCURRENT_STREAMS (0x3): 表明发送端容许接收端建立的最大流数目
  • SETTINGS_INITIAL_WINDOW_SIZE (0x4): 指明发送端全部流的流量控制窗口的初始大小,会影响全部流,该初始值是 2^16 - 1(65535) 字节,最大值是 2^31 - 1,若是超出最大值则会返回 FLOW_CONTROL_ERROR
  • SETTINGS_MAX_FRAME_SIZE (0x5): 指明发送端容许接收的最大帧负载的字节数,初始值是 2^14(16384) 字节,若是该值不在初始值 (2^14) 和最大值 (2^24 - 1) 之间,返回 PROTOCOL_ERROR
  • SETTINGS_MAX_HEADER_LIST_SIZE (0x6): 通知对端,发送端准备接收的首部列表大小的最大字节数。该值是基于未压缩的首部域大小,包括名称和值的字节长度,外加每一个首部域的 32 字节的开销

SETTINGS 帧有如下标识 (flags):

  • ACK: bit 0 设为 1 表明已接收到对方的 SETTINGS 请求并赞成设置,设置此标志的 SETTINGS 帧 payload 必须为空

例子:

image

实际抓包会发现 HTTP2 请求建立链接发送 SETTINGS 帧初始化前还有一个 Magic 帧 (创建 HTTP/2 请求的前言)。

在 HTTP/2 中,要求两端都要发送一个链接前言,做为对所使用协议的最终确认,并肯定 HTTP/2 链接的初始设置,客户端和服务端各自发送不一样的链接前言。

客户端的前言内容 (对应上图中编号 23 的帧) 包含一个内容为 PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n 的序列加上一个能够为空的 SETTINGS 帧,在收到 101(Switching Protocols) 响应 (表明 upgrade 成功) 后发送,或者做为 TLS 链接的第一个传输的应用数据。若是在预先知道服务端支持 HTTP/2 的状况下启用 HTTP/2 链接,客户端链接前言在链接创建时发送。

服务端的前言 (对应上图中编号 26 的帧) 包含一个能够为空的 SETTINGS 帧,在创建 HTTP/2 链接后做为第一帧发送。详见 HTTP/2 Connection Preface

发送完前言后双方都得向对方发送带有 ACK 标识的 SETTINGS 帧表示确认,对应上图中编号 29 和 31 的帧。

请求站点的所有帧序列,帧后面的数字表明所属流的 id,最后以 GOAWAY 帧关闭链接:

image

GOAWAY 帧带有最大的那个流标识符 (好比图中第 29 帧是最大流),对于发送方来讲会继续处理完不大于此数字的流,而后再真正关闭链接

流 - Stream

流只是一个逻辑上的概念,表明 HTTP/2 链接中在客户端和服务器之间交换的独立双向帧序列,每一个帧的 Stream Identifier 字段指明了它属于哪一个流。

流有如下特性:

  • 单个 h2 链接能够包含多个并发的流,两端之间能够交叉发送不一样流的帧
  • 流能够由客户端或服务器来单方面地创建和使用,或者共享
  • 流能够由任一方关闭
  • 帧在流上发送的顺序很是重要,最后接收方会把相同 Stream Identifier (同一个流) 的帧从新组装成完整消息报文

流的状态

image

注意图中的 send 和 recv 对象是指端点,不是指当前的流

idle

全部流以“空闲”状态开始。在这种状态下,没有任何帧的交换

其状态转换:

  • 发送或者接收一个 HEADERS 帧会使空闲 idle 流变成打开 open 状态,其中 HEADERS 帧的 Stream Identifier 字段指明了流 id。一样的 HEADERS 帧(带有 END_STREAM )也可使一个流当即进入 half-closed 状态。
  • 服务端必须在一个打开 open 或者半关闭 (远端) half-closed(remote) 状态的流 (由客户端发起的) 上发送 PUSH_PROMISE 帧,其中 PUSH_PROMISE 帧的 Promised Stream ID 字段指定了一个预示的新流 (由服务端发起),
    • 在服务端该新流会由空闲 idle 状态进入被保留的 (本地) reserved(local) 状态
    • 在客户端该新流会由空闲 idle 状态进入被保留的 (远端) reserved(remote) 状态

3.2 - Starting HTTP/2 for "http" URIs 中介绍了一种特殊状况:

客户端发起一个 HTTP/1.1 请求,请求带有 Upgrade 机制,想建立 h2c 链接,服务端赞成升级返回 101 响应。 升级以前发送的 HTTP/1.1 请求被分配一个流标识符 0x1,并被赋予默认优先级值。从客户端到服务端这个流 1 隐式地转为 "half-closed" 状态,由于做为 HTTP/1.1 请求它已经完成了。HTTP/2 链接开始后,流 1 用于响应。详细过程能够看下文的 HTTP/2 的协议协商机制

此状态下接收到 HEADERS 和 PRIORITY 之外的帧被视为 PROTOCOL_ERROR

状态图中 send PPrecv PP 是指链接的双方端点发送或接收了 PUSH_PROMISE,不是指某个空闲流发送或接收了 PUSH_PROMISE,是 PUSH_PROMISE 的出现促使一个预示的流从 idle 状态转为 reserved

在下文 Server-Push 中会详细介绍服务端推送的内容和 PUSH_PROMISE 的使用情形

reserved (local) / reserved (remote)

PUSH_PROMISE 预示的流由 idle 状态进入此状态,表明准备进行 Server push

其状态转换:

  • PUSH_PROMISE 帧预示的流的响应以 HEADERS 帧开始,这会当即将该流在服务端置于半关闭 (远端) half-closed(remote) 状态,在客户端置于半关闭 (本地) half-closed(local) 状态,最后以携带 END_STREAM 的帧结束,这会将流置于关闭 closed 状态
  • 任一端点均可以发送 RST_STREAM 帧来终止这个流,其状态由 reserved 转为 closed

reserved(local) 状态下的流不能发送 HEADERS、RST_STREAM、PRIORITY 之外的帧,接收到 RST_STREAM、PRIORITY、WINDOW_UPDATE 之外的帧被视为 PROTOCOL_ERROR

reserved(remote) 状态下的流不能发送 RST_STREAM、WINDOW_UPDATE、PRIORITY 之外的帧,接收到 HEADERS、RST_STREAM、PRIORITY 之外的帧被视为 PROTOCOL_ERROR

open

处于 open 状态的流能够被两个对端用来发送任何类型的帧

其状态转换:

  • 任一端均可以发送带有 END_STREAM 标识的帧,发送方会转入 half-closed(local) 状态;接收方会转入 half-closed(remote) 状态
  • 任一端均可以发送 RST_STREAM 帧,这会使流当即进入 closed 状态
half-closed (local)

流是双向的,半关闭表示这个流单向关闭了,local 表明本端到对端的方向关闭了,remote 表明对端到本端的方向关闭了

此状态下的流不能发送 WINDOW_UPDATE、PRIORITY、RST_STREAM 之外的帧

当此状态下的流收到带有 END_STREAM 标识的帧或者任一方发送 RST_STREAM 帧,会转为 closed 状态

此状态下的流收到的 PRIORITY 帧用以调整流的依赖关系顺序,能够看下文的流优先级

half-closed (remote)

此状态下的流不会被对端用于发送帧,执行流量控制的端点再也不有义务维护接收方的流控制窗口。

一个端点在此状态的流上接收到 WINDOW_UPDATE、PRIORITY、RST_STREAM 之外的帧,应该响应一个 STREAM_CLOSED 流错误

此状态下的流能够被端点用于发送任意类型的帧,且此状态下该端点仍会观察流级别的流控制的限制

当此状态下的流发送带有 END_STREAM 标识的帧或者任一方发送 RST_STREAM 帧,会转为 closed 状态

closed

表明流已关闭

此状态下的流不能发送 PRIORITY 之外的帧,发送 PRIORITY 帧是调整那些依赖这个已关闭的流的流优先级,端点都应该处理 PRIORITY 帧,尽管若是该流从依赖关系树中移除了也能够忽略优先级帧

此状态下在收到带有 END_STREAM 标识的 DATA 或 HEADERS 帧后的一小段时间内 (period) 仍可能接收到 WINDOW_UPDATE 或 RST_STREAM 帧,由于在远程对端接收并处理 RST_STREAM 或带有 END_STREAM 标志的帧以前,它可能会发送这些类型的帧。可是端点必须忽略接收到的 WINDOW_UPDATE 或 RST_STREAM

若是一个流发送了 RST_STREAM 帧后转入此状态,而对端接收到 RST_STREAM 帧时可能已经发送了或者处在发送队列中,这些帧是不可撤销的,发送 RST_STREAM 帧的端点必须忽略这些帧。

一个端点能够限制 period 的长短,在 period 内接受的帧会忽略,超出 period 的帧被视为错误。

一个端点发送了 RST_STREAM 帧后接收到流控制帧(好比 DATA),仍会计入流量窗口,即便这些帧会被忽略,由于对端确定是在接收到 RST_STREAM 帧前发送的流控制帧,对端会认为流控制已生效

一个端点可能会在发送了 RST_STREAM 帧后收到 PUSH_PROMISE 帧,即使预示的流已经被重置 (reset),PUSH_PROMISE 帧也能使预示流变成 reserved 状态。所以,须要 RST_STREAM 来关闭一个不想要的预示流。

PRIORITY 帧能够被任意状态的流发送和接收,未知类型的帧会被忽略

流状态的转换

下面看两个例子来理解流状态:

image

(1)、Server 在 Client 发起的一个流上发送 PUSH_PROMISE 帧,其 Promised Stream ID 指定一个预示流用于后续推送,send PP 后这个预示流在服务端从 idle 状态转为 reserve(local) 状态,客户端 recv PP 后这个流从 idle 状态转为 reserve(remote) 状态

(2)(3)、此时预示流处于保留状态,客户端若是选择拒绝接受推送,能够发送 RST 帧关闭这个流;服务端若是此时出问题了也能够发送 RST 帧取消推送。无论哪一方发送或接收到 RST,此状态都转为 closed

(4)、没有出现重置说明推送仍有效,则服务端开始推送,首先发送的确定是响应的 HEADERS 首部块,此时流状态转为半关闭 half-closed(remote);客户端接收到 HEADERS 后流状态转为半关闭 half-closed(local)

(5)(6)、半关闭状态下的流应该还会继续推送诸如 DATA 帧、CONTINUATION 帧这样的数据帧,若是这个过程碰到任一方发起重置,则流会关闭进入 closed 状态

(7)、若是一切顺利,资源随着数据帧响应完毕,最后一帧会带上 END_STREAM 标识表明这个流结束了,此时流转为 closed 状态

image

(1)、客户端发起请求,首先发送一个 HEADERS 帧,其 Stream Identifier 建立一个新流,此流从 idle 状态转为 open 状态

(2)(3)、若是客户端取消请求能够发送 RST 帧,服务端出错也能够发送 RST 帧,无论哪一方接收或发送 RST,流关闭进入 closed 状态;

(4)、若是请求结束(END_STREAM),流转为半关闭状态。假如是 GET 请求,通常 HEADERS 帧就是最后一帧,send H 后流会当即进入半关闭状态。假如是 POST 请求,待数据传完,最后一帧带上 END_STREAM 标识,流转为半关闭

(5)(6)、客户端半关闭后服务端开始返回响应,此时任一方接收或发送 RST,流关闭;

(7)、若是一切顺利,等待响应结束(END_STREAM),流关闭

流的标识符

流 ID 是 31 位无符号整数,客户端发起的流必须是奇数,服务端发起的流必须是偶数,0x0 保留为链接控制消息不能用于创建新流。

HTTP/1.1 Upgrade to HTTP/2 时响应的流 ID 是 0x1,在升级完成以后,流 0x1 在客户端会转为 half-closed (local) 状态,所以这种状况下客户端不能用 0x1 初始化一个流

新创建的流的 ID 必须大于全部已使用过的数字,接收到一个错误大小的 ID 应该返回 PROTOCOL_ERROR 响应

使用一个新流时隐式地关闭了对端发起的 ID 小于当前流的且处于 idle 状态的流,好比一个流发送一个 HEADERS 帧打开了 ID 为 7 的流,但还从未向 ID 为 5 的流发送过帧,则流 0x5 会在 0x7 发送完或接收完第一帧后转为 closed 状态

一个链接内的流 ID 不能重用

流的优先级

客户端能够经过 HEADERS 帧的 PRIORITY 信息指定一个新创建流的优先级,其余期间也能够发送 PRIORITY 帧调整流优先级

设置优先级的目的是为了让端点表达它所指望对端在并发的多个流之间如何分配资源的行为。更重要的是,当发送容量有限时,可使用优先级来选择用于发送帧的流。

流能够被标记为依赖其余流,所依赖的流完成后再处理当前流。每一个依赖 (dependency) 后都跟着一个权重 (weight),这一数字是用来肯定依赖于相同的流的可分配可用资源的相对比例

流依赖(Stream Dependencies)

每一个流均可以显示地依赖另外一个流,包含依赖关系表示优先将资源分配给指定的流(上层节点)而不是依赖流

一个不依赖于其余流的流会指定 stream dependency 为 0x0 值,由于不存在的 0x0 流表明依赖树的根

一个依赖于其余流的流叫作依赖流,被依赖的流是当前流的父级。若是被依赖的流不在当前依赖树中(好比状态为 idle 的流),被依赖的流会使用一个默认优先级

当依赖一个流时,该流会添加进父级的依赖关系中,共享相同父级的依赖流不会相对于彼此进行排序,好比 B 和 C 依赖 A,新添加一个依赖流 D,BCD 的顺序是不固定的:

A                 A
   / \      ==>      /|\
  B   C             B D C
复制代码

独占标识 (exclusive) 容许插入一个新层级(新的依赖关系),独占标识致使该流成为父级的惟一依赖流,而其余依赖流变为其子级,好比一样插入一个新依赖流 E (带有 exclusive):

A
    A                 |
   /|\      ==>       E
  B D C              /|\
                    B D C
复制代码

在依赖关系树中,只有当一个依赖流所依赖的全部流(父级最高为 0x0 的链)被关闭或者没法继续在上面执行,这个依赖流才应该被分配资源

依赖权重

全部依赖流都会分配一个 1~256 权重值

相同父级的依赖流按权重比例分配资源,好比流 B 依赖于 A 且权重值为 4,流 C 依赖于 A 且权重值为 12,当 A 再也不执行时,B 理论上能分配的资源只有 C 的三分之一

优先级调整 (Reprioritization)

使用 PRIORITY 帧能够调整流优先级

PRIORITY 帧内容与 HEADERS 帧的优先级模块相同:

+-+-------------------------------------------------------------+
 |E|                  Stream Dependency (31)                     |
 +-+-------------+-----------------------------------------------+
 |   Weight (8)  |
 +-+-------------+
复制代码
  • 若是父级从新设置了优先级,则依赖流会随其父级流一块儿移动。若调整优先级的流带有独占标识,会致使新的父流的全部子级依赖于这个流

  • 若是一个流调整为依赖本身的一个子级,则这个将被依赖的子级首先移至调整流的父级之下(即同一层),再移动那个调整流的整棵子树,移动的依赖关系保持其权重

看下面这个例子: 第一个图是初始关系树,如今 A 要调整为依赖 D,根据第二点,现将 D 移至 x 之下,再把 A 调整为 D 的子树(图 3),若是 A 调整时带有独占标识根据第一点 F 也归为 A 子级(图 4)

x                x                x                 x
    |               / \               |                 |
    A              D   A              D                 D
   / \            /   / \            / \                |
  B   C     ==>  F   B   C   ==>    F   A       OR      A
     / \                 |             / \             /|\
    D   E                E            B   C           B C F
    |                                     |             |
    F                                     E             E
               (intermediate)   (non-exclusive)    (exclusive)
复制代码
流优先级的状态管理

当一个流从依赖树中移除,它的子级能够调整为依赖被关闭流的父级(应该就是链接上一层节点),新的依赖权重将根据关闭流的权重以及流自身的权重从新计算。

从依赖树中移除流会致使某些优先级信息丢失。资源在具备相同父级的流之间共享,这意味着若是这个集合中的某个流关闭或者阻塞,任何空闲容量将分配给最近的相邻流。然而,若是此集合的共有依赖(即父级节点)从树中移除,这些子流将与更上一层的流共享资源

一个例子: 流 A 和流 B 依赖相同父级节点,而流 C 和流 D 都依赖 A,在移除流 A 以前的一段时间内,A 和 D 都没法执行(可能任务阻塞了),则 C 会分配到 A 的全部资源; 若是 A 被移除出树了,A 的权重按比从新计算分配给 C 和 D,此时 D 仍旧阻塞,C 分配的资源相较以前变少了。对于同等的初始权重,C 获取到的可用资源是三分之一而不是二分之一(为何是三分之一?文档中没有说明细节,权重如何从新分配也不太清楚,下面是按个人理解解释的)

X 的资源为 1,ABCD 初始权重均为 16,*号表明节点当前不可用,图一中 C 和 B 各占一半资源,而 A 移除后 CD 的权重从新分配变为 8,因此图二中 C 和 B 占比变为 1:2,R(C) 变为 1/3

X(v:1.0)               X(v:1.0)
         / \                    /|\
        /   \                  / | \
      *A     B       ==>      /  |  \
    (w:16) (w:16)            /   |   \
      / \                   C   *D    B
     /   \                (w:8)(w:8)(w:16)
    C    *D
 (w:16) (w:16)


 R(C)=16/(16+16)=1/2 ==>  R(C)=8/(8+16)=1/3
复制代码

可能向一个流建立依赖关系的优先级信息还在传输中,那个流就已经关闭了。若是一个依赖流的依赖指向没有相关优先级信息(即父节点无效),则这个依赖流会分配默认优先级,这可能会形成不理想的优先级,由于给流分配了不在预期的优先级。

为了不上述问题,一个端点应该在流关闭后的一段时间内保留流的优先级调整状态信息,此状态保留时间越长,流被分配错误的或者默认的优先级可能性越低。

相似地,处于“空闲”状态的流能够被分配优先级或成为其余流的父节点。这容许在依赖关系树中建立分组节点,从而实现更灵活的优先级表达式。空闲流以默认优先级开始

流优先级状态信息的保留可能增长终端的负担,所以这种状态能够被限制。终端可能根据负荷来决定保留的额外的状态的数目;在高负荷下,能够丢弃额外的优先级状态来限制资源的任务。在极端状况下,终端甚至能够丢弃激活或者保留状态流的优先级信息。若是使用了固定的限制,终端应当至少保留跟 SETTINGS_MAX_CONCURRENT_STREAMS 设置同样大小的流状态

默认优先级

全部流都是初始为非独占地依赖于流 0x0。

Pushed 流初始依赖于相关的流(见 Server-Push)。

以上两种状况,流的权重都指定为 16。

Server-Push

PUSH_PROMISE 帧格式

+---------------+
 |Pad Length? (8)|
 +-+-------------+-----------------------------------------------+
 |R|                  Promised Stream ID (31)                    |
 +-+-----------------------------+-------------------------------+
 |                   Header Block Fragment (*)                 ...
 +---------------------------------------------------------------+
 |                           Padding (*)                       ...
 +---------------------------------------------------------------+
复制代码
  • Pad Length: 指定 Padding 长度,存在则表明 PADDING flag 被设置
  • R: 保留的1bit位
  • Promised Stream ID: 31 位的无符号整数,表明 PUSH_PROMISE 帧保留的流,对于发送者来讲该流标识符必须是可用于下一个流的有效值
  • Header Block Fragment: 包含请求首部域的首部块片断
  • Padding: 填充字节,没有具体语义,做用与 DATA 的 Padding 同样,存在则表明 PADDING flag 被设置

PUSH_PROMISE 帧有如下标识 (flags):

  • END_HEADERS: bit 2 置 1 表明 header 块结束
  • PADDED: bit 3 置 1 表明 Pad 被设置,存在 Pad Length 和 Padding

Push 的过程

结合上文关于 Server-Push 的流状态转换

PUSH_PROMISE 帧只能在对端(客户端)发起的且流状态为 open 或者 half-closed (remote) 的流上发送

PUSH_PROMISE 帧准备推送的响应老是和来自于客户端的请求相关联。服务端在该请求所在的流上发送 PUSH_PROMISE 帧。PUSH_PROMISE 帧包含一个 Promised Stream ID,该流标识符是从服务端可用的流标识符里选出来的。

若是服务端收到了一个对文档的请求,该文档包含内嵌的指向多个图片文件的连接,且服务端选择向客户端推送那些额外的图片,那么在发送包含图片连接的 DATA 帧以前发送 PUSH_PROMISE 帧能够确保客户端在发现内嵌的连接以前,可以知道有一个资源将要被推送过来。一样地,若是服务端准备推送被首部块引用的响应 (好比,在 Link 首部字段 里的),在发送首部块以前发送一个 PUSH_PROMISE 帧,能够确保客户端再也不请求那些资源

一旦客户端收到了 PUSH_PROMISE 帧,并选择接收被推送的响应,客户端就不该该为准备推送的响应发起任何请求,直到预示的流被关闭之后。

image

image

注意图中推送的四个资源各预示了一个流 (Promised Stream ID),而发送 PUSH_PROMISE 帧的仍是在客户端发起的请求流 (Stream Identifier = 1) 上,客户端收到 PUSH_PROMISE 帧并选择接收便不会对这四个资源发起请求,以后服务端会发起预示的流而后推送资源相关的响应

无论出于什么缘由,若是客户端决定再也不从服务端接收准备推送的响应,或者若是服务端花费了太长时间准备发送被预示的响应,客户端能够发送一个 RST_STREAM 帧,该帧可使用 CANCEL 或者 REFUSED_STEAM 码,并引用被推送的流标识符。

nginx 配置 Server-Push

server-push 须要服务端设置,并非说浏览器发起请求,与此请求相关的资源服务端就会自动推送

以 nginx 为例,从版本 1.13.9 开始正式支持 hppt2 serverpush 功能,

在相应 server 或 location 模块中加入 http2_push 字段加上相对路径的文件便可在请求该资源时推送相关资源,好比个人博客设置以下,访问首页时有四个文件会由服务器主动推送过去而不须要客户端请求:

server_name  blog.wangriyu.wang;
  root /blog;
  index index.html index.htm;

  location = /index.html {
    http2_push /css/style.css;
    http2_push /js/main.js;
    http2_push /img/yule.jpg;
    http2_push /img/avatar.jpg;
  }
复制代码

经过浏览器控制台能够查看 Push 响应:

image

也能够用 nghttp 测试 push 响应 (* 号表明是服务端推送的):

image

上面 http2_push 的设置适合静态资源,服务端事先知道哪些文件是客户端须要的,而后选择性推送

假如是后台应用动态生成的文件(好比 json 文件),服务器事先不知道要推送什么,能够用 Link 响应头来作自动推送

在 server 模块中添加 http2_push_preload on;

server_name  blog.wangriyu.wang;
  root /blog;
  index index.html index.htm;

  http2_push_preload on;
复制代码

而后设置响应头 (add_header) 或者后台程序生成数据文件返回时带上响应头 Link 标签,好比

Link: </style.css>; as=style; rel=preload, </main.js>; as=script; rel=preload, </image.jpg>; as=image; rel=preload
复制代码

nginx 会根据 Link 响应头主动推送这些资源

更多nginx 官方介绍见 Introducing HTTP/2 Server Push with NGINX 1.13.9

Server-Push 潜在的问题

看了这篇文章 HTTP/2 中的 Server Push 讨论,发现 Server-Push 有个潜在的问题

Server-Push 知足条件时便会发起推送,但是客户端已经有缓存了想发送 RST 拒收,而服务器在收到 RST 以前已经推送资源了,虽然这部分推送无效可是确定会占用带宽

好比我上面博客关于 http2_push 的配置,我每次打开首页服务器都会推送那四个文件,而实际上浏览器知道本身有缓存使用的也是本地缓存,也就是说本地缓存未失效的期间内,服务器的 Server-Push 只是起到了占用带宽的做用

固然实际上对个人小站点来讲影响并不大,可是若是网站须要大量推送的话,须要考虑并测试 Server-Push 是否会影响用户的后续访问

另外服务端能够设置 Cookie 或者 Session 记录访问时间,而后以后的访问判断是否须要 Push;还有就是客户端能够限制 PUSH 流的数目,也能够设置一个很低的流量窗口来限制 PUSH 发送的数据大小

至于哪些资源须要推送,在《web 性能权威指南》中就提到几种策略,好比 Apache 的 mod_spdy 可以识别 X-Associated-Content 首部,当中列出了但愿服务器推送的资源;另外网上有人已经作了基于 Referer 首部的中间件来处理 Server-Push;或者服务端能更智能的识别文档,根据当前流量决定是否推送或者推送那些资源。相信之后会有更多关于 Server-Push 的实现和应用

流量控制

多路复用的流会竞争 TCP 资源,进而致使流被阻塞。流控制机制确保同一链接上的流不会相互干扰。流量控制做用于单个流或整个链接。HTTP/2 经过使用 WINDOW_UPDATE 帧来提供流量控制。

流控制具备如下特征:

  • 流量控制是特定于链接的。两种级别的流量控制都位于单跳的端点之间,而不是整个端到端的路径。好比 server 前面有一个 front-end proxy 如 Nginx,这时就会有两个 connection,browser-Nginx, Nginx—server,flow control 分别做用于两个 connection。详情见: How is HTTP/2 hop-by-hop flow control accomplished? - stackoverflow
  • 流量控制是基于 WINDOW_UPDATE 帧的。接收方公布本身打算在每一个流以及整个链接上分别接收多少字节。这是一个以信用为基础的方案。
  • 流量控制是有方向的,由接收者全面控制。接收方能够为每一个流和整个链接设置任意的窗口大小。发送方必须尊重接收方设置的流量控制限制。客户方、服务端和中间代理做为接收方时都独立地公布各自的流量控制窗口,做为发送方时都遵照对端的流量控制设置。
  • 不管是新流仍是整个链接,流量控制窗口的初始值是 65535 字节。
  • 帧的类型决定了流量控制是否适用于帧。目前,只有 DATA 帧会受流量控制影响,全部其它类型的帧并不消耗流量控制窗口的空间。这保证了重要的控制帧不会被流量控制阻塞。
  • 流量控制不能被禁用。
  • HTTP/2 只定义了 WINDOW_UPDATE 帧的格式和语义,并无规定接收方如何决定什么时候发送帧、发送什么样的值,也没有规定发送方如何选择发送包。具体实现能够选择任何知足需求的算法。

WINDOW_UPDATE 帧格式

+-+-------------------------------------------------------------+
|R|                Window Size Increment (31)                   |
+-+-------------------------------------------------------------+
复制代码

Window Size Increment 表示除了现有的流量控制窗口以外,发送端还能够传送的字节数。取值范围是 1 到 2^31 - 1 字节。

WINDOW_UPDATE 帧能够是针对一个流或者是针对整个链接的。若是是前者,WINDOW_UPDATE 帧的流标识符指明了受影响的流;若是是后者,流标识符为 0 表示做用于整个链接。

流量控制功能只适用于被标识的、受流量控制影响的帧。文档定义的帧类型中,只有 DATA 帧受流量控制影响。除非接收端不能再分配资源去处理这些帧,不然不受流量控制影响的帧必须被接收并处理。若是接收端不能再接收帧了,能够响应一个 FLOW_CONTROL_ERROR 类型的流错误或者链接错误。

WINDOW_UPDATE 能够由发送过带有 END_STREAM 标志的帧的对端发送。这意味着接收端可能会在 half-closed (remote) 或者 closed 状态的流上收到 WINDOW_UPDATE 帧,接收端不能将其当作错误。

流量控制窗口

流量控制窗口是一个简单的整数值,指出了准许发送端传送的数据的字节数。窗口值衡量了接收端的缓存能力。

除非将其当作链接错误,不然当接收端收到 DATA 帧时,必须老是从流量控制窗口中减掉其长度(不包括帧头的长度,并且两个级别的控制窗口都要减)。即便帧有错误,这也是有必要的,由于发送端已经将该帧计入流量控制窗口,若是接收端没有这样作,发送端和接收端的流量控制窗口就会不一致。

发送端不能发送受流量控制影响的、其长度超出接收端告知的两种级别的流量控制窗口可用空间的帧。即便这两种级别的流量控制窗口都没有可用空间了,也能够发送长度为 0、设置了 END_STREAM 标志的帧(即空的 DATA 帧)。

当帧的接收端消耗了数据并释放了流量控制窗口的空间时,能够发送一个 WINDOW_UPDATE 帧。对于流级别和链接级别的流量控制窗口,须要分别发送 WINDOW_UPDATE 帧。

新建链接时,流和链接的初始窗口大小都是 2^16 - 1(65535) 字节。能够经过设置链接前言中 SETTINGS 帧的 SETTINGS_INITIAL_WINDOW_SIZE 参数改变流的初始窗口大小,这会做用于全部流。而链接的初始窗口大小不能改,但能够用 WINDOW_UPDATE 帧来改变流量控制窗口,这是为何链接前言每每带有一个 WINDOW_UPDATE 帧的缘由。

除了改变还未激活的流的流量控制窗口外,SETTIGNS 帧还能够改变已活跃的流 (处于 open 或 half-closed (remote) 状态的流)的初始流量控制窗口的大小。也就是说,当 SETTINGS_INITIAL_WINDOW_SIZE 的值变化时,接收端必须调整它所维护的全部流的流量控制窗口的值,无论是以前就打开的流仍是还没有打开的流。

改变 SETTINGS_INITIAL_WINDOW_SIZE 可能引起流量控制窗口的可用空间变成负值。发送端必须追踪负的流量控制窗口,而且直到它收到了使流量控制窗口变成正值的 WINDOW_UPDATE 帧,才能发送新的 DATA 帧。

例如,若是链接一创建客户端就当即发送 60KB 的数据,而服务端却将初始窗口大小设置为 16KB,那么客户端一收到 SETTINGS 帧,就会将可用的流量控制窗口从新计算为 -44KB。客户端保持负的流量控制窗口,直到 WINDOW_UPDATE 帧将窗口值恢复为正值,客户端才能够继续发送数据。

若是改变 SETTINGS_INITIAL_WINDOW_SIZE 致使流量控制窗口超出了最大值,一端必须 将其当作类型为 FLOW_CONTROL_ERROR 的链接错误

若是接收端但愿使用比当前值小的流量控制窗口,能够发送一个新的 SETTINGS 帧。可是,接收端必须准备好接收超出该窗口值的数据,由于可能在收到 SETTIGNS 帧以前,发送端已经发送了超出该较小窗口值的数据。

合理使用流控制

流量控制的定义是用来保护端点在资源约束条件下的操做。例如,一个代理须要在不少链接之间共享内存,也有可能有缓慢的上游链接和快速的下游链接。流量控制解决了接收方没法在一个流上处理数据,但仍但愿继续处理同一链接中的其余流的状况。

不须要此功能的部署能够通告最大大小 (2^31 - 1) 的流量控制窗口,而且能够经过在收到任何数据时发送 WINDOW_UPDATE 帧来维护此窗口大小保持不变。这能够有效禁用接受方的流控制。相反地,发送方老是受控于接收方通告的流控制窗口的限制。

资源约束下(例如内存)的调度可使用流量来限制一个对端能够消耗的内存量。须要注意的是若是在不知道带宽延迟积的时候启用流量控制可能致使没法最优的利用可用的网络资源 (RFC1323)。

即使是对当前的网络延迟乘积有充分的认识,流量控制的实现也可能很复杂。当使用流量控制时,接收端必须及时地从 TCP 接收缓冲区读取数据。这样作可能致使在一些例如 WINDOW_UPDATE 的关键帧在 HTTP/2 不可用时致使死锁。可是流量控制能够保证约束资源能在不须要减小链接利用的状况下获得保护。

HTTP/2 的协议协商机制

非加密下的协商 - h2c

客户端使用 HTTP Upgrade 机制请求升级,HTTP2-Settings 首部字段是一个专用于链接的首部字段,它包含管理 HTTP/2 链接的参数(使用 Base64 编码),其前提是假设服务端会接受升级请求

GET / HTTP/1.1
 Host: server.example.com
 Connection: Upgrade, HTTP2-Settings
 Upgrade: h2c
 HTTP2-Settings: <base64url encoding of HTTP/2 SETTINGS payload>
复制代码

服务器若是支持 http/2 并赞成升级,则转换协议,不然忽略

HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: h2c
复制代码

此时潜在的存在一个流 0x1,客户端上这个流在完成 h1 请求后便转为 half-closed 状态,服务端会用这个流返回响应

image

image

image

注意图中第一个响应所在的流是 0x1,与上文所说的一致

目前浏览器只支持 TLS 加密下的 HTTP/2 通讯,因此上述状况在浏览器中目前是不可能碰到的,图中显示的是 nghttp 客户端发起的请求

加密的协商机制 - h2

TLS 加密中在 Client-Hello 和 Server-Hello 的过程当中经过 ALPN 进行协议协商

image

应用层协议协商在 TLS 握手第一步的扩展中,Client Hello 中客户端指定 ALPN Next Protocol 为 h2 或者 http/1.1 说明客户端支持的协议

image

服务端若是在 Server Hello 中选择 h2 扩展,说明协商协议为 h2,后续请求响应跟着变化;若是服务端未设置 http/2 或者不支持 h2,则继续用 http/1.1 通讯

分析实例

image

196: TLS 握手第一步 Client Hello,开始协议协商,且此处带上了 Session Ticket

200: Server Hello 赞成使用 h2,并且客户端的会话票证有效,恢复会话,握手成功

202: 客户端也恢复会话,开始加密后续消息

205: 服务端发起一个链接前言 (SETTINGS),SETTINGS 帧中设置了最大并行流数量、初始窗口大小、最大帧长度,而后 (WINDOW_UPDATE) 扩大窗口大小

310: 客户端也发送一个链接前言 Magic,并初始化设置 (SETTINGS),SETTINGS 帧中设置了 HEADER TABLE 大小、初始窗口大小、最大并行流数量,而后 (WINDOW_UPDATE) 扩大窗口大小

311: 客户端发送完链接前言后能够当即跟上一个请求,GET / (HEADERS[1]),并且这个 HEADERS 帧还带有 END_STREAM,这会使流 1 从 idle 状态当即转为 half-closed(local) 状态 (open 是中间态)

image

311: 此消息中还包含一个客户端发送给服务端的带 ACK 的 SETTINGS 帧

312: 服务端也响应带 ACK 的 SETTINGS 帧

321: 服务端在流 1 (此时状态为 half-closed(remote)) 上发送了四个 PUSH_PROMISE 帧,它们分别保留了流 二、四、六、8 用于后续推送,

image

321: 此消息中还返回了上面请求的响应 (HEADERS - DATA),最后 DATA 带上 END_STREAM,流 1 从 half-closed 转为 closed

329: 调整流优先级,依赖关系: 8 -> 6 -> 4 -> 2 -> 1 (都带有独占标志,并且权重均为 110)

image

342: 流 1 关闭后,流 2 获得分配资源,服务器开始推送,数据由两个 DATA 帧返回

344: 流 2 结束,开始推送流 4

356: 调整依赖关系

image

1         1         1         1(w: 110)
  |         |         |         |
  2         2         2         2(w: 110)
  |         |         |         |
  4   ==>   4   ==>   6   ==>   6(w: 147)
  |         |         |         |
  6         8         4         8(w: 147)
  |         |         |         |
  8         6         8         4(w: 110)
复制代码

36七、36九、372: 推送 6 和 8 的流数据

377: 发起一个请求,打开流 3,其中客户端发起的请求都是依赖流 0x0

以后都是一样的套路完成请求 - 响应,最后以 GOAWAY 帧关闭链接结束

HPACK 算法

image

上图来自 Ilya Grigorik 的 PPT - HTTP/2 is here, let's optimize!

能够清楚地看到 HTTP2 头部使用的也是键值对形式的值,并且 HTTP1 当中的请求行以及状态行也被分割成键值对,还有全部键都是小写,不一样于 HTTP1。除此以外,还有一个包含静态索引表和动态索引表的索引空间,实际传输时会把头部键值表压缩,使用的算法即 HPACK,其原理就是匹配当前链接存在的索引空间,若某个键值已存在,则用相应的索引代替首部条目,好比 “:method: GET” 能够匹配到静态索引中的 index 2,传输时只须要传输一个包含 2 的字节便可;若索引空间中不存在,则用字符编码传输,字符编码能够选择哈夫曼编码,而后分状况判断是否须要存入动态索引表中

索引表

静态索引

静态索引表是固定的,对于客户端服务端都同样,目前协议商定的静态索引包含 61 个键值,详见 Static Table Definition - RFC 7541

好比前几个以下

索引 字段值 键值
index Header Name Header Value
1 :authority
2 :method GET
3 :method POST
4 :path /
5 :path /index.html
6 :scheme http
7 :scheme https
8 :status 200
动态索引

动态索引表是一个 FIFO 队列维护的有空间限制的表,里面含有非静态表的索引。 动态索引表是须要链接双方维护的,其内容基于链接上下文,一个 HTTP2 链接有且仅有一份动态表。 当一个首部匹配不到索引时,能够选择把它插入动态索引表中,下次同名的值就可能会在表中查到索引并替换。 可是并不是全部首部键值都会存入动态索引,由于动态索引表是有空间限制的,最大值由 SETTING 帧中的 SETTINGS_HEADER_TABLE_SIZE (默认 4096 字节) 设置

  • 如何计算动态索引表的大小 (Table Size):

大小均以字节为单位,动态索引表的大小等于全部条目大小之和,每一个条目的大小 = 字段长度 + 键值长度 + 32

这个额外的 32 字节是预估的条目开销,好比一个条目使用了两个 64-bit 指针分别指向字段和键值,并使用两个 64-bit 整数来记录字段和键值的引用次数

golang 实现也是加上了 32: golang.org/x/net/http2…

SETTING 帧规定了动态表的最大大小,但编码器能够另外选择一个比 SETTINGS_HEADER_TABLE_SIZE 小的值做为动态表的有效负载量

  • 如何更新动态索引表的最大容量

修改最大动态表容量能够发送一个 dynamic table size update 信号来更改:

+---+---+---+---+---+---+---+---+
| 0 | 0 | 1 |   Max size (5+)   |
+---+---------------------------+
复制代码

前缀 001 表明此字节为 dynamic table size update 信号,后面使用 N=5 的整数编码方法表示新的最大动态表容量(不能超过 SETTINGS_HEADER_TABLE_SIZE),其计算方法下文会介绍。

须要注意的是这个信号必须在首部块发送以前或者两个首部块传输的间隔发送,能够经过发送一个 Max size 为 0 的更新信号来清空现有动态表

  • 动态索引表何时须要驱逐条目
  1. 每当出现表大小更新的信号时,须要判断并驱逐队尾的条目,即旧的索引,直到当前大小小于等于新的容量
  2. 每当插入新条目时,须要判断并驱逐队尾的条目,直到当前大小小于等于容量。这个情形下插入一个比 Max size 还大的新条目不会视做错误,但其结果是会清空动态索引表

关于动态索引表如何管理的,推荐看下 golang 的实现: golang.org/x/net/http2…,经过代码能更明白这个过程

索引地址空间

由静态索引表和动态索引表能够组成一个索引地址空间:

<----------  Index Address Space ---------->
  <-- Static  Table -->  <-- Dynamic Table -->
  +---+-----------+---+  +---+-----------+---+
  | 1 |    ...    | s |  |s+1|    ...    |s+k|
  +---+-----------+---+  +---+-----------+---+
                         ⍋                   |
                         |                   ⍒
                  Insertion Point      Dropping Point
复制代码

目前 s 就是 61,而有新键值要插入动态索引表时,从 index 62 开始插入队列,因此动态索引表中索引从小到大依次存着重新到旧的键值

编码类型表示

HPACK 编码使用两种原始类型: 无符号可变长度整数和八位字节表示的字符串,相应地规定了如下两种编码方式

整数编码

一个整数编码能够用于表示字段索引值、首部条目索引值或者字符串长度。 一个整数编码含两部分: 一个前缀字节和可选的后跟字节序列,只有前缀字节不足以表达整数值时才须要后跟字节,前缀字节中可用比特位 N 是整数编码的一个参数

好比下面所示的是一个 N=5 的整数编码(前三比特用于其余标识),若是咱们要编码的整数值小于 2^N - 1,直接用一个前缀字节表示便可,好比 10 就用 ???01010 表示

+---+---+---+---+---+---+---+---+
| ? | ? | ? |       Value       |
+---+---+---+-------------------+
复制代码

若是要编码的整数值 X 大于等于 2^N - 1,前缀字节的可用比特位都设成 1,而后把 X 减去 2^N - 1 获得值 R,并用一个或多个字节序列表示 R,字节序列中每一个字节的最高有效位 (msb) 用于表示是否结束,msb 设为 0 时表明是最后一个字节。具体编码看下面的伪代码和例子

+---+---+---+---+---+---+---+---+
| ? | ? | ? | 1   1   1   1   1 |
+---+---+---+-------------------+
| 1 |    Value-(2^N-1) LSB      |
+---+---------------------------+
               ...
+---+---------------------------+
| 0 |    Value-(2^N-1) MSB      |
+---+---------------------------+
复制代码

编码:

if I < 2^N - 1, encode I on N bits
else
    encode (2^N - 1) on N bits
    I = I - (2^N - 1)
    while I >= 128
         encode (I % 128 + 128) on 8 bits
         I = I / 128
    encode I on 8 bits
复制代码

解码:

decode I from the next N bits
if I < 2^N - 1, return I
else
    M = 0
    repeat
        B = next octet
        I = I + (B & 127) * 2^M
        M = M + 7
    while B & 128 == 128
    return I
复制代码

好比使用 N=5 的整数编码表示 1337:

1337 大于 31 (2^5 - 1), 将前缀字节后五位填满 1

I = 1337 - (2^5 - 1) = 1306

I 仍然大于 128, I % 128 = 26, 26 + 128 = 154

154 二进制编码: 10011010, 这便是第一个后跟字节

I = 1306 / 128 = 10, I 小于 128, 循环结束

将 I 编码成二进制: 00001010, 这便是最后一个字节

+---+---+---+---+---+---+---+---+
| X | X | X | 1 | 1 | 1 | 1 | 1 |  Prefix = 31, I = 1306
| 1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 |  1306 >= 128, encode(154), I=1306/128=10
| 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 |  10 < 128, encode(10), done
+---+---+---+---+---+---+---+---+
复制代码

解码时读取第一个字节,发现后五位 (11111) 对应的值 I 等于 31(>= 2^N - 1),说明还有后跟字节;令 M=0,继续读下一个字节 B,I = I + (B & 127) * 2^M = 31 + 26 * 1 = 57,M = M + 7 = 7,最高有效位为 1,表示字节序列未结束,B 指向下一个字节;I = I + (B & 127) * 2^M = 57 + 10 * 128 = 1337,最高有效位为 0,表示字节码结束,返回 I

这里也能够这样处理 1306: 1306 = 0x51a = (0101 0001 1010)B,将 bit 序列从低到高按 7 个一组分组,则有第一组 001 1010,第二组 000 1010,加上最高有效位 0/1 便与上面的后跟字节对应

字符编码

一个字符串可能表明 Header 条目的字段或者键值。字符编码使用字节序列表示,要么直接使用字符的八位字节码要么使用哈夫曼编码。

+---+---+---+---+---+---+---+---+
| H |    String Length (7+)     |
+---+---------------------------+
|  String Data (Length octets)  |
+-------------------------------+
复制代码
  • H: 一个比特位表示是否使用哈夫曼编码
  • String Length: 表明字节序列长度,即 String Data 的长度,使用 N=7 的整数编码方式表示
  • String Data: 字符串的八位字节码序列表示,若是 H 为 0,则此处就是原字符的八位字节码表示;若是 H 为 1,则此处为原字符的哈夫曼编码

RFC 7541 给出了一份字符的哈夫曼编码表: Huffman Code,这是基于大量 HTTP 首部数据生成的哈夫曼编码。

  • 当中第一列 (sym) 表示要编码的字符,最后的特殊字符 “EOS” 表明字符串结束
  • 第二列 (code as bits) 是二进制哈夫曼编码,向最高有效位对齐
  • 第三列 (code as hex) 是十六进制哈夫曼编码,向最低有效位对齐
  • 最后一列 (len) 表明编码长度,单位 bit

使用哈夫曼编码可能存在编码不是整字节的,会在后面填充 1 使其变成整字节

好比下面的例子:

Literal Header Field with Incremental Indexing - Indexed Name

:authority: blog.wangriyu.wang 首部对应的编码为:

41 8e 8e 83 cc bf 81 d5    35 86 f5 6a fe 07 54 df
复制代码

Literal Header Field with Incremental Indexing — Indexed Name 的编码格式见下文

41 (0100 0001) 表示字段存在索引值 1,即对应静态表中第一项 :authority

8e (1000 1110) 最高有效位为 1 表示键值使用哈夫曼编码,000 1110 表示字节序列长度为 14

后面 8e 83 cc bf 81 d5 35 86 f5 6a fe 07 54 df 是一段哈夫曼编码序列

由哈夫曼编码表可知 100011 -> 'b', 101000 -> 'l', 00111 -> 'o', 100110 -> 'g', 010111 -> '.', 1111000 -> 'w', 00011 -> 'a', 101010 -> 'n', 100110 -> 'g', 101100 -> 'r', 00110 -> 'i', 1111010 -> 'y', 101101 -> 'u'

8e 83 cc bf 81 d5 35 86 f5 6a fe 07 54 df
                         |
                         ⍒
1000 1110 1000 0011 1100 1100 1011 1111 1000 0001 1101 0101 0011 0101 1000 0110 1111 0101 0110 1010 1111 1110 0000 0111 0101 0100 1101 1111
                         |
                         ⍒
100011 101000 00111 100110 010111 1111000 00011 101010 100110 101100 00110 1111010 101101 010111 1111000 00011 101010 100110 11111
                         |
                         ⍒
blog.wangriyu.wang  最后 11111 用于填充
复制代码

二进制编码

如今开始是 HPACK 真正的编解码规范

已索引首部条目表示 (Indexed Header Field Representation)
  • Indexed Header Field

以 1 开始为标识,能在索引空间匹配到索引的首部会替换成这种形式,后面的 index 使用上述的整数编码方式且 N = 7。 好比 :method: GET 能够用 0x82,即 10000010 表示

+---+---+---+---+---+---+---+---+
| 1 |        Index (7+)         |
+---+---------------------------+
复制代码

Indexed Header Field

未索引文字首部条目表示 (Literal Header Field Representation)

还没有被索引的首部有三种表示形式,第一种会添加进索引,第二种对于当前跳来讲不会添加进索引,第三种绝对不被容许添加进索引

  1. 会添加索引的文字首部 (Literal Header Field with Incremental Indexing)

以 01 开始为标识,此首部会加入到解码后的首部列表 (Header List) 中而且会把它做为新条目插入到动态索引表中

  • Literal Header Field with Incremental Indexing — Indexed Name

若是字段已经存在索引,但键值未被索引,好比首部 :authority: blog.wangriyu.wang 的字段 :authority 已存在索引但键值 blog.wangriyu.wang 不存在索引,则会替换成以下形式 (index 使用 N=6 的整数编码表示)

+---+---+---+---+---+---+---+---+
| 0 | 1 |      Index (6+)       |
+---+---+-----------------------+
| H |     Value Length (7+)     |
+---+---------------------------+
| Value String (Length octets)  |
+-------------------------------+
复制代码

Literal Header Field with Incremental Indexing - Indexed Name

  • Literal Header Field with Incremental Indexing — New Name

若是字段和键值均未被索引,好比 upgrade-insecure-requests: 1,则会替换成以下形式

+---+---+---+---+---+---+---+---+
| 0 | 1 |           0           |
+---+---+-----------------------+
| H |     Name Length (7+)      |
+---+---------------------------+
|  Name String (Length octets)  |
+---+---------------------------+
| H |     Value Length (7+)     |
+---+---------------------------+
| Value String (Length octets)  |
+-------------------------------+
复制代码

Literal Header Field with Incremental Indexing — New Name

  1. 不添加索引的首部 (Literal Header Field without Indexing)

以 0000 开始为标识,此首部会加入到解码后的首部列表中,但不会插入到动态索引表中

  • Literal Header Field without Indexing — Indexed Name

若是字段已经存在索引,但键值未被索引,则会替换成以下形式 (index 使用 N=4 的整数编码表示)

+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 0 |  Index (4+)   |
+---+---+-----------------------+
| H |     Value Length (7+)     |
+---+---------------------------+
| Value String (Length octets)  |
+-------------------------------+
复制代码

Literal Header Field without Indexing - Indexed Name

  • Literal Header Field without Indexing — New Name

若是字段和键值均未被索引,则会替换成以下形式。好比 strict-transport-security: max-age=63072000; includeSubdomains

+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 0 |       0       |
+---+---+-----------------------+
| H |     Name Length (7+)      |
+---+---------------------------+
|  Name String (Length octets)  |
+---+---------------------------+
| H |     Value Length (7+)     |
+---+---------------------------+
| Value String (Length octets)  |
+-------------------------------+
复制代码

Literal Header Field without Indexing - New Name

  1. 绝对不添加索引的首部 (Literal Header Field Never Indexed)

这与上一种首部相似,只是标识为 0001,首部也是会添加进解码后的首部列表中但不会插入动态更新表。

区别在于这类首部发出是什么格式表示,接收也是同样的格式,做用于每一跳 (hop),若是中间经过代理,代理必须原样转发不能另行编码。

而上一种首部只是做用当前跳,经过代理后可能会被从新编码

golang 实现中使用一个 Sensitive 标明哪些字段是绝对不添加索引的: golang.org/x/net/http2…

RFC 文档中详细说明了这么作的缘由: Never-Indexed Literals

表示形式除了标识其余都跟上一种首部同样:

  • Literal Header Field Never Indexed — Indexed Name
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 1 |  Index (4+)   |
+---+---+-----------------------+
| H |     Value Length (7+)     |
+---+---------------------------+
| Value String (Length octets)  |
+-------------------------------+
复制代码
  • Literal Header Field Never Indexed — New Name
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 1 |       0       |
+---+---+-----------------------+
| H |     Name Length (7+)      |
+---+---------------------------+
|  Name String (Length octets)  |
+---+---------------------------+
| H |     Value Length (7+)     |
+---+---------------------------+
| Value String (Length octets)  |
+-------------------------------+
复制代码
动态表最大容量更新 (Dynamic Table Size Update)

以 001 开始为标识,做用前面已经提过

+---+---+---+---+---+---+---+---+
| 0 | 0 | 1 |   Max size (5+)   |
+---+---------------------------+
复制代码

Literal Header Field without Indexing - Indexed Name

能够发送 Max Size 为 0 的更新来清空动态索引表

Literal Header Field without Indexing - Indexed Name

实例

RFC 中给出了不少实例 Examples - RFC 7541,推荐看一遍加深理解

What then ?

HTTP/2 演示

http2.akamai.com/demo

http2.golang.org/

网站启用 h2 的先后对比,使用 WebPageTest 作的测试,第一张是 h1,第二张是 h2:

image
image

使用 HTTP/2 建议

nginx 开启 HTTP2 只需在相应的 HTTPS 设置后加上 http2 便可

listen [::]:443 ssl http2 ipv6only=on;
listen 443 ssl http2;
复制代码

如下几点是 HTTP/1 和 HTTP/2 都一样适用的

一、开启压缩

配置 gzip 等可使传输内容更小,传输速度更快

例如 nginx 能够再 http 模块中加入如下字段,其余字段和详细解释能够谷歌

gzip  on; // 开启
    gzip_min_length 1k;
    gzip_comp_level 1; // 压缩级别
    gzip_types text/plain application/javascript application/x-javascript application/octet-stream application/json text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png font/ttf font/otf image/svg+xml; // 须要压缩的文件类型
    gzip_vary on;
    gzip_disable "MSIE [1-6]\.";
复制代码

二、使用缓存

给静态资源设置一个缓存期是很是有必要的,关于缓存见另外一篇博文 HTTP Message

例如 nginx 在 server 模块中添加如下字段能够设置缓存时间

location ~* ^.+\.(ico|gif|jpg|jpeg|png|moc|mtn|mp3|mp4|mov)$ {
   access_log   off;
   expires      30d;
 }

 location ~* ^.+\.(css|js|txt|xml|swf|wav|json)$ {
   access_log   off;
   expires      5d;
 }

 location ~* ^.+\.(html|htm)$ {
   expires      24h;
 }

 location ~* ^.+\.(eot|ttf|otf|woff|svg)$ {
   access_log   off;
   expires 30d;
 }
复制代码

三、CDN 加速

CDN 的好处是就近访问,延迟低,访问快

四、减小 DNS 查询

每一个域名都须要 DNS 查询,通常须要几毫秒到几百毫秒,移动环境下会更慢。DNS 解析完成以前,请求会被阻塞。减小 DNS 查询也是优化项之一

浏览器的 DNS Prefetching 技术也是一种优化手段

五、减小重定向

重定向可能引入新的 DNS 查询、新的 TCP 链接以及新的 HTTP 请求,因此减小重定向也很重要。

浏览器基本都会缓存经过 301 Moved Permanently 指定的跳转,因此对于永久性跳转,能够考虑使用状态码 301。对于启用了 HTTPS 的网站,配置 HSTS 策略,也能够减小从 HTTP 到 HTTPS 的重定向

但如下几点就不推荐在 HTTP/2 中用了

一、域名分片

HTTP/2 对于同一域名使用一个 TCP 链接足矣,过多 TCP 链接浪费资源并且效果不见得必定好

并且资源分域会破坏 HTTP/2 的优先级特性,还会下降头部压缩效果

二、资源合并

资源合并会不利于缓存机制,并且单文件过大对于 HTTP/2 的传输很差,尽可能作到细粒化更有利于 HTTP/2 传输

三、资源内联

HTTP/2 支持 Server-Push,相比较内联优点更大效果更好

并且内联的资源不能有效缓存

若是有共用,多页面内联也会形成浪费

HTTP/2 最佳实践

使用 HTTP/2 尽量用最少的链接,由于同一个链接上产生的请求和响应越多,动态字典积累得越全,头部压缩效果也就越好,并且多路复用效率高,不会像多链接那样形成资源浪费

为此须要注意如下两点:

  • 同一域名下的资源使用同一个链接,这是 HTTP/2 的特性
  • 不一样域名下的资源,若是知足能解析到同一 IP 或者使用的是同一个证书(好比泛域名证书),HTTP/2 能够合并多个链接

因此使用相同的 IP 和证书部署 Web 服务是目前最好的选择,由于这让支持 HTTP/2 的终端能够复用同一个链接,实现 HTTP/2 协议带来的好处;而只支持 HTTP/1.1 的终端则会不一样域名创建不一样链接,达到同时更多并发请求的目的

好比 Google 一系列网站都是用的同一个证书:

image

可是这好像也会形成一个问题,我使用 nginx 搭建的 webserver,有三个虚拟主机,它们共用一套证书,其中两个我显示地配置了 http2,而剩下一个我并无配置 http2,结果我访问未配置 http2 的站点时也变成了 http2。

大图片传输碰到的问题

先比较一下 h1 和 h2 的页面加载时间,图中绿色表明发起请求收到响应等待负载的时间,蓝色表明下载负载的时间:

image
image

能够发现 h2 加载时间还比 h1 慢一点,特别是碰到大图片时差异更明显

这篇文章对不一样场景下 h1 和 h2 加载图片作了测试: Real–world HTTP/2: 400gb of images per day

其结果是:

  • 对一个典型的富图像,延迟限制 (latency–bound) 的界面来讲。使用一个高速,低延迟的链接,视觉完成度 (visual completion) 平均会快 5%。

  • 对一个图像极其多,带宽限制 (bandwidth–bound) 的页面来讲。使用一样的链接,视觉完成度平均将会慢 5–10%,但页面的总体加载时间实际是减小了,由于得益于链接延迟少。

  • 一个高延迟,低速度的链接(好比移动端的慢速 3G) 会对页面的视觉完成形成极大的延迟,但 h2 的视觉完成度明显更高更好。

在全部的测试中,均可以看到: h2 使总体页面的加载速度提升了,而且在初次绘制 (initial render) 上作的更好,虽然第二种状况中视觉完成度略微降低,但整体效果仍是好的

视觉完成度降低的缘由是由于没有 HTTP/1.x 同时链接数量的限制,h2 能够同时发起多张图片的请求,服务器能够同时响应图片的负载,能够从下面的动图中看到

image

一旦图片下载完成,浏览器就会绘制出它们,然而,小图片下载后会渲染地更快,可是若是一个大图片刚好是初始的视图,那就会花费较长的时间加载,延迟视觉上的完成度。

chrome bug

上面的动图是在 Safari 上的测试结果,图片最后都下载成功了,而我在 Chrome 上测试时后面的部分图片直接挂了,都报 ERR_SPDY_PROTOCOL_ERROR 错误,并且是百分百复现

image

去看了下 ERR_SPDY_PROTOCOL_ERROR 出在哪,发现是 Server reset stream,应该是哪出错了致使流提早终止

image

而后再研究了一下 HTTP/2 的帧序列,发出的请求都在 629 号消息中响应成功了,可是返回的数据帧只有流 15 上的,实际收到的图片又不止流 15 对应的图片,这是为何?

image

后面我继续测试发现连续请求几张大图片,虽然 HEADERS 帧都打开的是不一样的流,返回的响应的 HEADERS 帧也仍是对应前面的流 ID,可是响应的 DATA 帧都是从第一个打开的流上返回的。

若是是小图片的话,一个请求响应事后这个流就关闭了,下一张小图是在其本身对应的流上返回的。只有连续几张大图会出现上述情形,这个机制很奇怪,我暂时尚未找到解释的文档。

至于 chrome 为何出错呢,看一下 TCP 报文就会发现全部数据在一个链接上发送,到后面 TCP 包会出现各类问题,丢包、重传、失序、重包等等,不清楚 Safari 是否也是这样,由于 wireshark 只能解 chrome 的包解不了 Safari 的包

image

《web 性能权威指南》中说起 HTTP/2 中一个 TCP 可能会形成的问题: 虽然消除了 HTTP 队首阻塞现象,但 TCP 层次上仍存在队首阻塞问题;若是 TCP 窗口缩放被禁用,那带宽延迟积效应可能会限制链接的吞吐量;丢包时 TCP 拥塞窗口会缩小;

TCP 是一方面缘由,还有另外一方面应该是浏览器策略问题,估计也是 chrome bug,对比两张动图你会发现,safari 接收负载是轮流接收,咱们几个接收一点而后换几我的接收,直到全部都接受完;而 chrome 则是按顺序接收,这个接收完才轮到下一个接收,结果后面的图片可能长时间未响应就挂了。

使用渐进式图片

渐进式 jpg 代替普通 jpg 有利于提升视觉完成度,并且文件更小:

输入 convert --version 看看是否已安装 ImageMagic,若是没有先安装: Mac 能够用 brew install imagemagick,Centos 能够用 yum install imagemagick

检测是否为 progressive jpeg,若是输出 None 说明不是 progressive jpeg;若是输出 JPEG 说明是 progressive jpeg:

$ identify -verbose filename.jpg | grep Interlace
复制代码

将 basic jpeg 转换成 progressive jpeg,interlace 参数:

$ convert -strip -interlace Plane source.jpg destination.jpg // 还能够指定质量 -quality 90

// 批量处理
$ for i in ./*.jpg; do convert -strip -interlace Plane $i $i; done
复制代码

也能够转换 PNG 和 GIF,可是我试过 convert -strip -interlace Plane source.png destination.png 但转换后的图片每每会更大,不推荐这么用,能够 convert source.png destination.jpg

ImageMagic 还有不少强大的功能

// 图片缩放
$ convert -resize 50%x50% source.jpg destination.jpg
// 图片格式转换
$ convert source.jpg destination.png
// 配合 find 命令,将当前目录下大于 100kb 的图片按 75% 质量进行压缩
$ find -E . -iregex '.*\.(jpg|png|bmp)' -size +100k -exec convert -strip +profile “*” -quality 75 {} {} \;
复制代码

png 压缩推荐使用 pngquant

另外 photoshop 保存图片时也能够设置渐进或交错:

渐进式图片:选择图片格式为 JPEG => 选中“连续”

交错式图片:选择图片格式为 PNG/GIF => 选中“交错”

SPDY 与 HTTP2 的关系

SPDY 是 HTTP2 的前身,大部分特性与 HTTP2 保持一致,包括服务器端推送,多路复用和帧做为传输的最小单位。但 SPDY 与 HTTP2 也有一些实现上的不一样,好比 SPDY 的头部压缩使用的是 DEFLATE 算法,而 HTTP2 使用的是 HPACK 算法,压缩率更高。

QUIC 协议

Google 的 QUIC(Quick UDP Internet Connections) 协议,继承了 SPDY 的特色。QUIC 是一个 UDP 版的 TCP + TLS + HTTP/2 替代实现。

QUIC 能够建立更低延迟的链接,而且也像 HTTP/2 同样,经过仅仅阻塞部分流解决了包裹丢失这个问题,让链接在不一样网络上创建变得更简单 - 这其实正是 MPTCP 想去解决的问题。

QUIC 如今还只有 Google 的 Chrome 和它后台服务器上的实现,虽然有第三方库 libquic,但这些代码仍然很难在其余地方被复用。该协议也被 IETF 通讯工做组引入了草案。

Caddy: 基于 Go 语言开发的 Web Server, 对 HTTP/2 和 HTTPS 有着良好的支持,也开始支持 QUIC 协议 (试验性)

推荐工具

若是你访问的站点开启了 HTTP/2,图标会亮起,并且点击会进入 chrome 内置的 HTTP/2 监视工具

C 语言实现的 HTTP/2,能够用它调试 HTTP/2 请求

直接 brew install nghttp2 就能够安装,安装好后输入 nghttp -nv https://nghttp2.org 就能够查看 h2 请求

image

若是没法解包看一下 sslkeylog.log 文件有没有写入数据,若是没有数听说明浏览器打开方式不对,得用命令行打开浏览器,这样才能让浏览器读取环境变量而后向 sslkeylog 写入密钥,另外此方法好像支持谷歌浏览器和火狐,对 Safari 无效

若是 sslkeylog.log 有数据,wireshark 仍是没法解包,打开设置的 SSL 选项从新选择一下文件试试,若是仍是不行也用命令行打开 Wireshark

一次不行多试几回

  • h2o: 优化的 HTTP Server,对 HTTP/2 的支持性作的比较好

References

相关文章
相关标签/搜索