2.4 连接管理 #
2.4.1 短连接与长连接 #
HTTP 协议最初(0.9/1.0)是个非常简单的协议,通信过程也采用了简单的 “请求 - 应答” 方式。它底层的数据传输基于 TCP/IP,每次发送请求前需要先与服务器建立连接,收到响应报文后会立即关闭连接。因为客户端与服务器的整个连接过程很短暂,不会与服务器保持长时间的连接状态,所以就被称为 “短连接”(short-lived connections)。
早期的 HTTP 协议也被称为是 “无连接” 的协议。短连接的缺点相当严重,因为在 TCP 协议里,建立连接和关闭连接都是非常 “昂贵” 的操作
针对短连接暴露出的缺点,HTTP 协议就提出了 “长连接” 的通信方式,也叫 “持久连接”(persistent connections)、“连接保活”(keep alive)、“连接复用”(connection reuse)。
长连接使用的 “成本均摊” 的思路,既然 TCP 的连接和关闭非常耗时间,那么就把这个时间成本由原来的一个 “请求 - 应答” 均摊到多个 “请求 - 应答” 上。这样虽然不能改善 TCP 的连接效率,但基于 “分母效应”,每个 “请求 - 应答” 的无效时间就会降低不少,整体传输效率也就提高了。
由于长连接对性能的改善效果非常显著,所以在 HTTP/1.1 中的连接都会默认启用长连接。不需要用什么特殊的头字段指定,只要向服务器发送了第一次请求,后续的请求都会重复利用第一次打开的 TCP 连接,也就是长连接,在这个连接上收发数据。
2.4.2 关闭连接 #
因为 TCP 连接长时间不关闭,服务器必须在内存里保存它的状态,这就占用了服务器的资源。如果有大量的空闲长连接只连不发,就会很快耗尽服务器的资源,导致服务器无法为真正有需要的用户提供服务。所以,长连接也需要在恰当的时间关闭,不能永远保持与服务器的连接,这在客户端或者服务器都可以做到。
在客户端,可以在请求头里加上 “Connection: close” 字段,告诉服务器:“这次通信后就关闭连接”。服务器看到这个字段,就知道客户端要主动关闭连接,于是在响应报文里也加上这个字段,发送之后就调用 Socket API 关闭 TCP 连接。
服务器端通常不会主动关闭连接,但也可以使用一些策略。拿 Nginx 来举例,它有两种方式:
-
使用 “keepalive_timeout” 指令,设置长连接的超时时间,如果在一段时间内连接上没有任何数据收发就主动断开连接,避免空闲连接占用系统资源。
-
使用 “keepalive_requests” 指令,设置长连接上可发送的最大请求次数。比如设置成 1000,那么当 Nginx 在这个连接上处理了 1000 个请求后,也会主动断开连接。
2.4.3 队头阻塞 #
“队头阻塞” 与短连接和长连接无关,而是由 HTTP 基本的 “请求 - 应答” 模型所导致的。
因为 HTTP 规定报文必须是 “一发一收”,这就形成了一个先进先出的 “串行” 队列。队列里的请求没有轻重缓急的优先级,只有入队的先后顺序,排在最前面的请求被最优先处理。
如果队首的请求因为处理的太慢耽误了时间,那么队列里后面的所有请求也不得不跟着一起等待,结果就是其他的请求承担了不应有的时间成本。
优化 #
因为 “请求 - 应答” 模型不能变,所以 “队头阻塞” 问题在 HTTP/1.1 里无法解决,只能缓解。
并发连接 #
同时对一个域名发起多个长连接,用数量来解决质量的问题。但这种方式也存在缺陷。如果每个客户端都想自己快,建立很多个连接,用户数×并发数就会是个天文数字。服务器的资源根本就扛不住,或者被服务器认为是恶意攻击,反而会造成 “拒绝服务”。
域名分片 #
HTTP 协议和浏览器不是限制并发连接数量吗?好,那就多开几个域名,比如 shard1.example.com、shard2.example.com,而这些域名都指向 example.com 域名对应的 ip 地址。这样实际长连接的数量就又上去了。
域名分片解决的是客户端并发的问题,可以创建更多的连接。比如浏览器限制一个域名最多 6 个连接,域名分 3 片,那么浏览器就可以同时建立 3*6=18 个连接,显然就能够并发更多请求,获取数据也就快了。
利用HTTP的长连接特性对服务器发起大量请求,导致服务器最终耗尽资源 “拒绝服务”,这就是常说的 DoS。