概述
在我之前的一篇文章:《GRPC Go 连接握手问题处理 》 中,我描述了我遇到过的一个 Go GRPC 的问题,以及最终的解决方式,最后给自己留了一个坑:http2 的连接是如何建立的。这里就准备来填上这个坑。
http2 的分类
http2 有多种不同的连接建立情况,简单来看可以分为两种:
- h2c:不使用 TLS 的 http2
- h2:使用了 TLS 的 http2
但是,在实际使用中这样划分缺乏实操性,所以实际情况会这么分:
- 从 http/1.1 升级到 h2c
- 带先验知识的 h2c
- TLS-ALPN
从 http/1.1 升级到 h2c
当客户端不知道服务器是否支持 http2 时,先尝试以 http/1.1 的形式向服务器发送 http 请求,同时,带上一个 Header:Upgrade: h2c
,这样,如果服务器是支持 http2 的,那么就会响应一个升级操作:例如:
[root@liqiang.io]# cat http1.req
GET / HTTP/1.1
Host: server.example.com
Connection: Upgrade, HTTP2-Settings
Upgrade: h2c
HTTP2-Settings: <base64url encoding of HTTP/2 SETTINGS payload>
如果服务器支持 http2,响应的就是一个 Switch Protocols 的 101 响应:
[root@liqiang.io]# cat http101.resp
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: h2c
[ 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 信息为:
[root@liqiang.io]# cat https-alpn-client-hello.req
Handshake Type: Client Hello (1)
Length: 141
Version: TLS 1.2 (0x0303)
Random: dd67b5943e5efd0740519f38071008b59efbd68ab3114587...
Session ID Length: 0
Cipher Suites Length: 10
Cipher Suites (5 suites)
Compression Methods Length: 1
Compression Methods (1 method)
Extensions Length: 90
[other extensions omitted]
Extension: application_layer_protocol_negotiation (len=14)
Type: application_layer_protocol_negotiation (16)
Length: 14
ALPN Extension Length: 12
ALPN Protocol
ALPN string length: 2
ALPN Next Protocol: h2
ALPN string length: 8
ALPN Next Protocol: http/1.1
Connection Preface
在前面的连接建立过程中,我总是提到 Connection Preface,在 RFC 里面对这个的要求是,Client 和 Server 双方在正式使用一个连接之前都必须向对方发送这个 Connection Preface。
Connection Preface 其实就是一个 HTTP 二进制请求,但是可以被部分 ASCII 解码,例如一个 Client 的请求为:
[root@liqiang.io]# cat client-connection-preface.req
0x505249202a20485454502f322e300d0a0d0a534d0d0a0d0a
可以被翻译成 ASCII 的:
[root@liqiang.io]# cat client-connection-preface-decode.req
PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n
除了这一段之后,在原来的 http/1.1 的 “Body” 部分,还可能包含 Setting Frame。从 RFC 中看,这么做的必要性还是很高的,因为这个 Connection Preface 有这些作用:
- 给 Client 和 Server 端提供一个了解双方配置的方式;
- 在 Server 端或者中间 Proxy 等渠道不支持 http2 时,可以及时得获得反馈(因为他们不支持 PRI 和 HTTP/2.0 这些语义)
- 对于带先验知识的 Client,如果 Server 端变动了或者增加了不支持 http2 的 proxy,那么也可以及时地识别出来
当 Client 或者 Server 端收到对方的 Connection Preface 的请求之后,都需要给出 ACK:
- 对于 Client 来说,Server 的 ACK 可能是 Server 接受了 Client 端的 Setting 之后,反馈回来的 Connection Preface
- 对于 Server 端来说,Client 的 ACK 就是一个带 ACK 标记的 Setting Frame
Setting Frame
在 http2 中,Setting Frame 用于协调 Client 和 Server 双方的通信配置信息,在初始化连接时,RFC 要求双方都必须发送 Setting Frame,且接收到的对方必须给予 ACK(带 ACK flag 的 Setting Frame)。Client 和 Server 可以多次发送 Setting Frame,并且双方都以收到的最新的 Setting 进行处理。
那么 Setting Frame 中有哪些协商信息,目前已经定义在 RFC 中的有:
- SETTINGS_HEADER_TABLE_SIZE (0x1)
- SETTINGS_ENABLE_PUSH (0x2)
- SETTINGS_MAX_CONCURRENT_STREAMS (0x3)
- SETTINGS_INITIAL_WINDOW_SIZE (0x4)
- SETTINGS_MAX_FRAME_SIZE (0x5)
- SETTINGS_MAX_HEADER_LIST_SIZE (0x6)
其他如果没有定义的出现在 Setting Frame 里面,将会被忽略。
演示操作
协议观察
以下是我运行 h2c server 之后,通过 curl 查看发现的协议切换过程:
[root@liqiang.io]# curl --http2 -v http://localhost:8115
* Trying ::1:8115...
* connect to ::1 port 8115 failed: 拒绝连接
* Trying 127.0.0.1:8115...
* Connected to localhost (127.0.0.1) port 8115 (#0)
> GET / HTTP/1.1
> Host: localhost:8115
> User-Agent: curl/7.74.0
> Accept: */*
> Connection: Upgrade, HTTP2-Settings
> Upgrade: h2c
> HTTP2-Settings: AAMAAABkAAQCAAAAAAIAAAAA
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 101 Switching Protocols
< Connection: Upgrade
< Upgrade: h2c
* Received 101
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
< HTTP/2 200
< accept-ranges: bytes
< content-type: text/html; charset=utf-8
< last-modified: Mon, 22 Feb 2021 07:04:12 GMT
< content-length: 44912
< date: Mon, 22 Feb 2021 07:50:21 GMT
<
<html>
<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 的原因了)。
所以总结起来整个流程是这样的:
- grpc client 和 中间件(cmux) 都不安全地使用 http2(但是符合 RFC,因为 RFC 中描述了只要 Client 自己发送玩 Setting Frame 之后,就可以发送数据了);
- grpc client 先行修复不安全因素,必须收到 server 的 setting frame 之后才开始发送数据,因此和现存中间件不吻;
- grpc server 和中间件修复不安全因素,事情解决。