在Golang中重用HTTP连接


81

我目前正在努力寻找一种在Golang中进行HTTP发布时重用连接的方法。

我已经创建了一个运输和客户,如下所示:

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

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

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

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

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


2
这个问题的正确答案发布在此副本上:Go客户端程序在TIME_WAIT状态下生成大量套接字
Brent Bradburn

Answers:


94

确保您已阅读,直到响应完成并致电Close()

例如

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

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

  • 读取直到响应完成(即ioutil.ReadAll(resp.Body)
  • 呼叫 Body.Close()

1
我要发布到同一主机。但是,我的理解是MaxIdleConnsPerHost会导致空闲连接被关闭。不是吗?
sicr

5
+1,因为我调用defer res.Body.Close()了类似的程序,但是在执行该部分之前(resp.StatusCode != 200例如if )偶尔从函数返回,结果导致许多打开的文件描述符处于空闲状态,并最终杀死了我的程序。击中该线程使我自己重新访问了一部分代码和facepalm。谢谢。
2014年

3
有趣的一点是,读取步骤似乎是必要和充分的。单独的读取步骤将使连接返回到池,但是单独的关闭将不会。连接将以TCP_WAIT结尾。也遇到了麻烦,因为我使用的是json.NewDecoder()来读取response.Body,但没有完全读取它。如果不确定,请确保包含io.Copy(ioutil.Discard,res.Body)。
山姆·罗素

2
有没有一种方法可以检查身体是否已被完全读取?是否可以ioutil.ReadAll()保证足够,还是我仍然需要io.Copy()在各处散布电话,以防万一?
Patrik Iselind '18

4
我看了看源代码,似乎响应主体Close()已经负责耗尽主体:github.com/golang/go/blob/…–
dr.scre

44

如果有人仍在寻找答案,这就是我的做法。

package main

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

var httpClient *http.Client

const (
    MaxIdleConnections int = 20
    RequestTimeout     int = 5
)

func init() {
    httpClient = createHTTPClient()
}

// createHTTPClient for connection re-use
func createHTTPClient() *http.Client {
    client := &http.Client{
        Transport: &http.Transport{
            MaxIdleConnsPerHost: MaxIdleConnections,
        },
        Timeout: time.Duration(RequestTimeout) * time.Second,
    }

    return client
}

func main() {
    endPoint := "https://localhost:8080/doSomething"

    req, err := http.NewRequest("POST", endPoint, bytes.NewBuffer([]byte("Post this data")))
    if err != nil {
        log.Fatalf("Error Occured. %+v", err)
    }
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

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

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

    // Let's check if the work actually is done
    // We have seen inconsistencies even when we get 200 OK response
    body, err := ioutil.ReadAll(response.Body)
    if err != nil {
        log.Fatalf("Couldn't parse response body. %+v", err)
    }

    log.Println("Response Body:", string(body))    
}

前往游乐场:http : //play.golang.org/p/oliqHLmzSX

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

defer response.Body.Close() 

这将关闭连接并使其准备好再次使用。

希望这会帮助某人。


1
如果有多个goroutine使用该变量来调用函数,是否将http.Client用作全局变量而免受竞争条件影响?
巴特·西尔弗斯特

3
@ bn00d是defer response.Body.Close()正确的吗?我问是因为延迟关闭实际上我们不会在主要功能退出之前关闭conn以供重用,因此应该.Close()直接在之后调用.ReadAll()。在您的示例b / c中,这似乎似乎不是问题,它实际上并没有演示制作多个req,它只是制作了一个req然后退出,但是如果我们要背对背进行多个req,那么看来从defered开始,.Close()不会称为til func exits。或者...我想念什么吗?谢谢。
mad.meesh

1
@ mad.meesh如果您进行了多次调用(例如,在循环内),只需将对Body.Close()的调用包装在闭包内,这样,一旦处理完数据,它将立即关闭。
安托万·科腾

如何以这种方式为每个请求设置不同的代理?可能吗 ?
Amir Khoshhal

@ bn00d您的示例似乎无法正常工作。添加循环后,每个请求仍将导致一个新的连接。play.golang.org/p/9Ah_lyfYxgV
Lewis Chan

37

编辑:对于为每个请求构造运输和客户的人员,这更是一个注释。

Edit2:已更改为godoc的链接。

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

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


请使用特定提交提供链接。您的链接不再正确。
伊南克·古姆斯

play.golang.org/p/9Ah_lyfYxgV此示例仅显示一种传输方式,但仍会为每个请求生成一个连接。这是为什么 ?
刘易斯·陈

12

IIRC,默认客户端重用连接。您是否正在关闭响应

读取完后,呼叫者应关闭res.Body。如果未关闭resp.Body,则客户端的基础RoundTripper(通常是Transport)可能无法将与服务器的持久TCP连接重新用于后续的“保持活动”请求。


嗨,谢谢您的回复。是的,对不起,我也应该包括在内。我正在用r.Body.Close()关闭连接。
sicr

@sicr,您确定服务器实际上并未关闭连接本身吗?我的意思是,这些杰出的联系可能在某个*_WAIT州或类似州
kostix

1
@kostix在查看netstat时,我看到状态为ESTABLISHED的大量连接。似乎在每个POST请求中都产生了一个新连接,而不是重复使用同一连接。
sicr

@sicr,您找到了有关连接重用的解决方案吗?非常感谢Daniele
Daniele B

3

关于身体

// 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。建议使用函数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
}

2

另一种方法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)
    // ...
}


这非常适合我每秒处理100个HTTP请求
philip mudenyo

0

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

可能您也想设置IdleConnTimeout和/或ResponseHeaderTimeout


0

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

您应该MaxConnsPerHost明确设置为http.ClientTransport确实重用了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
}

-3

有两种可能的方法:

  1. 使用一个库,该库在内部重用和管理与每个请求关联的文件描述符。Http Client在内部执行相同的操作,但是您可以控制要打开的并发连接数以及如何管理资源。如果您有兴趣,请查看netpoll实现,该实现在内部使用epoll / kqueue对其进行管理。

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

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

在此处输入图片说明

在此处输入图片说明

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.