小谈Go中易出错的点:2、不能复用的TCP链接

280 阅读4分钟

Background

HTTP/1.1以前,HTTP默认使用短连接发送请求。这意味着每次发送HTTP请求,都需要建立一条新的TCP连接。

This model is the default model used in HTTP/1.0 (if there is no Connection header, or if its value is set to close). In HTTP/1.1, this model is only used when the Connection header is sent with a value of close.

但自HTTP/1.1后,默认开启长连接,即我们可以在一个TCP连接上,多次发送HTTP请求和响应。提高了资源利用率,将三次握手、四次挥手的成本平摊到多次的请求中。

总结:使用HTTP/1.1的请求中,如果不是显式的设置Connection: Close,那么默认采用长连接。

Question

假如我们需要请求一个地址,但不关心返回结果,只要响应的status code = 200,我们就认为成功。

下面的代码有什么问题?

func TestRequestWithoutReadRespBody(t *testing.T) {
    url := "http://www.baidu.com"
    for i := 0; i < 10; i++ {
        resp, err := http.Get(url)
        assert.Nil(t, err)
        resp.Body.Close()
        assert.Equal(t, http.StatusOK, resp.StatusCode)
    }
}

因为我们的目标url一直都是http://www.baidu.com,所以我们预期这些请求会通过一条tcp连接发送出去。

果真如此吗?

Check

我采用Wireshark抓包的方式进行验证。

step1、开启抓包

image.png

指定要抓的域名和网卡。

step2、开始运行测试代码

运行上述的测试代码。

step3、结束抓包,分析抓到的网络包

想要得知——抓到的网络包到底开启几条TCP连接——的方式有很多种。

我们采用最简单的一种方式,在Wireshark面板上依次点击Statistics->Conversations

image.png 我们请求了10http://www.baidu.com,结果居然开启了10TCP连接。

到底发生了什么?

(此时可以重新试验,你会发现,请求20次就会有20TCP连接,这绝不是偶然)

Dive into

难道我们开启的是HTTP短连接?

此时,我们直接通过Wireshark查看请求详情即可。

image.png

绝不是主动开启了短连接。

因为使用的是HTTP/1.1协议,并且请求Header没有主动关闭的字样。

Connection: close

那是为何?

官方文档已经进行了标注,具体的实现可自寻查找代码。

// 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.

Reuse and Not Reuse

按照文档所说,当我们主动全部读取body的内容,就可以复用底层的TCP连接。

我们来验证它,验证的方式除了上述的wireshark抓包,其实还有一种“内部指示器法”。

如下述代码所示:

func createHTTPGetRequestWithTrace(ctx context.Context, url string) (*http.Request, error) {
    trace := &httptrace.ClientTrace{
    // httptrace.GotConnInfo's filed Reused represents the
    // connection has been previously used for another HTTP request
    GotConn: func(info httptrace.GotConnInfo) {
        log.Printf("Got Conn :%+v\n", info)
        },
    }

    ctxTrace := httptrace.WithClientTrace(ctx, trace)
    req, err := http.NewRequestWithContext(ctxTrace, http.MethodGet, url, nil)
    if err != nil {
        return nil, err
    }
    return req, err
}

t.Run("request with trace -- read resp", func(t *testing.T) {
    req, err := createHTTPGetRequestWithTrace(context.TODO(), url)
    assert.Nil(t, err)
    for i := 0; i < 10; i++ {
        // notice here do http request with self build `req`
        resp, err := http.DefaultClient.Do(req)
        assert.Nil(t, err)
        // read body completely
        io.ReadAll(resp.Body)
        resp.Body.Close()
        assert.Equal(t, http.StatusOK, resp.StatusCode)
    }
})

// when you run the test, you will see:
// only the first connection'Reused is false

2023/09/11 19:54:45 Got Conn :{Conn:0x14000186000 Reused:false WasIdle:false IdleTime:0s}
2023/09/11 19:54:45 Got Conn :{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:190.75µs}
2023/09/11 19:54:45 Got Conn :{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:144.416µs}
2023/09/11 19:54:46 Got Conn :{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:61.416µs}
2023/09/11 19:54:46 Got Conn :{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:36.958µs}
2023/09/11 19:54:46 Got Conn :{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:70µs}
2023/09/11 19:54:46 Got Conn :{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:52.208µs}
2023/09/11 19:54:46 Got Conn :{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:20.75µs}
2023/09/11 19:54:46 Got Conn :{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:36.792µs}
2023/09/11 19:54:46 Got Conn :{Conn:0x14000186000 Reused:true WasIdle:true IdleTime:85.959µs}

QED.

Inspiration

当我们使用Go发送HTTP网络请求时,要牢记:

  • 及时的关闭body,resp.Body.Close()

  • 为了复用底层的TCP连接,不论是否需要body的内容,都要完整的读取

完整的测试代码 http test

Reference