有關[Http持久連接]的一切,撕碎給你看

上文中我的結論是: HTTP Keep-Alive 是在應用層對TCP連接進行滑動續約複用, 如果客戶端/服務器穩定續約,就成了名副其實的長連接。

目前所有的Http網絡庫都默認開啓了HTTP Keep-Alive,今天我們從底層TCP連接和排障角度撕碎HTTP持久連接。

使用go語言倒騰一個httpServer/httpClient,粗略聊一聊go的使用風格。


使用go語言net/http包快速搭建httpserver,注入用於記錄請求日誌的Handler

package main

import (
	"fmt"
	"log"
	"net/http"
)

// IndexHandler記錄請求的基本信息: 請關注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)
	}
}
  1. ListenAndServe創建了默認的httpServer服務器,go通過首字母大小寫來控制訪問權限,如果首字母大寫,則可以被外部包訪問, 類比C#全局函數、靜態函數。
func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}
  1. net/http服務器默認開啓了Keep-Alive, 由Server的私有變量disableKeepAlives體現。
type  Server  struct {
  ...
  disableKeepAlives int32     // accessed atomically. 
  ...
}

使用者也可以手動關閉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包的基本製作/使用風格。

  1. 請注意我在httpserver插入了IndexHander,記錄httpclient的基本信息。
    這裏有個知識點: 如果httpclient使用了新的TCP連接,系統會按照一定規則給你分配隨機端口。

go run main.go 啓動之後,瀏覽器訪問localhost:8081,
服務器會收到如下日誌, 圖中紅圈處表明瀏覽器使用了系統隨機端口開啓tcp連接,


使用net/http編寫客戶端: 間隔1s向服務器發起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))
}

服務器收到的請求日誌如下:


圖中紅框顯示httpclient使用固定端口61799發起了http請求,客戶端/服務器維持了HTTP Keep-alive。

使用netstat -an | grep 127.0.0.1:8081可圍觀系統針對特定ip的TCP連接:

**2條記錄,但是由於客戶端/服務器在一臺機器上, 所以netstat顯示2條記錄。

實際表示建立了一個tcp連接,tcp連接的端口是61799,與上文呼應。**

使用wireshark查看localhost網卡發生的tcp連接

  • 可以看到每次http請求/響應之前均沒有tcp三次握手
  • tcp每次發包後,對端需要會ACK確認包

反面教材-高能預警

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客戶端在每次請求結束後,如果resp的body部位空,而客戶端不讀完body或者沒有關閉body, 可能會導致Keep-alive失效

//  下面的代碼沒有讀完body,導致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")
}

此次服務端日誌如下:

上圖紅框顯示客戶端持續使用新的隨機端口發起了TCP連接。

查看系統建立的tcp連接:

Wireshark抓包結果:

圖中紅框顯示每次HTTP請求/響應 前後均發生了三次握手、四次揮手。

全文梳理

  1. 目前已知的httpclient、httpServer均默認開啓keep-alive
  2. 禁用keep-alive或者keep-alive失效,會導致客戶端、服務器頻繁建立tcp連接, 可通過 netstat -an | grep {ip} 查看客戶機上被佔用的tcp連接端口
  3. Wireshark抓包, 明確keep-alive和非Keep-alive的抓包效果
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章