写点什么

二、《图解 HTTP》- HTTP 协议历史发展(重点)

作者:懒时小窝
  • 2022 年 8 月 12 日
    广东
  • 本文字数:14559 字

    阅读完需:约 48 分钟

二、《图解HTTP》- HTTP协议历史发展(重点)

#tjhttp 二、《图解 HTTP》- HTTP 协议历史发展(重点)

知识点

  1. 请求和响应报文的结构。

  2. HTTP 协议进化历史,介绍不同 HTTP 版本从无到有的重大特性改变。(重点)

  3. HTTP 几个比较常见的问题讨论。

2.0 介绍

这一章节基本上大部分为个人扩展,因为书中的内容讲的实在是比较浅。本文内容非常长,另外哪怕这么长也只是讲到了 HTTP 协议的一部分而已,HTTP 协议本身十分复杂。

2.1 请求和响应报文结构

请求报文的基本内容:



请求内容需要客户端发给服务端:


GET /index.htm HTTP/1.1 Host: hackr.jp
复制代码


响应报文的基本内容:



服务器按照请求内容处理结果返回:


开头部分是 HTTP 协议版本,紧接着是状态码 200 以及原因短语。


下一行则包含了创建响应的日期时间,包括了首部字段的属性。


HTTP/1.1 200 OK Date: Tue, 10 Jul 2012 06:50:15 GMT Content-Length: 362 Content-Type: text/html
<html> ……
复制代码

2.2 HTTP 进化历史

2.2.1 概览

我们复盘 HTTP 的进化历史,下面是抛去所有细节,整个 HTTP 连接大致的进化路线。


注意:有关协议的升级内容挑了具备代表性的部分,完整内容需要阅读 RFC 原始协议了解


  • http0.9:只具备最基础的 HTTP 连接模型,在非常短的一段时间内存在,后面被快速完善。

  • http1.0: 1.0 版本中每个 TCP 连接只能发送一个请求,数据发送完毕连接就关闭,如果还要请求其他资源,就必须重新建立 TCP 连接。(TCP 为了保证正确性和可靠性需要客户端和服务器三次握手和四次挥手,因此建立连接成本很高)

  • http1.1:

  • 长连接:新增 Connection 字段,默认为 keep-alive,保持连接不断开,即 TCP 连接默认不关闭,可以被多个请求复用;

  • 管道化:在同一个 TCP 连接中,客户端可以发送多个请求,但响应的顺序还是按照请求的顺序返回,在服务端只有处理完一个回应,才会进行下一个回应;

  • host 字段:Host 字段用来指定服务器的域名,这样就可以将多种请求发往同一台服务器上的不同网站,提高了机器的复用,这个也是重要的优化;

  • HTTP/2:

  • 二进制格式:1.x 是文本协议,然而 2.0 是以二进制帧为基本单位,可以说是一个二进制协议,将所有传输的信息分割为消息和帧,并采用二进制格式的编码,一帧中包含数据和标识符,使得网络传输变得高效而灵活;

  • 多路复用:2.0 版本的多路复用多个请求共用一个连接,多个请求可以同时在一个 TCP 连接上并发,主要借助于二进制帧中的标识进行区分实现链路的复用;

  • 头部压缩:2.0 版本使用使用 HPACK 算法对头部 header 数据进行压缩,从而减少请求的大小提高效率,这个非常好理解,之前每次发送都要带相同的 header,显得很冗余,2.0 版本对头部信息进行增量更新有效减少了头部数据的传输;

  • 服务端推送:在 2.0 版本允许服务器主动向客户端发送资源,这样在客户端可以起到加速的作用;

  • HTTP/3:


​ 这个版本是划时代的改变,在 HTTP/3 中,将弃用TCP协议,改为使用基于UDP协议的QUIC协议实现。需要注意 QUIC 是谷歌提出的(和 2.0 的 SPDY 一样),QUIC 指的是快速 UDP Internet 连接,既然使用了 UDP,那么也意味着网络可能存在丢包和稳定性下降。谷歌当然不会让这样的事情发生,所以他们提出的 QUIC 既可以保证稳定性,又可以保证 SSL 的兼容,因为 HTTP3 上来就会和 TLS1.3 一起上线。


​ 基于这些原因,制定网络协议 IETF 的人马上基本都同意了 QUIC 的提案(太好了又能白嫖成果),于是 HTTP3.0 就这样来了。但是这只是最基本的草案,后续的讨论中希望 QUIC 可以兼容其他的传输协议,最终的排序如下 IP / UDP / QUIC / HTTP。另外 TLS 有一个细节优化是在进行连接的时候浏览器第一次就把自己的密钥交换的素材发给服务器,这样进一步缩短了交换的时间。


​ 为什么 HTTP3.0 要从协议根本上动刀,那是因为 HTTP/2 虽然解决了 HTTP 协议无法多路复用的问题,但是没有从 TCP 层面解决问题,具体的 TCP 问题体现如下:


  • 队头阻塞HTTP/2 多个请求跑在一个 TCP 连接中,如果此时序号较低的网络请求被阻塞,那么即使序列号较高的 TCP 段已经被接收了,应用层也无法从内核中读取到这部分数据,从 HTTP 视角看就是多个请求被阻塞了,并且页面也只是加载了一部分内容;

  • TCPTLS 握手时延缩短TCL 三次握手和 TLS 四次握手,共有 3-RTT 的时延,HTPT/3 最终压缩到 1 RTT(难以想象有多快);

  • 连接迁移需要重新连接,移动设备从 4G 网络环境切换到 WIFI 时,由于 TCP 是基于四元组来确认一条 TCP 连接的,那么网络环境变化后,就会导致 IP 地址或端口变化,于是 TCP 只能断开连接,然后再重新建立连接,切换网络环境的成本高;


