最近由于做一些相关项目,需要使用到HTTP/2的一些特性,花了两天的时间看了下HTTP/2的RFC-7540 的文档,又花了一天时间看了下go语言中http server中对HTTP/2的实现,做一些笔记,记录一些心得。内容比较多,会分多篇写,具体是几篇,看情况定吧。

HTTP/2 RFC7540

先来看一下什么是HTTP/2,为什么不是HTTP/1.2?HTTP/2 没有改动 HTTP 的应用语义。HTTP 方法、状态代码、URI 等概念都跟HTTP/1.1一样,但是HTTP/2在数据传输过程中做了二进制分帧(frame)处理,这点跟之前不一样,通过分帧,HTTP/2对我们的应用隐藏了其复杂性,达到了既能支持一些新特性,又能兼容之前的所有应用。所以,如果我们是跟之前一样,做一些普通的web应用,对HTTP/2的使用跟HTTP/1没有任何区别。但如果我们希望能利用到HTTP/2的一些新特性,就需要对它有一些更深入的了解。

HTTP/2新增特性

  • 二进制分帧(HTTP Frames)
  • 多路复用
  • 头部压缩
  • 服务端推送(server push)

二进制分帧(HTTP Frames)

HTTP/2最革命性的原因就在于这个二进制分帧了,要了解二进制分帧在客户端和服务端传输的过程,需要了解三个概念:

  • Frame,帧,HTTP/2协议里通信的最小单位,每个帧有自己的格式,不同类型的帧负责传输不同的消息
  • Message, 消息,类似Request/Response消息,每个消息包含一个或多个帧
  • Stream,流,建立链接后的一个双向字节流,用来传输消息,每次传输的是一个或多个帧

HTTP/2里边,这些概念的关系是这样的:

  • 所有的通信都在一个tcp链接上完成,会建立一个或多个stream来传递数据
  • 每个stream都有唯一的id标识和一些优先级信息,客户端发起的stream的id为单数,服务端发起的stream id为偶数
  • 每个message就是一次Request或Response消息,包含一个或多个帧,比如只返回header帧,相当于HTTP里HEAD method请求的返回;或者同时返回header和Data帧,就是正常的Response响应。
  • Frame是最小的通信单位,承载着特定类型的数据,例如 Headers, Data, Ping, Setting等等。 来自不同stream的frame可以交错发送,然后再根据每个Frame的header中的数据流标识符重新组装。

简言之,HTTP/2 将 HTTP 协议通信分解为二进制编码Frame的交换,这些Frame对应着特定Stream中的Message。所有这些都在一个 TCP 连接内复用。这是 HTTP/2 协议所有其他功能和性能优化的基础。

下面来看下Frame的基础结构

+-----------------------------------------------+
|                 Length (24)                   |
+---------------+---------------+---------------+
|   Type (8)    |   Flags (8)   |
+-+-------------+---------------+-------------------------------+
|R|                 Stream Identifier (31)                      |
+=+=============================================================+
|                   Frame Payload (0...)                      ...
+---------------------------------------------------------------+
  • Length: 表示Frame Payload的大小,是一个24-bit的整型,表明Frame Payload的大小不应该超过2^24-1字节,但其实payload默认的大小是不超过2^14字节,可以通过SETTING Frame来设置SETTINGS_MAX_FRAME_SIZE修改允许的Payload大小。
  • Type: 表示Frame的类型,目前定义了0-9共10种类型。
  • Flags: 为一些特定类型的Frame预留的标志位,比如Header, Data, Setting, Ping等,都会用到。
  • R: 1-bit的保留位,目前没用,值必须为0
  • Stream Identifier: Steam的id标识,表明id的范围只能为0到2^31-1之间,其中0用来传输控制信息,比如Setting, Ping;客户端发起的Stream id 必须为奇数,服务端发起的Stream id必须为偶数;并且每次建立新Stream的时候,id必须比上一次的建立的Stream的id大;当在一个连接里,如果无限建立Stream,最后id大于2^31时,必须从新建立TCP连接,来发送请求。如果是服务端的Stream id超过上限,需要对客户端发送一个GOWAY的Frame来强制客户端重新发起连接。

Frame定义

下面来认识下各个类型的Frame。

DATA

DATA Frame(type=0x0),用来传输可变长度的二进制流,这部分最主要的用途就是用来传递之前HTTP/1中的Request或Response的Body部分。 DATA Frame 的 Payload格式如下:

 +---------------+
 |Pad Length? (8)|
 +---------------+-----------------------------------------------+
 |                            Data (*)                         ...
 +---------------------------------------------------------------+
 |                           Padding (*)                       ...
 +---------------------------------------------------------------+

