使用golang创建http2和h2c服务端
2024-02-26 15:13:18

生成tls证书

注意:网上有说可以使用 golang.org/x/crypto/acme/autocert 去自动申请证书,但是这个是要在公网环境才行的,你使用了这个库,会提交一个申请,let's encrypt 会去访问你的应用程序来验证,但是在非公网环境是没法验证成功的,有的帖子说可以将自己的域名解析为 127.0.0.1,然后用 autocert 去申请自己域名的证书就可以,实测不行。

使用 openssl 生成证书的 shell 脚本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash
set -x
$(openssl genrsa -out rootCA.key 4096)
$(openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 1024 -out rootCA.crt -subj "/C=GB/L=China/O=CN/CN=LOCALHOST")

$(openssl genrsa -out mydomain.com.key 2048)
$(openssl req -new -sha256 -key mydomain.com.key -subj "/C=US/ST=CA/O=MyOrg, Inc./CN=$(hostname -I)" -out mydomain.com.csr)

echo "basicConstraints = CA:FALSE" > mydomain.com.ext
echo "keyUsage = nonRepudiation, digitalSignature, keyEncipherment" >> mydomain.com.ext
echo "subjectAltName=@alt_names" >> mydomain.com.ext
echo "" >> mydomain.com.ext
echo "[alt_names]" >> mydomain.com.ext
echo "IP.1=$(hostname -I)" >> mydomain.com.ext
echo "DNS.1=localhost" >> mydomain.com.ext

$(openssl x509 -req -in mydomain.com.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out mydomain.com.crt -days 500 -sha256 -extfile mydomain.com.ext)

服务端代码

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package main

import (
"fmt"
"log"
"net/http"
"sync"

"golang.org/x/net/http2"
)

func main() {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Protocol: ", r.Proto)
log.Println("Protocol: ", r.Proto)
})

var wg sync.WaitGroup

h1s := &http.Server{
Addr: "0.0.0.0:8080",
Handler: handler,
}

log.Println("start http1.1 server with tls server on: 8080")
wg.Add(1)
go func() {
defer wg.Done()
err := h1s.ListenAndServeTLS("mydomain.com.crt", "mydomain.com.key") //即使没有手动指定 http2.Server,但是提供证书以后,go 就支持 http2 了。
// err := h1s.ListenAndServe()
log.Fatal(err)
}()

h2s := &http.Server{
Addr: "0.0.0.0:8090",
Handler: handler,
}
http2.ConfigureServer(h2s, &http2.Server{})
log.Println("start http2 server with tls on: 8090")
wg.Add(1)
go func() {
defer wg.Done()
err := h2s.ListenAndServeTLS("mydomain.com.crt", "mydomain.com.key")
log.Fatal(err)
}()

h2c := &http.Server{
Addr: "0.0.0.0:9000",
Handler: handler,
}
http2.ConfigureServer(h2c, &http2.Server{})
log.Println("start http2 server without tls on: 9000")
wg.Add(1)
go func() {
defer wg.Done()
err := h2c.ListenAndServe()
log.Fatal(err)
}()

wg.Wait()
}

验证

使用 curl 访问 8080 端口,这里需要带上 --insecure 参数跳过证书的验证。如果是使用浏览器访问,那么需要在浏览器中导入前面使用脚本生成的 rootCA.crt
ALPN, offering h2ALPN, offering http/1.1 ALPN是一种协议扩展机制,用于在 TLS 握手期间协商客户端和服务器之间要使用的应用层协议。在HTTP/2中,h2是用于指示使用HTTP/2协议的标识符。我猜测 h2 的优先级高于 http/1.1,所以这里在协商以后使用的是 h2。

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
48
49
50
51
52
53
54
55
56
57
58
root@kusaka-virtual-machine:~/test/client# curl -v --insecure  https://localhost:8080
* Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* TLSv1.0 (OUT), TLS header, Certificate Status (22):
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS header, Finished (20):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.2 (OUT), TLS header, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256
* ALPN, server accepted to use h2
* Server certificate:
* subject: C=US; ST=CA; O=MyOrg, Inc.; CN=192.168.58.132 172.17.0.1
* start date: Feb 25 14:52:24 2024 GMT
* expire date: Jul 9 14:52:24 2025 GMT
* issuer: C=GB; L=China; O=CN; CN=LOCALHOST
* SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.
* Using HTTP2, server supports multiplexing
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* Using Stream ID: 1 (easy handle 0x562afa8e7e90)
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
> GET / HTTP/2
> Host: localhost:8080
> user-agent: curl/7.81.0
> accept: */*
>
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
< HTTP/2 200
< content-type: text/plain; charset=utf-8
< content-length: 20
< date: Sun, 25 Feb 2024 15:28:12 GMT
<
Protocol: HTTP/2.0
* Connection #0 to host localhost left intact

使用 curl 访问 8090 端口。这里的表现和 8080 端口是一致的。

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
48
49
50
51
52
53
54
55
56
57
58
root@kusaka-virtual-machine:~/test/client# curl -v --insecure  https://localhost:8090
* Trying 127.0.0.1:8090...
* Connected to localhost (127.0.0.1) port 8090 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* TLSv1.0 (OUT), TLS header, Certificate Status (22):
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS header, Finished (20):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.2 (OUT), TLS header, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256
* ALPN, server accepted to use h2
* Server certificate:
* subject: C=US; ST=CA; O=MyOrg, Inc.; CN=192.168.58.132 172.17.0.1
* start date: Feb 25 14:52:24 2024 GMT
* expire date: Jul 9 14:52:24 2025 GMT
* issuer: C=GB; L=China; O=CN; CN=LOCALHOST
* SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.
* Using HTTP2, server supports multiplexing
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* Using Stream ID: 1 (easy handle 0x5585cf28ce90)
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
> GET / HTTP/2
> Host: localhost:8090
> user-agent: curl/7.81.0
> accept: */*
>
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
< HTTP/2 200
< content-type: text/plain; charset=utf-8
< content-length: 20
< date: Sun, 25 Feb 2024 15:30:58 GMT
<
Protocol: HTTP/2.0
* Connection #0 to host localhost left intact