RTT:RTT Round Trip Time 的缩写,简单来说就是通信一来一回的时间


下面是官方对于 RTT 速度缩短的对比,最终只在初次连接需要 1RTT 的密钥交换,之后的连接均为 0RTT!


2.2.2 HTTP 0.9

这个版本基本就是草稿纸协议,但是它具备了 HTTP 最原始的基础模型,比如只有 GET 命令,没有 Header 信息,传达的目的地也十分简单,没有多重数据格式,只有最简单的文本。


此外服务器一次建立发送请求内容之后就会立马关闭 TCP 连接,这时候的版本一个 TCP 还只能发送一个 HTTP 请求,采用一应一答的方式。


当然在后面的版本中对于这些内容进行升级改进。

2.2.3 HTTP 1.0

协议原文:https://datatracker.ietf.org/doc/html/rfc1945


显然 HTTP 0.9 缺陷非常多并且不能满足网络传输要求。浏览器现在需要传输更为复杂的图片,脚本,音频视频数据。


1996 年 HTTP 进行了一次大升级,主要的更新如下:


  • 增加更多请求方法:POST、HEAD

  • 添加 Header 头部支持更多的情况变化

  • 第一次引入协议版本号的概念

  • 传输不再限于文本数据

  • 添加响应状态码


在 HTTP1.0 协议原文中开头有一句话:


原文:
Status of This Memo:
This memo provides information for the Internet community. This memo
does not specify an Internet standard of any kind. Distribution of
this memo is unlimited.
复制代码


这份协议用了 memo 这个单词,memo 的意思是备忘录,也就是说虽然洋洋洒洒写了一大堆看似类似标准的规定,但是实际上还是草稿,没有规定任何的协议和标准,另外这份协议是在麻省理工的一个分校起草的,所以可以认为是讨论之后临时的一份方案。


HTTP1.0 主要改动点介绍


在了解了这是一份备忘录的前提下,我们来介绍协议的一些重要概念提出。


HTTP1.0 定义了无状态、无连接的应用层协议,纸面化定义了 HTTP 协议本身。


无状态、无连接定义:HTTP1.0 规定服务器和客户端之间可以保持短暂连接,每次请求都需要发起一次新的 TCP 连接(无连接),连接完成之后立马断开连接,同时服务器不负责记录过去的请求(无状态)。


这样就出现一个问题,那就是通常一次访问需要多个 HTTP 请求,所以每一次请求都要建立一次 TCP 连接效率非常低,此外还存在两个比较严重的问题:队头阻塞无法复用连接


队头阻塞:因为 TCP 连接是类似排队的方式发送,如果前一个请求没有到达或者丢失,后一个请求就需要等待前面的请求完成或者完成重传才能进行请求。


无法复用连接:TCO 连接资源本身就是有限的,同时因为 TCP 自身调节(滑动窗口)的关系,TCP 为了防止网络拥堵会有一个慢启动的过程。


RTT 时间计算:TCP 三次握手共计需要至少 1.5 个 RTT,注意是 HTTP 连接不是 HTTPS 连接。


滑动窗口:简单理解是 TCP 提供一种可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量的机制

2.2.4 HTTP1.1

HTTP 1.1 的升级改动较大,主要的改动点是解决建立连接传输数据的问题,在 HTTP1.1 中引入了下面的内容进行改进:


  • 长连接:也就是Keep-alive头部字段,让 TCP 默认不进行关闭,保证一个 TCP 可以传递多个 HTTP 请求

  • 并发连接:一个域名允许指定多个长连接(注意如果超出上限依然阻塞);

  • 管道机制:一个 TCP 可以同时发送多个请求(但是实际效果很鸡肋还会增加服务器压力,所以通常被禁用);

  • 增加更多方法:PUT、DELETE、OPTIONS、PATCH 等;

  • HTTP1.0 新增缓存字段(If-Modified-Since, If-None-Match),而 HTTP1.1 则引入了更多字段,比如Entity tag,If-Unmodified-Since, If-Match, If-None-Match等更多缓存头部的缓存策略。

  • 允许数据分块传输(Chunked),对于大数据传输很重要;

  • 强制使用 Host 头部,为互联网的主机托管创造条件;

  • 请求头中引入了 支持断点续传的 range 字段;


下面为书中第二章节记录的笔记内容,写书日期是 HTTP1.1 蓬勃发展的时候 ,基本对应了 HTTP1.1 协议中的一些显著特点。


无状态协议


HTTP 协议自身不具备保存之前发送过的请求或响应的功能,换句话说 HTTP 协议本身只保证协议报文的格式符合 HTTP 的要求,除此之外的传输和网络通信其实都是需要依赖更下层的协议完成。


HTTP 设计成如此简单的形式,本质上就是除开协议本身外的内容一切都不考虑,达到高速传输的效果。但是因为 HTTP 的简单粗暴,协议本身需要很多辅助的组件来完成 WEB 的各种访问效果,比如保持登陆状态,保存近期的浏览器访问信息,记忆密码等功能,这些都需要 Cookie 以及 Session 来完成。


HTTP/1.1 引入了 Cookie 以及 Session 协助 HTTP 完成状态存储等操作。


请求资源定位


HTTP 大多数时候是通过 URL 的域名来访问资源的,定位 URL 要访问的真实服务需要 DNS 的配合,DNS 是什么这里不再赘述。