DATA字段比较好解释,就是要传输的数据内容,那么Pad Length和Padding是干什么用的?HTTP/2在设计的时候就更多的考虑了数据的安全性,所以默认使用HTTPS,除此之外,协议本身也对传输的数据做了一些安全考虑,填充就是其中一个。填充可以模糊帧的大小,使攻击者更难通过帧的数量来猜测传输内容的长度,减少破解的可能性。 DATA帧使用到了Flag字段,其中最重要的是一个END_STREAM (0x1)Flag,这个标志用来表示Data Frame的传输是否结束,当该标志位为1时,表示Stream的传输结束,发起Stream的一方会进入half-closed(local)或者closed状态,关于Stream状态机的问题,后边再详细说,这部分也是一个需要用心理解的点。END_STREAM在Header帧中也有用到,含义一样,不再单独说明。

HEADERS

HEADERS Frame(type=0x1)用于开启一个Stream,当然也用于传输正常HTTP请求中的Header信息。

 +---------------+
 |Pad Length? (8)|
 +-+-------------+-----------------------------------------------+
 |E|                 Stream Dependency? (31)                     |
 +-+-------------+-----------------------------------------------+
 |  Weight? (8)  |
 +-+-------------+-----------------------------------------------+
 |                   Header Block Fragment (*)                 ...
 +---------------------------------------------------------------+
 |                           Padding (*)                       ...
 +---------------------------------------------------------------+

HEADERS的结构比较简单,Header Block Fragment字段用于存储正常的Http Header头信息,EStream DependencyWeight字段都是用于权重控制。由于HTTP/2是支持多路复用,也就是多个流同时进行传输,那么这个时候哪个流更重要,应该优先传输哪个,就需要用这些字段来进行控制了。

PRIORITY

PRIORITY Frame(type=0x2)用于指定Stream的优先级,这个在Stream Dependencies, Dependency Weighting等场景下会用到,PRIORITY帧不能在id为0的stream上发送。由于我这次业务需求的场景用不到这块,所以没有特别深入的了解。

 +-+-------------------------------------------------------------+
 |E|                  Stream Dependency (31)                     |
 +-+-------------+-----------------------------------------------+
 |   Weight (8)  |
 +-+-------------+

RST_STREAM

RST_STREAM Frame(type=0x3)用于立即终止Stream.主要用来取消流,或者发生异常时表明需要终止。

 +---------------------------------------------------------------+
 |                        Error Code (32)                        |
 +---------------------------------------------------------------+

错误包含一个32-bit的整型数来表示错误的原因。

SETTINGS

SETTINGS Frame(type=0x4)用来控制客户端和服务端之间通信的一些配置。SETTINGS帧必须在连接开始时由通信双方发送,并且可以在任何其他时间由任一端点在连接的生命周期内发送。SETTINGS帧必须在id为0的stream上进行发送,不能通过其他stream发送;SETTINGS影响的是整个TCP链接,而不是某个stream;在SETTINGS设置出现错误时,必须当做connection error重置整个链接。SETTINGS帧带有Ack的Flag,接收方必须收到ack为0的SETTINGS后,应马上启用SETTING的配置并返回一个Ack为1的SETTINGS帧。

ack=false Flag Ack=false

ack=true Flag Ack=true

 +-------------------------------+
 |       Identifier (16)         |
 +-------------------------------+-------------------------------+
 |                        Value (32)                             |
 +---------------------------------------------------------------+

常用的SETTINGS有几类:

  • SETTINGS_HEADER_TABLE_SIZE (0x1): 控制每个Header帧中的HTTP头信息的大小
  • SETTINGS_ENABLE_PUSH (0x2): 是否启用服务端推送(Server Push),默认开启;不管是服务端还是客户端发送了禁用的配置,那么服务端就不应该发送PUSH_PROMISE
  • SETTINGS_MAX_CONCURRENT_STREAMS (0x3): 用来控制多路复用中Stream并发的数量,这个主要是用来限制单个链接对服务端的资源的占用过大,这个值默认是没有限制,如果做一个server服务,那么建议一定要设置这个值,RFC文档中建议不要小于100,那么我们设置100就可以了。亚马逊的Alexa中HTTP/2协议服务端设置的这个值就是100.
  • …其他几个如SETTINGS_INITIAL_WINDOW_SIZE(0x4)SETTINGS_MAX_FRAME_SIZE(0x5)SETTINGS_MAX_HEADER_LIST_SIZE(0x6) 就不一一介绍了。

PUSH_PROMISE

PUSH_PROMISE Frame(type=0x5)用于服务端在发送PUSH之前先发送PUSH_PROMISE帧来通知客户端将要发送的PUSH信息。PUSH_PROMISE涉及到server push的相关信息,内容比较多,这里不展开讲了,后边单独介绍。