使用 curl 测试 9000 端口 的 h2c 服务端,因为 9000 端口的服务端没有证书,所以这里不能使用 https 去请求,同时也就不需要 --insecure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
root@kusaka-virtual-machine:~/test/client# curl -v   http://localhost:9000
* Trying 127.0.0.1:9000...
* Connected to localhost (127.0.0.1) port 9000 (#0)
> GET / HTTP/1.1
> Host: localhost:9000
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Sun, 25 Feb 2024 15:35:27 GMT
< Content-Length: 20
< Content-Type: text/plain; charset=utf-8
<
Protocol: HTTP/1.1
* Connection #0 to host localhost left intact

在 h2c 中,设置了 http2.Server{},这是一种非加密的 http2 服务端,在 curl 中,我们可以手动添加参数 --http2-prior-knowledge 指定使用 http2 来通信。

1
http2.ConfigureServer(h2c, &http2.Server{})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
root@kusaka-virtual-machine:~/test/client# curl -v --http2-prior-knowledge localhost:9000
* Trying 127.0.0.1:9000...
* Connected to localhost (127.0.0.1) port 9000 (#0)
* Using HTTP2, server supports multiplexing
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x559418d6ae90)
> GET / HTTP/2
> Host: localhost:9000
> user-agent: curl/7.81.0
> accept: */*
>
* Empty reply from server
* Closing connection 0
curl: (52) Empty reply from server

这里出了点问题,h2c server 没有返回任何数据?按照正常来说,server 应该要返回一个 “Protocol: HTTP/2.0” 才对,看这里 curl 的信息,确实使用了 http2 无疑,在 server 端也打印了一个 “Protocol: HTTP/2.0” 的信息,但是最终 client 没有接收到被写入到 http.ResponseWriter 的数据。

这里 server 端打印出来了 HTTP/2.0,说明我们是用的 HTTP/2.0 的客户端发送的请求,但是 golang 的 http.Server 在非 TLS 时,默认是支持 HTTP/1.1 的,因此服务端给的响应就是 HTTP/1.1 协议的,客户端接收到以后对不上了。这里如果我们使用 golang 的 client 去发送请求,就会是下面的情况。

HTTP/2.0 client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func HttpClientExample() {
client := http.Client{
Transport: &http2.Transport{
AllowHTTP: true,
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
var d net.Dialer
return d.DialContext(ctx, network, addr)
},
},
}

resp, err := client.Get(url)
checkErr(err, "during get")
fmt.Println(resp)
fmt.Printf("Client Proto: %d\n", resp.ProtoMajor)
}

使用 H2 Client 发送请求

1
2
3
root@kusaka-virtual-machine:~/test/client# go run main.go 
ERROR: during get: Get "http://localhost:9000/": unexpected EOF
exit status 1

修改 H2C 服务端代码

由于 http.Server 默认不支持 H2,所以我们使用 http2 库来实现一个 H2 服务端,将第三个 server 的代码修改如下。此时再使用 curl 或者 go 的 client 去调用,就可以使用非加密的 H2 协议了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
h2c := &http2.Server{}

log.Println("start http2 server without tls on: 9000")
wg.Add(1)
go func() {
defer wg.Done()
l, err := net.Listen("tcp", "0.0.0.0:9000")
if err != nil {
log.Fatal(err)
}
for {
conn, err := l.Accept()
if err != nil {
log.Println(err)
}

h2c.ServeConn(conn, &http2.ServeConnOpts{
Handler: handler,
})
}
}()

使用 go client

1
2
3
root@kusaka-virtual-machine:~/test/client# go run main.go
&{200 OK 200 HTTP/2.0 2 0 map[Content-Length:[20] Content-Type:[text/plain; charset=utf-8] Date:[Mon, 26 Feb 2024 15:06:39 GMT]] {0xc00011c180} 20 [] false false map[] 0xc0000d6000 <nil>}
Client Proto: 2

使用 curl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
root@kusaka-virtual-machine:~/test/client# curl -v --http2-prior-knowledge http://localhost:9000
* Trying 127.0.0.1:9000...
* Connected to localhost (127.0.0.1) port 9000 (#0)
* Using HTTP2, server supports multiplexing
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x55b41cc3ceb0)
> GET / HTTP/2
> Host: localhost:9000
> user-agent: curl/7.81.0
> accept: */*
>
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
< HTTP/2 200
< content-type: text/plain; charset=utf-8
< content-length: 20
< date: Mon, 26 Feb 2024 15:06:59 GMT
<
Protocol: HTTP/2.0
* Connection #0 to host localhost left intact
root@kusaka-virtual-machine:~/test/client#

结语

golang 的 net/http 默认支持 H2,但是需要配置了 TLS 加密才行,如果使用 H2C 服务端,那么就不能使用默认的 http.Server。另外,我抽了点时间看了下 http.Server 的源码,http.Server 在升级协议为 H2 时,并不是调的 http2 的库,而是在 http 库里面写了一个叫做 http2server 的结构体。

参考文档

https://github.com/thrawn01/h2c-golang-example/blob/master/README.md

https://colobu.com/2018/09/06/Go-http2-%E5%92%8C-h2c/

https://zhuanlan.zhihu.com/p/531003047

https://posener.github.io/http2/

https://studygolang.com/articles/10102