HTTP/3 in Go

Posted 相思的雨

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HTTP/3 in Go相关的知识,希望对你有一定的参考价值。

写在前面,目前 HTTP/3 协议仍在实验阶段,RFC 进行到了第 29 号版本,将于 2020/12/11 日失效,本文所讨论的均为已经达成一致的部分;前半部分介绍了一些简单的理论,后半部分进行代码实现,希望通过代码实现能够让你对 HTTP/3 有个大概的概念,在后续的概念普及中有所印象。

概念部分

本篇的概念部分主要介绍协议的升级方式,由于在协议普及过程中,互联网中存在着大量的旧服务应用,必须提供一种从低版本协商升级到新版本协议的方式。

我们首先复习一下 HTTP1.1 升级到 HTTP2的方式,分为 h2c 和 h2 ;然后我们会聊到 HTTP Alternative Services(HTTP 替代服务),这是一个很有趣的协议,然后举例 HTTP/3 是如何通过 ALTSVC 协议进行自举的。

0x01 H2C

如果你在浏览器中输入 HTTP 的域名,未进入 HTTPS 协议,如果浏览器启用 HTTP/2,那么会在发起时设置 Upgrade 和 HTTP2-Settings:

GET / HTTP/1. 1
Host: server. example. com
Connection: Upgrade, HTTP2-Settings
Upgrade: h2c
HTTP2-Settings: <base64url encoding of HTTP/2 SETTINGS payload>

服务端如果支持 HTTP/2,则响应:

HTTP/1. 1 101 Switching Protocols
Connection: Upgrade
Upgrade: h2c

客户端在接收到 101 响应后,就会发起 HTTP/2 的第一个帧(SETTINGS 帧),即开始 HTTP/2。

0x02 H2

如果你在浏览器输入的是 HTTPS 域名或 URL,则浏览器会在启用 HTTP/2 时,进行 H2 的升级协商。

H2 协商建立在 TLS 之上,采用 ALPN 扩展协议,采用 h2 作为协议标识符,由于通过 TLS 所以称之为 安全版本

流程如下:

  1. 客户端和服务端 TCP 握手

  2. 客户端和服务端 TLS 层协商

  3. 客户端发送链接序言(SETTINGS 帧)

  4. 服务端响应链接序言

  5. 双方各自确认序言

  6. 其他帧传输

下面的一张图提供了一个简单对比,可以参考:

图片来自:《HTTP/2 笔记之连接建立

H2C 协商升级,是为了 HTTP/1.1 的广泛存在,在未启用 TLS 时提供一种协议切换的升级,HTTPS 则是一个强制的协商,在 TLS 的 ALPN 扩展中直接完成协议的交换。

HTTP/1.1 升级到 HTTP/2 用了近十六年的时间,中间经过了很多变迁,网际中存在着对 HTTP/1.1 和 TCP 大量优化和应用,由于 HTTP/1.1 是文本协议,HTTP/2 是二进制协议,两者直接跨度很大,所以设计了两种对应的协议迁移方式;又由于此,HTTP/2 并非全部基于安全协议。

0x03 HTTP Alternative Services

HTTP Alternative Services(HTTP 替代服务)是发布于 2016 年上半年的一份 RFC7838,协议约定当前请求的资源,可以通过备用主机,端口或者协议获取。

在当前的架构中,我们常用的分流方式一般分为两种:

  1. DNS 分区域或供应商解析不同 IP

  2. SLB 进行负载均衡,大多数有专有机器提供服务

不同于使用 30x 状态码进行重定向分流,HTTP Alternative Services 只改变浏览器获取资源的网络方式,上层应用不会感觉到任何变化。

HTTP Alternative Services 协议 定义 HTTP 头部如下:

Alt-Svc: h2="alt.example.com:8000", h2=":443"; ma=2592000; persist=1

h2 代表着 HTTP/2,ma 是 max-age 的缩写,persist 表示在遇到网络变更时依然有效。

