HTTP史记 - 从HTTP/1到HTTP/3

avatar
公众号「 微医大前端技术 」

56.gif

权明扬,微医前端技术部,兴趣爱好:吉他、羽毛球、下棋

诞生

说起http必然先了解 《万维网(World Wide Web)》简称WWW

WWW是基于客户机 <=> 服务器方式 '利用链接跳转站点' 和 '传输超文本标记语言(HTML)' 的技术综合。

1989年仲夏之夜,蒂姆·伯纳斯·李成功开发出世界上第一个Web服务器和第一个Web客户机,这个时候能做的还只是一本电子版的电话号码簿。

HTTP (HyperText Transfer Protocol)万维网的基础协议,制定了浏览器与服务器之间的通讯规则。

通常使用的网络(包括互联网)是在TCP/IP协议族的基础上动作的。而HTTP属于它的内部的一个子集。

http不断的实现更多功能,到目前从HTTP 0.9已经演化到了HTTP 3.0。


HTTP/0.9

HTTP问世之初并没有作为标准建立,被正式制定为标准是在1996年公布的HTTP/1.0协议。因此,在这之前的协议被称为HTTP/0.9。

request只有一行且只有一个GET命令,命令后面跟着的是资源路径。

GET /index.$html$

reponse仅包含文件内容本身

<html>
  <body>HELLO WORLD!</body>
</html>

HTTP/0.9没有header的概念,也没有content-type的概念,仅能传递html文件。同样由于没有status code,当发生错误的时候是通过传递回一个包含错误描述的html文件来处理的。

HTTP/1.0

随着互联网技术的飞速发展,HTTP协议被使用的越来越广泛,协议本身的局限性已经不能满足互联网功能的多样性。因此,1996年5月HTTP/1.0诞生,其内容和功能都大大增加了。对比与HTTP/0.9,新的版本包含了以下功能:

  • 在每个request的GET一行后面添加版本号
  • 在response第一行中添加状态行
  • 在request和response中添加header的概念
  • 在header中添加content-type以此可以传输html之外类型的文件
  • 在header中添加content-encoding来支持不同编码格式文件的传输
  • 引入了POST和HEAD命令
  • 支持长连接(默认短连接)
GET /index.html HTTP/1.0
User-Agent: NCSA_Mosaic/2.0 (Windows 3.1)

200 OK
Date: Tue, 15 Nov 1994 08:12:31 GMT
Server: CERN/3.0 libwww/2.17
Content-Type: text/htmlcharset=utf-8 // 类型,编码。
<HTML>
A page with an image
  <IMG src="/image.gif">
<HTML>

content

简单的文字页面自然无法满足用户的需求,于是1.0加入了更多的文件类型

常见Content-Type
text/plantext/htmltext/css
image/jpegimage/pngimage/svg + xml
application/javascriptapplication/zipapplication/pdf

也同样可以用在html中

<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />

Content-encoding

由于支持任意数据格式的发送,因此可以先把数据进行压缩再发送。HTTP/1.0进入了Content-Encoding来表示数据的压缩方式。

  • Content-Encoding: gzip。【表示采用 Lempel-Ziv coding (LZ77) 压缩算法,以及32位CRC校验的编码方式】
  • Content-Encoding: compress。【采用 Lempel-Ziv-Welch (LZW) 压缩算法】
  • Content-Encoding: deflate。【采用 zlib 】

客户端发送请求带有表明我可以接受gzipdeflate两种压缩方式

Accept-Encoding: gzip, deflate

服务器在 Content-Encoding 响应首部提供了实际采用的压缩模式

Content-Encoding: gzip