如果是对服务器访问请求,可以通过 * 的方式发起请求,比如OPTIONS * HTTP/1.1


请求方法


实际上用的比较多的还是GETPOST


GET:通常视为无需要服务端校验可以直接通过 URL 公开访问的资源,但是通常会在 URL 中携带大量的请求参数,但是这些参数通常无关敏感信息所以放在 URL 当中非常方便简单。


POST:通常情况为表单提交的参数,需要服务端的拦截校验才能获取,比如下载文件或者访问一些敏感资源,实际上 POST 请求要比 GET 请求使用更为频繁,因为 POST 请求对于请求的数据进行“加密”保护。相比于 GET 请求要安全和靠谱很多。



持久连接


所谓的持久连接包含一定的历史原因,HTTP1.0 最早期每次访问和响应都是一些非常小的资源交互,所以一次请求结束之后基本就可以和服务端断开,等到下一次需要再次请求再次连接。


但是随着互联网发展,资源包越来越大,对于互联网的需求和挑战也越来越大。


在后续 HTTP/1.1 中所有的连接默认都是持久连接,目的是减少客户端和服务端的频繁请求连接和响应。


支持 HTTP1.1 需要双方都能支持持久连接才能完成通信。


管道化


注意 HTTP 真正意义上的全双工的协议是在 HTTP/2 才实现的,实现的核心是多路复用


管道化可以看做是为了让半双工的 HTTP1.1 也能支持全双工协议的一种强化,通俗的话说就是围魏救赵


全双工协议:指的是 HTTP 连接的两端不需要等待响应数据给对方就可以直接发送请求给对方,实现同一时间内同时处理多个请求和响应的功能。


HTTP/1.1 允许多个 http 请求通过一个套接字同时被输出 ,而不用等待相应的响应(这里提示一下管道化同样需要连接双方都支持才能完成)。


需要注意这里本质上是在一个 TCP 请求封装了多次请求然后直接丢给服务端去处理,客户端接下来可以干别的事情,要么等待服务端慢慢等待,要么自己去访问别的资源。


客户端通过 FIFO 队列把多个 TCP 请求封装成一个发给了服务端,服务端虽然可以通过处理 FIFO 队列的多个请求,但是必须等所有请求完成再按照 FIFO 发送的顺序挨个响应回去,也就是说其实并没有根本上解决堵塞问题。


管道化的技术虽然很方便,但是限制和规矩比好处要多得多,并且有点脱裤子放屁的意思。结果是并没有十分普及也没有多少服务端使用,多数的 HTTP 请求也会禁用管道化防止服务端请求堵塞迟迟不进行响应。


管道化小结


  1. 实际上管道化可以看做原本阻塞在客户端一条条处理的请求,变为阻塞在服务端的一条条请求。

  2. 管道化请求通常是 GET 和 HEAD 请求,POST 和 PUT 不需要管道化, 管道化只能利用已存在的keep-alive连接。

  3. 管道化是 HTTP1.1 协议下,服务器不能很好处理并行请求的改进,但是这个方案不理想,围魏救赵失败并且最终被各大浏览器禁用掉。

  4. FIFO 队列的有序和 TCP 的有序性区别可以简单认为是强一致性和弱一致性的区别。FIFO 队列有序性指的是请求和响应必须按照队列发送的规则完全一样,而 TCP 仅仅是保证了发送和响应的大致逻辑顺序,真实的情况和描述的情况可能不一致。

  5. 因为管道是把累赘丢给了服务端,从客户端的角度来看自己完成了全双工的通信。实际上这只是伪全双工通信。


Cookie


Cookie 的内容不是本书重点,如果需要了解相关知识可以直接往上查询资料了解,基本一抓一大把。

2.2.5 [HTTP/2](HTTP/2 (ietf.org))

HTTP2 的协议改动比较大,从整体上来看主要是下面一些重要调整:


  • SPDY:这个概念是谷歌提出的,起初是希望作为一个独立协议,但是最终 SPDY 的相关技术人员参与到了 HTTP/2,所以谷歌浏览器后面全面支持 HTTP/2 放弃了 SPDY 单独成为协议的想法,对于 SPDY,具有如下的改进点:

  • HTTP Speed + Mobility:微软提出改善移动端通信的速度和性能标准,它建立在 Google 公司提出的 SPDY 与 WebSocket 的基础之上。

  • Network-Friendly HTTP Upgrade:移动端通信时改善 HTTP 性能。


从三者的影响力来看,显然是 Google 的影响力是最大的,从 HTTP3.0 开始以谷歌发起可以看出 HTTP 协议的标准制定现在基本就是谷歌说了算。


接着我们就来看看最重要 SPDY,谷歌是一个极客公司,SPDY 可以看做是 HTTP1.1 和 HTTP/2 正式发布之间谷歌弄出来的一个提高 HTTP 协议传输效率的“玩具” ,重点优化了 HTTP1.X 的请求延迟问题,以及解决 HTTP1.X 的安全性问题:


  • 降低延迟(多路复用):使用多路复用来降低高延迟的问题,多路复用指的是使用 Stream 让多个请求可以共享一个 TCP 连接,解决 HOL Blocking(head of line blocking)(队头阻塞)的问题,同时提升带宽利用率。

  • HTTP1.1 中keep-alive用的是http pipelining本质上也是multiplexing,但是具体实现方案不理想 。

  • 主流浏览器都默认禁止pipelining,也是因为 HOL 阻塞问题导致。

  • 服务端推送:HTTP1.X 的推送都是半双工,所以在 2.0 是实现真正的服务端发起请求的全双工,另外在 WebSocket 在这全双工一块大放异彩。

  • 请求优先级:针对引入多路复用的一个兜底方案,多路复用使用多个 Stream 的时候容易单请求阻塞问题。也就是前文所说的和管道连接一样的问题,SPDY 通过设置优先级的方式让重要请求优先处理,比如页面的内容应该先进行展示,之后再加载 CSS 文件美化以及加载脚本互动等等,实际减少用户不会在等待过程中关闭页面的几率。

  • Header 压缩:HTTP1.X 的 header 很多时候都是多余的,所以 2.0 会自动选择合适的压缩算法自动压缩请求加快请求和响应速度。

  • 基于 HTTPS 的加密协议传输:HTTP1.X 本身是不会加入 SSL 加密的,而 2.0 让 HTTP 自带 SSL,从而提高传输可靠和稳定性。


