Golang中HTTP2客户端连接的使用与优化
Golang中HTTP2客户端连接的使用与优化 我正在尝试验证 go1.10.2 HTTP2 客户端是否正确通过单个 HTTP2 连接复用请求。我使用当前版本的 NginX 反向代理,该代理已配置为支持 HTTP2。我已通过 h2c 验证它确实支持 HTTP2。
我的 Go HTTP2 客户端配置为对所有请求使用带有所有回调的 httptrace。
当这个 Go HTTP2 客户端发出三个并发请求(简单请求函数的三次连续 go 调用)时,跟踪显示它正在创建并缓存三个独立的连接。
这里似乎有问题。要么客户端未启用 HTTP2,要么它没有正确通过单个 HTTP2 连接复用请求。
如果使用多个 HTTP2 连接与单个 HTTP2 服务通信是 http 包的预期行为,那么对于期望高效使用 HTTP2 客户端流的客户端来说,这将是一个严重的性能问题。
有人能对此提供一些见解吗?
如果感兴趣,这是客户端代码。
/*
Command simSwitch 模拟拨出交换机配置协议的交换机端。
此 simSwitch 假设其控制器服务位于:
https://localhost:8443
simSwitch 的本地目录必须包含一个 cert 目录,其中包含三个文件:
client.shivaram-1.tenants.servicefractal.com.cert.pem - 客户端证书
client.shivaram-1.tenants.servicefractal.com.key.pem - 客户端证书密钥
shivaram-1.tenants.servicefractal.com-ca-chain.cert.pem - 验证交换机控制器证书所需的证书链
*/
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/http/httptrace"
"net/url"
"os"
)
const (
//控制器服务 URL
controllerURL = "https://localhost:8443"
)
var (
//双向认证 HTTP 客户端
client *http.Client
//租户的客户端 SSL 证书
clientCert tls.Certificate
//跟踪上下文
clientTrace = httptrace.ClientTrace{GetConn: getConn, GotConn: gotConn, PutIdleConn: putIdleConn, GotFirstResponseByte: gotFirstResponseByte, Got100Continue: got100Continue, DNSStart: dnsStart, DNSDone: dnsDone, ConnectStart: connectStart, ConnectDone: connectDone, TLSHandshakeStart: tlsHandshakeStart, TLSHandshakeDone: tlsHandshakeDone, WroteHeaders: wroteHeaders, Wait100Continue: wait100Continue, WroteRequest: wroteRequest}
)
//HTTP 跟踪钩子
func getConn(s string) {
log.Printf("GetCon %s\n", s)
}
func gotConn(p httptrace.GotConnInfo) {
log.Printf("gotConn %+v\n", p)
}
func putIdleConn(err error) {
log.Printf("putIdleConn\n")
}
func gotFirstResponseByte() {
log.Printf("gotFirstResponseByte\n")
}
func got100Continue() {
log.Printf("got100Continue\n")
}
func dnsStart(p httptrace.DNSStartInfo) {
log.Printf("dnsStart %+v\n", p)
}
func dnsDone(p httptrace.DNSDoneInfo) {
log.Printf("dnsDone %+v\n", p)
}
func connectStart(network, addr string) {
log.Printf("connectStart %s: %s\n", network, addr)
}
func connectDone(network, addr string, err error) {
log.Printf("connectDone %s: %s\n", network, addr)
}
func tlsHandshakeStart() {
log.Printf("tlsHandshakeStart\n")
}
func tlsHandshakeDone(conState tls.ConnectionState, err error) {
log.Printf("tlsHandshakeDone %+v\n", conState)
}
func wroteHeaders() {
log.Printf("wroteHeaders\n")
}
func wait100Continue() {
log.Printf("wait100Continue\n")
}
func wroteRequest(wri httptrace.WroteRequestInfo) {
log.Printf("wroteRequest\n")
}
//ping 请求控制器 ping 并带有延迟间隔
func ping(ch chan int, interval int) {
var (
req http.Request
rsp *http.Response
intervalS string
err error
)
log.Printf("Ping interval %d\n", interval)
intervalS = fmt.Sprintf("%d", interval)
req.Method = http.MethodGet
req.URL, _ = url.ParseRequestURI("https://controller-1.shivaram-1.tenants.servicefractal.com:8443/ping?interval=" + intervalS)
tracedReq := req.WithContext(httptrace.WithClientTrace(req.Context(), &clientTrace))
rsp, err = client.Do(tracedReq)
if err != nil {
log.Printf("Ping error: %s\n", err)
os.Exit(1)
}
if rsp.StatusCode != http.StatusOK {
log.Printf("Ping status: %s\n", rsp.Status)
os.Exit(1)
}
//信号 ping 已完成
ch <- 1
log.Printf("Ping interval %d response received\n", interval)
} //ping
func main() {
var (
caCertBytes []byte
caCertPool *x509.CertPool
tlsConfig *tls.Config
transport *http.Transport
pingCh chan int
pingDone int
err error
)
//加载客户端证书
clientCert, err = tls.LoadX509KeyPair("cert/client.shivaram-1.tenants.servicefractal.com.cert.pem", "cert/client.shivaram-1.tenants.servicefractal.com.key.pem")
if err != nil {
fmt.Printf("The simSwitch cert subdirectory must contain the switch's cert and key pem files: %s\n", err)
os.Exit(1)
}
//加载 CA 证书包
caCertBytes, err = ioutil.ReadFile("cert/shivaram-1.tenants.servicefractal.com-ca-chain.cert.pem")
if err != nil {
fmt.Println("The simSwitch cert subdirectory must contain the cert bundle required to verify its controller's certificate.")
os.Exit(1)
}
caCertPool = x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCertBytes)
//设置 HTTPS 双向认证客户端
tlsConfig = &tls.Config{
Certificates: []tls.Certificate{clientCert},
RootCAs: caCertPool,
}
tlsConfig.BuildNameToCertificate()
transport = &http.Transport{TLSClientConfig: tlsConfig}
client = &http.Client{Transport: transport}
//启动三个并发 Ping
pingCh = make(chan int, 100)
go ping(pingCh, 5)
go ping(pingCh, 10)
go ping(pingCh, 15)
gatherPings:
for {
select {
case <-pingCh:
pingDone += 1
if pingDone >= 3 {
break gatherPings
}
}
}
log.Println("simSwitch has finished.")
}
更多关于Golang中HTTP2客户端连接的使用与优化的实战教程也可以访问 https://www.itying.com/category-94-b0.html
更多关于Golang中HTTP2客户端连接的使用与优化的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
在Go 1.10.2中,HTTP/2客户端确实支持连接复用,但你的代码中存在几个问题导致无法正确复用HTTP/2连接。以下是具体分析和修复方案:
问题分析
- URL不一致:代码中硬编码了不同的URL(
localhost:8443vscontroller-1.shivaram-1.tenants.servicefractal.com:8443) - 缺少HTTP/2强制启用:需要显式配置Transport以支持HTTP/2
- 连接池配置:需要合理配置连接池参数
修复后的代码
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/http/httptrace"
"net/url"
"os"
"golang.org/x/net/http2"
)
const (
controllerURL = "https://localhost:8443"
)
var (
client *http.Client
clientCert tls.Certificate
clientTrace = httptrace.ClientTrace{
GetConn: getConn,
GotConn: gotConn,
PutIdleConn: putIdleConn,
GotFirstResponseByte: gotFirstResponseByte,
Got100Continue: got100Continue,
DNSStart: dnsStart,
DNSDone: dnsDone,
ConnectStart: connectStart,
ConnectDone: connectDone,
TLSHandshakeStart: tlsHandshakeStart,
TLSHandshakeDone: tlsHandshakeDone,
WroteHeaders: wroteHeaders,
Wait100Continue: wait100Continue,
WroteRequest: wroteRequest,
}
)
// HTTP跟踪钩子保持不变
func getConn(s string) {
log.Printf("GetConn %s\n", s)
}
func gotConn(p httptrace.GotConnInfo) {
log.Printf("gotConn Reused: %t, WasIdle: %t, IdleTime: %v\n",
p.Reused, p.WasIdle, p.IdleTime)
}
func putIdleConn(err error) {
log.Printf("putIdleConn error: %v\n", err)
}
func gotFirstResponseByte() {
log.Printf("gotFirstResponseByte\n")
}
func got100Continue() {
log.Printf("got100Continue\n")
}
func dnsStart(p httptrace.DNSStartInfo) {
log.Printf("dnsStart %+v\n", p)
}
func dnsDone(p httptrace.DNSDoneInfo) {
log.Printf("dnsDone %+v\n", p)
}
func connectStart(network, addr string) {
log.Printf("connectStart %s: %s\n", network, addr)
}
func connectDone(network, addr string, err error) {
log.Printf("connectDone %s: %s, error: %v\n", network, addr, err)
}
func tlsHandshakeStart() {
log.Printf("tlsHandshakeStart\n")
}
func tlsHandshakeDone(conState tls.ConnectionState, err error) {
log.Printf("tlsHandshakeDone NegotiatedProtocol: %s, error: %v\n",
conState.NegotiatedProtocol, err)
}
func wroteHeaders() {
log.Printf("wroteHeaders\n")
}
func wait100Continue() {
log.Printf("wait100Continue\n")
}
func wroteRequest(wri httptrace.WroteRequestInfo) {
log.Printf("wroteRequest error: %v\n", wri.Err)
}
func ping(ch chan int, interval int) {
// 使用统一的URL
reqURL := fmt.Sprintf("%s/ping?interval=%d", controllerURL, interval)
req, err := http.NewRequest(http.MethodGet, reqURL, nil)
if err != nil {
log.Printf("Ping error creating request: %s\n", err)
ch <- 1
return
}
tracedReq := req.WithContext(httptrace.WithClientTrace(req.Context(), &clientTrace))
rsp, err := client.Do(tracedReq)
if err != nil {
log.Printf("Ping error: %s\n", err)
ch <- 1
return
}
defer rsp.Body.Close()
if rsp.StatusCode != http.StatusOK {
log.Printf("Ping status: %s\n", rsp.Status)
ch <- 1
return
}
log.Printf("Ping interval %d response received, Proto: %s\n", interval, rsp.Proto)
ch <- 1
}
func main() {
var (
caCertBytes []byte
caCertPool *x509.CertPool
tlsConfig *tls.Config
transport *http.Transport
err error
)
// 加载客户端证书
clientCert, err = tls.LoadX509KeyPair(
"cert/client.shivaram-1.tenants.servicefractal.com.cert.pem",
"cert/client.shivaram-1.tenants.servicefractal.com.key.pem",
)
if err != nil {
fmt.Printf("Failed to load client certificate: %s\n", err)
os.Exit(1)
}
// 加载CA证书包
caCertBytes, err = ioutil.ReadFile("cert/shivaram-1.tenants.servicefractal.com-ca-chain.cert.pem")
if err != nil {
fmt.Println("Failed to load CA certificate bundle.")
os.Exit(1)
}
caCertPool = x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCertBytes)
// 配置TLS支持HTTP/2
tlsConfig = &tls.Config{
Certificates: []tls.Certificate{clientCert},
RootCAs: caCertPool,
NextProtos: []string{"h2", "http/1.1"}, // 明确支持HTTP/2
}
// 配置Transport以优化连接复用
transport = &http.Transport{
TLSClientConfig: tlsConfig,
ForceAttemptHTTP2: true, // 强制尝试HTTP/2
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10, // 每个主机最大空闲连接数
MaxConnsPerHost: 10, // 每个主机最大连接数
IdleConnTimeout: 90, // 空闲连接超时时间(秒)
}
// 显式启用HTTP/2
http2.ConfigureTransport(transport)
client = &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
}
// 启动三个并发Ping
pingCh := make(chan int, 3)
go ping(pingCh, 5)
go ping(pingCh, 10)
go ping(pingCh, 15)
// 等待所有ping完成
pingDone := 0
for pingDone < 3 {
<-pingCh
pingDone++
}
log.Println("simSwitch has finished.")
}
关键修改点
- 统一URL:所有请求使用相同的
controllerURL - 强制HTTP/2:设置
ForceAttemptHTTP2: true和NextProtos: []string{"h2", "http/1.1"} - 连接池优化:配置合理的
MaxIdleConnsPerHost和MaxConnsPerHost - 显式HTTP/2配置:使用
http2.ConfigureTransport(transport)
修复后,跟踪日志应该显示连接被复用(gotConn Reused: true),并且NegotiatedProtocol: h2确认使用了HTTP/2协议。

