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

1 回复

更多关于Golang中HTTP2客户端连接的使用与优化的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在Go 1.10.2中,HTTP/2客户端确实支持连接复用,但你的代码中存在几个问题导致无法正确复用HTTP/2连接。以下是具体分析和修复方案:

问题分析

  1. URL不一致:代码中硬编码了不同的URL(localhost:8443 vs controller-1.shivaram-1.tenants.servicefractal.com:8443
  2. 缺少HTTP/2强制启用:需要显式配置Transport以支持HTTP/2
  3. 连接池配置:需要合理配置连接池参数

修复后的代码

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.")
}

关键修改点

  1. 统一URL:所有请求使用相同的controllerURL
  2. 强制HTTP/2:设置ForceAttemptHTTP2: trueNextProtos: []string{"h2", "http/1.1"}
  3. 连接池优化:配置合理的MaxIdleConnsPerHostMaxConnsPerHost
  4. 显式HTTP/2配置:使用http2.ConfigureTransport(transport)

修复后,跟踪日志应该显示连接被复用(gotConn Reused: true),并且NegotiatedProtocol: h2确认使用了HTTP/2协议。

回到顶部