HTTP的连接管理
谈起HTTP协议,我们知道现在HTTP已有诸多个版本(1.0、1,1、2.0、3.0)。其中,平常应用的比较多的便是HTTP /1.1和/2.0,由于3.0还比较新,本篇就不涉及了。
而处于应用层的HTTP要如何优化才能使其在通信中提高效率的同时保证安全,就涉及到我们对HTTP连接的管理了,本篇我们就来讲一讲HTTP主要的几种连接管理模型。
HTTP连接之前
在了解HTTP的连接管理以及相应的性能表现之前,我们必须知道的是,HTTP作为应用层的协议,是非常依赖于传输层TCP协议的,因此无论连接管理模型为何,性能表现、安全水平等指标都取决于TCP。
同时必须注意,HTTP本身没有连接这一说 ,这里说的连接都是TCP连接,本文所要讨论的连接管理,也更多的是指在连接之上如何发送请求对TCP连接进行管理。
针对这一点,本文会穿插进TCP连接的知识,从TCP的角度分析问题,这对我们理解HTTP的连接管理是很有帮助的。
并且,对于一个完整的请求处理(请求发送+响应确认),我们引用数据库并发控制的概念,认为这是一个完整的事务,是不可分割的。
HTTP 短连接的不足
我们知道,HTTP/1.0默认的连接方式是短连接 ------在一个TCP连接中只能处理一次事务 。每请求一个文档,不仅需要单独建立对应的TCP连接花费开销(时间、缓存和变量),还需要两倍的RTT
(Return Trip Time往返时延)。当万维网上客户量比较大的时候,这无疑会使服务器难以负担。这样的话,用户想要获取一个站点的完整的资源(比如网站),就必须多次建立TCP连接并发送多次请求,才能完成------这样的效率是非常低的。
这样的短链接又被称为非持续连接,串行连接。
实际上,TCP本身具备热连接的能力,但是HTTP 短连接的机制没能让其发挥,不能够减轻减少连接建立和断开的开销、提高通信的效率。
热连接(Hot):即在一段时间内保持持久的TCP连接。
稍微一提,对于每一条新建立的TCP连接,我们都会存在连接时延 和慢启动时延。如果我们一直采取HTTP短连接,互联网的规模那么庞大,发送的请求总数也必然是巨大的,这样给每一位造成的时延就几乎是龟速了。
连接时延:可以简单理解成一条TCP连接的初始化。
慢启动时延:为了适应对方的接收窗口而产生的时延。
计算机网络发展至今,如今已有多种方案可以提高HTTP的连接性能。接下来我们主要介绍四种连接管理模型(技术):
- 并行连接:通过多条TCP连接发起的HTTP请求
- 持久连接:重用TCP连接,以消除连接及关闭时延
- 管道化连接:通过共享的TCP连接发起并发的HTTP请求
- 复用连接:交替传送请求和响应报文
HTTP并行连接 多条连接多个事务
首先必须认识到,不管是哪个HTTP版本,想要获取某个资源,就只能单独发送一次HTTP请求,因为在每一份HTTP报文当中只能有一个URL。那既然无法在请求上做出优化,我们便只能在连接上下功夫、做优化了。
简单粗暴的一种方法是,在同一个时间段内,直接创建多条连接,并行地执行多个HTTP事务,即每个事务单独对应一条连接。
这似乎可以提高页面的加载速度。只要因特网的带宽足够,并行的方式不仅能够提高加载速度,还可以提高带宽的利用率。
但问题也出在这里。倘若真的每一个事务都开一条连接,就像我们之前讨论短连接的那样,完整加载一个网页就要开一百多个连接,这样很容易就会耗尽带宽资源 ,此时事务之间可能还会为了有限的带宽产生竞争------每个事务的处理速度就更慢了,还不如短连接呢。
从性能的角度来看,问题也很明显。TCP连接本身就很复杂,这种复杂不仅体现在TCP报文首部的构成 ,也体现在TCP连接为了保证可靠性和效率所执行的一系列机制。本篇重点不在TCP,因此只给出体现出TCP知识大纲的图片。
那么既然维持一条TCP连接,就必须有相应的控制程序,因而也必然会占用相当一部分内存。回到本小节举的例子,服务器要为一个用户创建数百条连接,且为一条连接都得开辟独立的内存(无论这条连接所负责的事务是大是小)------而通常,Web服务器要处理很多用户的请求,这无疑会造成服务器性能的严重下降。对高负荷的代理来说也是如此。
这没有解决问题,还是短连接的毛病。
但是这样的并行,从用户的角度来看,好像真的变得更快了------草率地来看,同一时段处理多件事,就是在一个时段傻傻地只做一件事而其他工作全部搁置,要快一些。
但即便如此,我们也并不是就此弃用了并行连接,而是对并行进行了限制,即将并行的连接总数限制为一个较小的值(通常是4个),而且服务器也可以随意关闭来自特定客户端的超量连接。
HTTP 持久连接 一条连接多次事务
也叫长连接、持久连接
从本质上,并行连接的作用原理和短连接是差不多的,毛病的原因也是相同的:一条连接只处理一个事务 。既然如此,为什么不尝试一下一条连接处理多个事务呢?
HTTP当中也确实有这样的连接管理,我们将其称之为持久连接。
持久连接 和热连接 最大的不同,就是二者的协议层次不同。长连接是应用层 的概念,是一种高层次的连接,主要应用在HTTP服务中;而热连接是传输层的,比长连接要更加通用。
持久连接 会在不同事务之间保持打开状态,直到客户端或服务器决定将其关闭为止。重用 已对目标服务器打开的空闲持久连接,就可以避免缓慢的连接建立阶段。而且,已经打开的连接还可以避免慢启动造成的时延,以便于我们更快速地进行数据传输。
持久连接分为两类:**HTTP/1.0+"keep-alive"连接和HTTP/1.1 "persistent"**连接。我们分开讲一讲。
HTTP/1.0+keep-alive连接
其实早在1996年的时候,持久连接就已经出现了。因为HTTP/1.0默认是短连接,需要设置请求报文的首部字段Connection
值为Keep-Alive ,并且服务器同意该请求 之后才能将一条连接保持在打开状态,因此这样的持久连接称之为keep-alive连接,是一种早期实验型的持久连接。
TCP连接都是双向/全双工的,通信双方可以发出请求,也必须能够接收到响应。因此我们接着刚才所讲,发出keep-alive
请求不一定能将连接设置为持久连接(或者说,保持在活跃状态),只有接收到了对方的**同意(即对方也发送一份包含有keep-alive的响应报文)**之后才可以真正建立。并且由于HTTP/1.0默认都是短连接和keep-alive
连接本身的限制,在请求发送并获得同意之后,也绝对不意味着一劳永逸。
**在keep-alive
连接刚建立时,之后通信双方发给对方的每一份报文首部Connection
字段都必须有Keep-Alive
,否则对方受到请求后就会以为现在要关闭连接。**用更加官方的话来讲就是:Connection:Keep-Alive
首部必须随所有希望保持持久连接的报文一起发送。
- 对于客户端,如果它没有发送
Connection:Keep-Alive
首部,服务器就会在那条请求之后关闭连接。 - 对于服务器,它没有在响应中包含
Connection:Keep-Alive
首部,客户端就会在那条响应之后关闭连接。
有同学可能会注意到,Keep-Alive
通用首部有两个选项值max
和timeout
可以调节keep-alive
的行为,但这两个都是希望值,没有实际的作用。
- Timeout选项:表示服务器愿意在没有收到进一步请求的情况下保持连接的时间。这个选项值并不是一个确切的承诺,而是服务器建议的时间范围。实际的保持连接时间取决于服务器的策略和配置。
- Max选项:表示服务器愿意在单个连接上处理的最大请求数。同样,这个选项值也不是一个确切的承诺,而是服务器建议的数量。实际的最大请求数取决于服务器的配置和限制。
Keep-Alive和哑代理
其实如果只是在每个报文都加上keep-alive
的话,我们一直使用HTTP/1.0 keep-alive
持久连接好像也没什么不方便。但在客户端和服务器的通信过程中还会出现这样一个问题,那就是通信过程中的代理可能无法理解Connection
首部。
在网络当中存在很多老旧或简单的代理对待发来和发出的网络包,都只是起一个中继的作用------只负责将字节从一个连接转发到另一个连接中去,不对Connection
首部进行特殊的处理。这样一来的话,本就属于逐跳首部 的Connection
字段以及包括它的值就会原封不动地发送到另一个连接------这会引发一个称作哑代理的问题。
逐跳标头(Hop-by-hop):这类标头仅对单次传输连接有意义,并且不得由代理重传或者缓存 。注意,只能使用
Connection
标头来设置逐跳标头。
这里我们直接引用《HTTP权威指南》当中的一幅图来解释,我觉得这幅图非常的直观:
- 首先客户端向服务器请求建立一条
keep-alive
连接,不过客户端必须先经过一个代理。 - 代理虽然接收到了这条HTTP请求,但是它根本不理解
Connection
首部,不知道该如何处理,于是将其原封不动地带着报文一同交付给了服务器。 - 服务器接收到了这条请求建立
keep-alive
连接的报文,但是在服务器看来,这是代理发来的请求 (这也是逐跳首部的特点。在建立连接这一点上,代理、客户端和服务器没有严格的区分界限,服务器把代理看作是客户端是很正常的),那么此时服务器就误以为代理想要和它建立keep-alive
连接 ,而代理连Connection
首部都理解不了,怎么建立的起来呢?! - 服务器可不知道马上要与之进行
keep-alive
对话的客户端是个连字都不认识的小毛孩,这就同意了此次keep-alive
请求。接下来的工作,服务器会按照keep-alive
的规则进行。 - 代理收到了同意报文,同样,还是不对
Connection
首部做任何处理,直接回送了客户端。 - 客户端很高兴,以为服务器同意了和它进行
keep-alive
通话。此时,客户端和服务器都产生了误解,都以为自己正在和服务器/客户端进行keep-alive
通信,但实际上连接的另一端都是大字不识的代理。 - 为什么这样就会出大问题呢?因为对于代理来说,正是因为它的不作为,
keep-alive
连接根本就没有建立成功 :- 代理不能处理
Connection
首部,也就无法识别keep-alive
,因此建立的连接全都是短连接,处理完一个事务,立刻就要关闭。 - 但是服务器认为代理想要建立
keep-alive
连接,客户端认为服务器同意了建立keep-alive
连接,它俩都将按照keep-alive
的规则运作。这和作为中继的代理就产生了不一致。
- 代理不能处理
- 接下来只要代理为客户端处理完一个事务,代理负责的两条连接(连接到客户端的连接1,连接到服务器的连接2)都应该断开。代理会请求关闭连接(TCP连接释放也需要请求)
- 但就如刚才所说的,客户端和服务器都会按照
keep-alive
连接方式去工作。代理会一直等待连接释放的响应,也不会继续处理客户端发来的其他报文。浏览器就一直这样挂着,直到连接超时。
这就是哑代理 问题,而其中无法处理Connection
首部的代理称为盲中继。
使用Proxy-Connection仍然无法解决问题
从上述的讲解当中,我们不难发现哑代理问题的根源是转发了逐跳首部并且认错了人 ------逐跳首部只与一条特定的连接有关,不能被转发,且当下游服务器误将转发来的首部来作为代理自身的请求解释,并用来控制自己的连接时,就会引发问题。
Netscape
公司为了解决这样的问题,曾想出一种方法:设置浏览器会向代理发送非标准的Proxy-Connection
扩展首部而不是官方支持的Connection
首部。这样,即便盲中继不加处理地将Proxy-Connection
首部转发给了Web服务器,服务器也会因为不认可该首部而选择忽略,也许持久连接不能被建立,也至少不会出现浏览器持续挂起的糟糕状况。
而如果我们的代理能够理解持久连接(这样的代理我们就称之为聪明的代理 ),就会用一个Connection
首部来取代无意义的Proxy-Connection
首部,并将其转发给服务器,达成建立持久连接的目的。
然而,这只适用于客户端和服务器之间只存在一个盲中继的情况。只要聪明的代理一侧有盲中继,我们就又会遭遇哑代理的问题。这里还是给出《HTTP 权威指南》的一张图,如果上一小节关于哑代理的陈述都能理解的话,这里的问题也应该很容易看得出来。
HTTP/1.1 持久连接
认识到HTTP/1.0 +keep-alive
持久连接存在的问题后,我们再来学习HTTP/1.1的持久连接,就知道HTTP/1.1 持久连接的一些设定是为什么了。
与HTTP/1.0 +keep-alive
连接不同的第一点就是,HTTP/1.1 默认状态都是持久连接,不必要在之后的每个报文首部都添加持久连接的字段提示。想要关闭的话,就必须在报文中显式地添加一个Connection:close
首部,不发送,就一直保持着持久连接的状态。并且,这样的关闭是不需要响应的,HTTP/1.1 设备可以在任意时刻关闭连接。------比HTTP/1.0更为灵活,也比较安全。
对于HTTP/1.0的哑代理问题,HTTP/1.1的设定是:
- HTTP/1.1的代理必须能够分别管理与客户端和服务器的持久连接------每个持久连接都只适用于一跳传输。
- HTTP/1.1的代理服务器不应该与HTTP/1.0客户端建立持久连接。
目前我们使用的持久连接都是HTTP/1.1 持久连接。然而,在当今的互联网,还有相当一部分仍在使用HTTP/1.0+keep-alive
连接,并且代理也有一部分是老旧的、会违反规定转发Connection
首部的代理。因此,HTTP
的实现者应该做好与之进行交互操作的准备
顺道一说,当前,很多Web应用程序都会将并行连接 和持久连接 结合起来使用,即创建少量并行连接,而每一条连接都是持久连接。
HTTP 1.1 流水线模型
也称作管道化连接。
这是基于HTTP/1.1 持久连接之上的又一种性能更加优秀的连接管理模型。
在一般的事务当中,下一个的请求的发送必须等到上一个请求的响应被受到后才能发送。但是流水线模型可就不一样了------当第一个请求发往服务器的过程中,不必等待响应,第二天第三条请求就可以开始发送。这在高时延的网络条件下,可以明显降低网络的环回时间,提高性能。
不过,依据官方文档,流水线模型想要使用也是有限制的:
- 必须是基于持久连接的。
- 必须按照与请求相同的顺序回送HTTP响应
- HTTP客户端必须做好连接会在任意时刻关闭的准备,且应当准备好重发在流水线中所有未完成的请求。
- HTTP客户端不应该用流水线模型发送会产生副作用的请求(比如非幂等的POST请求)
POST在设计上被认为是非幂等的,即多次重复发送相同的POST请求可能会导致不同的结果或产生不同的影响------在这个前提下,系统擅自重发请求很有可能对数据进行多次修改,而对用户而言动作只希望执行一次。举个例子,倘若在一个电商网站上,用户购买这一动作属于流水线模型的POST请求,这时网络发生故障/流水线关闭,POST请求还没有完成,系统擅自重发,可能会造成用户多次购买的结果!
相反,幂等的请求方法(如GET)不会对服务器状态产生影响,多次重复发送相同的请求得到的结果都是一样的。------这时,重发对用户也不会有什么坏处,至少不会在不知情的情况下下单了好几次......
HTTP/2.0 多路复用
来到HTTP/2,我们对每一个报文处理的颗粒度更小了,在HTTP/1.1之上又做出了优化。由于当今我们最常使用的是HTTP/1.1,对HTTP/2.0的应用较少,因此这里只简单说明一下HTTP/2.0 连接管理机制之一的分路复用机制。
HTTP/2.0 多路复用同样允许在单个TCP连接上以帧为单位同时传输多个HTTP请求和响应,解决了HTTP/1.x中的串行传输和队头阻塞问题,提高了性能和效率。不过相比于之前的HTTP 连接管理模型,HTTP/2.0还有以下的特点:
- 报文分解成帧(Frame) :HTTP/2将请求和响应划分为多个帧,每个帧都有自己的标识和优先级。帧可以同时在单个TCP连接上发送和接收,允许多个请求和响应同时进行。
- 流工作模式(Stream) :HTTP/2通过流的概念来管理和标识多个请求和响应。每个流都有唯一的标识符(Stream ID),用于区分不同的请求和响应。在一个TCP连接上可以同时存在多个流,它们可以乱序发送和接收。
- 头部压缩(Header Compression) :HTTP/2使用HPACK算法对请求和响应的头部进行压缩。这样可以减小头部的大小,减少网络传输的数据量。头部压缩是在客户端和服务器之间共享的,通过维护一个动态的头部表,避免了重复的头部信息传输。
- 优先级(Priority) :HTTP/2允许为每个流设置优先级,以指定其重要性和处理顺序 。通过优先级设置,可以确保重要的请求优先得到处理,避免低优先级请求被阻塞。
- 并发处理(Concurrency) :由于多路复用的特性,HTTP/2可以并发处理多个请求和响应。服务器可以同时发送多个响应,而客户端可以同时发送多个请求,而无需等待前一个请求的响应。这显著提高了性能和效率。
- 服务器推送(Server Push):HTTP/2还引入了服务器推送的功能,允许服务器在客户端请求之前主动推送相关资源。服务器可以根据页面内容或预测算法主动推送与请求相关的资源,减少客户端的请求次数和延迟。
HTTP/1.x 域名分片
这是从MDN资料中翻阅出来的一个概念,和HTTP并行连接机理是相似的。即请求一个站点的资源时,为这个站点的域名拆分成多个域名,并让这些域名都指向同一台服务器,浏览器就会同时为每个域名分别建立连接。这里也可以采用并行连接,一个域名建立多条连接。
假设有个域名www.example.com
,我们可以把它拆分成好几个域名:www1.example.com
、www2.example.com
、www3.example.com
。所有这些域名都指向同一台服务器,假设浏览器会同时为每个域名建立2条连接,那么现在我们的连接总数就有6条了。
可以看出,这和HTTP并行连接大同小异。实际上,域名分片已经时一门过时的技术了。在 HTTP/2 里,做域名分片就没必要了:HTTP/2 的连接可以很好的处理并发的无优先级的请求。域名分片甚至会影响性能。大多数 HTTP/2 的实现还会使用一种称作连接聚合的技术去尝试合并被分片的域名。
参考资料
-
[图灵指南]《HTTP权威指南》