在Golang中重用http连接

sic*_*icr 65 go

我目前正在努力寻找在Golang中发布HTTP帖子时重用连接的方法.

我创建了一个像这样的传输和客户端:

// Create a new transport and HTTP client
tr := &http.Transport{}
client := &http.Client{Transport: tr}
Run Code Online (Sandbox Code Playgroud)

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

r, err := client.Post(url, "application/json", post)
Run Code Online (Sandbox Code Playgroud)

查看netstat,这似乎导致每个帖子的新连接导致大量并发连接被打开.

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

Mat*_*elf 83

在调用之前,您应该确保阅读完毕,直到响应完成Close().

例如

res, _ := client.Do(req)
io.Copy(ioutil.Discard, res.Body)
res.Body.Close()
Run Code Online (Sandbox Code Playgroud)

要确保http.Client连接重用,请务必执行以下两项操作:

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

  • +1,因为我在一个类似的程序中调用了'defer res.Body.Close()`,但是在执行该部分之前偶尔会从函数返回(例如,如果`resp.StatusCode!= 200`),左******打开文件描述符空闲并最终杀死我的程序.点击这个帖子让我重新审视了代码的一部分,并自己进行了表面处理.谢谢. (5认同)
  • 有没有办法检查正文是否已完全读取?`ioutil.ReadAll()` 是否保证就足够了,还是我仍然需要在所有地方撒上 `io.Copy()` 调用,以防万一? (3认同)
  • 我看了看源代码,似乎响应主体Close()已经负责耗尽主体的内容:https://github.com/golang/go/blob/9d23975d89e6cc3df4f2156b2ae0df5d2cef16fb/src/net/http/transfer.go#L979 (3认同)
  • 一个有趣的说明是阅读步骤似乎是必要和充分的.仅读取步骤将返回到池的连接,但单独关闭不会; 连接最终将在TCP_WAIT中.也遇到了麻烦,因为我正在使用json.NewDecoder()来读取response.Body,它没有完全读取它.如果您不确定,请确保包含io.Copy(ioutil.Discard,res.Body). (2认同)

DrJ*_*000 36

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

Edit2:更改了godoc的链接.

Transport是用于重用连接的结构; 请参阅https://godoc.org/net/http#Transport ("默认情况下,传输缓存连接以供将来重复使用.")

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


bn0*_*00d 36

如果有人仍在寻找如何做到这一点的答案,那么我就是这样做的.

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))    
}
Run Code Online (Sandbox Code Playgroud)

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

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

defer response.Body.Close() 
Run Code Online (Sandbox Code Playgroud)

这将关闭连接并将其设置为可以再次使用.

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

  • @ bn00d是`defer response.Body.Close()`正确吗?我问因为通过延迟关闭我们实际上不会关闭conn以便重用直到main函数退出,因此应该在`.ReadAll()`之后直接调用`.Close()`.这似乎不是你的例子中的一个问题b/c它实际上并没有展示制作多个req,它只是制作一个req然后退出但是如果我们要做几个req背靠背,它似乎是因为`defer `ed,`..Close()`不会被称为直接func退出.或者......我错过了什么?谢谢. (3认同)
  • 如果有多个 goroutine 使用该变量调用函数,是否使用 http.Client 作为全局变量可以避免竞争条件? (2认同)

zzz*_*zzz 11

IIRC,默认客户端确实重用连接.你是在结束回复吗?

呼叫者在完成阅读后应该关闭resp.Body.如果resp.Body未关闭,则客户端的基础RoundTripper(通常为Transport)可能无法重新使用到服务器的持久TCP连接以用于后续"保持活动"请求.


Bil*_*uan 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.
Run Code Online (Sandbox Code Playgroud)

所以如果要复用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
}
Run Code Online (Sandbox Code Playgroud)

并且不要像下面这样调用 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()
Run Code Online (Sandbox Code Playgroud)