This page looks best with JavaScript enabled

浅尝QUIC和HTTP3

 ·  ☕ 5 min read · 👀... views

HTTP概述

HTTP是互联网的基石,它的简单、灵活使得各种各样的互联网应用成为可能。但HTTP的一些先天不足也为应用的开发增加了难度,为此,业界各大公司和标准化组织一直在对HTTP做各种改进和扩展,包括缓存、Range、长连接、Pipelining,还有基于SSL/TLS的HTTPS。后来Google设计的SPDY协议,在HTTP的基础上实现了多路复用和Header压缩,以进一步改善基于HTTP的传输性能和应用的交互体验。SPDY最终被IETF接受并做为HTTP/2的基础。虽然多路复用提高了数据并发传输的性能,但由于SPDY是基于TCP的,而TCP使用的是统一的流控,还是存在Head-of-line blocking的问题。而且TLS层每次都需要协商密钥,导致连接建立的过程比较复杂且低效,所以就有了QUIC协议的出现。

为什么使用UDP,想想需求就很简单,需要的是一个比TCP简单但是比UDP功能多的协议,所以只能基于UDP用户态扩展而没法基于TCP精简(因为协议在内核中)。

HTTP3的优化

Head-of-line blocking

HOL blocking是当一行数据包被第一个数据包阻塞在队列中时出现的性能限制现象。HTTP/1.1 中的一种 HOL 阻塞形式是当浏览器中允许的并行请求数用完时,后续请求需要等待前一个请求完成;HTTP/2 通过请求复用消除了应用层的 HOL 阻塞,但 HOL 在传输层(TCP)仍然存在;HTTP/3 使用 QUIC 替代 TCP,消除了传输层中的 HOL 阻塞。[1]

0RTT

HTTPS over TCP&TLS建立连接至少需要十个包。HTTP3对此进行改进:首次连接,使用QUIC协议的客户端和服务端要使用1RTT使用DH(Diffie-Hellman)迪菲-赫尔曼算法进行密钥交换;非首次连接,客户端保存config(有失效时间)跳过1RTT实现0RTT业务数据交互。前向安全保证了过去进行的通讯不受密钥在未来暴露的威胁,QUIC通过每次使用临时密码然后销毁的方式实现前向安全,不必担心config泄露。QUIC 像 TLS 1.3 一样允许客户端在连接握手完成之前开始发送 HTTP 请求

FEC

前向纠错(Forward Error Correction),是指通过发送冗余的编码数据,使得接收方当传输过程中出现错误时,接收方能从收到的数据中重建和还原得到正确的原始数据。

FEC算法有很多种,包括内存中使用的汉明码,磁盘存储系统中常用的Reed-Solomon码,还有移动通信中用到的卷积码、Turbo码、LDPC码等。

早期QUIC中使用的FEC算法是基于XOR的简单实现,不过IETF的QUIC协议标准中已经没有FEC的踪影,猜测是FEC在QUIC协议的应用场景中难以被高效的使用。

连接迁移

TCP协议使用五元组来表示一条唯一的连接,当我们从4G环境切换到wifi环境时,手机的IP地址就会发生变化,这时必须创建新的TCP连接才能继续传输数据。QUIC协议基于UDP实现摒弃了五元组的概念,使用64位的随机数作为连接的ID,并使用该ID表示连接。基于QUIC协议之下,我们在日常wifi和4G切换时,或者不同基站之间切换都不会重连,从而提高业务层的体验。

现有库实现

具体的实现有很多可以看WIKI上的表格[6],Cloudflare作为很早就发展http3的企业开源了一个rust写的库[4],但是Go的开源库就只有一个quic-go

Gin的HTTP3支持

Gin现在并没有release支持HTTP3,但是很早就有过issue和pr实验,链接如下

Can gin support HTTP/3?

experimental: support http3

其中PR的修改十分简单,只是同Gin以往的逻辑,在gin.go中添加了一个支持QUIC协议的Run函数。因QUIC协议自身支持加密,所以函数实现更像是RunTLS,需要传入证书密钥文件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import "github.com/lucas-clemente/quic-go/http3"
func (engine *Engine) RunQUIC(addr, certFile, keyFile string) (err error) {
	debugPrint("Listening and serving QUIC on %s\n", addr)
	defer func() { debugPrintError(err) }()

	if engine.isUnsafeTrustedProxies() {
		debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
			"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
	}

	err = http3.ListenAndServeQUIC(addr, certFile, keyFile, engine.Handler())
	return
}

其中quic-go是纯Go的QUIC实现,许多大型项目已经在使用这个库。

测试也跟TestRunTLS一模一样

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import "github.com/stretchr/testify/assert"
// params[0]=url example:http://127.0.0.1:8080/index (cannot be empty)
// params[1]=response status (custom compare status) default:"200 OK"
// params[2]=response body (custom compare content)  default:"it worked"
func testRequest(t *testing.T, params ...string) {
	if len(params) == 0 {
		t.Fatal("url cannot be empty")
	}
	tr := &http.Transport{
		TLSClientConfig: &tls.Config{
			InsecureSkipVerify: true,
		},
	}
	client := &http.Client{Transport: tr}
	resp, err := client.Get(params[0])
	assert.NoError(t, err)
	defer resp.Body.Close()
	body, ioerr := ioutil.ReadAll(resp.Body)
	assert.NoError(t, ioerr)
	var responseStatus = "200 OK"
	if len(params) > 1 && params[1] != "" {
		responseStatus = params[1]
	}
	var responseBody = "it worked"
	if len(params) > 2 && params[2] != "" {
		responseBody = params[2]
	}
	assert.Equal(t, responseStatus, resp.Status, "should get a "+responseStatus)
	if responseStatus == "200 OK" {
		assert.Equal(t, responseBody, string(body), "resp body should match")
	}
}
func TestRunQUIC(t *testing.T) {
	router := New()
	go func() {
		router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })

		assert.NoError(t, router.RunQUIC(":8443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
	}()

	// have to wait for the goroutine to start and run the server
	// otherwise the main thread will complete
	time.Sleep(5 * time.Millisecond)

	assert.Error(t, router.RunQUIC(":8443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
	testRequest(t, "https://localhost:8443/example")
}

Curl http3

直接修改Gin源码添加RunQUIC函数支持http3,并编写简单server

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

func main() {
	router := gin.New()
	router.GET("/example", func(c *gin.Context) { c.String(http.StatusOK, "ruokeqx http3 worked") })
	router.RunQUIC(":8080", "./cert.pem", "./key.pem")
}

手动编译[5]支持http3的curl进行访问,编译的时候要注意点是<somewhere1>/lib实际上是<somewhere1>/lib64 否则会编译出错[5]

image-20220709132919677

Ref

[1] wiki HOL blocking

[2] 图解|为什么HTTP3.0使用UDP协议

[3] Gin PR 3210 experimental: support http3

[4] Cloudflare http3

[5] build http3 curl

[6] Wiki http3

Share on

ruokeqx
WRITTEN BY
ruokeqx