概述

在我之前的一篇文章:《GRPC Go 连接握手问题处理 》 中,我描述了我遇到过的一个 Go GRPC 的问题,以及最终的解决方式,最后给自己留了一个坑:http2 的连接是如何建立的。这里就准备来填上这个坑。

http2 的分类

http2 有多种不同的连接建立情况,简单来看可以分为两种:

但是,在实际使用中这样划分缺乏实操性,所以实际情况会这么分:

从 http/1.1 升级到 h2c

当客户端不知道服务器是否支持 http2 时,先尝试以 http/1.1 的形式向服务器发送 http 请求,同时,带上一个 Header:Upgrade: h2c,这样,如果服务器是支持 http2 的,那么就会响应一个升级操作:例如:

  1. [root@liqiang.io]# cat http1.req
  2. GET / HTTP/1.1
  3. Host: server.example.com
  4. Connection: Upgrade, HTTP2-Settings
  5. Upgrade: h2c
  6. HTTP2-Settings: <base64url encoding of HTTP/2 SETTINGS payload>

如果服务器支持 http2,响应的就是一个 Switch Protocols 的 101 响应:

  1. [root@liqiang.io]# cat http101.resp
  2. HTTP/1.1 101 Switching Protocols
  3. Connection: Upgrade
  4. Upgrade: h2c
  5. [ HTTP/2 connection ...

这里有个细节需要注意,请求中有一个 Header 名为:HTTP2-Settings,里面放的是 http2 的 Setting 信息(Base64 编码,带 “=” 结尾),也就是说,如果服务器接受了 http2,那么就会使用这个 Setting 进行 http2 通信,并且,在下面的 101 响应中的 body 里面,放的肯定是服务器的 Connection Preface,里面也包含 Server 端的 Setting 信息,这里的 Body 是满足特定条件的二进制流,具体的内容参见后面的:Connection Preface

虽然这里 Client 发送了一个 HTTP2-Settings,但是 RFC 要求 Cleint 端在收到 101 响应之后还应该再发送一个 Setting Frame,并且将覆盖初始的 Setting(有点奇怪,为什么还需要这一步)。

至于 Setting 中放置的是什么信息,可以看后面的:Setting Frame

带先验知识的 h2c

如果 Client (例如 Browser)之前已经和 Server 通信过了,那么已经有了一个经验:这个 Server 支持 http2。那么 Client 就可以直接使用 http2 和 Server 进行通信了,也就是省去了第一步的 http/1.1 的步骤,但是,根据 RFC 的要求,当 TCP 连接(h2c 就是值不通过 TLS 连接的 http2)建立之后,Client 发送的第一个数据包应该是 Connection Preface。

原因同样参见下面的:Connection Preface

然后 Server 端如果正确处理 http2 的话,那么也必须响应一个 Connection Preface,然后 Client 端就可以和 Server 端愉快地进行 http2 通信了。

TLS-ALPN

通过 TLS 的 http2 才是被推荐的 http2,而通过 TLS 的 http2 和 h2c 有一些不同,在 h2c 中,我们得先知道 Server 端是否支持 http2,要么通过 101 切换协议,要么就是有先验知识。但是在 TLS 中,因为 TLS 在网络协议栈中的特殊性(介于 Transport Layer 和 Application Layer),所以,Google 就提出了可以在 TLS 握手的时候进行 Application Layer 的协议协商,初版为 NPN(Next Protocol Negotiation),后被抛弃,转为 ALPN(Application Layer Protocol Negotiation)。

也就是说,支持在 TLS 握手期间协商应用层是否使用 http2,这样,当 TLS 通道建立完成之后,其实也就有 “先验知识” 知道 Server 端是否支持 http2 了,如果支持,那么就是来回的一个 Connection Preface,如果不支持,那么就是普通的 http/1.1 的 https。ALPN 的协商信息是在 https 的 Client Hello 阶段发送的,一个示例的 Client Hello 信息为:

  1. [root@liqiang.io]# cat https-alpn-client-hello.req
  2. Handshake Type: Client Hello (1)
  3. Length: 141
  4. Version: TLS 1.2 (0x0303)
  5. Random: dd67b5943e5efd0740519f38071008b59efbd68ab3114587...
  6. Session ID Length: 0
  7. Cipher Suites Length: 10
  8. Cipher Suites (5 suites)
  9. Compression Methods Length: 1
  10. Compression Methods (1 method)
  11. Extensions Length: 90
  12. [other extensions omitted]
  13. Extension: application_layer_protocol_negotiation (len=14)
  14. Type: application_layer_protocol_negotiation (16)
  15. Length: 14
  16. ALPN Extension Length: 12
  17. ALPN Protocol
  18. ALPN string length: 2
  19. ALPN Next Protocol: h2
  20. ALPN string length: 8
  21. ALPN Next Protocol: http/1.1

Connection Preface

在前面的连接建立过程中,我总是提到 Connection Preface,在 RFC 里面对这个的要求是,Client 和 Server 双方在正式使用一个连接之前都必须向对方发送这个 Connection Preface。

Connection Preface 其实就是一个 HTTP 二进制请求,但是可以被部分 ASCII 解码,例如一个 Client 的请求为:

  1. [root@liqiang.io]# cat client-connection-preface.req
  2. 0x505249202a20485454502f322e300d0a0d0a534d0d0a0d0a

可以被翻译成 ASCII 的:

  1. [root@liqiang.io]# cat client-connection-preface-decode.req
  2. PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n

除了这一段之后,在原来的 http/1.1 的 “Body” 部分,还可能包含 Setting Frame。从 RFC 中看,这么做的必要性还是很高的,因为这个 Connection Preface 有这些作用:

当 Client 或者 Server 端收到对方的 Connection Preface 的请求之后,都需要给出 ACK:

Setting Frame

在 http2 中,Setting Frame 用于协调 Client 和 Server 双方的通信配置信息,在初始化连接时,RFC 要求双方都必须发送 Setting Frame,且接收到的对方必须给予 ACK(带 ACK flag 的 Setting Frame)。Client 和 Server 可以多次发送 Setting Frame,并且双方都以收到的最新的 Setting 进行处理。

那么 Setting Frame 中有哪些协商信息,目前已经定义在 RFC 中的有:

其他如果没有定义的出现在 Setting Frame 里面,将会被忽略。

演示操作

协议观察

以下是我运行 h2c server 之后,通过 curl 查看发现的协议切换过程:

  1. [root@liqiang.io]# curl --http2 -v http://localhost:8115
  2. * Trying ::1:8115...
  3. * connect to ::1 port 8115 failed: 拒绝连接
  4. * Trying 127.0.0.1:8115...
  5. * Connected to localhost (127.0.0.1) port 8115 (#0)
  6. > GET / HTTP/1.1
  7. > Host: localhost:8115
  8. > User-Agent: curl/7.74.0
  9. > Accept: */*
  10. > Connection: Upgrade, HTTP2-Settings
  11. > Upgrade: h2c
  12. > HTTP2-Settings: AAMAAABkAAQCAAAAAAIAAAAA
  13. >
  14. * Mark bundle as not supporting multiuse
  15. < HTTP/1.1 101 Switching Protocols
  16. < Connection: Upgrade
  17. < Upgrade: h2c
  18. * Received 101
  19. * Using HTTP2, server supports multi-use
  20. * Connection state changed (HTTP/2 confirmed)
  21. * Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
  22. * Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
  23. < HTTP/2 200
  24. < accept-ranges: bytes
  25. < content-type: text/html; charset=utf-8
  26. < last-modified: Mon, 22 Feb 2021 07:04:12 GMT
  27. < content-length: 44912
  28. < date: Mon, 22 Feb 2021 07:50:21 GMT
  29. <
  30. <html>
  31. <head lang="en"></head>

如果有兴趣,源码我都放出来了,可以直接使用:https://github.com/liuliqiang/http2

题外知识

HTTPS 连接建立示意图

个人见解

Connection Preface 中 Setting Frame 可以为空矛盾

RFC 在 Connection Preface 中说 Setting Frame 可以为空,但是在 Setting Frame 中又说,在初始化连接时必须发送 Setting Frame,这是否矛盾?

我的理解是 Connection Preface 发送阶段不代表 http2 连接已经成功建立,所以如果 Connection Preface 中没有携带 Setting Frame,那么后面还会单独再发送一个 Setting Frame(在任何数据 Frame 之前)。

Grpc Go 中的坑怎么踩的

在前面我提到的遇到 GRPC 的问题,问题的描述为:

图 1:grpc go 问题

这里的意思就是说 Client 发送了 Setting Frame,然后一直在等待 Server 的 Setting Frame,但是 Server 一直都没有发过来(最开始这是 cmux 这个 Lib 的 Bug,然后后面因为 Java Client 先因为这个问题 Block 了,所以 cmux 就增加了一个 HTTP2MatchHeaderFieldSendSettings 的选项,这样就会在连接中发送 Setting Frame,这就变成了我们错误使用 cmux 的原因了)。

所以总结起来整个流程是这样的:

Ref