这些内容在后续大部分都被 HTTP/2 采纳,下面就来看看 HTTP/2 具体的实施细节。


HTTP/2 具体实施(重点)


当然这一部分也只讲到了协议中一些重点的升级内容,详细内容请参考“参考资料”或点击 HTTP/2 的标题。


二进制帧(Stream)


HTTP/2 使用流(二进制)替代 ASCII 编码传输提升传输效率,客户端发送的请求都会封装为带有编号的二进制帧,然后再发送给服务端处理。


HTTP/2 通过 一个 TCP 连接完成多次请求操作,服务端接受流数据并且检查编号将其合并为一个完整的请求内容,这样同样需要按照二进制帧的拆分规则拆分响应。像这样利用二进制分帧 的方式切分数据,客户端和服务端只需要一个请求就可以完成通信,也就是 SPDY 提到的多个 Stream 合并到一个 TCP 连接中完成。


二进制分帧把数据切分成更小的消息和帧,采用了二进制的格式进行编码,在HTTP1.1 当中首部消息封装到 Headers 当中,然后把Request body 封装到 Data 帧。



使用二进制分帧目的是向前兼容,需要在应用层和传输层之间加一层二进制分帧层,让 HTTP1.X 协议更加简单的升级同时不会对过去的协议产生冲突 。


帧、消息、Stream 之间的关系


  • 帧:可以认为是流当中的最小单位。

  • 消息:表示 HTTP1.X 中的一次请求。

  • Stream:包含 1 条或者多条 message。


二进制分帧结构


二进制分帧结构主要包含了头部帧和数据帧两个部分,头部在帧数只有 9 个字节,注意 R 属于标志位保留。所以整个算下来是:


3 个字节帧长度+1 个字节帧类型 + 31bit 流标识符、1bit 未使用标志位 构成。



帧长度:数据帧长度,24 位的 3 字节大小,取值为 2^14(16384) - 2^24(1677215)之间,接收方的 SETTINGS_MAS_FRAM_SIZE 设置。


帧类型:分辨数据帧还是控制帧。


标志位:携带简单控制信息,标志位表示流的优先级。


流标识符:表示帧属于哪一个流的,上限为 2 的 31 次方,接收方需要根据流标识的 ID 组装还原报文,同一个 Stream 的消息必须是有序的。此外客户端和服务端分别用奇数和偶数标识流,并发流使用了标识才可以应用多路复用。


R:1 位保留标志位,暂未定义,0x0 为结尾。


帧数据:实际传输内容由帧类型指定。


如果想要知道更多细节,可以参考“参考资料”部分的官方介绍以及结合 WireShark 抓包使用,本读书笔记没法面面俱到和深入


最后是补充帧类型的具体内容,帧类型定义了 10 种类型的帧数:



多路复用 (Multiplexing)


有了前面二进制帧结构的铺垫,现在再来看看多路复用是怎么回事,这里首先需要说明在过去的 HTTP1.1 中存在的问题:


同一时间同一域名的请求存在访问限制,超过限制的请求会自动阻塞。


在传统的解决方案中是利用多域名访问以及服务器分发的方式让资源到特定服务器加载,让整个页面的响应速度提升。比如利用多个域名的 CDN 进行访问加速


随着 HTTP/2 的更新,HTTP2 改用了二进制帧作为替代方案,允许单一的 HTTP2 请求复用多个请求和响应内容,也就是说可以一个包里面打包很多份“外卖”一起给你送过来。



此外,流控制数据也意味着可以支持多流并行而不过多依赖 TCP,因为通信缩小为一个个帧,帧内部对应了一个个消息,可以实现并行的交换消息。


Header 压缩(Header Compression)


HTTP1.X 不支持 Header 压缩,如果页面非常多的去看下会导致带宽消耗和不必要的浪费。


针对这个问题在 SPDY 中 的解决方案是利用 DEFLATE 格式的字段,这种设计非常有效,但是实际上存在 CRIME 信息泄露的攻击手段。


在 HTTP/2 当中定义了 HPACK,HPACK 算法通过静态的哈夫曼编码对于请求头部进行编码减少传输大小,但是需要让客户端和服务端之间维护首部表,首部表可以维护和存储之前发过的键值对信息,对于重复发送的报文内容可以直接通过查表获取,减少冗余数据产生,后续的第二个请求将会发送不重复的数据。


HPACK 压缩算法主要包含两个模块,索引表哈夫曼编码,索引表同时分为动态表和静态表,静态表内部预定义了 61 个 Header 的 K/V 数值,而动态表是先进先出的队列,初始情况下内容为空,而解压 header 则需要每次添加的时候放到队头,移除从队尾开始。


注意动态表为了防止过度膨胀占用内存导致客户端崩溃,在超过一定长度过后会自动释放 HTTP/2 请求。


HPACK 算法


