使用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

12 回复

你好,@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,这可能相关,因为你正在使用非零的 DialTLSTLSClientConfig。除此之外,请验证服务器是否支持 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 - The Go Programming Language

这看起来像是连接建立时的“惊群”竞争。所以没有连接可以复用,因为此时还没有任何一个连接建立完成。

尝试先完成一个单独的请求,然后再并行运行其余的请求。

所以,请看一下上面的 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.TransportDialTLSContext方法:

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()

关键点:

  1. 所有请求使用同一个http.Client实例
  2. 目标服务器必须支持HTTP/2
  3. 并发请求会自动复用连接
  4. 连接复用由HTTP/2协议层自动处理
回到顶部