查看原文
其他

【第2651期】HTTP史记 - 从HTTP/1到HTTP/3

权明扬 前端早读课 2022-08-08

前言

近日基于 QUIC 协议的 HTTP/3 正式公布。今日前端早读课文章由微医 @权明扬分享,公号:微医大前端技术授权。

正文从这开始~~

诞生

说起 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/html;charset=utf-8 // 类型,编码。
<HTML>
A page with an image
<IMG src="/fwc/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 】

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

Accept-Encoding: gzip, deflate

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

Content-Encoding: gzip
HTTP/1.0 缺点
  • 队头阻塞(Head-of-Line Blocking ,每个 TCP 连接只能发送一个请求。发送数据完毕,连接就关闭,如果还要请求其他资源,就必须再新建一个连接

  • 默认是短连接,即每个 HTTP 请求都要使用 TCP 协议通过三次握手和四次挥手实现

  • 仅定义了 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 就可以连续发送的技术。


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

host 头域

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

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

Host: www.demo.com
cache 机制

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

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

  • 判断哪些资源可以被 Cache 及访问访问策略

  • 在本地判断 Cache 资源是否已经过期

  • 向服务端发起问询,查看已过期的 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 缺陷
  • 高延迟,带来页面加载速度的降低,(网络延迟问题只要由于队头阻塞,导致宽带无法被充分利用)

  • 无状态特性,带来巨大的 Http 头部

  • 明文传输,不安全

  • 不支持服务器推送消息

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

Header 压缩

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

看上去协议的格式和 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 的功能,多路复用意味着线头阻塞将不再是一个问题。

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

服务端推送 server push

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

使用服务器推送

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

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

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

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

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

  • 所有的通信都建立在一个 TCP 连接上,可以传递大量的双向流通的流

  • 每个流都有独一无二的标志和优先级

  • 每个消息都是逻辑上的请求和相应消息。由一个或者多个帧组成

  • 来自不同流的帧可以通过帧头的标志来关联和组装起来

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

二进制帧层

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

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

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

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

缺点

  • TCP 以及 TCP+TLS 建立连接的延迟(握手延迟)

  • 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。

0-RTT 握手

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

1-RTT
  • 客服端生成一个随机数 a 然后选择一个公开的加密数 X ,通过计算得出 a*X = A, 将 X 和 A 发送给服务端

  • 客服端生成一个随机数 b,通过计算得出 b*X = B, 将 B 发送给服务端

  • 客户端使用 ECDH 生成通讯密钥 key = aB = a(b*X)

  • 服务器使用 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

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

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

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

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

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

有序交付

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 中给每个请求流都分配一个独立的滑动窗口。


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

连接迁移

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


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

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

参考文献

  • https://developer.mozilla.org/en-US/docs/Web/HTTP

  • https://datatracker.ietf.org/doc/html/rfc7540

关于本文
作者:@权明扬
原文:https://mp.weixin.qq.com/s/B7K00-wTUSmy87caDHRWFA

关于【HTTP】相关推荐,欢迎读者们自荐投稿,前端早读课等你。

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存