HTTP/1.0 缺点

  1. 队头阻塞(Head-of-Line Blocking ,每个TCP连接只能发送一个请求。发送数据完毕,连接就关闭,如果还要请求其他资源,就必须再新建一个连接
  2. 默认是短连接,即每个HTTP请求都要使用TCP协议通过三次握手和四次挥手实现
  3. 仅定义了16种状态码

HTTP/1.1

仅仅在HTTP/1.0公布后的几个月,HTTP/1.1发布了,到目前为止HTTP1.1协议都是作为主流的版本,以至于随后的近10年时间里都没有新的HTTP协议版本发布。

对比之前的版本,其主要更新如下:

  • 可以重复使用连接(keep-alive),从而节省时间,不再需要多次打开才能显示嵌入在单个原始文档中的资源
  • 添加了Pipeline,这允许在第一个请求的答案完全传输之前发送第二个请求这降低了通信的延迟
  • chunked机制,分块响应
  • 引入了额外的缓存控制机制
  • 引入了内容协商,包括语言、编码和类型,客户端和服务器现在可以就交换哪些内容达成一致
  • 由于Host标头,从同一 IP 地址托管不同域的能力允许服务器搭配

keep-alive

由于建立一个连接的过程需要DNS解析过程以及TCP的三次握手,但在同服务器获取资源不断的建立和断开链接需要消耗的资源和时间是巨大的,为了提升连接的效率。 HTTP/1.1的及时出现将长连接加入了标准并作为默认实现,服务器端也按照协议保持客户端的长连接状态,一个服务端上的多个资源都可以通过这一条连接多个request来获取

可以在request header中引入如下信息来告知服务器完成一次request请求后不要关闭连接。

Connection: keep-alive

服务器端也会答复一个相同的信息表示连接仍然有效,但是在当时这只是属于程序员的自定义行为,在1.0中没有被纳入标准, 这其中的提升对于通讯之间的效率提升几乎是倍增的,

这也为管线化方式(pipelining)打下基础。

Pipeline (管线化)

HTTP/1.1尝试通过HTTP管线化技术来解决性能瓶颈,诞生了pipeline机制,如图从每次response返回结果才能进行下一次request,变为一次连接上多个http request不需要等待response就可以连续发送的技术。

image.png

不幸的是因为HTTP是一个无状态的协议,一个体积很大的或慢response仍然会阻塞后面所有的请求,每条request无法知道哪条response是返回给他的,服务端只能根据顺序来返回response,这就是队头阻塞,这导致主流浏览器上默认下该功能都是关闭状态,在http2.0中会解决这个问题

host头域

在 HTTP1.0 中认为每台服务器都绑定一个唯一的 IP 地址,因此,请求消息中的 URL 并没有传递主机名(hostname),1.1中新增的host用来处理一个 IP 地址上面多个虚拟主机的情况。

在请求头域中新增了Host字段,其用来指定服务器的域名。有了Host字段,在同一台服务器上就可以搭建不同的网站了,这也为后来虚拟化的发展建好啦地基。

Host: www.alibaba-inc.com

cache机制

Cache不仅可以提高用户的访问速率,在移动端设备上还可以为用户极大节省流量。因此,在HTTP/1.1中新增了很多与Cache相关的头域并围绕这些头域设计了更灵活、更丰富的Cache机制。

Cache机制需要解决的问题包括:

  1. 判断哪些资源可以被Cache及访问访问策略
  2. 在本地判断Cache资源是否已经过期
  3. 向服务端发起问询,查看已过期的Cache资源是否在服务端发生了变化

chunked机制

建立好链接之后客户端可以使用该链接发送多个请求,用户通常会通过response header中返回的Content-Length来判断服务端返回数据的大小。但随着网络技术的不断发展,越来越多的动态资源被引入进来,这时候服务端就无法在传输之前知道待传递资源的大小,也就无法通过Content-Length来告知用户资源大小。服务器可以一边动态产生资源,一边传递给用户,这种机制称为“分块传输编码”(Chunkded Transfer Encoding),允许服务端发送给客户端的数据分为多个部分,此时服务器端需要在header中添加“Transfer-Encoding: chunked”头域来替代传统的“Content-Length。

Transfer-Encoding: chunked

HTTP 缓存机制

相比 HTTP 1.0,HTTP 1.1 新增了若干项缓存机制:

强缓存

强缓存,是浏览器优先命中的缓存,速度最快。当我们在状态码后面看到 (from memory disk) 时,就表示浏览器从内存种读取了缓存,当进程结束后,也就是 tab 关闭以后,内存里的数据也将不复存在。只有当强缓存不被命中的时候,才会进行协商缓存的查找。

Pragma

Pragma头域是HTTP/1.0的产物。目前仅作为与HTTP/1.0的向后兼容而定义。它现在仅在请求首部中出现,表示要求所有中间服务器不返回缓存的资源,与Cache-Control: no-cache的意义相同。

Pragma: no-cache

Expires

Expires仅在响应头域中出现,表示资源的时效性当发生请求时,浏览器将会把 Expires  的值与本地时间进行对比,如果本地时间小于设置的时间,则读取缓存。

Expires 的值为标准的 GMT 格式:

Expires: Wed, 21 Oct 2015 07:28:00 GMT

这里需要注意的是:当header中同时存在Cache-Control: max-age=xx和Expires的时候,以Cache-Control: max-age的时间为准

Cache-Control

由于 Expires 的局限性, Cache-Control 登场了, 下面说明几个常用的字段

  • no-store:缓存不应存储有关客户端请求或服务器响应的任何内容
  • no-cache:在发布缓存副本之前,强制要求缓存把请求提交给原始服务器进行验证
  • max-age:相对过期时间,单位为秒(s),告知服务器资源在多少以内是有效的,无需向服务器请求

协商缓存

当浏览器没有命中强缓存后,便会命中协商缓存,协商缓存由以下几个 HTTP 字段控制。

Last-Modified

服务端将资源传送给客户端的时候,会将资源最后的修改时间以 Last-Modified: GMT 的形式加在实体首部上返回。

Last-Modified: Fri, 22 Jul 2019 01:47:00 GMT

客户端接收到后会为此资源信息做上标记,等下次重新请求该资源的时候将会带上时间信息给服务器做检查,若传递的值与服务器上的值一致,则返回 304 ,表示文件没有被修改过,若时间不一致,则重新进行资源请求并返回 200

优先级

强缓存 --> 协商缓存 Cache-Control  ->  Expires  -> ETag -> Last-Modified 

新增了五种请求方法

OPTIONS: 浏览器为确定跨域请求资源的安全做的预请求

PUT:从客户端向服务器传送的数据取代指定的文档的内容

DELETE :请求服务器删除指定的页面

TRACE: 回显服务器收到的请求,主要用于测试或诊断

CONNECT: HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器

新增一系列的状态码

可以参考状态码大全

Http1.1缺陷

  1. 高延迟,带来页面加载速度的降低,(网络延迟问题只要由于队头阻塞,导致宽带无法被充分利用)
  2. 无状态特性,带来巨大的Http头部
  3. 明文传输,不安全
  4. 不支持服务器推送消息

HTTP/2.0

根据时代的发展网页变得更加复杂。其中一些甚至本身就是应用程序。显示了更多的视觉媒体,增加了交互性的脚本的数量和大小也增加了。更多的数据通过更多的 HTTP 请求传输,这为 HTTP/1.1 连接带来了更多的复杂性和开销。为此,谷歌在 2010 年代初实施了一个实验性协议 SPDY。鉴于SPDY的成功,HTTP/2也采用了SPDY作为整个方案的蓝图进行开发。HTTP/2 于 2015 年 5 月正式标准化

HTTP/2 于 HTTP/1.1 区别:

  • 二进制帧层
  • 多路复用协议。可以通过同一连接发出并行请求,从而消除 HTTP/1.x 协议的约束
  • 头部压缩算法HPACK。由于一些请求在一组请求中通常是相似的,因此这消除了传输数据的重复和开销
  • 它允许服务器通过称为服务器推送的机制在客户端缓存中填充数据 一张图来理解HTTP/2 和 HTTP/1.1 image.png

Header压缩

HTTP1.x的header带有大量信息,而且每次都要重复发送,为 HTTP/2 的专门量身打造的 HPACK 便是类似这样的思路延伸。它使用一份索引表来定义常用的 HTTP Header,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小。

image.png

看上去协议的格式和HTTP1.x完全不同了,实际上HTTP2并没有改变HTTP1.x的语义,只是把原来HTTP1.x的header和body部分用frame重新封装了一层而已。

多路复用

为了解决HTTP/1.x中存在的队头阻塞问题,HTTP/2提出了多路复用的概念。即将一个request/response作为一个stream,并将一个stream根据负载分为多种类型的frame(例如 header frame,data frame等),在同一条connection之上可以混合发送分属于不同stream的frame,这样就实现了同时发送多个request的功能,多路复用意味着线头阻塞将不再是一个问题。

image.png

HTTP/2 虽然通过多路复用解决了 HTTP 层的队头阻塞,但仍然存在 TCP 层的队头阻塞

服务端推送 server push

服务可以主动向客户端发送消息。在浏览器刚请求 HTML 的时候,服务端会把某些资源存在一定的关联性 JS、CSS 等文件等静态资源主动发给客户端,这样客户端可以直接从本地加载这些资源,不用再通过网络再次请求,以此来达到节省浏览器发送request请求的过程。 image.png

使用服务器推送

Link: </css/styles.css>; rel=preload; as=style,
</img/example.png>; rel=preload; as=image

可以看到服务器initiator中的push状态表示这是服务端进行主动推送。

image.png

对于主动推送的文件势必会带来多余或已经浏览器已有一份的文件

客户端使用一个简洁的 Cache Digest 来告诉服务器,哪些东西已经在缓存,因此服务器也就会知道哪些是客户端所需要的。

服务器和客户端在HTTP/2连接内用于交换帧数据的独立双向序列,HTTP/2 在单个 TCP 连接上虚拟出多个 Stream, 多个 Stream 实现对一个 TCP 连接的多路复用, 为了合理地利用传输链路, 实现在有限资源内达到传输性能的最优化。

  • 所有的通信都建立在一个TCP连接上,可以传递大量的双向流通的流
  • 每个流都有独一无二的标志和优先级
  • 每个消息都是逻辑上的请求和相应消息。由一个或者多个帧组成
  • 来自不同流的帧可以通过帧头的标志来关联和组装起来

image.png

流的概念提出是为了实现多路复用,在单个连接上实现同时进行多个业务单元数据的传输

二进制帧层

在HTTP/1.x中,用户为了提高性能建立多个TCP连接.会导致队头阻塞和重要TCP连接不能稳定获得 HTTP/2中的二进制帧层允许请求和响应数据分割为更小的帧,并且它们采用二进制编码(http1.0基于文本格式)。多个帧之间可以乱序发送,根据帧首部的流(比如每个流都有自己的id)表示可以重新组装

image.png

显然这对二进制的计算机是非常友好,无需再将收到明文的报文转成二进制,而是直接解析二进制报文,进一步提高数据传输的效率。

每一个帧可看做是一个学生,流是小组(流标识符为帧的属性值),一个班级(一个连接)内学生被分为若干个小组,每一个小组分配不同的具体任务,多个小组任务可同时并行在班级内执行。一旦某个小组任务耗时严重,但不会影响到其它小组任务正常执行。

最后我们来看一看理想状态下http2带来的提升

v2-4589b6566a444100bf32aca829f66a73_720w.gif

缺点

  1. TCP以及TCP+TLS建立连接的延迟(握手延迟)
  2. http2.0中TCP的队头阻塞依然没有彻底解决,连接双方的有任一个数据包丢失,或任一方的网络中断,整个TCP连接就会暂停,丢失的数据包需要被重新传输,从而阻塞该TCP连接中的所有请求,反而在网络较差或不稳定情况下,使用多个连接表现更好。

HTTP/3.0 (HTTP-over-QUIC)

在限定条件下,TCP下解决队头阻塞的问题相当困难,但是随着互联网的爆炸式发展,更高的稳定性和安全性需要得到满足,谷歌在2016年11月国际互联网工程任务组(IETF)召开了第一次QUIC(Quick UDP Internet Connections)工作组会议,制定的一种基于UDP的低时延的互联网传输层协议,HTTP-over-QUIC于2018年11月更名为HTTP/3。 image.png

0-RTT 握手

tcp中 客户端发送syn包(syn seq=x)到服务器, 服务器接收并且需要发送(SYN seq =y; ACK x+1)包给客户端,客户端向服务器发送确认包ACK(seq = x+1; ack=y+1),至此客户端和服务器进入ESTABLISHED状态,完成三次握手。

1-RTT

  1. 客服端生成一个随机数 a 然后选择一个公开的加密数 X ,通过计算得出 a*X = A, 将X 和 A发送给服务端
  2. 客服端生成一个随机数 b,通过计算得出 b*X = B, 将B发送给服务端
  3. 客户端使用ECDH生成通讯密钥 key = aB = a(b*X)
  4. 服务器使用ECDH生成通讯密钥 key = bA = b(a*X)
sequenceDiagram
客服端->>服务端: clinet Hello
服务端-->>客服端: Server Hello

所以,这里的关键就是 ECDH 算法,a 和 b 是客户端和服务器的私钥,是不公开的,即使知道 A、X,通过 A = a*X 公式也是无法推导出 a 的,保证了私钥的安全性。

0-RTT

0-RTT则是客户端缓存了 ServerConfig(B=b*X),下次建连直接使用缓存数据计算通信密钥:

sequenceDiagram
客服端->>服务端: clinet Hello + 应用数据
服务端-->>客服端: ACK
  1. 客户端:生成随机数 c,选择公开的大数 X,计算 A=cX,将 A 和 X 发送给服务器,也就是 Client Hello 消息后,客户端直接使用缓存的 B 计算通信密钥 KEY = cB = cbX,加密发送应用数据

  2. 服务器:根据 Client Hello 消息计算通信密钥 key = bA = b(c*X)

客户端不需要经过握手直接通过缓存的B生成key就可以发送应用数据。

再来思考一个问题:假设攻击者记录下所有的通信数据和公开参数A1,A2,一旦服务器的随机数 b(私钥)泄漏了,那之前通信的所有数据就都可以破解了。

image.png

为了解决这个问题,需要为每次会话都创建一个新的通信密钥,来保证前向安全性

有序交付

QUIC 是基于 UDP 协议的,而 UDP 是不可靠传输协议,QUIC 在每个数据包都设有一个 offset 字段(偏移量),接收端根据 offset 字段就可以对异步到达的数据包进行排序了,保证了有序性。

sequenceDiagram
客服端->>服务端: PKN=1;offset=0
客服端->>服务端: PKN=2;offset=1
客服端->>服务端: PKN=3;offset=2
服务端-->>客服端: SACK = 1,3
客服端->>服务端: 重传:PKN=4;offset=1

队头堵塞

HTTP/2 之所以存在 TCP 层的队头阻塞,是因为所有请求流都共享一个滑动窗口,而QUIC中给每个请求流都分配一个独立的滑动窗口

image.png

A 请求流上的丢包不会影响 B 请求流上的数据发送。但是,对于每个请求流而言,也是存在队头阻塞问题的,也就是说,虽然 QUIC 解决了 TCP 层的队头阻塞,但仍然存在单条流上的队头阻塞。这就是 QUIC 声明的无队头阻塞的多路复用。

连接迁移

连接迁移:当客户端切换网络时,和服务器的连接并不会断开,仍然可以正常通信,对于 TCP 协议而言,这是不可能做到的。因为 TCP 的连接基于 4 元组:源 IP、源端口、目的 IP、目的端口,只要其中 1 个发生变化,就需要重新建立连接。但 QUIC 的连接是基于 64 位的 Connection ID,网络切换并不会影响 Connection ID 的变化,连接在逻辑上仍然是通的。

image.png

假设客户端先使用 IP1 发送了 1 和 2 数据包,之后切换网络,IP 变更为 IP2,发送了 3 和 4 数据包,服务器根据数据包头部的 Connection ID 字段可以判断这 4 个包是来自于同一个客户端。QUIC 能实现连接迁移的根本原因是底层使用 UDP 协议就是面向无连接的。

最后我们一张图来看一下http的升级

image.png

[参考文献]

developer.mozilla.org/en-US/docs/… datatracker.ietf.org/doc/html/rf…

副本_副本_未命名_自定义px_2021-09-18-0.gif