使用Golang的x/net/http2实现单TCP连接发送多请求
使用Golang的x/net/http2实现单TCP连接发送多请求 你好,
我正在尝试使用 http2 包发送多个请求,但当我使用以下代码行时,却建立了多个 TCP 连接:
conntransport := &http2.Transport{
DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
return net.Dial(network, addr)
},
AllowHTTP: true,
}
client := &http.Client{
Transport: conntransport,
}
http.NewRequest(…)
client.Do()
或
client.Post()
非常感谢您的帮助。
更多关于使用Golang的x/net/http2实现单TCP连接发送多请求的实战教程也可以访问 https://www.itying.com/category-94-b0.html
你好,@Dean_Davidson,我检查了一下,问题是我的服务器支持 http1.1 升级到 h2c,这意味着无需 TLS。那么你认为我可以在 h2c 上实现多路复用吗?
更多关于使用Golang的x/net/http2实现单TCP连接发送多请求的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
那么,在这种情况下它应该会自动选择 HTTP/2。使用我上面展示的代码,你能指出一个同时支持 HTTP/1.1 和 HTTP/2 的公共服务器,并让它选择 HTTP/1.1 吗?我在想问题是否出在你尝试连接的那个服务器不支持 HTTP/2。
是的,它支持 HTTP/2,并且我也执行了 curl 命令。但由于默认情况下同时启用了 HTTP/1.1 和 HTTP/2,它会选择 HTTP/1.1。这就是我尝试使用 http1.transport 的原因,因为我可以强制使用 HTTP/2,但那样我将无法在单个 TCP 连接中获得多个请求。
我的服务器支持 HTTP/1.1 升级到 h2c,这意味着无需 TLS。
哦。我不确定你能否使用标准库实现这一点。看起来你已经找到了这个变通方案,但也许它现在不能按预期工作了。或许你应该在 Github 上提个 issue,也许有人会有更好的主意?
你指的是 HTTP/2 的多路复用 吗?如果是的话,可以看看 golang nuts 上的这个讨论。里面可能有一些对你有用的示例可以参考。另外,我在你的代码中没有看到你尝试发送多个请求的地方。
我能想到的另一件事是查看 net/http/transport.go 第 278 行:
// ForceAttemptHTTP2 controls whether HTTP/2 is enabled when a non-zero
// Dial, DialTLS, or DialContext func or TLSClientConfig is provided.
// By default, use of any those fields conservatively disables HTTP/2.
// To use a custom dialer or TLS config and still attempt HTTP/2
// upgrades, set this to true.
如果你没有使用 http2.Transport,这可能相关,因为你正在使用非零的 DialTLS 和 TLSClientConfig。除此之外,请验证服务器是否支持 http2。
Dean_Davidson:
ue Connection reused? true Connection reused? true Connection reused? true
是的,当我从 goodle.com 请求时,我也得到了相同的结果。
如果我从一个同时支持 HTTP/1.1 和 HTTP/2 的服务器请求,由于我没有在传输配置中指定使用 HTTP/2,它会选择 HTTP/1.1。
开始请求 #5 开始请求 #3 开始请求 #1 开始请求 #2 连接被复用? false 开始请求 #4 连接被复用? false 连接被复用? false 连接被复用? false 使用 HTTP/1.1 获得 #5 的响应 使用 HTTP/1.1 获得 #3 的响应 连接被复用? false 使用 HTTP/1.1 获得 #1 的响应 使用 HTTP/1.1 获得 #4 的响应 使用 HTTP/1.1 获得 #2 的响应
如果你能帮助我,告诉我如何以简单的方式指定使用 HTTP/2,我将不胜感激。
非常感谢
package main
import (
"crypto/tls"
"fmt"
"log"
"net"
"net/http"
"net/http/httptest"
"time"
"golang.org/x/net/http2"
)
func main() {
stallResp := make(chan bool)
cst := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Printf("Request proto: %s\n", r.Proto)
<-stallResp
w.Write([]byte("Done here"))
}))
if err := http2.ConfigureServer(cst.Config, new(http2.Server)); err != nil {
log.Fatalf("Failed to configure HTTP/2 server: %v", err)
}
cst.TLS = cst.Config.TLSConfig
cst.StartTLS()
tr := &http2.Transport{
TLSClientConfig: cst.Config.TLSConfig,
AllowHTTP: true,
DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
return net.Dial(network, addr)
},
}
tr.TLSClientConfig.InsecureSkipVerify = true
client := &http.Client{
Timeout: 400 * time.Millisecond,
Transport: tr,
}
url := "https://1example:port/"
res, err := client.Get(url)
if err == nil {
res.Body.Close()
}
for i := 1; i <= 50; i++ {
res, err := client.Get(url)
if err == nil {
res.Body.Close()
}
}
defer cst.Close()
close(stallResp)
}
在开始循环之前,我尝试发送了一个请求,结果得到了多个TCP连接。 这是我的代码。
是的,我正在尝试应用 HTTP/2 多路复用,这是我的代码,但我收到了多个 TCP 连接:
package main
import (
"crypto/tls"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"net/http/httptest"
"os"
"time"
"golang.org/x/net/http2"
)
func main() {
stallResp := make(chan bool)
cst := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Printf("Request proto: %s\n", r.Proto)
<-stallResp
w.Write([]byte("Done here"))
}))
if err := http2.ConfigureServer(cst.Config, new(http2.Server)); err != nil {
log.Fatalf("Failed to configure HTTP/2 server: %v", err)
}
cst.TLS = cst.Config.TLSConfig
cst.StartTLS()
tr := &http2.Transport{
TLSClientConfig: cst.Config.TLSConfig,
AllowHTTP: true,
DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
return net.DialTCP(network, clienta, servAddr)
},
}
tr.TLSClientConfig.InsecureSkipVerify = true
client := &http.Client{
Timeout: 400 * time.Millisecond,
Transport: tr,
}
url := "https://example:port/"
for i := 1; i <= 50; i++ {
res, err := client.Get(url)
if err == nil {
res.Body.Close()
}
}
defer cst.Close()
close(stallResp)
}
是的,这听起来和我链接的那个帖子中描述的情况一模一样。具体来说,是这个回复解决了问题:
2017年8月3日星期四上午11:07:50(UTC+2),pala.d…@gmail.com写道: 以DuckDuckGo为默认目标的示例代码:
这看起来像是连接建立时的“惊群”竞争。所以没有连接可以复用,因为此时还没有任何一个连接建立完成。
尝试先完成一个单独的请求,然后再并行运行其余的请求。
所以,请看一下上面的 Go Playground 示例,修改你的代码,先完成一个单独的请求,然后再并行运行其余的请求。
另外,我认为你不需要直接使用 golang.org/x/net/http2。根据文档所述:
这个包是底层的,预期只有极少数人会直接使用它。大多数用户将通过 net/http 包的自动使用间接地使用它(从 Go 1.6 及更高版本开始)。关于在早期 Go 版本中的使用,请参阅 ConfigureServer。(传输支持需要 Go 1.6 或更高版本)
你很可能可以使用 net/http,只要你使用的是 Go 1.6 或更高版本(我猜你是,因为那大概是 2015 年左右的东西),它就包含了对 HTTP/2 的透明支持:
从 Go 1.6 开始,当使用 HTTPS 时,http 包对 HTTP/2 协议提供了透明支持。必须禁用 HTTP/2 的程序可以通过将 Transport.TLSNextProto(对于客户端)或 Server.TLSNextProto(对于服务器)设置为非 nil 的空映射来实现。或者,目前支持以下 GODEBUG 环境变量:
GODEBUG=http2client=0 # 禁用 HTTP/2 客户端支持
GODEBUG=http2server=0 # 禁用 HTTP/2 服务器支持
GODEBUG=http2debug=1 # 启用详细的 HTTP/2 调试日志
GODEBUG=http2debug=2 # ... 更详细,包含帧转储
你如何验证它正在使用多个连接?
此外,在没有并发/异步的情况下(你当前的代码是同步的),我的理解是,HTTP/2 带来的好处可能有限,因为 HTTP/2 多路复用与 HTTP/1 流水线的核心区别 在于你无需等待并发返回的结果。
最后,我想知道 HTTP 跟踪 是否对你有用。例如,我使用了如下跟踪:
package main
import (
"fmt"
"log"
"net/http"
"net/http/httptrace"
"sync"
)
// transport 是一个 http.RoundTripper,用于跟踪正在进行的请求
// 并实现钩子来报告 HTTP 跟踪事件。
type transport struct {
current *http.Request
}
// RoundTrip 包装 http.DefaultTransport.RoundTrip 以跟踪
// 当前请求。
func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
t.current = req
return http.DefaultTransport.RoundTrip(req)
}
// GotConn 打印当前请求的连接是否曾被重用。
func (t *transport) GotConn(info httptrace.GotConnInfo) {
fmt.Printf("Connection reused? %v\n", info.Reused)
}
func main() {
tr := &transport{}
client := &http.Client{Transport: tr}
wg := sync.WaitGroup{}
url := "https://www.google.com/"
for i := 1; i <= 5; i++ {
// 增加 WaitGroup 计数器。
wg.Add(1)
// 启动一个 goroutine 来获取 URL。
go func(reqNum int) {
fmt.Println("Beginning request #", reqNum)
// goroutine 完成时递减计数器。
defer wg.Done()
// 获取 URL。
req, _ := http.NewRequest("GET", url, nil)
trace := &httptrace.ClientTrace{
GotConn: tr.GotConn,
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
if resp, err := client.Do(req); err != nil {
log.Fatal(err)
} else {
fmt.Printf("Got response for #%v using %v\n", reqNum, resp.Proto)
resp.Body.Close()
}
}(i)
}
wg.Wait()
}
… 它产生了以下输出:
$ go run main.go
Beginning request # 5
Beginning request # 1
Beginning request # 2
Beginning request # 3
Beginning request # 4
Connection reused? false
Connection reused? true
Connection reused? true
Connection reused? true
Connection reused? true
Got response for #3 using HTTP/2.0
Got response for #2 using HTTP/2.0
Got response for #5 using HTTP/2.0
Got response for #4 using HTTP/2.0
Got response for #1 using HTTP/2.0
… 这正是我想要的。我的连接在后续所有请求中都被重用了。
要实现单TCP连接发送多请求,需要使用HTTP/2的连接复用特性。你的代码已经接近正确,但需要确保服务器支持HTTP/2,并且请求要复用同一个连接。以下是完整的示例:
package main
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"time"
"golang.org/x/net/http2"
)
func main() {
// 创建支持HTTP/2的Transport
transport := &http2.Transport{
AllowHTTP: true, // 允许HTTP(非TLS)连接
DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
// 使用普通TCP连接(非TLS)
return net.Dial(network, addr)
},
}
// 创建HTTP客户端
client := &http.Client{
Transport: transport,
Timeout: 10 * time.Second,
}
// 创建多个请求
req1, _ := http.NewRequest("GET", "http://localhost:8080/api/1", nil)
req2, _ := http.NewRequest("GET", "http://localhost:8080/api/2", nil)
req3, _ := http.NewRequest("POST", "http://localhost:8080/api/3", nil)
// 使用相同的客户端发送请求(会自动复用连接)
var wg sync.WaitGroup
wg.Add(3)
go func() {
defer wg.Done()
resp, err := client.Do(req1)
if err != nil {
fmt.Printf("Request 1 error: %v\n", err)
return
}
defer resp.Body.Close()
fmt.Println("Request 1 completed")
}()
go func() {
defer wg.Done()
resp, err := client.Do(req2)
if err != nil {
fmt.Printf("Request 2 error: %v\n", err)
return
}
defer resp.Body.Close()
fmt.Println("Request 2 completed")
}()
go func() {
defer wg.Done()
resp, err := client.Do(req3)
if err != nil {
fmt.Printf("Request 3 error: %v\n", err)
return
}
defer resp.Body.Close()
fmt.Println("Request 3 completed")
}()
wg.Wait()
}
如果你需要更精确地控制连接复用,可以使用http2.Transport的DialTLSContext方法:
transport := &http2.Transport{
AllowHTTP: true,
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
dialer := &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}
return dialer.DialContext(ctx, network, addr)
},
}
要验证是否真正复用了连接,可以添加连接统计:
// 添加连接状态监控
var connectionCount int32
transport = &http2.Transport{
AllowHTTP: true,
DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
atomic.AddInt32(&connectionCount, 1)
fmt.Printf("Creating new connection #%d to %s\n", connectionCount, addr)
return net.Dial(network, addr)
},
}
确保服务器端也支持HTTP/2。对于Go的标准HTTP服务器,需要这样启用:
server := &http.Server{
Addr: ":8080",
Handler: yourHandler,
}
// 启用HTTP/2支持
http2.ConfigureServer(server, &http2.Server{})
server.ListenAndServe()
关键点:
- 所有请求使用同一个
http.Client实例 - 目标服务器必须支持HTTP/2
- 并发请求会自动复用连接
- 连接复用由HTTP/2协议层自动处理