在 HTTP/1 中,Alt-Svc 必须在首次响应的头部返回,只有在第二次请求时,浏览器才会使用备用服务,在 HTTP2 中新增 ALTSVC 帧,可以单独发送,浏览器可以在首次请求就切换为备用服务。

0x04 H3

由于互联网中存在大量不可预知的情况,所以正如 HTTP/1 到 HTTP/2 一样,需要有一种协议协商的手段,让浏览器知道什么时候可以使用 HTTP/3,所以 H3 也一样规定了协议协商的方式。

HTTP/3 通过 ALTSVC 协议实现自举,告知浏览器其已支持 HTTP/3,浏览器可切换至 UDP 的方式进行通讯。

Alt-Svc: h3-29=":4443"; ma=2592000

根据 draft-ietf-quic-http-29#section-3.1 《Draft Version Identification》章节所述:

HTTP/3 uses the token "h3" to identify itself in ALPN and Alt-Svc.
Only implementations of the final, published RFC can identify themselves as "h3". Until such an RFC exists, implementations MUST NOT identify themselves using this string.

在 HTTP/3 正式发布之前,必须不能使用 h3 作为相关标识,所以做为第 29 版实验支持版本,上面协议部分标识为 h3-29;自举样例表示含义,本服务支持 h3 的第 29 号实验版本,通过与响应相同的 IP 的 4443 端口进行通信,有效期 2592000 秒。

基于此,浏览器可以在 HTTP/2 甚至是 HTTP/1 中完成自举,告知浏览器自举已经支持 HTTP/3,同上文中提到的一样,在 HTTP/2 中,可以实现在首次请求时切换协议,而在 HTTP/1 中,只能在第二次请求中切换到新的协议(在实验中,大多是在第二次请求才切换 HTTP/3)。

实验部分

由于 HTTP/3 通过 QUIC 实现通讯,QUIC 使用 TLS 1.3 传输层安全协议(RFC 8446),且不存在非加密版本;所以在本实验中,你必须提前准备秘钥和证书,你可以申请正式的秘钥和证书,也可以根据《》 生成自签名证书和秘钥。

上篇中,我们通过自签名在 Go 语言下实现了 HTTPS Service;本节我们通过实验的方式,一步步来了解 HTTP3;如果你没有读过,那么建议你阅读一下,当然如果你对自签名的原理和用法十分清晰,那么直接进行本节也是没有任何难度的。

0x05 HTTPS

我们先实现一个最简单的 HTTPS Service:

package main

import (
"fmt"
"net/http"
)

func main() {
// 请注意匹配正确的路径
certFile := "/Users/Shared/go/quic/tls/server.crt"
keyFile := "/Users/Shared/go/quic/tls/server.key"

handle := func(w http.ResponseWriter, req *http.Request) {
_, _ = fmt.Fprintf(w, "<html><body>hello world</body></html>")
}
mux := http.NewServeMux()
mux.HandleFunc("/", handle)

err := http.ListenAndServeTLS(":443", certFile, keyFile, mux)
fmt.Println(err)
}

请替换上面代码中的证书和私钥,你可以通过《》章节的内容进行自签名,并将 CA 证书导入对应的浏览器或系统证书,并选择信任;或者你可以去申请正式的证书,并通过本地 HOST 指向去测试。

你可以直接通过 IDE 直接运行或者在对应的目录下执行 go run main.go,执行后控制台没有任何输出,但是请注意由于 443 端口,可能需要 sudo 权限,这取决于你运行的系统和用户。

如果你正确的导入了信任证书,或者使用了正式的可信证书,那么此时你用浏览器访问 https://localhost 应该能够正确的看到 hello world,或者通过 curl https://localhost/ -i -k (-k 用户忽略证书验证,否则你需要通过 --cacert 指定 CA 证书位置),那么你可以看到以下输出:

HTTP/2 200
content-type: text/html; charset=utf-8
content-length: 37
date: Mon, 10 Aug 2020 10:05:36 GMT

<html><body>hello world</body></html>

