有關(guān)Http持久連接的一切,卷給你看
上文中我的結(jié)論是: HTTP Keep-Alive 是在應(yīng)用層對TCP連接進行滑動續(xù)約復(fù)用, 如果客戶端/服務(wù)器穩(wěn)定續(xù)約,就成了名副其實的長連接。
目前所有的Http網(wǎng)絡(luò)庫都默認開啟了HTTP Keep-Alive,今天我們從底層TCP連接和排障角度撕碎HTTP持久連接。
“我只是一個寫web程序的猿,我為什么要知道這么多??????”。
使用go語言倒騰一個httpServer/httpClient,粗略聊一聊go的使用風格。
使用go語言net/http包快速搭建httpserver,注入用于記錄請求日志的Handler
- package main
 - import (
 - "fmt"
 - "log"
 - "net/http"
 - )
 - // IndexHandler記錄請求的基本信息: 請關(guān)注r.RemoteAddr
 - func Index(w http.ResponseWriter, r *http.Request) {
 - fmt.Println("receive a request from:", r.RemoteAddr, r.Header)
 - w.Write([]byte("ok"))
 - }
 - // net/http 默認開啟持久連接
 - func main() {
 - fmt.Printf("Starting server at port 8081\n")
 - if err := http.ListenAndServe(":8081", http.HandlerFunc(Index)); err != nil {
 - log.Fatal(err)
 - }
 - }
 
ListenAndServe創(chuàng)建了默認的httpServer服務(wù)器,go通過首字母大小寫來控制訪問權(quán)限,如果首字母大寫,則可以被外部包訪問, 類比C#全局函數(shù)、靜態(tài)函數(shù)。
- func ListenAndServe(addr string, handler Handler) error {
 - server := &Server{Addr: addr, Handler: handler}
 - return server.ListenAndServe()
 - }
 
net/http服務(wù)器默認開啟了Keep-Alive, 由Server的私有變量disableKeepAlives體現(xiàn)。
- type Server struct {
 - ...
 - disableKeepAlives int32 // accessed atomically.
 - ...
 - }
 
使用者也可以手動關(guān)閉Keep-Alive, SetKeepAlivesEnabled()會修改私有變量disableKeepAlives的值
- s := &http.Server{
 - Addr: ":8081",
 - Handler: http.HandlerFunc(Index),
 - ReadTimeout: 10 * time.Second,
 - WriteTimeout: 10 * time.Second,
 - MaxHeaderBytes: 1 << 20,
 - }
 - s.SetKeepAlivesEnabled(true)
 - if err := s.ListenAndServe(); err != nil {
 - log.Fatal(err)
 - }
 
以上也是go語言包的基本制作/使用風格。
請注意我在httpserver插入了IndexHander,記錄httpclient的基本信息。
這里有個知識點:如果httpclient建立新的TCP連接,系統(tǒng)會按照一定規(guī)則給你分配隨機端口。
啟動服務(wù)器程序,瀏覽器訪問localhost:8081,
服務(wù)器會收到如下日志, 圖中紅圈處表明瀏覽器使用了系統(tǒng)隨機的固定端口建立tcp連接。
使用net/http編寫客戶端:間隔1s向服務(wù)器發(fā)起HTTP請求
- package main
 - import (
 - "fmt"
 - "io/ioutil"
 - "log"
 - "net/http"
 - "time"
 - )
 - func main() {
 - client := &http.Client{
 - Timeout: 10 * time.Second,
 - }
 - for {
 - requestWithClose(client)
 - time.Sleep(time.Second * 1)
 - }
 - }
 - func requestWithClose(client *http.Client) {
 - resp, err := client.Get("http://127.0.0.1:8081")
 - if err != nil {
 - fmt.Printf("error occurred while fetching page, error: %s", err.Error())
 - return
 - }
 - defer resp.Body.Close()
 - c, err := ioutil.ReadAll(resp.Body)
 - if err != nil {
 - log.Fatalf("Couldn't parse response body. %+v", err)
 - }
 - fmt.Println(string(c))
 - }
 
服務(wù)器收到的請求日志如下:
圖中紅框顯示httpclient使用固定端口61799發(fā)起了http請求,客戶端/服務(wù)器維持了HTTP Keep-alive。
使用netstat -an | grep 127.0.0.1:8081可圍觀系統(tǒng)針對特定ip的TCP連接:客戶端系統(tǒng)中針對 服務(wù)端也只建立了一個tcp連接,tcp連接的端口是61799,與上文呼應(yīng)。
使用Wireshark查看localhost網(wǎng)卡發(fā)生的tcp連接
可以看到每次http請求/響應(yīng)之前均沒有tcp三次握手
tcp每次發(fā)包后,對端需要回ACK確認包
反面教材-高能預(yù)警
go的net/http明確提出:
If the Body is not both read to EOF and closed, the Client's underlying RoundTripper (typically Transport) may not be able to re-use a persistent TCP connection to the server for a subsequent "keep-alive" request.
也就是說:httpclient客戶端在每次請求結(jié)束后,如果不讀完body或者沒有關(guān)閉body, 可能會導(dǎo)致Keep-alive失效,也會導(dǎo)致goroutine泄露。
- // 下面的代碼沒有讀完body,導(dǎo)致Keep-alive失效
 - func requestWithClose(client *http.Client) {
 - resp, err := client.Get("http://127.0.0.1:8081")
 - if err != nil {
 - fmt.Printf("error occurred while fetching page, error: %s", err.Error())
 - return
 - }
 - defer resp.Body.Close()
 - //_, err = ioutil.ReadAll(resp.Body)
 - fmt.Println("ok")
 - }
 
此次服務(wù)端日志如下:
上圖紅框顯示客戶端持續(xù)使用新的隨機端口建立了TCP連接。
查看客戶端系統(tǒng)建立的tcp連接:
Wireshark抓包結(jié)果:
圖中紅框顯示每次HTTP請求/響應(yīng) 前后均發(fā)生了三次握手、四次揮手。
全文梳理
目前已知的httpclient、httpServer均默認開啟keep-alive
禁用keep-alive或者keep-alive失效,會導(dǎo)致客戶端、服務(wù)器頻繁建立tcp連接, 可通過 netstat -an | grep {ip} 查看客戶機上建立的tcp連接
Wireshark抓包, 明確keep-alive和非Keep-alive的抓包效果






















 
 
 







 
 
 
 