上文中我的结论是: HTTP Keep-Alive 是在应用层对TCP连接进行滑动续约复用, 如果客户端/服务器稳定续约,就成了名副其实的长连接。
目前所有的Http网络库都默认开启了HTTP Keep-Alive,今天我们从底层TCP连接和排障角度撕碎HTTP持久连接。
“我只是一个写web程序的猿,我为什么要知道这么多😂😂😂”。
使用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连接,系统会按照一定规则给你分配随机端口。
启动服务器程序,浏览器访问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连接:
客户端系统中针对 服务端也只建立了一个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
失效,也会导致goroutine泄露。
// 下面的代码没有读完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请求/响应 前后均发生了三次握手、四次挥手。
全文梳理
- 目前已知的httpclient、httpServer均默认开启keep-alive
- 禁用keep-alive或者keep-alive失效,会导致特定场景客户端频繁建立tcp连接, 可通过 netstat -an | grep {ip} 查看客户机上建立的tcp连接
- Wireshark抓包, 明确keep-alive和非Keep-alive的抓包效果