计算机网络学习笔记(8)—— HTTP2.0

Posted by 皮皮潘 on 11-01,2022

最近接触到了gRPC协议的具体网络通信协议实现,之前一直以为gRPC是直接在TCP层实现的,现在才知道它是基于HTTP 2.0实现的,不免阿潘震撼,再加上之前再实验室或者项目中接触到的HTTP服务器都是基于HTTP1.X的,因此不免对于HTTP 2.0这一新生协议有了几分好奇,因此查阅了一下相关资料去了解了一下HTTP 2.0,并看看这玩意儿是否真的能带来网络性能上的提升

核心

HTTP 2.0在不改动HTTP语义的基础上解决了原先HTTP 1.X的一系列问题

HTTP 2.0的核心点主要包括以下三点:

  1. 通过支持请求与响应的多路复用來减少延迟
  2. 通过压缩HTTP首部宇段将协议开销降至最低,
  3. 增加服务器端推送的支持

在此基础上HTTP2.0不会改动HTTP的语义,包括,HTTP方法,状态码,URI,及首部字段这些核心概念都如常,只是修改了格式化数据分帧的方式以及客户端和服务器间的传输,不过由于这些都是底层协议传输侧的修改,应用可以透明地从HTTP 1.x升级到HTTP 2.0

接下来将就这三个核心点进行展开介绍

多路复用

HTTP2.0 中每个请求响应对应一个数据流以及StreamId,每个数据流以消息的形式发送与接收,而消息由一或多个帧组成,这些帧以乱序发送,然后再根据每个帧首部的流标识符重新组装。在HTTP 1.x 中,如果客户端想发送多个并行的请求以及改进性能,那么必须使用多个TCP 连接,这是HTTP 1.x 交付模型的直接结果,该模型会使得每个请求只有在得到了对应的响应之后才能发起下一个请求,不然就不知道对应的响应是归属于哪个请求的了,虽然HTTP1.x 的管道特性模型(发完请求后不用等待响应就可以发送下一个请求,根据响应的到达顺序决定它归属于哪一个请求)优化了这一点,使得同一条连接可以并行发送多个请求,但是,这种模型仍然会导致响应的队首阻塞问题——在第一个请求的响应到达之前后续请求的响应都到达不了。HTTP 2.0 中新的二进制分帧层则突破了上述限制,其核心在于把每个HTTP消息都分解为互不依赖的帧,且对于每个帧根据对应的请求打上对应的StreamId然后乱序并行发送,最后再在另一端把它们重新组合起来,这种方式改变了原来的交付模型,实现了在同一条连接上请求与响应的多路复用,从而使得原来的性能问题迎刃而解

服务器推送

顾名思义,服务器推送就是服务器可以主动将一些数据推送到客户端,服务器端与客户端都可以双向推送,服务器推送流由 PUSH PROMISE 帧开始,客户端推送流则是由 HEAD 帧开始,为了避免混淆,客户端发起的流id是单数,服务端发起的流id是双数。PUSH PROMISE 中只包含 promise资源的HTTP 首部。客户端接收到 PUSH_ PROMISE 帧之后,可以视自身需求选择拒绝这个流(比如已经缓存了相应資源)

不过服务器推送有以下几点限制:

  1. 首先,服务器必须遵循请求一响应的循环,只能借着对请求的响应推送资源。也就是说服务器不能随意发起推送流
  2. 其次PUSH PROMISE 帧必须在返回响应之前发送,以免客户端出现竞态条件。否则,可能出现如下情况:客户端请求的怡好是服务器打算推送的资源。

格外需要注意的一点是,HTTP2.0的服务端推送初衷与 SSE、websocket 不同,前者是用来提前推送一些资源,比如img,media等,从而避免了等客户端在解析完html之后再来要对应资源所需要的延时,而后者则是用来推送实时的业务数据,避免了客户端轮询带来的性能损耗与滞后。SSE 的底层实现就是 http + chunked + text/event-stram 从而可以流式地基于UTF-8的编码传递响应,不同数据之间用空行划分,但是不能流式地传递请求。相较于SSE,websocket 可以实现客户端与服务器间双向、基于消息的文本或二进制数据传输,因此它额外实现了一套不同于http的websocket协议,该协议使用二进制分帧,但是由于没有流id,因此它仍然会队头阻塞,同时不支持多路复用

另外需要注意的一点是,在gRPC中其基于HTTP2.0的流模型(客户端和服务端都可以在一次请求或者响应中多次推送数据)的实现并不是使用的服务端推送特性的,而只是使用了HTTP2.0中的分帧以及多路复用特性,一次响应可以有多个DATA帧(同时也不会阻塞其他响应)直到END_STRRAM 标记被置位(通过在gRPC的流模型中,推送端的 StreamId 与请求端的 StreamId 相同可以推断出它没有使用 HTTP2.0 的服务端推送特性),另外从中也可以看出gRPC的流模型的目的是对于一批数据客户端或者服务端都可以来一个数据立即处理一个数据,而不是等到全都来了才一股脑地处理,从而达到降低单个数据响应时延的目的

首部压缩

请求与响应首部的定义在 HTTP 2.0 中基本没有改变,只是所有首部键必须全部小写,而且请求行独立为了:method、:scheme、:host 和:path 这些键一值对。另外为解决在HTTP 1.x中每次传输都要携带首部的问题,HTTP 2.0 会压缩首部数据,具体实现如下:

  • HTTP 2.0在客户端和服务器端使用 “首部表”来跟踪和存储之前发送的键一值对,对于相同的数据,不再通过每次请求和响应发送;
  • 首部表在HTTP 2.0的连接存续期内始终存在,由客户端和服务器共同渐进地更新:
  • 每个新的首部键一值对要么被追加到当前表的未尾,要么替换表中之前的值。

由于首部帧是最小发送粒度,因此发送方只要简单地传输差异数据,而接收方只要结合首部表即可在本地得到全量的首部信息