HPACK 算法是对于哈夫曼算法的一种应用和改进,哈夫曼算法经典案例是就是 ZIP 压缩,也就是虽然我们可能不清楚却是可能天天在用的一个东西。


HPACK 算法的思路是在客户端和服务端两边各维护一个哈希表,然后双端通过表中缓存 Headers 字段减少流中二进制数据传输,进而提高传输效率。


HPACK 三个主要组件有如下细节:


  • 静态表:HTTP2 为出现在头部的字符串和字段静态表,包含 61 个基本的 headers 内容,

  • 动态表:静态表只有 61 个字段,所以利用动态表存储不在静态表的字段,从 62 开始进行索引,在传输没有出现的字段时候,首先对于建立索引号,然后字符串需要经过哈夫曼编码完成二进制转化发给服务器,如果是第二次发送则找到对应的动态表的索引找到即可,这样有效避免一些冗余数据的传输。

  • 哈夫曼编码:这一算法非常重要,对于近代互联网的发展有着重大影响。


哈夫曼编码:是一种用于无损数据压缩熵编码(权编码)算法。由美国计算机科学家大卫·霍夫曼(David Albert Huffman)在 1952 年发明。 霍夫曼在 1952 年提出了最优二叉树的构造方法,也就是构造最优二元前缀编码的方法,所以最优二叉树也别叫做霍夫曼树,对应最优二元前缀码也叫做霍夫曼编码。


下面为哈夫曼编码对应的原始论文:


哈夫曼编码原始论文:

链接:https://pan.baidu.com/s/1r_yOVytVXb-zlfZ6csUb2A?pwd=694k 提取码:694k


此外这里有个讲的比较通俗的霍夫曼的视频,强烈建议反复观看,能帮你快速了解哈夫曼编码是怎么回事,当然前提是得会使用魔法。


https://www.youtube.com/watch?v=Jrje7ep5Ff8&t=29s


请求优先级


请求优先级实际上并不是 HTTP/2 才出现的,在此之前的的 RFC7540 中定义了一套优先级的相关指令,但是由于它过于复杂最后并没被普及,但是里面的信息依然是值得参考的。


HTTP/2 的内容取消了所有关于 RFC7540 优先级的指令,所有的描述被删除并且被保留在原本的协议当中。


HTTP/2 利用多路复用,所以有必要优先使用重要的资源分配到前面优先加载,但是实际上在实现方案过程中优先级是不均衡的,许多服务器实际上并不会观察客户端的请求和行为。


最后还有根本性的缺点,也就是 TCP 层是无法并行的,在单个请求当中的使用优先级甚至有可能性能弱于 HTTP1.X。


流量控制


所谓流量控制就是数据流之间的竞争问题,需要注意 HTTP2 只有流数据才会进行控制,通过使用WINDOW_UPDATE帧来提供流量控制。


注意长度不是 4 个八位字节的window_update 帧需要视为 frame_size_error的错误进行响应。


PS:下面的设计中有效载荷是保留位+ 31 位的无符号整数,表示除了现在已经有的流控制窗口之外还能额外传输 8 个字节数的数据,所以最终合法范围是 1到 2^31 - 1 (2,147,483,647) 个八位字节。


WINDOW_UPDATE Frame {  Length (24) = 0x04,  Type (8) = 0x08,
Unused Flags (8),
Reserved (1), Stream Identifier (31),
Reserved (1), Window Size Increment (31),}
复制代码


对于流量控制,存在下面几个显著特征:


  • 流量控制需要基于 HTTP 中间的各种代理服务器控制,不是非端到端的控制;

  • 基于信用基础公布每个流在每个连接上接收了多少字节,WINDOW_UPDATE 框架没有定义任何标志,并没有强制规定;

  • 流量的控制存在方向概念,接收方负责流量控制,并且可以设置每一个流窗口的大小;

  • WINDOW_UPDATE 可以对于已设置了 END_STREAM 标志的帧进行发送,表示接收方这时候有可能进入了半关闭或者已经关闭的状态接收到 WINDOW_UPDATE 帧,但是接收者不能视作错误对待;

  • 接收者必须将接收到流控制窗口增量为 0 的 WINDOW_UPDATE 帧视为PROTOCOL_ERROR类型的流错误 ;


服务器推送


服务器推送意图解决 HTTP1.X 中请求总是从客户端发起的弊端,服务端推送的目的是更少客户端的等待以及延迟。但是实际上服务端推送很难应用,因为这意味着要预测用户的行为。服务端推送包含推送请求推送响应的部分。


推送请求


推送请求使用PUSH_PROMISE 帧作为发送,这个帧包含字段块,控制信息和完整的请求头字段,但是不能携带包含消息内容的相关信息,因为是指定的帧结构,所以客户端也需要显式的和服务端进行关联,所以服务端推送 请求也叫做“Promised requests”。


当请求客户端接收之后是传送CONTINUATION帧,CONTINUATION帧头字段必须是一组有效的请求头字段,服务器必须通过":method"伪字段头部添加安全可缓存的方法,如果客户端收到的缓存方法不安全则需要在PUSH_PROMISE帧上响应错误,这样的设计有点类似两个特务对暗号,一个暗号对错了就得立马把对方弊了。


PUSH_PROMISE可以在任意的客户端和服务端进行传输,但是有个前提是流对于服务器需要保证“半关闭“或者“打开“的状态,否则不允许通过CONTINUATION或者HEADERS 字段块传输。


PUSH_PROMISE帧只能通过服务端发起,因为专为服务端推送设计,使用客户端推送是“不合法“的。


PUSH_PROMISE 帧结构:


