0%

聊聊HTTP协议的keep-alive

介绍

HTTP 协议里的keep-alive机制和长连接协议的keep-alive机制有所不同,HTTP 中的作用是为了复用 TCP 连接,而长连接中大多数作用是为了保活,例如 TCP 通过 keep-alive 心跳包来检测对方是否存活。

HTTP/1.1版本中keep-alive默认是开启的,通过复用 TCP 连接,可以有效的降低 TCP 连接创建的开销,大多数浏览器只允许同时对同一个域名建立 6 个 TCP 连接。

HTTP/1.0

HTTP/1.0版本中是没有keep-alive机制的,意味着每次 HTTP 请求都会创建一个新的 TCP 连接,在响应完成后关闭当前 TCP 连接,为了验证我使用wireshark来监听网卡上的 HTTP 报文。

  1. 通过curl指定 HTTP/1.0 版本
1
curl --http1.0 http://www.baidu.com
  1. 报文截图

可以看到在服务器发送完响应之后就主动发送了FIN包来关闭连接,验证了之前的内容。

HTTP/1.1

HTTP/1.0时代,由于 TCP 连接创建成本很高,很多服务器和浏览器使用了了一套非标准的keep-alive机制,用于复用 TCP 连接,当然最后HTTP/1.1将这套东西纳入到了标准中,这个标准就是Connection头,用于客户端和服务端协商是否要复用 TCP 连接,在 HTTP/1.1 版本中默认值就是keep-alive,即保持连接。

1
Connection: keep-alive

客户端或服务器发现对方一段时间没有活动,就可以主动关闭连接。不过,规范的做法是,客户端在最后一个请求时,发送 Connection: close,明确要求服务器关闭 TCP 连接。

1
Connection: close

同样的为了验证以上内容,需要抓个包来看看。

  1. 通过curl连续访问同一地址
1
curl http://www.baidu.com http://www.baidu.com
  1. 报文截图

通过报文可以看到在建立了 TCP 连接之后,发生了两次 HTTP 请求之后由客户端发送FIN包关闭连接,说明这两次 HTTP 请求是在同一个 TCP 连接上进行的。

一些细节

HTTP/1.0版本中,服务器不用返回Content-Length响应头来标识响应体的报文长度,因为每次请求之后都会主动断开 TCP 连接,所以客户端在接收到EOF(报文结束标识:-1)时,就说明响应已经全部接收完了。

而在HTTP/1.1版本中,由于 TCP 连接的复用,服务器必须得通过Content-Length来告诉客户端响应体报文应该读到哪里,不过Content-Length需要提前知道响应体的报文长度,对应一些很耗时的动态操作来说(例如:HTTP 服务器推送),服务器要等到所有操作完成,才能发送数据,显然这样的效率不高。

对于这种情况 HTTP 协议提出了另一种Chunked编码来用于 HTTP 报文的传输,有一点数据就发送一点数据,直到数据发送完成,示例:

1
2
3
4
5
6
7
8
9
HTTP/1.1 200 OK
Content-Type: text/html
Transfer-Encoding: chunked

5
hello
4
word
0

报文格式为:

1
2
3
4
5
<length>\r\n
<data>\r\n
<length>\r\n
<data>\r\n
0\r\n

数据块长度(16进制)+\r\n+数据块+\r\n以此循环,直到数据块长度=0+\r\n结束

缺点

虽然keep-alive是复用了 TCP 连接,但是由于HTTP1/1协议是不支持并行的,如果同一个 TCP 连接上需要完成多个 HTTP 请求,那么后一个就会被前一个 HTTP 请求阻塞着,如果前一个请求响应的特别慢,那么后面的请求就会等待的越久,在许多HTTP客户端中为了避免这个问题,都会允许启用多个 TCP 连接来处理,当然在应用层也可以通过合并请求的方式来减少请求数(例如:多个小图片合成一个大图),为了解决这些问题又衍生出了HTTP/2版本,这里就不做过多描述了。

衍生

我顺带测试了下javago的 http client,发现其实它们也都实现了keep-alive
,之前从代码的上来看,一直以为是请求完之后会直接关闭 TCP 连接,要设置一个连接池之类的东西去实现,没想到底层全部实现好了。

java 测试

  • 测试代码
    使用URL库发起三个 HTTP 请求,并抓包分析。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public static void main(String[] args) throws IOException {
    for (int i = 0; i < 3; i++) {
    URL url = new URL("http://www.baidu.com");
    URLConnection urlConnection = url.openConnection();
    try (InputStream inputStream = urlConnection.getInputStream()) {
    byte[] temp = new byte[8192];
    // 空读
    while (inputStream.read(temp) != -1) {
    }
    }
    }
    }
  • 报文截图
    通过第一列的连线可以观察到是同一个 TCP 连接

go 测试

  • 测试代码
    使用http标准库来测试,发起三个 HTTP 请求并抓包分析。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    func main() {
    for i := 0; i < 3; i++ {
    resp, err := http.Get("http://www.baidu.com")
    if err != nil {
    panic(err)
    }
    // 空读,注:golang里一定要把数据读完,否则不会复用TCP连接
    io.Copy(ioutil.Discard, resp.Body)
    resp.Body.Close()
    }
    }
  • 报文截图
    结果也是一样,复用了一个 TCP 连接


我是MonkeyWie,欢迎扫码👇👇关注!不定期在公众号中分享JAVAGolang前端dockerk8s等干货知识。

如果觉得本文对您有帮助,可以请我喝一杯咖啡☕