30 | 时代之风(上):HTTP/2特性概览

  • Q:HTTP/2怎么做头部压缩?
    • 开发了专门的“HPACK”算法,在客户端和服务器两端建立“字典”,用索引号表示重复的字符串,还釆用哈夫曼编码来压缩整数和字符串,可以达到 50%~90% 的高压缩率
  • Q:HTTP/2怎么使用二进制
    • 把TCP协议的部分特性挪到了应用层,把原来的「Header+Body」的消息打散为数个小片的二进制「帧」(Frame),用「HEADERS」帧存放头数据、「DATA」帧存放实体数据
  • Q:HTTP/2的流(Stream)是什么?
    • 它是二进制帧的双向传输序列,同一个消息往返的帧会分配一个唯一的流ID。在里面的是一串有先后顺序的数据帧,这些数据帧按照次序组装起来就是 HTTP/1 里的请求报文和响应报文
    • 因为「流」是虚拟的,实际上并不存在,所以 HTTP/2 就可以在一个 TCP 连接上用「流」同时发送多个「碎片化」的消息,这就是常说的“多路复用”(Multiplexing)——多个往返通信都复用一个连接来处理
    • 在「流」的层面上看,消息是一些有序的「帧」序列,而在「连接」的层面上看,消息是乱序收发的「帧」。多个请求/响应之间没有了顺序关系,不需要排队等待, 也就不会再出现「队头阻塞」问题,降低了延迟,大幅度提高了连接的利用率
    • 为了更好地利用连接,加大吞吐量,HTTP/2 还添加了一些控制帧来管理虚拟的「流」,实现了优先级和流量控制,这些特性也和 TCP 协议非常相似
    • HTTP/2 还在一定程度上改变了传统的「请求 - 应答」工作模式,服务器不再是完全被动地响应请求,也可以新建「流」主动向客户端发送消息。比如,在浏览器刚请求 HTML 的时候就提前把可能会用到的 JS、CSS 文件发给客户端,减少等待的延迟,这被称为“服务器推送”(Server Push,也叫 Cache Push)

