长连接与短连接
HTTP/0.9
HTTP 的第一个文档版本是HTTP/0.9,于 1991 年提出。它是有史以来最简单的协议;有且只有一个名为 GET 的方法。如果客户端必须访问服务器上的某个网页,它会发出如下简单的请求
GET /index.html
服务器会收到请求,用 HTML 作为响应进行回复,一旦内容传输完毕,连接就会关闭,服务器的响应如下所示
(response body)
(connection closed)
可以见得,当时的HTTP协议非常简单,并且通常它只需要发出一次请求就可获得所有的内容,当时的网络条件完全能满足此时HTTP协议的需求,因此人们并不在意发出的请求是否应该保持TCP网络连接,所以当时并未规定HTTP协议是否应该保持TCP网络连接。
HTTP/1.0
1996 年,HTTP 的下一个版本即 HTTP/1.0 比原始版本有了很大改进。与仅为 HTML 响应设计的 HTTP/0.9 不同,HTTP/1.0 现在可以处理其他响应格式,即图像、视频文件、纯文本或任何其他内容类型。它添加了更多方法(即 POST 和 HEAD),更改了请求/响应格式,将 HTTP 标头添加到请求和响应中,添加了状态代码以识别响应,引入了字符集支持,多部分类型,授权、缓存、内容编码等。
以下是示例 HTTP/1.0 请求的样子:
GET / HTTP/1.0
Host: cs.fyi
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)
Accept: */*
以下是示例 HTTP/1.0 响应的样子:
HTTP/1.0 200 OK
Content-Type: text/plain
Content-Length: 137582
Expires: Thu, 05 Dec 1997 16:00:00 GMT
Last-Modified: Wed, 5 August 1996 15:55:28 GMT
Server: Apache 0.84
(response body)
(connection closed)
相较于HTTP/0.9,可以看到HTTP/1.0可以处理并且响应除了HTML以外的其他类型的请求,如图像、视频、HTML、纯文本等。也是由于加入了很多其他类型的内容,如今不再能够发出一次请求就可获得所有的内容,通常获取完整内容需要发送几次甚至十几次请求才能获得,比如您访问的网页有 10 个图像、5 个样式表和 5 个 javascript 文件,当您需要获取网页完整且正确的内容时,需要发出总共 20 个请求。由于HTTP/1.0与HTTP/0.9一样,每次请求它就必须打开一个新的 TCP 连接,并且在满足该单个请求后,连接将关闭。对于任何下一个要求,它都必须建立在新的连接上。因此上述网页将会发起一系列 20 个独立的连接,并且服务器只能根据这些独立的连接一个接一个地处理提供响应。我们都知道每打开一个 TCP 连接都是相当耗费资源的操作,客户端和服务器端之间需要交换很多次消息,并且其中的网络延迟和带宽都会对这个过程造成巨幅的影响,从而导致整个一系列的请求变慢,对性能造成了影响。 为了解决这个问题 HTTP/1.0 的实现试图通过引入一个名为 Connection: keep-alive 的新标头来解决这个问题,但是,最终它仍然没有得到广泛支持,问题仍然存在。
HTTP/1.1
在 HTTP/1.0 仅推出 3 年后,下一个版本即 HTTP/1.1 于 1999 年发布,在 HTTP/1.0 中,每个连接只有一个请求,并且连接在请求完成后立即关闭,这导致了严重的性能损失和延迟问题。HTTP/1.1 引入了持久连接,即默认情况下连接不会关闭(Connection:keep-alive),而是保持打开状态,允许多个顺序请求。要关闭连接,请求头Connection: close
必须可用。客户端通常在最后一个请求中发送此标头以安全关闭连接。当然持久连接不全然都是好处,它也有自身的缺点,在空闲状态,它还是会消耗服务器资源,而且在重负载时,还有可能遭受 DoS 攻击。
此外,HTTP/1.1还引入了HTTP pipelining(HTTP管道化)技术,默认情况下,HTTP请求是按顺序发出的。下一个请求只有在当前请求收到响应过后才会被发出。由于会受到网络延迟和带宽的限制,在下一个请求被发送到服务器之前,可能需要等待很长时间。HTTP pipelining其实就是把多个HTTP请求放到一个TCP连接中一一发送,只不过在之前的协议中,下一个请求需要等到当前请求响应后发送,而HTTP pipelining则是在发送过程中不需要等待服务器对前一个请求的响应就可以发送下一个请求,这样可以避免连接延迟。想法确实很美好,但由于线头阻塞
问题即使到了今天,大部分桌面浏览器仍然会选择默认关闭HTTP pipelining这一功能。
至此,HTTP/1.1初步完成了对网络链接的优化:短连接、长连接、HTTP管道化。
短连接、长连接、HTTP管道化
网络连接 | 标头 | 特点 | 缺点 |
---|---|---|---|
短连接 | Connection: close | HTTP 最早期的和 HTTP/1.0 的值, 每次请求它就必须打开一个新的 TCP 连接,并且在满足该单个请 求后,连接将关闭。对于任何下一 个请求,它都必须建立在新的连接上。 | 每次 TCP 连接都是相当耗费资源的, 极大影响了请求的响应速度 |
长链接 | Connection: keep-alive | HTTP/1.1请求的默认值,每次请求 它会保持TCP连接一定时间,后续 对同一服务器的请求它将使用该 连接完成,无需重新建立连接 | 在空闲状态,它还是会消耗服务器 资源,而且在重负载时,还有可能 遭受 DoS 攻击 |
管道化 | 无 | 同一条长连接上发出连续的请求, 而不用等待应答返回 | 线头阻塞 |
相关标头
Connection 请求标头
Connection
控制网络连接在当前请求完成后是否仍然保持连接状态。 如果发送的值是 keep-alive
,它会保持连接去完成后续对同一服务器的请求;如果发送值是close
,它每发起一个请求时都会创建一个新的网络连接,并在收到应答时立即关闭。
参数
该请求标头并无其他参数
取值
close
短连接:不保持网络连接,它每发起一个请求时都会创建一个新的网络连接,并在收到应答时立即关闭。 这是 HTTP/1.0 请求的默认值
keep-alive
长连接:保持网络连接,它会保持连接去完成后续对同一服务器的请求,这是 HTTP/1.1请求的默认值
示例
Connection: keep-alive
Connection: close
警告
在 HTTP/2 和 HTTP/3 中,禁止使用特定于连接的标头字段,如 Connection
和 Keep-Alive
。Chrome 和 Firefox 会在 HTTP/2 响应中忽略它们,但 Safari 遵循 HTTP/2 规范要求,不会加载包含这些字段的任何响应。
Keep-Alive 请求标头
当请求头Connection
为keep-alive
时(请求保持连接去完成后续对同一服务器的请求),可通过设置Keep-Alive
请求头来指定空闲的连接需要保持的最小时长以及该连接可以发送的最大请求数量。
参数
timeout=<number>
指定了一个空闲连接需要保持打开状态的最小时长(以秒为单位)。需要注意的是,如果没有在传输层设置 keep-alive TCP message 的话,大于 TCP 层面的超时设置会被忽略。
max=<number>
在连接关闭之前,在此连接可以发送的请求的最大值。在非管道连接中,除了 0 以外,这个值是被忽略的,因为需要在紧跟着的响应中发送新一次的请求。HTTP 管道连接则可以用它来限制管道的使用
示例
Keep-Alive: timeout=5, max=1000
警告
需要将 请求头 Connection
的值设置为 "keep-alive"
这个标头才有意义。同时需要注意的是,在 HTTP/2 协议中, Connection
和 Keep-Alive
是被忽略的;在其中采用其他机制来进行连接管理。
线头阻塞(Head-of-line blocking)
HTTP pipelining将多个HTTP请求放到一个TCP连接中一一发送,而在发送过程中不需要等待服务器对前一个请求的响应;只不过,客户端还是要按照发送请求的顺序来接收响应。但不管怎么处理,服务器是要按照顺序处理请求的,如果前一个请求非常耗时,那么后续的请求都会受到影响,这就是所谓的线头阻塞(head-of-line blocking)。
当然,你可以在选择队伍时候就做好功课,去排一个你认为最快的队伍,或者甚至另起一个新的队伍(译者注:即新建一个TCP连接)。但不管怎么样,你总归得先选择一个队伍,而且一旦选定之后,就不能更换队伍。
但是,另起新队伍会导致资源耗费和性能损失(译者注:新建 TCP 连接的开销非常大)。这种另起新队伍的方式只在新队伍数量很少的情况下有作用,因此它并不具备可扩展性。(译者注:这段话意思是说,靠大量新建连接是不能有效解决延迟问题的,即HTTP pipelining并不能彻底解决head-of-line blocking问题。)所以针对此问题并没有完美的解决方案。
这就是为什么即使到了今天,大部分桌面浏览器仍然会选择默认关闭HTTP pipelining这一功能的原因。
那些年,克服延迟之道
再困难的问题也有解决的方案,但这些方案却良莠不齐。
Spriting
Spriting是一种将很多较小的图片合并成一张大图,再用JavaScript或者CSS将小图重新“切割”出来的技术。
网站可以利用这一技巧来达到提速的目的——在HTTP 1.1里,下载一张大图比下载100张小图快得多。
但是当某些页面只需要显示其中一两张小图时,这种缓存整张大图的方案就显得过于臃肿。同时,当缓存被清除的时候的时候,Spriting会导致所有小图片被同时删除,而不能选择保留其中最常用的几个。
内联(Inlining)
Inlining是另外一种防止发送很多小图请求的技巧,它将图片的原始数据嵌入在CSS文件里面的URL里。而这种方案的优缺点跟Spriting很类似。
.icon1 {
background: url(data:image/png;base64,<data>) no-repeat;
}
.icon2 {
background: url(data:image/png;base64,<data>) no-repeat;
}
拼接(Concatenation)
大型网站往往会包含大量的JavaScript文件。开发人员可以利用一些前端工具将这些文件合并为一个大的文件,从而让浏览器能只花费一个请求就将其下载完,而不是发无数请求去分别下载那些琐碎的JavaScript文件。但凡事往往有利有弊,如果某页面只需要其中一小部分代码,它也必须下载完整的那份;而文件中一个小小的改动也会造成大量数据的被重新下载。
这种方案也给开发者造成了很大的不便。
分片(Sharding)
最后一个我要说的性能优化技术叫做“Sharding”。顾名思义,Sharding就是把你的服务分散在尽可能多的主机上。这种方案乍一听比较奇怪,但是实际上在这背后却蕴藏了它独辟蹊径的道理!
最初的HTTP 1.1规范提到一个客户端最多只能对同一主机建立两个TCP连接。因此,为了不和规范冲突,一些聪明的网站使用了新的主机名,这样的话,用户就能和网站建立更多的连接,从而降低载入时间。
后来,两个连接的限制被取消了,现在的客户端可以轻松地和每个主机建立6-8个连接。但由于连接的上限依然存在,所以网站还是会用这种技术来提升连接的数量。而随着资源个数的提升(上面章节的图例),网站会需要更多的连接来保证HTTP协议的效率,从而提升载入速度。在现今的网站上,使用50甚至100个连接来打开一个页面已经并不罕见。根据httparchive.org的最新记录显示,在Top 30万个URL中平均使用40(!)个TCP连接来显示页面,而且这个数字仍然在缓慢的增长中。
另外一个将图片或者其他资源分发到不同主机的理由是可以不使用cookies,毕竟现今cookies的大小已经非常可观了。无cookies的图片服务器往往意味着更小的HTTP请求以及更好的性能!
下面的图片展示了访问一个瑞典著名网站的时产生的数据包,请注意这些请求是如何被分发到不同主机的。
HTTP/2
HTTP/2是专为降低内容传输延迟而设计,我们可以看下改善核心:
- 降低协议对延迟的敏感
- 修复pipelining和head of line blocking的问题
- 防止主机需求更高的连接数量
- 保留所有现有的接口,内容,URI格式和结构
- 由IETF的HTTPbis工作组来制定
二进制协议
HTTP/2 倾向于通过使其成为二进制协议来解决 HTTP/1.x 中存在的延迟增加的问题。作为一个二进制协议,它更容易解析,但与 HTTP/1.x 不同的是,它不再被人眼读取。HTTP/2 的主要构建块是帧和流。
帧与流
HTTP 消息现在由一个或多个帧组成。有一个用于元数据的 HEADERS 帧和用于有效负载的 DATA 帧,并且存在几种其他类型的帧(HEADERS、DATA、RST_STREAM、SETTINGS、PRIORITY 等)。
每个 HTTP/2 请求和响应都被赋予一个唯一的流 ID,并且它被分成帧。帧不过是二进制数据。帧的集合称为流。每个帧都有一个流 ID,用于标识它所属的流,并且每个帧都有一个公共标头。此外,除了流 ID 是唯一的,值得一提的是,客户端发起的任何请求都使用奇数,而来自服务器的响应具有偶数流 ID。
中断连接
HTTP 1.1的有一个缺点是:当一个含有确切值的Content-Length的HTTP消息被送出之后,你就很难中断它了。当然,通常你可以断开整个TCP链接(但也不总是可以这样),但这样导致的代价就是需要通过三次握手来重新建立一个新的TCP连接。
在http2里面,我们可以通过发送RST_STREAM帧来实现这种需求,它是一种特殊的帧类型,用于中止某些流,即客户端可以发送此帧让服务器知道我不需要此流了。客户端可以使用 RST_STREAM 并停止接收特定流,同时连接不会被关闭其他流仍会正常运行。
优先级
每个流都包含一个优先级(也就是“权重”),它被用来告诉对端哪个流更重要。当资源有限的时候,服务器会根据优先级来选择应该先发送哪些流。
借助于PRIORITY帧,客户端同样可以告知服务器当前的流依赖于其他哪个流。该功能让客户端能建立一个优先级“树”,所有“子流”会依赖于“父流”的传输完成情况。
优先级和依赖关系可以在传输过程中被动态的改变。这样当用户滚动一个全是图片的页面的时候,浏览器就能够指定哪个图片拥有更高的优先级。或者是在你切换标签页的时候,浏览器可以提升新切换到页面所包含流的优先级。
多路复用
由于 HTTP/2 现在是二进制协议,并且正如我上面所说,它使用帧和流来进行请求和响应,因此一旦打开 TCP 连接,所有流都会通过同一连接异步发送,而无需打开任何其他连接。反过来,服务器以相同的异步方式响应,即响应没有顺序,客户端使用分配的流 ID 来识别特定数据包所属的流。流的多路复用解决了 HTTP/1.x 中存在的线头阻塞问题,即客户端不必等待正在花费时间的请求,其他请求仍将得到处理。
头压缩
HTTP是一种无状态的协议。简而言之,这意味着每个请求必须要携带服务器需要的所有细节,而不是让服务器保存住之前请求的元数据。因为http2并没有改变这个范式,所以它也以同样原理工作。这也保证了HTTP可重复性。当一个客户端从同一服务器请求了大量资源(例如页面的图片)的时候,所有这些请求看起来几乎都是一致的,而这些大量一致的东西则正好值得被压缩。
本节参考
- https://cs.fyi/guide/http-in-depth
- https://http2-explained.haxx.se/zh/part2
- https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Connection_management_in_HTTP_1.x
- https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Keep-Alive
转载需要经过本人同意!