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、开启抓包
指定要抓的域名和网卡。
step2、开始运行测试代码
运行上述的测试代码。
step3、结束抓包,分析抓到的网络包
想要得知——抓到的网络包到底开启几条TCP连接——的方式有很多种。
我们采用最简单的一种方式,在Wireshark面板上依次点击Statistics->Conversations
我们请求了
10次http://www.baidu.com,结果居然开启了10条TCP连接。
到底发生了什么?
(此时可以重新试验,你会发现,请求20次就会有20个TCP连接,这绝不是偶然)
Dive into
难道我们开启的是HTTP短连接?
此时,我们直接通过Wireshark查看请求详情即可。
绝不是主动开启了短连接。
因为使用的是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
-
📚《100 Go Mistakes》
-
📚《Practical Go: Building Scalable Network and Non-Network Applications》