再次强调有效载荷是一个保留位+ 31 位的无符号整数。有效载荷是什么?是对于 HTTP1.1 协议中实体的术语重新定义,可以简单看做是报文的请求 Body。


下面是对应源代码定义:


PUSH_PROMISE帧定义


PUSH_PROMISE Frame {  Length (24),  Type (8) = 0x05,    Unused Flags (4),  PADDED Flag (1),  END_HEADERS Flag (1),  Unused Flags (2),
Reserved (1), Stream Identifier (31),
[Pad Length (8)], Reserved (1), Promised Stream ID (31), Field Block Fragment (..), Padding (..2040),}
复制代码


CONTINUATION 帧:用于请求接通之后继续传输,注意这个帧不是专用于服务端推送的。


CONTINUATION Frame {  Length (24),  Type (8) = 0x09,
Unused Flags (5), END_HEADERS Flag (1), Unused Flags (2),
Reserved (1), Stream Identifier (31),
Field Block Fragment (..),}
复制代码


推送响应


如果客户端不想接受请求或者服务器发起请求的时间过长,可以通过RST_STREAM 帧代码标识发送CANCEL 或者REFUSED_STREAM 内容告诉服务器自己不接受服务端请求推送。


而如果客户端需要接收这些响应信息,则需要按照之前所说传递CONTINUATION以及PUSH_PROMISE接收服务端请求。


其他特点:


  1. 客户端可以使用SETTINGS_MAX_CONCURRENT_STREAMS设置来限制服务器可以同时推送的响应数量。

  2. 如果客户端不想要接收服务端的推送流,可以把SETTINGS_MAX_CONCURRENT_STREAMS设置为 0 或者重置PUSH_PROMISE保留流进行处理。

2.2.6 HTTP/3

进度追踪:RFC 9114 - HTTP/3 (ietf.org)


为什么会存在 3?


可以发现 HTTP/2 虽然有了质的飞跃,但是因为 TCP 协议本身的缺陷,队头阻塞的问题依然可能存在,同时一旦出现网络拥堵会比 HTTP1.X 情况更为严重(用户只能看到一个白板)。


所以后续谷歌的研究方向转为研究 QUIC,实际上就是改良 UDP 协议来解决 TCP 协议自身存在的问题。但是现在看来这种改良不是很完美,目前国内部分厂商对于 QUIC 进行自己的改进。


HTTP/3 为什么选择 UDP


这就引出另一个问题,为什么 3.0 有很多协议可以选择,为什么使用 UDP?通常有下面的几个点:


  • 基于 TCP 协议的设备很多,兼容十分困难。

  • TCP 是 Linux 内部的重要组成,修改非常麻烦,或者说压根不敢动。

  • UDP 本身无连接的,没有建立连接和断连的成本。

  • UDP 数据包本身就不保证稳定传输所以不存在阻塞问题(属于爱要不要)。

  • UDP 改造相对其他协议改造成本低很多


HTTP/3 新特性


  • QUIC(无队头阻塞):优化多路复用,使用 QUIC 协议代替 TCP 协议解决队头阻塞问题,QUIC 也是基于流设计但是不同的是一个流丢包只会影响这一条流的数据重传,TCP 基于 IP 和端口进行连接,多变的移动网络环境之下十分麻烦,QUIC 通过 ID 识别连接,只要 ID 不变,网络环境变化是可以迅速继续连接的。

  • 0RTT:注意建立连接的 0TT 在 HTTP/3 上目前依然没有实现,至少需要 1RTT。


RTT:RTT Round Trip Time 的缩写,简单来说就是通信一来一回的时间。 RTT 包含三部分:

  • 往返传播延迟。

  • 网络设备排队延迟。

  • 应用程序处理延迟。


HTTPS 建立完整连接通常需要 TCP 握手和 TLS 握手,至少要 2-3 个 RTT,普通的 HTTP 也至少要 1 个 RTT。QUIC 的目的是除开初次连接需要消耗 1RTT 时间之外,其他的连接可以实现 0RTT。


为什么无法做到初次交互 0RTT? 因为初次传输说白了依然需要传输两边到密钥信息,因为存在数据传输所以依然需要 1 个 RTT 的时间完成动作,但是在完成握手之后的数据传输只需要 0RTT 的时间。


  • 前向纠错:QUIC 的数据包除了本身的内容之外,还允许携带其他数据包,在丢失一个包的时候,通过携带其他包的数据获取到丢包内容。

  • 具体要怎么做呢?例如 3 个包丢失一个包,可以通过其他数据包(实际上是校验包)异或值计算出丢失包的“编号”然后进行重传,但是这种异或操作只能针对一个数据包丢失计算,如果多个包丢失,用异或值是无法算出一个以上的包的,所以这时候还是需要重传(但是 QUIC 重传代价比 TCP 的重传低很多)。

  • 连接迁移:QUIC 放弃了 TCP 的五元组概念,使用了 64 位的随机数 ID 充当连接 ID,QUIC 协议在切换网络环境的时候只要 ID 一致就可以立马重连。对于现代社会经常 wifi 和手机流量切换的情况十分好用的一次改进。


术语解释⚠️:

5 元组:是一个通信术语,英文名称为 five-tuple,或 5-tuple,通常指由**源 Ip (source IP), 源端口(source port),目标 Ip (destination IP), 目标端口(destination port),4 层通信协议 (the layer 4 protocol)**等 5 个字段来表示一个会话,是会话哦。这个概念在《网络是怎么样连接的》这本书中也有提到类似的概念。那就是在第一章中创建套接字的步骤,创建套接字实际上就需要用到这个五元祖的概念,因为要创建“通道”需要双方给自告知自己的信息给对方自己的 IP 和端口,这样才能完成通道创建和后续的协议通信。

