94

我目前正在努力寻找一种在 Go 中创建 HTTP 帖子时重用连接的方法。

我创建了一个传输和客户端,如下所示:

// Create a new transport and HTTP client
tr := &http.Transport{}
client := &http.Client{Transport: tr}

然后,我将此客户端指针传递给一个 goroutine,该 goroutine 将多个帖子发送到同一个端点,如下所示:

r, err := client.Post(url, "application/json", post)

查看 netstat 这似乎会导致每个帖子都有一个新连接,从而导致打开大量并发连接。

在这种情况下重用连接的正确方法是什么?

4

11 回答 11

118

确保您阅读,直到响应完成并调用Close()

例如

res, _ := client.Do(req)
io.Copy(ioutil.Discard, res.Body)
res.Body.Close()

再次...为确保http.Client连接重用,请务必:

  • 阅读直到响应完成(即ioutil.ReadAll(resp.Body)
  • 称呼Body.Close()
于 2013-07-30T17:27:08.147 回答
50

如果有人仍在寻找如何做的答案,这就是我正在做的事情。

package main

import (
  "bytes"
  "io/ioutil"
  "log"
  "net/http"
  "time"
)

func httpClient() *http.Client {
    client := &http.Client{
        Transport: &http.Transport{
            MaxIdleConnsPerHost: 20,
        },
        Timeout: 10 * time.Second,
    }

    return client
}

func sendRequest(client *http.Client, method string) []byte {
    endpoint := "https://httpbin.org/post"
    req, err := http.NewRequest(method, endpoint, bytes.NewBuffer([]byte("Post this data")))
    if err != nil {
        log.Fatalf("Error Occured. %+v", err)
    }

    response, err := client.Do(req)
    if err != nil {
        log.Fatalf("Error sending request to API endpoint. %+v", err)
    }

    // Close the connection to reuse it
    defer response.Body.Close()

    body, err := ioutil.ReadAll(response.Body)
    if err != nil {
        log.Fatalf("Couldn't parse response body. %+v", err)
    }

    return body
}

func main() {
    c := httpClient()
    response := sendRequest(c, http.MethodPost)
    log.Println("Response Body:", string(response))
}

去游乐场: https: //play.golang.org/p/cYWdFu0r62e

总之,我正在创建一种不同的方法来创建 HTTP 客户端并将其分配给一个变量,然后使用它来发出请求。注意

defer response.Body.Close() 

这将在函数执行结束时请求完成后关闭连接,您可以多次重用客户端。

如果要循环发送请求,请调用循环发送请求的函数。

如果您想更改客户端传输配置中的任何内容,例如添加代理配置,请在客户端配置中进行更改。

希望这会对某人有所帮助。

于 2015-06-12T16:12:30.067 回答
40

编辑:对于为每个请求构建传输和客户端的人来说,这更像是一个注释。

Edit2:更改了 godoc 的链接。

Transport是保持连接以供重用的结构;请参阅https://godoc.org/net/http#Transport(“默认情况下,Transport 缓存连接以供将来重用。”)

因此,如果您为每个请求创建一个新的传输,它每次都会创建新的连接。在这种情况下,解决方案是在客户端之间共享一个传输实例。

于 2014-10-23T21:07:36.190 回答
14

IIRC,默认客户端确实重用连接。您要关闭响应吗?

调用者在完成读取后应关闭 resp.Body。如果 resp.Body 没有关闭,客户端的底层 RoundTripper(通常是 Transport)可能无法重新使用到服务器的持久 TCP 连接来进行后续的“保持活动”请求。

于 2013-07-30T13:50:40.347 回答
5

关于身体

// It is the caller's responsibility to
// close Body. The default HTTP client's Transport may not
// reuse HTTP/1.x "keep-alive" TCP connections if the Body is
// not read to completion and closed.

所以如果你想重用 TCP 连接,你必须在每次读取完成后关闭 Body。此外,使用defer,您可以确保Body.Close()毕竟被调用。建议使用这样的函数 ReadBody(io.ReadCloser)。

package main

import (
    "fmt"
    "io"
    "io/ioutil"
    "net/http"
    "time"
)

func main() {
    req, err := http.NewRequest(http.MethodGet, "https://github.com", nil)
    if err != nil {
        fmt.Println(err.Error())
        return
    }
    client := &http.Client{}
    i := 0
    for {
        resp, err := client.Do(req)
        if err != nil {
            fmt.Println(err.Error())
            return
        }
        _, _ = readBody(resp.Body)
        fmt.Println("done ", i)
        time.Sleep(5 * time.Second)
    }
}

func readBody(readCloser io.ReadCloser) ([]byte, error) {
    defer readCloser.Close()
    body, err := ioutil.ReadAll(readCloser)
    if err != nil {
        return nil, err
    }
    return body, nil
}

并且不要像下面这样调用 Close:

res, _ := client.Do(req)
io.Copy(ioutil.Discard, res.Body) // what if io.Copy panics, res.Body.Close() will not called.
res.Body.Close()
于 2018-12-03T08:12:55.760 回答
4

另一种方法init()是使用单例方法来获取 http 客户端。通过使用 sync.Once,您可以确保您的所有请求只使用一个实例。

var (
    once              sync.Once
    netClient         *http.Client
)

func newNetClient() *http.Client {
    once.Do(func() {
        var netTransport = &http.Transport{
            Dial: (&net.Dialer{
                Timeout: 2 * time.Second,
            }).Dial,
            TLSHandshakeTimeout: 2 * time.Second,
        }
        netClient = &http.Client{
            Timeout:   time.Second * 2,
            Transport: netTransport,
        }
    })

    return netClient
}

func yourFunc(){
    URL := "local.dev"
    req, err := http.NewRequest("POST", URL, nil)
    response, err := newNetClient().Do(req)
    // ...
}

于 2019-05-13T13:08:44.267 回答
2

这里缺少的一点是“goroutine”。传输有自己的连接池,默认情况下,该池中的每个连接都被重用(如果主体被完全读取并关闭),但如果有多个 goroutine 正在发送请求,则会创建新连接(池中所有连接都处于忙碌状态,并将创建新连接) )。为了解决这个问题,您需要限制每个主机的最大连接数:(Transport.MaxConnsPerHosthttps://golang.org/src/net/http/transport.go#L205

可能您还想设置IdleConnTimeout和/或ResponseHeaderTimeout.

于 2020-08-30T01:55:53.090 回答
1

https://golang.org/src/net/http/transport.go#L196

您应该MaxConnsPerHost明确设置为您的http.Client. Transport确实重用了 TCP 连接,但你应该限制MaxConnsPerHost(默认 0 表示没有限制)。

func init() {
    // singleton http.Client
    httpClient = createHTTPClient()
}

// createHTTPClient for connection re-use
func createHTTPClient() *http.Client {
    client := &http.Client{
        Transport: &http.Transport{
            MaxConnsPerHost:     1,
            // other option field
        },
        Timeout: time.Duration(RequestTimeout) * time.Second,
    }

    return client
}
于 2020-10-21T05:58:42.287 回答
0

这是 GO http 调用非常有用的功能,您可以保持连接处于活动状态并重新连接。

    var (
        respReadLimit       = int64(4096)
    )
    
    // Try to read the response body so we can reuse this connection.
    func (c *Client) drainBody(body io.ReadCloser) error {
        defer body.Close()
        _, err := io.Copy(ioutil.Discard, io.LimitReader(body, respReadLimit))
        if err != nil {
            return err
        }
        return nil
    }
于 2021-05-10T20:03:21.070 回答
0

client.Do() 将自动处理关闭响应正文。您只需构建请求并调用 client.Do(req)

    func (c clientImpl) BackupSite(siteId)
        client := &http.Client{}
        req, err := http.NewRequest("POST", 
        fmt.Sprintf("https://%s/backup/site/%s/", 
                c.APIBaseURL, siteId), nil)
        if err != nil {
        return err
        }
        req.Header.Add("Api-Key", c.APIKey)
        resp, err := client.Do(req)
        if err != nil {
            return err
        }
        if resp.StatusCode != http.StatusOK {
            return fmt.Errorf("failed to backup site," + 
                "site: %s, status code %d", siteId, resp.StatusCode)
        }
    }

在此处阅读有关 client.Do()的更多信息

于 2021-08-19T16:13:58.513 回答
-3

有两种可能的方式:

  1. 使用在内部重用和管理与每个请求关联的文件描述符的库。Http Client 在内部做同样的事情,但是你可以控制要打开多少并发连接,以及如何管理你的资源。如果您有兴趣,请查看 netpoll 实现,该实现在内部使用 epoll/kqueue 来管理它们。

  2. 最简单的方法是,为您的 goroutines 创建一个工作池,而不是池化网络连接。这将是一个简单且更好的解决方案,不会妨碍您当前的代码库,并且需要进行微小的更改。

假设您在收到请求后需要发出 n POST 请求。

在此处输入图像描述

在此处输入图像描述

您可以使用渠道来实现这一点。

或者,您可以简单地使用第三方库。
喜欢: https ://github.com/ivpusic/grpool

于 2018-08-07T10:24:04.130 回答