HTTP 系列协议

397 阅读8分钟

从 HTTP 1.0 问题开始讲起

HTTP 0.9 —— 单行文本协议

$> telnet google.com 80

Connected to 74.125.xxx.xxx

GET /about/

(hypertext response)
(connection closed)

核心目标是传输超文本文档。该版本协议有几个特征:

  1. 非常简单。请求只有一行,响应是一个 HTML。
  2. 当文档传输完成以后,连接就会断开。
  3. 请求是 ASCII 编码的字符串,响应是 ASCII 编码的字符流。基于 TCP/IP。

HTTP 1.0

0.9 版本单一的 HTML 传输形式,很快就无法满足新兴网络的需求。因此 1.0 版本主要在于解决内容协商和传输丰富度。主要特征:

  1. 每次请求过后,客户端和服务端的连接就会关闭。

  2. 支持丰富的传输类型(响应添加 Content-Type Header),支持内容协商(请求添加 Accept Header)

$> telnet website.org 80

Connected to xxx.xxx.xxx.xxx

GET /rfc/rfc1945.txt HTTP/1.0       (Request line with HTTP version number, followed by request headers)
User-Agent: CERN-LineMode/2.15 libwww/2.17b3
Accept: */*

HTTP/1.0 200 OK                   	(Response status, followed by response headers)
Content-Type: text/plain
Content-Length: 137582
Expires: Thu, 01 Dec 1997 16:00:00 GMT
Last-Modified: Wed, 1 May 1996 12:45:26 GMT
Server: Apache 0.84

(plain-text response)
(connection closed)

无法忽略的性能问题

HTTP 0.9 和 1.0 版本核心还是在解决传输丰富度,此时性能问题并不在核心考虑范围内。但是 TCP 连接的握手和慢启动所带来的性能影响是无法被忽略的,因此以下的版本将从不同角度和程度去解决性能问题。

HTTP 1.1

HTTP 1.1 工作组的关键设计目标就是改善 HTTP 协议的性能,标准中包含了非常多关键性能改善特征,其中比较有名的几个是:

  1. 通过持久化连接来达到连接复用的目的 (Connection: keep-alive, http 1.1 是默认的,http 1.0 需要添加头部开启这个能力),通过连接复用来减少TCP连接带来的性能损耗,但是这同时带来了应用层的队头阻塞问题。
  2. 通过 Transfer-Encoding: chunked 头部实现数据分块传输
  3. 改进缓存机制
  4. http pipeling允许请求的并发处理
  5. 通过 Accept-Ranges: bytes 和 Ranges 请求头,支持范围请求

两个关键特性

Connection: keep-alive

保持连接复用,可以消除额外的握手。

http 1.1.svg

HTTP pipeling

将客户端的FIFO队列转移到服务端维护FIFO队列。这样可以在服务器端开启多个线程并行处理,达到加速。但是这样有非常多的问题:

  1. 首先依然是队头阻塞,只不过这种阻塞挪到了服务器端;
  2. 失败的响应,可能会导致客户端重复请求,也就存在服务器重复处理的可能性;
  3. 当并行处理时,服务器必须流水线FIFO响应,这会占用服务器资源,让服务器承担巨大压力。 因为以上诸多问题,HTTP pipeling在大多数浏览器都是被禁用的。

http pipeling.svg

两大问题

队头阻塞

HTTP 1.* 返回响应要求严格序列化。也就是说不允许来自多个响应的数据在同一连接上交错(多路复用),必须每个响应都完整返回,才可以进行下一个响应的传送。

TCP 的队头阻塞问题:由于 TCP 要求严格的有序交付,丢失的 TCP 数据包会阻塞所有序列号更高的数据包,直到重传成功,导致额外的应用延迟。

HTTP keep-alive 带来的队头阻塞问题:请求是按照队列发出先进先出的处理机制,每调度一个请求,都会等待他完整的响应,因此一个缓慢的请求,会阻塞它后面的所有请求。

HTTP pipeling 带来的队头阻塞问题:响应是按照对列先进先出的处理机制,缓慢的响应会阻塞后面服务器已经处理好的响应。

队头阻塞存在于应用层和传输层。HTTP2通过多路复用解决了应用层带来的队头阻塞,HTTP3则是彻底解决了存在于应用层和传输层的队头阻塞问题。

HTTP 请求和响应标头过大

如今,每个HTTP请求都将携带额外的500-800字节的元数据,如果有Cookie信息,这个数值将增加更多。

如何达到性能最优呢——“歪招”(歪招都是旨在解决应用层的队头阻塞)

并发TCP连接

浏览器采取并发多个TCP连接来解决单一TCP排队处理导致请求过慢的问题。Chrome在实践中大致允许同一个主机名(注意,不是IP)开6个并发连接。6是一个权衡后的结果,限制越高对服务器和客户端的开销越高,限制过低又无法满足客户端的需求。

这样做带来的问题是:

  1. 消耗客户端和服务器端以及所有中介资源额外的套接字,额外的内存缓冲区和CPU开销

  2. 并行TCP流之间存在共享带宽竞争

  3. 套接字集合的实现复杂性很高

  4. 性能依然没有办法满足应用对性能的要求

  5. 需要谨防自我造成的DoS攻击

域名分片

HTTP1.1 的缺口迫使浏览器厂商引入和维护每个主机的并发TCP数,但是这远远无法满足如今应用对性能的要求。因此应用程序开发人员,采取域名分片的形式{shard1, sahrd2}.example.com,将主站域名分配到多个子域去拉取资源,隐士提高并行性,分片越多,并行度越高。

这样做的问题是:

  1. 每个新的主机名都需要额外的DNS查找 (DNS查找和慢启动的额外开销对高延迟客户端的影响很大,所以域名分片过多,有时反而更慢)
  2. 每个额外的套接字消耗双方的额外资源
  3. 需要应用程序开发人员自己管理资源分配

细节优化——减少请求数

  1. 雪碧图
  2. 资源内联
  3. 减少协议开销
  4. 包压缩

HTTP 2.0

HTTP/2 可以让我们的应用更快、更简单、更稳定。对于HTTP1.1存在的诸多问题,HTTP/2将解决方案内置在了传输层中。

HTTP/2 通过支持标头字段压缩多路复用,让应用程序更有效的利用网络资源。HTTP/2 还允许为请求设置优先级,让更重要的请求更快速的完成,从而进一步提升性能。

多路复用实现原理 —— 二进制分帧层

HTTP/2 所有性能增强的核心在于新的二进制分帧层,它定义了如何封装HTTP消息并在客户端与服务器之间传输。HTTP/1.x 以换行符作为纯文本的分隔符,而HTTP/2 将所有传输的信息分割为更小的消息(与逻辑请求或响应消息对应的完整的一个或多个帧)和(最小的通信单位),并采用二进制格式对它们进行编码。将HTTP消息分解为独立互不依赖的二进制编码帧,然后交错发送,最后再在另一端组装是HTTP/2 最重要的一项增强。

binary_framing_layer.svg

multiplexing01.svg

上图同时有三个并行数据流,客户端正在向服务器传输一个DATA帧,与此同时,服务器正在向客户端交错发送数据流1和数据流3。

数据流优先级

数据流依赖关系和权重的组合表达了资源的优先级。注意:该优先级树只是表示传输优先级,实际上由于服务器对不同资源处理速度不一致,客户端可能接受到的顺序不符合预期。

连接持续化

HTTP/2 中的连接都是永久的,也就是每个来源一个连接。

流控制

问题产生与TCP流控制基本相同,为了解决这一问题,HTTP/2 提供了一组简单的构建块,允许客户端和服务器实现自己的数据流和连接级流控制。

服务端推送

HTTP/2 新增的一个比较强大的功能是,服务器可以对一个客户端发送多个响应。它打破了严格的请求-响应语义。

标头压缩

头部压缩可以有效减少消息大小,提升传输性能。实现方式,请求中利用HPACK协议,其中原理就是哈夫曼编码和索引表。为了进一步优化,HPACK压缩上下文包含一个静态表和一个动态表。这两个表类似于字典的角色。

HTTP 3.0

HTTP 3 性能增强的核心在于改变了传输层协议。将前几代基于的TCP协议替换成了UDP协议。从根本上解决了TCP协议的三次握手和慢启动等性能损耗,但是这也带来了新的问题,UDP不具有TCP所具有的可靠性和错误处理机制,无法保证数据完整性能。因此Google工作组,就在UDP之上添加了QUIC层来提供可靠的传输信道,以保障稳定性和数据包的传输顺序等,当然还有安全性。

quic layer.jpg

quic.png

总结

每一代的 HTTP 协议都有自己的核心目标。0.9 和 1.0 版本主要在解决有无问题。 1.1 、2.0 和 3.0 已经从不同程度解决性能问题,小版本和大版本的变化也预示着解决的彻底程度,从最开始的基于原有设计打补丁到最后颠覆性的改变,解决的决心不断增强。

参考文章