31 | 时代之风(下):HTTP/2内核剖析

  • Q:HTTP/2的连接前言(connection preface)是什么
    • 由于 HTTP/2是基于 TLS,所以在正式收发数据之前,会有 TCP 握手和 TLS 握手,TLS 握手成功之后,客户端必须要发送一个连接前言用来确认建立 HTTP/2 连接
    • 这个连接前言是标准的HTTP/1请求报文,使用纯文本的ASCII码格式,请求方法是特别注册的一个关键字「PRI」,全文只有24个字节:
    PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n
    
    • 在 Wireshark 里,HTTP/2 的连接前言被称为「Magic」,只要服务器收到这个「有魔力的字符串」,就知道客户端在 TLS 上想要的是 HTTP/2 协议,后面就会都使用 HTTP/2 的数据格式
  • Q:HTTP/2的头部压缩是怎样?
    • 确立了连接之后,HTTP/2 就开始准备请求报文
    • 因为语义上它与 HTTP/1 兼容,所以报文还是由「Header+Body」构成的,但在请求发送前,必须要用「HPACK」算法来压缩头部数据
    • 「HPACK」算法是专门为压缩 HTTP 头部定制的算法,与 gzip、zlib 等压缩算法不同,它是一个“有状态”的算法,需要客户端和服务器各自维护一份“索引表”,压缩和解压缩就是查表和更新表的操作
    • 为了方便管理和压缩,HTTP/2 废除了原有的起始行概念,把起始行里面的请求方法、URI、状态码等统一转换成了头字段的形式,即「伪头字段」(pseudo-header fields)。并废除起始行里的版本号和错误原因短语
    • 伪头字段会在名字前加一个「:」,比如「:authority」、「:method」、「:status」,分别表示的是域名、请求方法和状态码
    • 现在HTTP报文头全都是「Key-Value」形式的字段,于是 HTTP/2 就为一些最常用的头字段定义了一个只读的静态表(Static Table),如下表,如数字「2」代表「GET」,数字「8」代表状态码200
    • 但如果表里只有 Key 没有 Value,或者是自定义字段根本找不到就要用到“动态表”(Dynamic Table),它添加在静态表后面,结构相同,但会在编码解码的时候随时更新
    • 如第一次发送请求时的「user-agent」字段长是一百多个字节,用哈夫曼压缩编码发送之后,客户端和服务器都更新自己的动态表,添加一个新的索引号「65」。那么下一次发送的时候就不用再重复发那么多字节了,只要用一个字节发送编号就好
    • 随着在 HTTP/2 连接上发送的报文越来越多,两边的「字典」也会越来越丰富,最终每次的头部字段都会变成一两个字节的代码,原来上千字节的头用几十个字节就可以表示了,压缩效果比 gzip 要好得多
  • Q:HTTP/2的二进制帧是怎样
    • 头部数据压缩之后,HTTP/2 就要把报文拆成二进制的帧准备发送
    • HTTP/2 的帧结构有点类似 TCP 的段或者 TLS 里的记录,但报头很小,只有 9 字节
    • 二进制的格式也保证了不会有歧义,而且使用位运算能够非常简单高效地解析
    • 帧开头是 3 个字节的长度(但不包括头的 9 个字节),默认上限是 2^14,最大是 2^24,也就是说 HTTP/2 的帧通常不超过 16K,最大是 16M
    • 长度后面的一个字节是帧类型,大致可分为数据帧和控制帧两类,HEADERS帧和DATA帧属于数据帧,存放的HTTP报文,而SETTINGS、PING、PRIORITY等则是用来管理流的控制帧
    • HTTP/2 总共定义了 10 种类型的帧,但一个字节可以表示最多 256 种,所以也允许在标准之外定义其他类型实现功能扩展。比如 Google 的 gRPC 就利用了这个特点,定义了几种自用的新帧类型
    • 第5个字节是帧标志,可以保存8个标志位,携带简单的控制信息。常用的标志位有END_HEADERS表示头数据结束,相当于HTTP/1里头后的空行「\r\0」,END_STREAM表示单方向数据发送接受(EOS,End Of Stream),相当于HTTP/1里Chunked分块结束标志「\0\r\n\r\n」
    • 报文投里最后4个字节是流标识符,即帧所属的「流」,接收方使用它就可从乱序的帧里识别出具有相同流ID的帧序列,按顺序组装起来就实现了虚拟的「流」
    • 流标识符虽然有4个字节,但最高位被保留不用,所以只有31位可使用,即流标识符的上限是2^31,大约21亿
  • Q:HTTP/2 的流与多路复用是怎样?
    • 流是二进制帧的双向传输序列
    • 在 HTTP/2 连接上,虽然帧是乱序收发的,但只要它们都拥有相同的流 ID,就都属于一个流,而且在这个流里帧不是无序的,而是有着严格的先后顺序
    • 在概念上,一个 HTTP/2 的流就等同于一个 HTTP/1 里的「请求-应答」。在 HTTP/1 里一个「请求 - 响应」报文来回是一次 HTTP 通信,在 HTTP/2 里一个流也承载了相同的功能
  • Q:HTTP/2 的流有哪些特点呢
    • 1.流是可并发的,一个 HTTP/2 连接上可以同时发出多个流传输数据,也就是并发多请求,实现“多路复用”
    • 2.客户端和服务器都可以创建流,双方互不干扰
    • 3.流是双向的,一个流里面客户端和服务器都可以发送或接收数据帧,也就是一个“请求 - 应答”来回
    • 4.流之间没有固定关系,彼此独立,但流内部的帧是有严格顺序的
    • 5.流可以设置优先级,让服务器优先处理,比如先传 HTML/CSS,后传图片,优化用户体验
    • 6.流 ID 不能重用,只能顺序递增,客户端发起的 ID 是奇数,服务器端发起的 ID 是偶数
    • 7.在流上发送“RST_STREAM”帧可以随时终止流,取消接收或发送
    • 8.第 0 号流比较特殊,不能关闭,也不能发送数据帧,只能发送控制帧,用于流量控制
  • Q:连接中无序的帧是如何依据流 ID 重组成流的
    • 在 Wireshark 抓包里,就有“0、1、3”一共三个流,实际上就是分配了三个流 ID 号,把这些帧按编号分组,再排一下队,就成了流
    • HTTP/2 在一个连接上使用多个流收发数据,那么它本身默认就会是长连接,所以永远不需要“Connection”头字段(keepalive 或 close)
    • 下载大文件的时候想取消接收,在 HTTP/1 里只能断开 TCP 连接重新“三次握手”,成本很高,而在 HTTP/2 里就可以简单地发送一个“RST_STREAM”中断流,而长连接会继续保持
    • 因为客户端和服务器两端都可以创建流,而流 ID 有奇数偶数和上限的区分,所以大多数的流 ID 都会是奇数,而且客户端在一个连接里最多只能发出 2^30,也就是 10 亿个请求
    • ID 用完了可以再发一个控制帧“GOAWAY”,真正关闭 TCP 连接
  • Q:HTTP/2如何根据帧的标志位实现流状态转换
    • 最开始的时候流都是空闲(idle)状态
    • 当客户端发送HEADERS帧后,有了流ID,流就进入了「打开」状态,两端都可以收发数据,然后客户端发送一个带「END_STREAM」标志位的帧,流就进入了「半关闭」状态。意味着客户端的请求数据已经发送完了,需要接受响应数据,而服务端也知道请求数据接收完毕,之后就要内部处理,再发送响应数据
    • 响应数据发完了之后,也要带上「END_STREAM」标志位,表示数据发送完毕,这样流两端就都进入了关闭状态,流就结束了
    • 流 ID 不能重用,所以流的生命周期就是 HTTP/1 里的一次完整的「请求 - 应答」,流关闭就是一次通信结束
    • 下一次再发请求就要开一个新流(而不是新连接),流 ID 不断增加,直到到达上限,发送「GOAWAY」帧开一个新的 TCP 连接,流 ID 就又可以重头计数
    • 因为流可以并发,所以 HTTP/2 就可以实现无阻塞的多路复用

