有关[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显示了双方tcp连接记录。
实际建立了一个tcp连接,tcp连接的端口是61799,与上文呼应。

使用wireshark查看localhost网卡发生的tcp连接

  • 可以看到每次http请求/响应之前均没有tcp三次握手
  • tcp每次发包后,对端需要会ACK确认包

反面教材-高能预警

go的net/http明确提出:

If the returned error is nil, the Response will contain a non-nil Body which the user is expected to close. 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.

也就是说:在http请求正常返回时,响应体将包含 non-nil 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)    或者  io.Copy(ioutil.Discard, resp.Body)  // <-- add this line
   fmt.Println("ok")
}

大多数时候,开发者会忘记 读完整个body,( defer resp.Body.Close()是刚需一般不会忘记。)

此次服务端日志如下:

上图红框显示客户端持续使用新的随机端口发起了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的抓包效果
posted @ 2021-12-03 10:45  博客猿马甲哥  阅读(534)  评论(2编辑  收藏  举报