HTTP 长连接

本文介绍 HTTP 长连接的协议、历史,TCP 长连接,和一些客户端、服务器实现。

Disclaimer: 本文所引用的资料都会给出链接,如有疑问应该去原资料验证,如果存在错误请不吝指出。

HTTP/1.0 会为每一个 HTTP 请求都建立 TCP 连接,这样显然是很低效的,所以就有人想在同一个 TCP 连接上发送多个 HTTP 请求,这样就省掉后面 TCP 连接建立的三次握手了。这就是“长连接”,英文里面也有其他的名字:HTTP Persistent Connection,HTTP Keep-Alive,或 HTTP Connection Reuse。

可以用 nc 来测试一下,这里我用 nc 连接到 httpbin.org 的服务器,然后用 tcpdump 抓包看其中的 TCP 连接情况。

用 nc 打开连接后,输入和输出如下。其中高亮的部分是我的输入,其余是服务器的输出:

可以看到我连续发送了两次 HTTP 请求,nc 都没有结束,并且还在等到我的输入,输入 ^c 之后才关闭,断开了连接。

抓包如下:

可以看到两次 HTTP 请求始终在同一个 TCP 连接上,只不过中间有很多保持心跳的 TCP 包。

HTTP/1.0 到 HTTP/1.1

在 HTTP/1.0 中,有人就尝试实现这样的长连接。实现的方式是客户端和服务器协商 Connection: Keep-Alive Header。加入浏览器支持长连接,那么就在 HTTP 请求的 Header 上添加 Connection: Keep-Alive ,如果服务器收到请求,就在 Response 中也加入这个 Header: Connection: Keep-Alive 。这样客户端再发送另一个请求的时候就会使用这个 TCP 连接,而不会新建一个。

然而,HTTP/1.0 的实现是错误的,主要的问题在代理。HTTP/1.0 的 Proxy Server 不懂这个字段,把它原样发给了后面的 Server,导致 Proxy 和 Server 之间建立了长连接,造成了一个 “Hung Connection”。有人提出一种处理的手段(RFC 7230 A.1.2),是想换一个字段来表示 Client 和 Proxy Server 之间的长连接 Proxy-Connection ,但是通常代理并不仅仅是一层,而是层层 HTTP 代理,这样依然就存在前面的问题,所以 Proxy-Connection 这个 Header 在任何情况下都用不到。

所以在 HTTP/1.0 使用长连接要小心,客户端要感知 hung 住的连接,要显示地关闭。因为 HTTP/1.0 的 Proxy 不支持长连接,所以在有 HTTP/1.0 Proxy 的情况下,不能使用长连接

然而,和 Proxy Server 的对话是长连接一个重要的使用场景,所以粗暴地规定不能对 Proxy 使用长连接是不能接受的。所以就需要一种新的机制,要满足以下的需求:1)对于旧版本的 Proxy Server ,忽略了长连接请求不会造成 Hung Connnection. 2)新版本的机制能够让 Real Server 和 Proxy Server 都能正确处理长连接。

HTTP/1.1 提出的方案(RFC2068 19.7.1)是:所有的 HTTP 连接默认都是长连接。当 Header 中添加 Connection: close 的时候,表示不保持长连接。这样可以满足上面说的需求,兼容旧版本的 Proxy Server(不会携带表示长连接的 Header)。

HTTP/1.1 的 Server 也可以跟 HTTP/1.0 的 Client 建立长连接,如果 Client 显式地携带了 Keep-Alive 的话。但是 HTTP/1.0 的客户端不支持 Chunked transfer-coding,所以必须每条信息都带上 Content-Length 表示边界。

另外除了 Connection 这个 Header,还有一个可选的 Header Keep-Alive 可以控制一些有关长连接的参数:

这个字段是可选的,只有带有参数的时候才生效。显而易见,这个 Header 必须和 Connection: Keep-Alive 一起发,毕竟它是控制长连接的,在没有长连接的情况下,这个 Header 就毫无意义。

最重要的一点,通过上面的描述你可能已经意识到了,就是 Connection: Keep-Alive 和下面要将的 Keep-Alive 这两个 Header 都是 hop-by-hop 的,就是说只对通讯的二者起作用,加入第二个人是 Proxy,那么 Proxy 和 Real Server 之间的连接是怎样的,就需要它们二者再自己商量了。

HTTP Keep-Alive Header

Hypertext Transfer Protocol (HTTP) Keep-Alive Header 这个 RFC 定义了 Keep-Alive Header 的一种形式。

为什么需要这些参数呢?

为了节省资源,Host 会选择关闭 idle 的长连接。比如一个连接很长时间没有使用,Host 会选择关闭它(显而易见)。

基于这个原因,很多客户端发送非幂等的请求的时候,会选择不复用现有的 idle 连接。理由如下,假设现在有一个非幂等请求进来,而恰好 Server 认为这个连接空闲了很长时间了,决定要关闭此连接。那么客户端收不到 HTTP 响应(也可能收不到 TCP 的 ACK),客户端就不知道这个非幂等请求到底被处理了没有。可能 Server 收到了这个请求之后又关闭的,也可能请求到达之前 Server 就关闭连接了。

所以很多客户端选择对所有非幂等请求都建立新的连接。但是每次都建立 TCP 连接,是很浪费资源的,也增加了请求的延迟。