通过 CURL 的 -i 参数,我们看到了,在配置证书和私钥文件后,Go http Server 会优先为我们启用 HTTP2

请注意,到这一步我们应该已经成功的实现了 HTTP2(这里我们暂不考虑 H2C)。

0x06 Quic-go

quic-go is an implementation of the QUIC protocol in Go. It roughly implements the IETF QUIC draft, although we don't fully support any of the draft versions at the moment.

go get github.com/lucas-clemente/quic-go

Quic-go 是 Go 语言实现的 QUIC 协议,知名 Caddy 服务器软件就是基于此包实现的 HTTP/3 协议的支持,接下来让我们通过样例,来将 HTTP/3 跑起来,让 HTTP/3 看起来离我们并不远。

package main

import (
"fmt"
"net/http"

"github.com/lucas-clemente/quic-go/http3"
)

func main() {
certFile := "/Users/Shared/go/quic/tls/server.crt"
keyFile := "/Users/Shared/go/quic/tls/server.key"

handle := func(w http.ResponseWriter, req *http.Request) {
_, _ = fmt.Fprintf(w, "<html><body>hello world</body></html>")
}
mux := http.NewServeMux()
mux.HandleFunc("/", handle)

err := http3.ListenAndServe(":443", certFile, keyFile, mux)
fmt.Println(err)
}

什么?这与上面的 HTTPS Service 有什么区别?

是的,只有 http3.ListenAndServe 从 http 变为 http3 并引入了 github.com/lucas-clemente/quic-go/http3 包,但是我却用了很长时间才将样例代码跑起来。

接下来我简单的介绍下 quic-go 包的实现:

  • 首先 提供了 ListenAndServeListenAndServeQUIC,以及 Service 去自己实现 Service.ListenAndServe

  • http3.ListenAndServe 加载了证书和秘钥,并同时监听了 Addr 的 TCP 和 UDP 连接

  • 并且为 TCP 的 Handle 注册一个全局 SetQuicHeaders 实现 ALTSVC 自举

  • TCP 连接则有 http 包实现 Service ,UDP 有 quic-go 实现 Service

  • quic-go 的 Service 有着与 http 的 Service 类似的接口,监听端口后进行处理

0x07 验证

上面的代码,你可能已经直接 Run 起来了,可是却不知道怎么验证,是否成功?

我在准备文章之前曾尝试几种方式:

  1. 新版本的 CURL,有依赖问题

  2. Curl + Quiche,编译麻烦

  3. http3-client,同样需要编译

  4. 谷歌浏览器,需要导入 CA 到系统证书

https://www.mozilla.org/zh-CN/firefox/developer/

安装完成后,需要进行下面的配置:

  1. 在弹出的 三思而后行 提示页面,点击 接受风险并继续

  2. 在 搜索首选项 输入框中输入:http3

  3. 双击 network.http.http3.enabled 所在行,启用 HTTP3

引用

  1. RFC7838: HTTP Alternative Services

  2. HTTP Alternative Services 介绍

  3. HTTP/2 笔记之连接建立

  4. HTTPS 深入浅出 - 什么是 ALPN?

  5. Hypertext Transfer Protocol Version 3 (HTTP/3)[draft-ietf-quic-http-29]

  6. Get a head start with QUIC

  7. 一文看完 HTTP3 的演化历程

  8. 如何玩转 HTTP 3?

  9. 科普:QUIC 协议原理分析


以上是关于HTTP/3 in Go的主要内容,如果未能解决你的问题,请参考以下文章

golang 片段7 for https://medium.com/@francesc/why-are-there-nil-channels-in-go-9877cc0b2308

GO 智能合约cannot use transactionRecordId + strconv.Itoa(id) (type string) as type byte in append(示例代码(代

[Go] 通过 17 个简短代码片段,切底弄懂 channel 基础

你知道的Go切片扩容机制可能是错的

一旦单击带有 in 片段的回收器列表项,如何将片段意向活动,以及如何获取回收器项目值?

What's the difference between @Component, @Repository & @Service annotations in Spring?(代码片段