时光
发布于

Go HTTP 重用底层 TCP 连接需要注意的关键点

前言

在写这篇文章之前,我在社区搜索了一下,找到了一个相关的帖子 can't assign requested address 错误解决,还是 *@*astaxie 自己写的。当然这里我之所以重复再写一个新帖子,是希望给大家提供一种新的验证的方式。

问题

有一次我在看某个项目(可能是 kafka 吧,记不清楚了)的源码的时候,我发现它的注释里面特别提到一句话,说是要读取完 http.ResponseBody 并关闭它,否则不会重用底层的 TCP 连接。我想了想为什么它这里一定要特别提出来呢?关闭 http.Response 不是一个常识性动作么?比如一般写代码我们都会遵循下面的模式:

resp, err := http.Get("http://www.example.com")
if err != nil {
    return err
}
defer resp.Body.Close()

respBody, err := ioutil.ReadAll(resp.Body)
// ...

在结合实际的场景之后,我发现其实有的时候问题出在 我们并不总是 会去读取完整个 http.ResponseBody。为什么这么说呢?

在常见的 API 开发的业务逻辑中,我们会定义一个 JSON 的对象来反序列化 http.ResponseBody,但是通常在反序列化这个回复之前,我们会做一些 http 的 StatusCode 检查,比如当 StatusCode200 的时候,我们才去读取 http.ResponseBody,如果不是 200,我们就直接返回一个包装好的错误。比如下面的模式:

resp, err := http.Get("http://www.example.com")
if err != nil {
    return err
}
defer resp.Body.Close()

if resp.StatusCode == http.StatusOK {
    var apiRet APIRet
    decoder := json.NewDecoder(resp.Body)
    err := decoder.Decode(&apiRet)
    // ...
}

如果代码是按照上面的这种方式写的话,那么在请求异常的时候,会导致大量的底层 TCP 无法重用,所以我们稍微改进下就可以了。

resp, err := http.Get("http://www.example.com")
if err != nil {
    return err
}
defer resp.Body.Close()

if resp.StatusCode == http.StatusOK {
    var apiRet APIRet
    decoder := json.NewDecoder(resp.Body)
    err := decoder.Decode(&apiRet)
    // ...
}else{
    io.Copy(ioutil.Discard, resp.Body)
    // ...
}

我们通过直接将 http.ResponseBody 丢弃掉就可以了。

原因

在 Go 的源码中,关于这个问题有特别的注释。

// Body represents the response body.
//
// The response body is streamed on demand as the Body field
// is read. If the network connection fails or the server
// terminates the response, Body.Read calls return an error.
//
// The http Client and Transport guarantee that Body is always
// non-nil, even on responses without a body or responses with
// a zero-length body. 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.
//
// The Body is automatically dechunked if the server replied
// with a "chunked" Transfer-Encoding.
//
// As of Go 1.12, the Body will also implement io.Writer
// on a successful "101 Switching Protocols" response,
// as used by WebSockets and HTTP/2's "h2c" mode.
Body io.ReadCloser

其中提到了必须将 http.ResponseBody 读取完毕并且关闭后,才会重用底层的 TCP 连接。

实验

为了验证一把上面的问题,我们写了一个简单的对比实验,并且通过 Wireshark 抓包分析了一下。这里使用的是 https://www.oschina.net 作为例子,由于这个站点用的是 HTTPS,所以重用了 TCP 的话,那么一次建立 TLS 连接后面就不用重建了,非常方便观察。

重用了 TCP 连接

package main

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

func main() {
    count := 100
    for i := 0; i < count; i++ {
        resp, err := http.Get("https://www.oschina.net")
        if err != nil {
            panic(err)
        }

        io.Copy(ioutil.Discard, resp.Body)
        resp.Body.Close()
    }
}

[

](https://static.gocn.vip/photo/2020/487cd064-22ec-4fe8-82d5-eda2f14349ed.png?x-oss-process=image/resize,w_1920)

未重用 TCP 连接

package main

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

func main() {
    count := 100
    for i := 0; i < count; i++ {
        resp, err := http.Get("https://www.oschina.net")
        if err != nil {
            panic(err)
        }

        //io.Copy(ioutil.Discard, resp.Body)
        resp.Body.Close()
    }
}

[

](https://static.gocn.vip/photo/2020/47292c7f-64f9-4b7e-b044-8c0a4cfeecad.png?x-oss-process=image/resize,w_1920)

小结

学无止境,小心翼翼。

备份链接:Go HTTP 重用底层 TCP 连接需要注意的关键点

评论