假如 Client 知道 Server 的 timeout,客户端就可以在快要 timeout 的时候选择不使用这个已有的连接,或者发送请求来保证不超过 timeout。另外,如果客户端知道 timeout,那么在没有后续请求发送,而 timeout 时间又比较长的时候,客户端可以显示地要求 Server 关闭连接,释放资源。

Keep-Alive Header 的形式定义如下:

‘timeout’ 参数

timeout 参数代表当一个连接至少 idle 多长时间才会被关闭。它的 value 是一个代表秒数的 int 值。通常,连接上没有数据传输就被认为是 idle 的。但是不同的客户端和服务器实现对 idle 的理解不同,还受网络延迟的影响。所以建议客户端评估 idle 的时候算上网络延迟。

‘max’ 参数

max 参数表示在一个连接上客户端可以发送请求数的最大值。客户端在一个连接上请求的次数到了这个值,Server 就会关闭这个连接。

max 的 value 是一个 int 值,表示请求数。

对于客户端来说,收到了带有这个参数的 Response,就可以限制在同一个连接中发送的请求。举例说,假如客户端用一个队列来存放即将发送的请求,那么根据 max 就可以对队列分段。对于服务器来说,当服务器接收的请求达到了计数值之后就可以关闭连接。

Keep-Alive Extensions

除了这两个参数之外,也可以在 keep-alive-extension 字段中添加自定义的字段,如果服务器不理解这些字段,将忽略。

存在中间人的情况

在本文开头已经看到,HTTP 的长连接是基于 TCP 的长连接的。所以情况要复杂的多,实际情况有可能 TCP 保持长连接的时间要比 HTTP 短,HTTP 长连接 timeout 还没达到的时候,像 Net Address Translation (NAT) 这样的设备就将此 TCP 连接断开了,导致此连接不再可用。

HTTP/1.1 的中间人在转发请求的时候会直接丢掉这个 Header,因为它不需要认识这个 Header,它可以在任何时候断开连接

HTTP/1.0 的中间人,我们前面说过了,会导致错误。

此外,如果客户端(或中间人)感知到后面中间人的中间人(或中间 TCP 转发设备)的 timeout 更短的话,可能会修改这个值。Again,这个 Header 是 hop-by-hop 的。

从以下这个例子可以看出,每一端的连接都是独立协商的。

这里 Client 想要建立一个 timeout 为 10 分钟的长连接,但是 Proxy Server 只支持 120 秒,所以 Client 和 Proxy 之间的长连接最终是 120 秒的 timeout。Proxy 想和 Real Server 建立 1200 timeout 的长连接,但是 Real Server 将其降低到 300s。(PS:上图中 120 这个数字在 RFC 中写的是 5000,这样的话跟 RFC 的解释就冲突了,我不是很理解,我觉得 RFC 这里这个数字可能写错了。如果我理解错了,请指出)

如果 Upgraded HTTP Connections 的情况,就更复杂一些。如果 upgraded 的协议没有指明 timeout,那么会继承长连接初始化时候的 timeout,max 这个参数没有意义,因为升级后的 request 和 subrequest 被视为一个 HTTP 请求。客户端、中间人、服务器对 Upgraded 的策略可能不同,但是这个长连接的各个参数不再像上面一样可以分别独立协商,而是从 Client 到 Proxy 到 Real Server 的连接属性是一样的。

由于 keepalive 这个词被用在很多地方,而意义不尽相同,下面介绍一下 TCP 的 keepalive。

TCP Keepalive

对于 TCP 来说,Keepalive 并不是标准 TCP 协议规定的,所以 TCP 本身并不知道这个东西的存在,这只是在 TCP 之上的一个实现。

简单说,TCP Keepalive 就是设置一个 timer,时间一到就发送一个 probe packet,并设置 ACK。如果对方发送回来一个 ACK,那么那就知道这个 TCP 连接依然是可用的。如果对方没有发送 ACK 回来,那么就知道这个连接已经被断开了(实际的实现,一般会在收不到请求 ACK 的情况下重复发送多次 probe packet)。以为 TCP 是面向流的连接,而不是面向包的,所以在这个“流”中插入一个长度为 0 的包不会对这个流的内容造成任何影响。

那这个 TCP 的 Keepalive 有什么用呢?

主要有两个,第一是检查连接是否可用。假设 TCP 的另一端断电了,或者中间的某一个转发的设备断电了,那么通过检查连接的可用性,就可以确保不会在一个已经不能用的连接上发消息,不会有 false-positive。

第二是可以起到类似心跳的作用,防火墙或 NAT 设备的内存只能保存有限的连接数,它们普遍采用的策略是保留最近用到的连接,丢弃最旧没有有消息的连接。通过 Keepalive 的机制,我们可以让 NAT 设备保持我们的连接在可用列表中。

对比一下:HTTP 的 Keep-Alive Header 做的是设置长连接的 idle 时间,超过了这个时间就关闭连接,TCP Keepalive 设置的 timer 到了就发送空的 probe packet。

最后再提一个 Nginx 里面的 keepalive 指令,这个指令就更加迷惑了,跟上面介绍的都不搭边。它表示的是:Nginx 与 upstream 之间保持最多多少个 idle 的长连接。这个 idle 很关键,比如 keepalive 100 ,那么收到 300 个请求的时候,Nginx 和 upstream 建立 300 个长连接,这 300 个请求结束后,又来了 50 个请求,那么只有 50 个长连接是工作的,idle 的连接就有 250 个,Nginx 会关闭 150 个。更具体的例子可以看这里

 

参考资料:

Leave a comment

电子邮件地址不会被公开。 必填项已用*标注