HTTP/2 in GO(一)

阅读数:52 2019 年 11 月 18 日 18:36

HTTP/2 in GO(一)

大家在平时的项目开发中,应用越来越多的使用到了 GO 语言。今天就给大家带来了一份关于 GO 结合 HTTP/2 的开发示例分享给大家。本文来自公众号“360 搜索技术团队”的投稿,作者付坤。

最近由于做一些相关项目,需要使用到 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)

1 二进制分帧 (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 in GO(一)

简言之,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 来强制客户端重新发起连接。

2 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 头信息,E、Stream Dependency、Weight 字段都是用于权重控制。由于 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 帧。

HTTP/2 in GO(一)

Flag Ack=false

HTTP/2 in GO(一)

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 (*) ...
+---------------------------------------------------------------+

3 多路复用

在 HTTP/1 中,如果客户端要想发起多个并行请求以提升性能,则必须使用多个 TCP 连接。在单个链接中,HTTP/1 对每个请求每次交付一个响应,并且必须受到影响后,才能继续发起请求。

HTTP/2 中新的二进制分帧层突破了这些限制,实现了完整的请求和响应复用:客户端和服务器可以将 HTTP 消息分解为互不依赖的帧,通过不同的 Stream 交错发送,最后再在另一端把它们重新组装起来。所以这里也能看到,他的请求响应模型跟 HTTP/1 也是一样的,只不过在传输的内容内部做了些手脚,来实现了多路复用。

HTTP/2 in GO(一)

如图,在一个 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 的一些特性,还是需要了解这些的,这次就先到这里吧,下次继续。

本文转载自公众号 360 云计算(ID:hulktalk)。

原文链接:

https://mp.weixin.qq.com/s/5tcqd40by8GBSsnTQEo-OQ

评论

发布