Go HTTP 重用底層 TCP 連線需要注意的關鍵點
前言
在寫這篇文章之前,我在社群搜尋了一下,找到了一個相關的帖子 can't assign requested address 錯誤解決,還是 @astaxie 自己寫的。當然這裡我之所以重複再寫一個新帖子,是希望給大家提供一種新的驗證的方式。
問題
有一次我在看某個專案(可能是 kafka 吧,記不清楚了)的原始碼的時候,我發現它的註釋裡面特別提到一句話,說是要讀取完 http.Response
的 Body
並關閉它,否則不會重用底層的 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.Response
的 Body
。為什麼這麼說呢?
在常見的 API 開發的業務邏輯中,我們會定義一個 JSON 的物件來反序列化 http.Response
的 Body
,但是通常在反序列化這個回覆之前,我們會做一些 http 的 StatusCode
檢查,比如當 StatusCode
為 200
的時候,我們才去讀取 http.Response
的 Body
,如果不是 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.Response
的 Body
丟棄掉就可以了。
原因
在 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.Response
的 Body
讀取完畢並且關閉後,才會重用底層的 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()
}
}
未重用 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()
}
}
小結
學無止境,小心翼翼。
備份連結:Go HTTP 重用底層 TCP 連線需要注意的關鍵點
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- Http 和TCP的關係,TCP長連線和短連線有什麼區別?HTTPTCP
- TCP連線注意事項TCP
- 專案管理中需要注意的四個關鍵控制點專案管理
- 開發者談設計遊戲時需要注意的7個關鍵點遊戲
- 關於資料連結底層
- Go 需要注意的坑Go
- TCP連線的關鍵之謎:揭秘三次握手的必要性TCP
- 4個實驗,徹底搞懂TCP連線的斷開TCP
- tcp 連線TCP
- TCP的連線建立TCP
- Go 1.16 中關於 go get 和 go install 你需要注意的地方Go
- 圖解Go的channel底層原理圖解Go
- 關於檢測TCP連線斷開TCP
- 卷積層和全連線層之間的關係卷積
- 網路分層TCP/IP 與HTTPTCPHTTP
- TCP 連線管理TCP
- 挑選好用的HTTP代理需要關注的三點HTTP
- 不為人知的網路程式設計(十一):從底層入手,深度分析TCP連線耗時的祕密程式設計TCP
- 使用HTTP需要注意什麼?HTTP
- 有關伺服器升級需要注意的點伺服器
- http的長連線和短連線HTTP
- 分享swoole/go底層內容Go
- Go語言map的底層實現Go
- tcp的半連線攻擊和全連線攻擊--TCP DEFER ACCEPTTCP
- Socket連線和Http連線HTTP
- 什麼是Socket連線?它與TCP連線有什麼關係TCP
- TCP/Socket/HTTP的簡單關係TCPHTTP
- 檢視http的併發請求數與其TCP連線狀態HTTPTCP
- TCP連線的坑總結TCP
- golang tcp連線池GolangTCP
- Go之底層利器-AST遍歷GoAST
- Go語言interface底層實現Go
- netty系列之:讓TCP連線快一點,再快一點NettyTCP
- 區分socket連線和tcp/ip連線TCP
- HTTP 持久連線HTTP
- HTTP長連線HTTP
- HTTP連線池HTTP
- 下一代HTTP底層協議將棄用TCP協議 改用QUIC技術HTTP協議TCPUI