顺带拓展一下 4 元组和 7 元组。

4 元组:即用 4 个维度来确定唯一连接,这 4 个维度分别是源 Ip (source IP), 源端口(source port),目标 Ip (destination IP), 目标端口(destination port)

7 元组:即用 7 个字段来确定网络流量,即源 Ip (source IP), 源端口(source port),目标 Ip (destination IP), 目标端口(destination port),4 层通信协议 (the layer 4 protocol),服务类型(ToS byte),接口索引(Input logical interface (ifIndex))


  • 加密认证的报文:QUIC 默认会对于报文头部加密,因为 TCP 头部公开传输,这项改进非常重要。

  • 流量控制,传输可靠性:QUICUDP 协议上加了一层数据可靠传输的可靠性传输,因此流量控制和传输可靠性都可以得到保证。

  • 帧格式变化

  • 下面是网上资料对比 HTTP2 和 3 之间的格式差距,可以发现HTTP/3 帧头只有两个字段:类型和长度。帧类型用来区分数据帧和控制帧,这一点是继承自 HTTP/2 的变化,数据帧包含 HEADERS 帧,DATA 帧,HTTP 包体。


  • 关于 2.0 的头部压缩算法升级成了 QPACK算法:需要注意 HTTP3 的 **QPACK**算法与 HTTP/2 中的 HPACK 编码方式相似,HTTP/3 中的 QPACK 也采用了静态表、动态表及 Huffman 编码。

  • 那么相对于之前的算法HPACKQPACK算法有什么升级呢?首先HTTP/2 中的 HPACK 的静态表只有 61 项,而 HTTP/3 中的 QPACK 的静态表扩大到 91 项。

  • 最大的区别是对于动态表做了优化,因为在 HTTP2.0 中动态表存在时序性的问题。

  • 所谓时序性问题是在传输的时候如果出现丢包,此时一端的动态表做了改动,但是另一端是没改变的,同样需要把编码重传,这也意味着整个请求都会阻塞掉。



因此 HTTP3 使用 UDP 的高速,同时保持 QUIC 的稳定性,并且没有忘记 TLS 的安全性,在 2018 年的 YTB 直播中宣布 QUIC 作为 HTTP3 的标准。


YTB 地址:(2) IETF103-HTTPBIS-20181108-0900 - YouTube,可怜互联网的天花板协议制定团队 IETF 连 1 万粉丝都没有。


2.3 HTTP 部分问题讨论

2.3.1 队头阻塞问题(head of line blocking)

队头阻塞问题不仅仅只是处在 HTTP 的问题,实际上更加底层的协议以及网络设备通信也会存在线头阻塞问题。


交换机


当交换机使用 FIFO 队列作为缓冲端口的缓冲区的时候,按照先进先出的原则,每次都只能是最旧的网络包被发送,这时候如果交换机输出端口存在阻塞,则会发生网络包等待进而造成网络延迟问题。


但是哪怕没有队头阻塞,FIFO 队列缓冲区本身也会卡住新的网络包,在旧的网络包后面排队发送,所以这是 FIFO 队列本身带来的问题。


有点类似核酸排队,前面的人不做完后面的人做不了,但是前面的人一直不做,后面也只能等着。


交换机 HO 问题解决方案


使用虚拟输出队列的解决方案,这种方案的思路是只有在输入缓冲区的网络包才会出现 HOL 阻塞,带宽足够的时候不需要经过缓冲区直接输出,这样就避免 HOL 阻塞问题。


无输入缓冲的架构在中小型的交换机比较常见。


线头阻塞问题演示


交换机:_交换机根据 MAC 地址表查找 MAC 地址, 然后将信号发送到相应的端口_一个网络信号转接设备,有点类似电话局中转站。


线头阻塞示例:第 1 和第 3 个输入流竞争时,将数据包发送到同一输出接口,在这种情况下如果交换结构决定从第 3 个输入流传输数据包,则无法在同一时隙中处理第 1 个输入流。


请注意,第一个输入流阻塞了输出接口 3 的数据包,该数据包可用于处理。


无序传输


因为 TCP 不保证网络包的传输顺序,所以可能会导致乱序传输,HOL 阻塞会显著的增加数据包重新排序问题。


同样为了保证有损网络可靠消息传输,原子广播算法虽然解决这个问题,但是本身也会产生 HOL 阻塞问题,同样是由于无序传输带来的通病。


Bimodal Multicast 算法是一种使用 gossip 协议的随机算法,通过允许乱序接收某些消息来避免线头阻塞。


HTTP 线头阻塞


HTTP 在 2.0 通过多路复用的方式解决了 HTTP 协议的弱点并且真正意义上消除应用层 HOL 阻塞问题,但是 TCP 协议层的无序传输依然是无法解决的。


于是在 3.0 中直接更换 TCP 协议为 QUIC 协议消除传输层的 HOL 阻塞问题。

2.4.2 HTTP/2 全双工支持

注意 HTTP 直到 2.0 才是真正意义上的全双工,所谓的 HTTP 支持全双工是混淆了 TCP 协议来讲的,因为 TCP 是支持全双工的,TCP 可以利用网卡同时收发数据。


为了搞清楚 TCP 和 HTTP 全双工的概念, 应该理解 HTTP 中双工的两种模式:半双工(http 1.0/1.1),全双工(http 2.0)


