了解一下http2和h2c (HTTP/2 over TCP,HTTP/2 without TLS)。
http/1.1 的服务器
我们经常会在代码中启动一个http服务器,最简单的http/1.1服务器如下所示:
1
2
3
4
5
|
http.Handle("/foo", fooHandler)
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})
log.Fatal(http.ListenAndServe(":8080", nil))
|
使用Go开发web服务非常的简单,快速。
http/1.1 的服务器 with TLS
如果想让http/1.1服务器支持TLS, 可以使用如下的代码:
1
2
3
4
5
|
http.Handle("/foo", fooHandler)
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})
log.Fatal(http.http.ListenAndServeTLS(":443", "server.crt", "server.key",nil))
|
至于server.crt 和 server.key,你可以使用你从CA购买的证书,你也可以使用下面的测试证书。
为了测试,你可以创建CA证书和你的服务器使用的证书。
1、创建CA证书
1
2
|
openssl genrsa -out rootCA.key 2048
openssl req -x509 -new -nodes -key rootCA.key -days 1024 -out rootCA.pem
|
然后把rootCA.pem加到你的浏览器的证书中
2、创建证书
1
2
3
|
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr
openssl x509 -req -in server.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out server.crt -days 500
|
免费证书
如果你不想从CA花钱购买证书, 也不想配置测试证书,那么你可以使用let’s encrypt的免费证书, 而且let’s encrypt目前支持通配符证书,使用也是很方便的。
Go的扩展包中提供了let’s encrypt的支持。
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
|
package main
import (
"crypto/tls"
"log"
"net/http"
"golang.org/x/crypto/acme/autocert"
)
func main() {
certManager := autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist("example.com"),
Cache: autocert.DirCache("certs"),
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello world"))
})
server := &http.Server{
Addr: ":443",
TLSConfig: &tls.Config{
GetCertificate: certManager.GetCertificate,
},
}
go http.ListenAndServe(":80", certManager.HTTPHandler(nil))
log.Fatal(server.ListenAndServeTLS("", "")) //Key and cert are coming from Let's Encrypt
}
|
或者更简单的:
1
|
log.Fatal(http.Serve(autocert.NewListener("example.com"), handler))
|
看上面的例子, 把example.com换成你的域名,证书暂存在certs文件夹。autocert会定期自动刷新,避免证书过期。它会自动申请证书,并进行验证。
不过比较遗憾的是, autocert目前不支持通配符域名。
1
|
HostWhitelist returns a policy where only the specified host names are allowed. Only exact matches are currently supported. Subdomains, regexp or wildcard will not match.
|
通配符(ACME v2)的支持也已经完成了,但是迟迟未通过review,所以你暂时还不能使用这个特性。 (issue#21081)
HTTP/2
Go 在 1.6的时候已经支持 HTTP/2 了, 1.8 开始支持PUSH功能,你什么时候开始采用HTTP/2的呢?
Go的http/2使用也非常简单,但是必须和TLS一起使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
package main
import (
"log"
"net/http"
"time"
"golang.org/x/net/http2"
)
const idleTimeout = 5 * time.Minute
const activeTimeout = 10 * time.Minute
func main() {
var srv http.Server
//http2.VerboseLogs = true
srv.Addr = ":8972"
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello http2"))
})
http2.ConfigureServer(&srv, &http2.Server{})
go func() {
log.Fatal(srv.ListenAndServeTLS("server.crt", "server.key"))
}()
select {}
}
|
http2封装并隐藏了http/2的处理逻辑,对于用户来说,可以不必关心内部的具体实现,像http/1.1一样简单的使用即可。
这里的证书可以使用上面提到证书,或者你购买的证书,或者免费let’s encrypt证书。
h2c
HTTP/2 协议本身和 TLS 无关,但是通常浏览器 (Chrome 等) 都要求必须结合 TLS 来使用。
h2c(HTTP/2 cleartext)是不带 TLS 的 HTTP/2。对于内部 API 服务来说,TLS 并非必须,反而会增加额外的资源开销。
Go 的标准库已经支持了 h2,不支持 h2c,但是在 golang.org/x/net/http2/h2c 中有对 h2c 的支持,算是半个标准库。
由于 HTTP/2 和 HTTP/1.1 高度兼容,Golang 中我们需要提供的 http.Handler 方法并没有什么变化,所以只需要替换 Transport 就可以实现升级到 HTTP/2 这一能力。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package main
import (
"fmt"
"log"
"net/http"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "You tell %s\n", r.Proto)
})
h2s := &http2.Server{}
h1s := &http.Server{Addr: ":9100", Handler: h2c.NewHandler(mux, h2s)}
log.Fatal(h1s.ListenAndServe())
}
|
使用起来也很简单,但是目前浏览器对http/2都是采用TLS的方式,所以用浏览器访问这个服务的话会退化为http/1.1的协议,测试的话你可以使用Go实现客户端的h2c访问。
服务端会监听在 9100 端口,并且同时支持 HTTP/1.1 和 HTTP/2。
通过 curl 用不同的协议访问的输出结果如下:
1
2
3
4
5
|
curl http://127.0.0.1:9100
You tell HTTP/1.1
curl http://127.0.0.1:9100 --http2
You tell HTTP/2.0
|
之所以能够在同一个端口上同时支持这两个协议,是因为 h2c.NewHandler
这个函数的封装。这个函数会在连接建立时先检测 Request
的内容,h2c 要求连接以 PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n
开头,如果匹配成功则交给 h2c 的 Handler 处理,否则交给 HTTP/1.1 的 Handler 处理。
h2c 这部分的源码如下:
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
|
// ServeHTTP implement the h2c support that is enabled by h2c.GetH2CHandler.
func (s h2cHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Handle h2c with prior knowledge (RFC 7540 Section 3.4)
if r.Method == "PRI" && len(r.Header) == 0 && r.URL.Path == "*" && r.Proto == "HTTP/2.0" {
if http2VerboseLogs {
log.Print("h2c: attempting h2c with prior knowledge.")
}
conn, err := initH2CWithPriorKnowledge(w)
if err != nil {
if http2VerboseLogs {
log.Printf("h2c: error h2c with prior knowledge: %v", err)
}
return
}
defer conn.Close()
s.s.ServeConn(conn, &http2.ServeConnOpts{Handler: s.Handler})
return
}
// Handle Upgrade to h2c (RFC 7540 Section 3.2)
if conn, err := h2cUpgrade(w, r); err == nil {
defer conn.Close()
s.s.ServeConn(conn, &http2.ServeConnOpts{Handler: s.Handler})
return
}
s.Handler.ServeHTTP(w, r)
return
}
|
由于我们的服务端同时支持 HTTP/1.1 和 HTTP/2,所以客户端可以通过任意的协议来通信,最好通过配置或环境变量的方式来决定是否启用升级 HTTP/2 的功能,后面会讲一下这个里面存在的坑。
客户端代码如下:
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
|
package main
import (
"crypto/tls"
"fmt"
"log"
"net"
"net/http"
"golang.org/x/net/http2"
)
func main() {
client := http.Client{
// Skip TLS dial
Transport: &http2.Transport{
AllowHTTP: true,
DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
return net.Dial(network, addr)
},
},
}
resp, err := client.Get("http://127.0.0.1:9100")
if err != nil {
log.Fatal(fmt.Errorf("get response error: %v", err))
}
fmt.Println(resp.StatusCode)
fmt.Println(resp.Proto)
}
|
http2.Transport 本身没有提供 Dial 方法来不启用 TLS,但是由于 HTTP/2 和 TLS 无关,只需要将 DialTLS 替换成我们自己方法,不建立 TLS 连接,对上层的 HTTP/2 的协议处理完全没有影响。
问题
实际上线此功能后,运行了两天,出现了服务超时的问题,排查后看起来和 https://github.com/golang/go/issues/28204 这个 issue 比较相似。
目前 Golang 标准库(Go1.11)中对于 HTTP/2 的流量控制在某些特殊场景下存在 Bug,会导致 Flow Control 的写窗口一直为 0,且无法恢复。这就导致了请求超时,并且复用的 Stream 没有被正常关闭,如果持续请求,默认单个 TCP 连接中存在 100 个 Stream 时,会新建一个 TCP 连接,此时后续的请求会恢复正常,但是如果又出现了有问题的请求,会重复之前的错误。
由于时间原因,并没有继续跟踪源码查看问题到底在哪,因为本地环境经过大量并发测试也并没有出现问题,说明应该是一个比较极端的 case 导致了这个错误。后续有精力时,可以考虑在线上开启流量复制的功能,将流量额外复制一份到单独版本的实例上,这个版本可以开启更多的 Debug 日志来辅助调试。
线上暂时关闭了此功能,待问题确认修复后再通过环境变量控制开启。
参考:
Service Mesh 探索之升级 HTTP/2 协议
Go http2 和 h2c