+---------------+
 |Pad Length? (8)|
 +-+-------------+-----------------------------------------------+
 |R|                  Promised Stream ID (31)                    |
 +-+-----------------------------+-------------------------------+
 |                   Header Block Fragment (*)                 ...
 +---------------------------------------------------------------+
 |                           Padding (*)                       ...
 +---------------------------------------------------------------+

PING

PING Frame(type=0x6) 是用来测量来自发送方的最小往返时间以及确定空闲连接是否仍然起作用的机制。 PING帧可以从任何一方发送。PING帧跟SETTINGS帧非常类似,一个是必须在id为0的stream上发送,另一个就是它也包含一个Ack的Flag,发送方发送ack=0的PING帧,接收方必须响应一个ack=1的PING帧,并且PING帧的响应 应该 优先于任何其他帧。

 +---------------------------------------------------------------+
 |                                                               |
 |                      Opaque Data (64)                         |
 |                                                               |
 +---------------------------------------------------------------+

GOAWAY

GOAWAY frame(type=0x7)用于关闭连接,GOAWAY允许端点优雅地停止接受新流,同时仍然完成先前建立的流的处理。这个就厉害了,当服务端需要维护时,发送一个GOAWAY的Frame给客户端,那么发送之前的Stream都正常处理了,发送GOAWAY后,客户端会新启用一个链接,继续刚才未完成的Stream发送。这样就可以做到完全不影响运行中的业务而进行服务端维护。它是如何做到这一点的呢,来看下GOAWAY的帧结构:

 +-+-------------------------------------------------------------+
 |R|                  Last-Stream-ID (31)                        |
 +-+-------------------------------------------------------------+
 |                      Error Code (32)                          |
 +---------------------------------------------------------------+
 |                  Additional Debug Data (*)                    |
 +---------------------------------------------------------------+

最明显的就是这个Last-Stream-ID,GOAWAY包含在此连接中已经或可能在发送端点上处理的最后一个对等启动Stream的ID标识符.例如,如果服务器发送GOAWAY帧,则识别的流是客户端发起的编号最高的流。 通过这个标识,双方就知道上次传输成功的一个Stream Id是多少,再重新发送数据的时候,就知道从哪个数据开始发送。避免了数据的丢失或者重复。

WINDOW_UPDATE

WINDOW_UPDATE frame(type=0x8)用于流控(flow control),此次需求用不到,偷个懒,不介绍了。

 +-+-------------------------------------------------------------+
 |R|              Window Size Increment (31)                     |
 +-+-------------------------------------------------------------+

CONTINUATION

CONTINUATION frame(type=0x9)用于持续的发送未发送完的HTTP header信息.如果前边是这三个帧(HEADERS, PUSH_PROMISE, or CONTINUATION),并且未携带END_HEADERS的flag,就可以继续发送CONTINUATION帧。

 +---------------------------------------------------------------+
 |                   Header Block Fragment (*)                 ...
 +---------------------------------------------------------------+

多路复用

在 HTTP/1 中,如果客户端要想发起多个并行请求以提升性能,则必须使用多个 TCP 连接。在单个链接中,HTTP/1对每个请求每次交付一个响应,并且必须受到影响后,才能继续发起请求。 HTTP/2 中新的二进制分帧层突破了这些限制,实现了完整的请求和响应复用:客户端和服务器可以将 HTTP 消息分解为互不依赖的帧,通过不同的Stream交错发送,最后再在另一端把它们重新组装起来。所以这里也能看到,他的请求响应模型跟HTTP/1也是一样的,只不过在传输的内容内部做了些手脚,来实现了多路复用。 如图,在一个TCP链接内,客户端发送了一个stream ID=5的DATA帧的数据包,但同时服务端响应的是Stream ID=1和ID=3的一些数据包,这样,就真正做到了,在一个链接内,同时有三个流并行的传输数据。

将 HTTP 消息分解为独立的帧,交错发送,然后在另一端重新组装是 HTTP/2 最重要的一项增强。事实上,这个机制会在整个网络技术栈中引发一系列连锁反应,从而带来巨大的性能提升,让我们可以:

  • 并行交错地发送多个请求,请求之间互不影响。
  • 并行交错地发送多个响应,响应之间互不干扰。
  • 使用一个连接并行发送多个请求和响应。
  • 不必再为绕过 HTTP/1.x 限制而做很多工作(例如级联文件、image sprites 和域名分片)
  • 消除不必要的延迟和提高现有网络容量的利用率,从而减少页面加载时间。
  • 等等… HTTP/2 中的新二进制分帧层解决了 HTTP/1.x 中存在的队首阻塞问题,也消除了并行处理和发送请求及响应时对多个连接的依赖。结果,应用速度更快、开发更简单、部署成本更低。

这一篇比较枯燥,讲的都是一些概念性的内容,但如果想真正能使用到HTTP/2的一些特性,还是需要了解这些的,这次就先到这里吧,下次继续。

参考资料