半双工:同一时间内链接上只能有一方发送数据而另一方接受数据。


  • http 1.0 是短连接模式,每个请求都要建立新的 tcp 连接,一次请求响应之后直接断开,下一个请求重复此步骤。

  • http 1.1 是长连接模式,可以多路复用,建立 tcp 连接不会立刻断开,资源 1 发送响应,资源 2 发送响应,资源 3 发送响应,免去了要为每个资源都建立一次 tcp 的开销。


全双工:同一时间内两端都可以发送或接受数据 。


  • http 2.0 资源 1 客户端发送请求不必等待响应就可以继续发送资源 2 的请求,最终实现一边发,一边收。

2.4.3 HTTP 2.0 缺点

  • 解决了 HTTP 的队头请求阻塞问题,但是没有解决 TC P 协议的队头请求阻塞问题,此外 HTTP/2 需要同时使用 TLS 握手和 HTTP 握手耗时,同时在 HTTPS 连接建立之上需要使用 TLS 进行传输。

  • HTTP/2 的队头阻塞出现在当 TCP 出现丢包的时候,因为所有的请求被放到一个包当中,所以需要重传,TCP 此时会阻塞所有的请求。但是如果是 HTTP1.X,那么至少是多个 TCP 连接效率还要高一些,

  • 多路复用会增大服务器压力,因为没有请求数量限制,短时间大量请求会瞬间增大服务器压力

  • 多路复用容易超时,因为多路复用无法鉴定带宽以及服务器能否承受多少请求。


丢包不如 HTTP1.X


丢包的时候出现的情况是 HTT P2.0 因为请求帧都在一个 TCP 连接,意味着所有的请求全部要跟着 TCP 阻塞,在以前使用多个 TCP 连接来完成数据交互,其中一个阻塞其他请求依然可以正常抵达反而效率高。


二进制分帧目的


根本目的其实是为了让更加有效的利用 TCP 底层协议,使用二进制传输进一步减少数据在不同通信层的转化开销。


HTTP1.X 的 Keep-alive 缺点


  • 必须按照请求响应的顺序进行交互,HTTP2 的多路复用则必须要按顺序响应。

  • 单个 TCP 一个时刻处理一个请求,但是 HTTP2 同一个时刻可以同时发送多个请求,同时没有请求上限。

2.4.4 HTTP 协议真的是无状态的么?

仔细阅读 HTTP1.x 和 HTTP/2 以及 HTTP3.0 三个版本的对比,其实会发现 HTTP 无状态的定义偷偷发生了变化的,为什么这么说?


在讲解具体内容之前,我们需要弄清一个概念,那就是 Cookie 和 Session 虽然让 HTTP 实现了“有状态”,但是其实这和 HTTP 协议本身的概念是没有关系的。


CookieSession的出现根本目的是保证会话状态本身的可见性,两者通过创立多种独立的状态“模拟”用户上一次的访问状态,但是每一次的 HTTP 请求本身并不会依赖上一次 HTTP 的请求,单纯从广义的角度看待其实所有的服务都是有状态的,但是这并不会干扰 HTTP1.X 本身无状态的定义。


此外 HTTP 协议所谓的无状态指的是每个请求是完全的独立的,在 1.0 备忘录定义也可以看出一次 HTTP 连接其实就是一次 TCP 连接,到了 HTTP1.1 实现了一个 TCP 多个 HTTP 连接依然是可以看做独立的 HTTP 请求。


说了这么多,其实就是说 HTTP1.X 在不靠 Cookie 和 Session 扶着的时候看做无状态是对的,就好比游戏里面的角色本身的数值和武器附加值的对比,武器虽然可以让角色获得某种状态,但是这种状态并不是角色本身特有的,而是靠外力借来的。


然而随着互联网发展,到了 HTTP/2 和 HTTP3 之后,HTTP 本身拥有了“状态”定义。比如 2.0 关于 HEADER 压缩产生的 HPACK 算法(需要维护静态表和动态表),3.0 还对 HPACK 算法再次升级为 QPACK 让传输更加高效。


所以总结就是严谨的来说 HTTP1.X 是无状态的,在 Cookie 和 Session 的辅助下实现了会话访问状态的保留。


到了 HTTP/2 之后 HTTP 是有状态的, 因为在通信协议中出现了一些状态表来维护双方重复传递的 Header 字段减少数据传输。

2.4 小结

这一章节本来应该是全书的核心内容,奈何作者似乎并不想让读者畏惧,所以讲的比较浅显,个人花费了不少精力收集网上资料结合自己的思考整理出第二章的内容。


关于 HTTP 的整个发展史是有必要掌握的,因为八股有时候会提到相关问题,问的深入一些确实有些顶不住,HTTP 协议也是应用层通信协议的核心,其次作为 WEB 开发人员个人认为是更是有必要掌握的。


另外了解 HTTP 的设计本身可以让我们过渡到 TCP 协议的了解,TCP 的设计导致了 HTTP 设计的影响等问题可以做更多思考。


关于更多内容建议可以看看《网络是怎么样连接》的这一篇读书笔记,原书从整个 TCP/IP 结构的角度通俗的讲述了有关互联网发展的基本脉络,而这一篇讲述了 HTTP 发展的基本历史和未来的发展方向。

用户头像

懒时小窝

关注

赐他一块白石,石头上写着新名 2020.09.23 加入

如果我们想要知道自己想要做什么,必须先找到自己的白色石头。欢迎关注个人公众号“懒时小窝”,不传播焦虑,只分享和思考有价值的内容。

评论

发布
暂无评论
二、《图解HTTP》- HTTP协议历史发展(重点)_HTTP_懒时小窝_InfoQ写作社区