有關[Http持久連接]的一切,撕碎給你看
- 2021 年 12 月 3 日
- 筆記
上文中我的結論是: 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)
}
}
ListenAndServe
創建了默認的httpServer伺服器,go通過首字母大小寫來控制訪問許可權,如果首字母大寫,則可以被外部包訪問, 類比C#全局函數、靜態函數。
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
- 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包的基本製作/使用風格。
- 請注意我在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("//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連接:
客戶端系統中針對 服務端也只建立了一個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客戶端在每次請求結束後,如果不讀完body或者沒有關閉body, 可能會導致Keep-alive
失效。
// 下面的程式碼沒有讀完body,導致Keep-alive失效
func requestWithClose(client *http.Client) {
resp, err := client.Get("//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請求/響應 前後均發生了三次握手、四次揮手。
全文梳理
- 目前已知的httpclient、httpServer均默認開啟keep-alive
- 禁用keep-alive或者keep-alive失效,會導致客戶端、伺服器頻繁建立tcp連接, 可通過 netstat -an | grep {ip} 查看客戶機上被佔用的tcp連接埠
- Wireshark抓包, 明確keep-alive和非Keep-alive的抓包效果