32 | 未来之路:HTTP/3展望

  • Q:为什么HTTP/2没有完全解决队头阻塞呢?
    • 因为 HTTP/2 虽然使用“帧”“流”“多路复用”,没有了“队头阻塞”,但这些手段都是在应用层里,而在下层,也就是 TCP 协议里,还是会发生“队头阻塞”
    • 从协议栈的角度来仔细看一下。在 HTTP/2 把多个“请求 - 响应”分解成流,交给 TCP 后,TCP 会再拆成更小的包依次发送(其实在 TCP 里应该叫 segment,也就 是“段”)
    • 在网络良好的情况下,包可以很快送达目的地。但如果网络质量比较差,而 TCP 为了保证可靠传输,有个特别的“丢包重传”机制,丢失的包必须要等待重新传输确认,其他的包即使已经收到了,也只能放在缓冲区里,上层的应用拿不出来,只能等待
  • Q:不同HTTP协议栈图的对比
  • Q:QUIC有什么特点?
    • 基于 UDP,而 UDP 是无连接的,不需要握手和挥手,所以比 TCP 快
    • 基于 UDP 实现了可靠传输,保证数据一定能够抵达目的地。它还引入了类似 HTTP/2 的“流”和“多路复用”,单个“流”是有序的,可能会因为丢包而阻塞,但其他“流”不会受到影响
    • 为了防止网络上的中间设备(Middle Box)识别协议的细节,QUIC 全面采用加密通信,可以很好地抵御窜改和“协议僵化”(ossification)
    • 直接应用了 TLS1.3,也就获得了 0-RTT、1-RTT 连接的好处
    • 不是建立在 TLS 之上,而是内部“包含”了 TLS。它使用自己的帧“接管”了 TLS 里的“记录”,握手消息、警报消息都不使用 TLS 记录,直接封装成 QUIC 的帧发送,省掉了一次开销
  • Q:QUIC内部介绍
    • QUIC 的基本数据传输单位是包(packet)和帧(frame),一个包由多个帧组成,包面向的是“连接”,帧面向的是“流”
    • QUIC 使用不透明的“连接 ID”来标记通信的两个端点,客户端和服务器可以自行选择一组 ID 来标记自己,这样就解除了 TCP 里连接对“IP 地址 + 端口”(即常说的四元组)的强绑定,支持“连接迁移”(Connection Migration)
    • 即当 IP 地址会发生变化,TCP 就必须重新建立连接。而 QUIC 连接里的两端连接 ID 不会变,所以连接在“逻辑上”没有中断,它就可以在新的 IP 地址上继续使用之前的连接,消除重连的成本,实现连接的无缝迁移
    • QUIC 的帧里有多种类型,PING、ACK 等帧用于管理连接,而 STREAM 帧专门用来实流
    • QUIC 里的流与 HTTP/2 的流非常相似,也是帧的序列,但 HTTP/2 里的流都是双向的,而 QUIC 则分为双向流和单向流
    • QUIC 帧普遍采用变长编码,最少只要 1 个字节,最多有 8 个字节。流 ID 的最大可用位数是 62,数量上比 HTTP/2 的 2^31 大大增加
    • 流 ID 还保留了最低两位用作标志,第 1 位标记流的发起者,0 表示客户端,1 表示服务器;第 2 位标记流的方向,0 表示双向流,1 表示单向流
    • 所以 QUIC 流 ID 的奇偶性质和 HTTP/2 刚好相反,客户端的 ID 是偶数,从 0 开始计数
  • Q:HTTP/3 协议
    • 因为 QUIC 本身就已经支持了加密、流和多路复用,所以 HTTP/3 的工作减轻了很多,把流控制都交给 QUIC 去做。调用的不再是 TLS 的安全接口,也不是 Socket API,而是专门的 QUIC 函数
    • HTTP/3 里使用QUIC流来发送「请求 - 响应」
    • HTTP/3 里的“双向流”可以完全对应到 HTTP/2 的流,而“单向流”在 HTTP/3 里用来实现控制和推送,近似地对应 HTTP/2 的 0 号流
    • HTTP/3 里帧的结构变简单了。帧头只有两个字段:类型和长度,而且同样都采用变长编码,最小只需要两个字节
    • HTTP/3 里的帧仍然分成数据帧和控制帧两类,HEADERS 帧和 DATA 帧传输数据,但其他一些帧因为在下层的 QUIC 里有了替代,所以在 HTTP/3 里就都消失了,比如 RST_STREAM、WINDOW_UPDATE、PING 等
    • 头部压缩算法在 HTTP/3 里升级成了“QPACK”,使用方式上也做了改变。虽然也分成静态表和动态表,但在流上发送 HEADERS 帧时不能更新字段,只能引用,索引表的更新需要在专门的单向流上发送指令来管理,解决了 HPACK 的“队头阻塞”问题
    • 另外,QPACK 的字典也做了优化,静态表由之前的 61 个增加到了 98 个,而且序号从 0 开始,也就是说“:authority”的编号是 0
  • Q:HTTP/3 服务发现是怎样的呢
    • 用到 HTTP/2 里的“扩展帧”了。浏览器需要先用 HTTP/2 协议连接服务器,然后服务器可以在启动 HTTP/2 连接后发送一个“Alt-Svc”帧,包含一个“h3=host:port”的字符串,告诉浏览器在另一个端点上提供等价的 HTTP/3 服务。浏览器收到“Alt-Svc”帧,会使用 QUIC 异步连接指定的端口,如果连接成功,就会断开 HTTP/2 连接,改用新的 HTTP/3 收发数据

