在开发一个基于 HTTP 的 CS 架构的代码时,我们经常性会遇到的一个问题就是关于连接的维护问题。可能有的人比较飘逸,认为无所谓啦,反正不就是要用时开一个,用完就关,多好的习惯,但是,殊不知,在某些时候这却不是一个好习惯。今天要来聊一下的就是关于 HTTP 的连接管理中的一种特性:Keep-Alive。

在我的博客中,以前我介绍过现在很多浏览器和服务器都支持 HTTP/2.0 (见:HTTP/2 的简单知识),然而,在这之前已经有了很多个版本,其中值得拿出来介绍的有 1.0/1.1 和 2.0 版本。先来回顾一番关于持久连接的历史:

在 HTTP/1.0 中,默认使用的就是要用的时候就开一个连接,用完一次就关掉的默认行为,这其实也是我们所熟知的 Request-Response 模型,然而,这种模型显然缺点还是很大的,例如现在开一个网页随便就几十个资源的前提下,那么得开几十个连接,这或许还好,但是对于服务端来说,当稍微并发数量上来之后,那么如果你出现了多次请求,光是 TCP 三次握手和四次挥手就占了不少的资源了。于是乎,就催生出了是不是可以对于同一个 C-S 的组合上共用同一条连接的想法,事实上,HTTP/1.0 是支持的,不过需要主动得在客户端的请求头中加入:Connection: keep-alive,还得等服务器回应:” 才表示和客户端得持久连接是建立成功得。

建立完持久连接之后,浏览器(客户端)和服务器之间就可以通过同一条连接收发多个请求和响应。但是《HTTP 权威指南》再三强调,无论服务器给了什么承诺,服务器都有权力随时断掉这条连接,这就要求客户端必须清楚得知道自己什么请求已经处理完成,什么请求可能需要重试。

因为持久连接有节省资源的好处,所以在 HTTP/1.1 默认是走持久连接的,反过来,如果客户端不希望启用持久连接,反而需要加上请求头:Connection: keep-alive

1. 持久连接的延迟

虽然持久连接解决了一些问题,但是还是存在很大的优化空间,例如 HTTP/1.1 就存在一个很明显的问题:延迟,假设对于一个 CDN,我有两个静态文件 main.cssmain.js,因为使用了持久连接,所以都需要从一条连接上来传输,又因为传输的第一个文件(main.js)比较大,所以当第一个文件传输完之前,第二个文件是得不到传输的。例如我随便打开一个网站,然后就可以发现这个问题:

所以在 HTTP/2.0 的时候,就这个问题提出了解决方案,实现了无需先入先出的多路复用,也就是说,不需要等前面一个请求完成了,再进行下一个请求,大家可以同时来,而为了实现这个,HTTP/2.0 中又引入了一个新的 “帧” 的概念,让连接得到充分的处理。

2. 连接断开的处理

除了延迟问题之外,使用持久连接还有很多问题需要处理,例如前面说了,服务器有权利随时断开连接,那么这意味着什么?如果你在一个持久连接上发起了多个请求,那么部分完成之后连接被断开,那么没有完整完成的请求需要重开连接进行重试。

当出现重试的时候就需要考虑请求的幂等性了,如果你是一个创建资源的请求的话,不考虑幂等性是会出问题的,问题的大小取决于你的系统架构和用户体验处理,所以一个良好的实践就是不在持久的管道连接中发送一个可能对系统产生副作用的请求(一般来说只有 POST)。在使用浏览器的时候我们很少去刻意控制这种行为,但是,浏览器却安守本分,例如当我们尝试刷新一个带有表单提交的页面的时候,浏览器会弹出一个确认框让你确认它会重新发起一个可能对系统产生副作用的请求。

但是,作为开发者,我们更多得确实通过 HTTP Client Library 来和 Server 打交道,这个时候,控制重试的事情还是会交给我们自己来处理,这是需要非常注意的!

3. 持久连接的使用

既然一个连接可以用于多个请求响应,那么双方是如何判断每次的请求的完整性的呢?例如,服务器如何知道客户端的第一个请求已经发送完全,并且准备等待着我的回应呢?在 HTTP/1.1 中,有两种常规方式告知,分别是:Content-Length分块传输编码

但是使用这两种方式都务必小心,可能会产生一些我们意外中的情况,例如拿 Content-Length 来说,它表示的含义为 Body 的长度,也就是说不包含 Headers 的 Size。那么问题就来了,如果我们传错了这个长度会怎样?我们来分析一下两种场景,第一个场景就是传过去的 Content-Length 比实际的小,也就是说服务器认为接收完了的时候,客户端还在发送,这个情况可能好一些,服务器可能就只认你发送长度的那么多数据,例如你发送了 “hello liqiang.io” 这几个字符,然后发送的 Content-Length: 5,那么可能服务器就只认前面的 5 个字符 “hello”,而忽略后面的 “liqiang.io” 了。

如果发送的 Content-Length 过长呢?那么这个问题就大了,服务器会认为你的 Body 还没有发送完全,就一直等着,而客户端呢,认为自己已经发送完了,该服务器响应了,这样双方都在等对方的数据,那么就会导致大家都没有数据,这条连接将会被 block 住,直到超时。一般来说 Server 端的超时都是比较长的,在我实际的应用中,用到的系统默认的超时都是 2 分钟往上的,所以如果出现了这种情况,对于 Server 来说是个很严重的问题,客户端需要设置一个比较合理的超时时间!