33 | 我应该迁移到HTTP/2吗?

  • Q:HTTP/2 的优点是什么
    • 1.完全保持了与 HTTP/1 的兼容,在语义上没有任何变化
    • 2.对 HTTPS 在各方面都做了强化。下层的 TLS 至少是 1.2,而且只能用前向安全的密码套件(即 ECDHE),这同时也就默认实现了“TLS False Start”,支持 1-RTT 握手,所以不需要再加额外的配置就可以自动实现 HTTPS 加速
    • 3.在 HTTP/1 里只能压缩 body,而 HTTP/2 则可以用 HPACK 算法压缩 header,这对高流量的网站非常有价值,有数据表明能节省大概 5%~10% 的流量
    • 4.HTTP/2 的“多路复用”特性要求对一个域名(或者 IP)只用一个 TCP 连接,所有的数据都在这一个连接上传输,这样不仅节约了客户端、服务器和网络的资源,还可以把带宽跑满
    • 5.“优先级”可以让客户端告诉服务器,哪个文件更重要,更需要优先传输,服务器就可以调高流的优先级,合理地分配有限的带宽资源,让高优先级的 HTML、图片更快地到达客户端,尽早加载显示
    • 6.“服务器推送”也是降低延迟的有效手段,它不需要客户端预先请求,服务器直接就发给客户端,这就省去了客户端解析 HTML 再请求的时间
  • Q:HTTP/2 的缺点是什么
    • 在 TCP 级别还是存在“队头阻塞”的问题。所以,如果网络连接质量差,发生丢包,那么 TCP 会等待重传,传输速度就会降低
    • 在移动网络中发生 IP 地址切换的时候,下层的 TCP 必须重新建连,要再次“握手”,经历“慢启动”,而且之前连接里积累的 HPACK 字典也都消失了,必须重头开始计算,导致带宽浪费和时延
    • HTTP/2 对一个域名只开一个连接,所以一旦这个连接出问题,那么整个网站的体验也就变差了
  • Q:如何配置HTTP/2
    • Nginx 里启用 HTTP/2 只需要在 server 配置里再多加一个参数就可以搞定
    server {
        listen       443 ssl http2;
    
        server_name  www.xxx.net;
        
        ssl_certificate         xxx.crt;
        ssl_certificate_key     xxx.key;
    
    • “listen”指令在“ssl”后面多了一个“http2”,这就表示在 443 端口上开启了 SSL 加密,然后再启用 HTTP/2
    • 配置服务器推送特性可以使用指令“http2_push”和“http2_push_preload”
    http2_push         /style/xxx.css;
    http2_push_preload on;
    
    • 不过如何合理地配置推送是个难题,如果推送给浏览器不需要的资源,反而浪费了带宽
    • 优化方面,HTTPS 的一些策略依然适用,比如精简密码套件、ECC 证书、会话复用、HSTS 减少重定向跳转等等
    • 但还有一些优化手段在 HTTP/2 里是不适用的,而且还会有反效果,比如说常见的精灵图(Spriting)、资源内联(inlining)、域名分片(Sharding)等,至于原因是什么,我把它留给你自己去思考(提示,与缓存有关)
    • 还要注意一点,HTTP/2 默认启用 header 压缩(HPACK),但并没有默认启用 body 压缩,所以不要忘了在 Nginx 配置文件里加上“gzip”指令,压缩 HTML、JS 等文本数据
  • Q:HTTP/2 的“服务发现”,浏览器怎么知道服务器支持 HTTP/2 呢
    • 在 TLS 的扩展里,使用ALPN(Application Layer Protocol Negotiation)来与服务器就 TLS 上跑的应用协议进行“协商”
    • 客户端在发起“Client Hello”握手的时候,后面会带上一个“ALPN”扩展,里面按照优先顺序列出客户端支持的应用协议
    • 如下图,最优先的是“h2”,其次是“http/1.1”,以前还有“spdy”,以后还可能会有“h3”
    • 服务器看到 ALPN 扩展以后就可以从列表里选择一种应用协议,在“Server Hello”里也带上“ALPN”扩展,告诉客户端服务器决定使用的是哪一种。因为我们在 Nginx 配置里使用了 HTTP/2 协议,所以在这里它选择的就是“h2”
    • 这样在 TLS 握手结束后,客户端和服务器就通过“ALPN”完成了应用层的协议协商,后面就可以使用 HTTP/2 通信了
Last Updated:
Contributors: Shiqi Lu