阅读笔记|《HTTP/2 in Action》

363 阅读18分钟

最近读《HTTP/2 in Action》,跟着作者过了一遍HTTP的“前世今生”,读完觉得对HTTP的发展史清楚了不少!我们每天都在和HTTP打交道,它所涉及的知识点也颇多,其重要程度不言而喻!趁着这股新鲜劲儿还没过去,赶紧写篇文章总结整理一下:)

一、HTTP的诞生

HTTP协议诞生自 1989 年,第一版是 HTTP 0.9,但 HTTP 0.9 并不是一个正式标准;直到 1996 年,根据 RFC 1945,HTTP 1.0 成为 IEFT 标准,1999 年,在 RFC 2616 中发布了 HTTP 1.1。

其版本路线如下:

  • HTTP/0.9
  • HTTP/1.0
  • HTTP/1.1
  • HTTP/2(2015)
  • HTTP/3(2018)

二、预备知识

1、 在浏览器地址栏输入一个URL后回车,背后会进行哪些技术步骤?

2、OSI七层模型

OSI模型(Open System Interconnection,开放式系统互联通信参考模型)是经常用来描述网络分层的概念模型。此模型包含7层,但这些层并不严格对应所有的网络。

image.png

  • 第一层:物理层

它直接面向原始比特流(0、1)的传输,需要解决好包括传输介质、信道类型、数据与信号之间的转换、信号传输中的衰减和噪声等一系列问题。可以理解为在网线、光纤中传输的由0和1组成的一长串数据,而物理层保证的就是使这些数据能完整的从发送端传送到接收端。

  • 第二层:数据链路层

它在物理层提供的服务的基础上向网络层提供服务,其最基本的服务是将发送端网络层发送的数据可靠的传输到接收端的网络层。物理层仅仅负责传输比特流,不保证数据的完整性,在实际环境中难免会受到干扰,导致信息丢失、顺序不正确等问题。在数据链路层层必须用纠错码来检错与纠错,是对物理层传输原始比特流的功能的加强,将物理层提供的可能出错的物理连接改造成为逻辑上无差错的数据链路。

  • 第三层:网络层

互联网好比是一张巨大的蜘蛛网,综合交错,节点众多。而网络层的任务就是在这个网络中选择一条合适的路径,使发送端传输层所传下来的数据能够通过所选择的路径到接收端。它提供的功能主要是寻址(一般见到的IP)和路由选择,使传输层不需要了解网络中的数据传输和交换技术。

  • 第四层:处理信息的传输层

传输层是OSI参考模型中承上启下的层,它的下3层,主要面向网络通信,确保信息被准确有效地传输;它的上3层,主要面向用户主机,进行数据处理,为用户提供各种服务。网络层只是根据网络地址(IP地址)将发送端发出的数据传送到接收端,而传输层则负责将数据可靠地传送到相应的端口。传输层通过弥补网络层服务质量的不足,为会话层提供端到端的可靠数据传输服务。它为会话层屏蔽了传输层以下的数据通信的细节,使会话层不受到下3层的技术变化的影响。

  • 第五层:会话层

会话字面意思就是“对话和交谈”,建立一个逻辑上的连接。它的主要功能使在两个节点间建立、维护和释放面向用户的连接,并对会话进行管理和控制( 允许信息同时双向传输或任一时刻只能单向传输 ),保证会话数据可靠传输。会话层还提供了同步服务, 例如,正在下载一个100MB的文件,当下载到95MB时,网络断线了,为了解决这个问题,便用到了会话层的同步服务,通过在数据流中定义检查点(Checkpoint)来把会话分割成明显的会话单元。当网络故障出现时,将仅重传最后一个检查点以后的数据即可。

  • 第六层:表示层

这一层主要解决信息的语法表示问题。它将欲交换的数据从适合于某一用户的抽象语法,转换为适合于OSI系统内部使用的传送语法。即提供格式化的表示和转换数据服务。数据的压缩和解压缩,加密和解密等工作都由表示层负责。

  • 第七层:应用层

应用层为操作系统或网络应用程序提供访问网络服务的接口。应用层协议的代表包括:Telnet、FTP、HTTP、SNMP等。

三、HTTP进化史🧬

1、HTTP/0.9

这是 HTTP 的第一个规范,此规范文档只有不到700个单词。该规范的主要内容有:

  • 通过TCP/IP(或类似的面向连接的服务)与服务器和端口(可选的,默认80)建立连接;
  • 客户端应发送一行ASCII文本,包括GET、文档地址(无空格)、回车符和换行符;
  • 服务器使用HTML格式的消息进行响应,该消息被定义为“ASCII字符的字节流”;
  • 通过服务器关闭连接来终止消息(这就是为什么每个请求之后都关闭了连接);
  • 请求是幂等的,服务器不需要在断开连接后存储关于请求的任何信息。 注:这里为我们提供了HTTP的无状态特性,这是一把双刃剑,有利(简单)也有弊(必须附加HTTP cookies等技术以允许状态跟踪,这对于复杂的应用程序是必需的)。

2、HTTP/1.0

1.0新增了很多特性:

  • 更多的请求方法:除了先前定义的GET方法,新增了HEAD和POST方法;
  • 为所有的消息添加HTTP版本号字段:此字段是可选的,为了向后兼容,默认情况下使用HTTP/0.9;
  • HTTP首部:它可以与请求和响应一起发送,以提供与正在执行的请求或发送的响应相关的更多信息;例如,通过使用HTTP响应首部来定义正文中的内容类型,我们终于可以向网页中添加多媒体内容,而不仅仅是超文本了。
  • 一个三位整数的响应状态码,(例如)用来表示响应是否成功、重定向请求、条件请求和错误状态。

3、HTTP/1.1

HTTP/1.1新增了几个特性:

  • 强制HOST首部:在HTTP/1.0中是可选的,但是在HTTP/1.1中它是必选项;
// 举个例子: 
GET /index.html Host: www.google.com 
/*为什么要强制HOST首部呢? 
HTTP请求中的URL不是一个包含绝对路径的URL
(如http://www.example.com/section/page.html) 
而是一个只包含相对路径的URL
(如/section/page.html) 
在设计HTTP协议之初,一个Web服务器只能托管一个网站,所以URL的主机名部分是不言自明的 
因为在发送HTTP请求之前,用户肯定已经连接到了主机 
如今,很多Web服务器上面有多个网站(虚拟主机托管),所以告诉服务器要访问哪个网站和访问哪个相对URL同样重要
此功能可以通过将HTTP请求中的URL修改为完整的包含绝对路径的URL实现
但如果采用这种方法,则很多现有的Web服务器和客户端都不能正常运行
所以,在请求首部中添加Host来实现该功能
*/
  • 持久连接:Connection: Keep-Alive首部;HTTP/1.1不仅将持久连接添加到文档标准中,还将其作为默认行为。即使响应中没有Connection:Keep-Alive首部,也可以假定任何HTTP/1.1连接都使用持久连接。如果服务器确实想要关闭连接,无论出于何种原因,则它必须在响应中显式包含Connection:close HTTP首部;
  • HTTP/1.1增加了管道的概念,因此应该可以通过同一个持久连接发送多个请求并按顺序获取响应;
  • 更好的缓存方法:Cache-Control头部。

HTTP 1.1 是划时代的,它解决了 HTTP 1.0 时代最重要的两个大问题:

  • TCP 连接无法复用,每次请求都需要重新建立 TCP 通道,也就要重复三次握手和四次挥手的情况;就是说每个 TCP 连接只能发送一个请求。
  • 队头阻塞,每个请求都要过“独木桥”,桥宽为一个请求的宽度;也就是说,即使多个请求并行发出,也只能一个接一个地进行请求排队。

HTTP 1.1 的改进点“对症下药”,它引入了:

  • 长连接:HTTP 1.1 支持长连接(Persistent Connection),且默认就开启了 Connection:keep-alive,这样在一个 TCP 连接上可以传送多个 HTTP 请求和响应,减少了建立和关闭连接的消耗和延迟。
  • 管线化:在长连接的基础上,管线化(HTTP Pipelining)使得多个请求使用同一个 tcp 连接使请求并按照并行方式成为可能:多个请求同时发起,无需等待上一个请求的回包。但是需要注意,管线化只是让请求并行,但并没有从根本上解决对头阻塞问题,因为响应仍然要遵循先进先出的原则,第一个请求的回包发出之后,才会响应第二个请求。同时,浏览器供应商很难实现管道,而且大多数浏览器默认禁用该特性,有的甚至完全删除了它。

除此以外,HTTP 1.1 还有一些创造性的改进,比如:

  • 缓存处理;
  • 带宽优化及网络连接的使用,比如,range 头,支持断点续传功能;
  • 错误通知的增强,响应码的增强;
  • Host 头处理,请求消息中如果没有 Host 头会报告一个错误。

基于 HTTP 1.1 的变革,一些成熟的方案也应运而出,比如:

  • http long-polling
  • http streaming
  • websocket

这么看来,HTTP 1.1 简直不要太完美!当然它还是有一些缺陷和痛点的。比如:

  • 队头堵塞问题没有真正解决
  • 明文传输,安全性有隐患
  • header 携带内容过多,增加了传输成本
  • 默认开启 keep-alive 可能会给服务端造成更大的性能压力,比如对于一次性的请求(图片 CDN 服务),在文件被请求之后还保持了不必要的连接很长时间
  • 尽管HTTP消息体可以包含二进制数据(比如图片,以及客户端和服务器能理解的任何格式),但请求和首部需要是文本的形式,导致首部信息过大、安全、隐私等问题。

4、HTTP/2

HTTP 2.0 的目标是显著改善性能,同时做到迁移透明。先来理解几个 HTTP 2.0 的相关基础概念:

  • 帧:HTTP 2.0 中,客户端与服务器通过交换帧来通信,帧是基于这个新协议通信的最小单位;
  • 消息:是指逻辑上的 HTTP 消息,比如请求、响应等,由一或多个帧组成;
  • 流:流是连接中的一个虚拟信道,可以承载双向的消息;每个流都有一个唯一的标识符。

HTTP 2.0 最主要的特性如下:

  • 二进制分帧

HTTP 2.0 的协议解析决定采用二进制格式,而非 HTTP 1.x 的文本格式,二进制协议解析起来更高效,这可以说是性能增强的焦点。

注意:HTTP/1.0允许发送二进制的HTTP消息体,但是HTTP/2是完全的二进制协议,HTTP消息被分成清晰定义的数据帧发送,所有的HTTP/2消息都使用分块的编码技术。

新协议称为二进制分帧层(Binary Framing Layer),每一个请求都有这些公共字段:

  • Type:帧的类型,标识帧的用途
  • Length:整个帧的开始到结束大小
  • Flags:指定帧的状态信息
  • Steam Identifier:用于流控制,可以跟踪逻辑流的帧成员关系
  • Frame payload:r 请求正文

总之,二进制协议将通信分解为帧的方式,这些帧交织在客户端与服务器之间的双向逻辑流中,这样就使得所有通信都在单个 TCP 连接上执行,而且该连接在整个对话期间一直处于打开状态。

  • 多路复用

上面提到,为每帧分配一个流标识符,这就可以在一个 TCP 连接上独立发送它们。此技术实现了完全双向的请求和响应消息复用,解决了 HTTP 队头阻塞的问题。换句话说:一个请求对应一个 stream 并分配一个 id,这样一个连接上可以有多个 stream,每个 stream 的 frame 可以混杂在一起,接收方可以根据 stream id 将 frame 再归属到各自不同的请求里面。

总结一下:所有的相同域名请求都通过同一个 TCP 连接并发完成。同一 TCP 中可以发送多个请求,对端可以通过帧中的标识知道属于哪个请求。通过这个技术,可以避免 HTTP 旧版本中的队头阻塞问题,极大的提高传输性能,这是真正意义上的多路复用。

http2.png

不像在HTTP/1中,大多数浏览器只能并发6个请求,使用HTTP/2就没有这种限制了。(比如默认允许同时存在100个活跃的流)

流是一个虚拟的概念,它是在每个帧上标示的一个数字,也就是流ID。所以关闭或创建一个流的开销,远小于创建HTTP/1.1连接(包含TCP三次握手,可能在发送请求之前还有HTTPS协议协商)的开销。实际上,HTTP/2连接比HTTP/1连接开销更高,因为在它上额外添加了HTTP/2“魔法”前奏消息,并且在发送请求之前还至少要发送一个SETTINGS帧。但HTTP/2的流开销更低。

  • 首部压缩

HTTP/1允许压缩HTTP正文内容(Accept-Encoding首部),但是不会压缩HTTP首部。首部压缩的实现方式是,要求客户端和服务器都维护之前看见的首部字段的列表。在第一个请求后,它仅需发送与前一个首部的不同之处,其他相同之处,服务器可以从标头的列表中恢复。

压缩方法1:查表法——将冗长的、重复的数据拿出来,使用索引来代替

比如我们有一个请求的头部是这样的:

image.png

我们建立这样一个表:

image.png

那请求头部可以压缩为:

image.png

字符减少了40%!!!客户端和服务端可以提前约定一个静态查询表,不需要每次都在请求中发送,这样就能达到压缩头部的需求咯。

压缩方法2:更高效的编码——例如Huffman编码

例如,如果你想编码单词face,你可以使用ASCII编码,或者Huffman编码(5+4+5+3=17位)。

压缩方法3:Lookback(反查)压缩

image.png

压缩之后:

image.png

文本中每个重复的部分都使用一个引用来代替,它标明解码器在之前多远处可以找到重复的文本,重复的文本有多长。引用(-20,3)说明向前去20个字符,然后取3个字符,替换该引用。

HTTP/2使用的是基于查询表和Huffman编码的头部压缩方法~~~

  • 服务器推送

当一个客户端主动请求资源 K,如果这时候服务器知道它很可能也需要资源 M,那么服务器可以主动将资源 M 推送给客户端。当客户端真的请求 M 时,便可以从缓存中读取。

以前:浏览器发送一个请求A,服务器:给你A!

HTTP/2: 浏览器发送一个请求A,服务器说:给你A,还有B、C你也会用到,都给你都给你!

这里有一个问题是:如何管理让服务器推送资源而不会让客户端过载?

事实上,针对服务端希望发送的每个资源,服务端会发送一个 PUSH_PROMISE 帧,但客户端可通过发送 RST_STREAM 帧作为响应来拒绝推送。

  • 流控制

流控制允许接收者主动示意停止或减少发送的数据量。比如一个视频应用,在观看一个视频流时,服务器会同时向客户端发送数据。如果视频暂停,客户端会通知服务器停止发送视频数据,以避免耗尽它的缓存。

  • 流优先级控制

消息帧通过流进行发送。我们提到每个流都分配了一个 id,同时也可以分配优先级。这样一来,服务端可以根据优先级确定它的处理顺序。当数据帧在排队时,服务器会给高优先级的请求发送更多的帧。

流优先级由请求方指定(比如客户端),但由响应方(比如服务器)最终决定发送什么帧。设置优先级的方式有:给stream设置权重,或者指定stream之间的依赖关系。

5、HTTP/3

创建HTTP连接有开销,这个开销不是来自于HTTP本身,而是下层两个用来创建连接的技术:TCP和用来提供HTTPS功能的TLS。

TCP给每个TCP数据包分配一个序列号,如果数据包在到达时顺序不对,则进行重排;如果丢失了部分数据包,则根据序列号来重新请求。TCP通过这种方式来确保数据完整性。TCP基于CWND(拥塞窗口)传输数据,因此能够发送的最大数据量取决于拥塞窗口的大小,发出数据减小窗口大小,接收到数据再把窗口大小加大。开始时窗口比较小,只要网络能够处理那些增加的负载,它就随着时间增长。如果客户端的处理速度跟不上,窗口就会减小。这个流程运行得相当好,这也是为什么TCP/IP成为了互联网的基石的原因。然而,TCP运行的基本方式会导致5个主要的问题,它们至少影响到了HTTP:

  • 有一个连接创建的延迟。要在连接开始时协商发送方和接收方可以使用的序列号;三次握手、HTTPS握手
  • TCP慢启动算法限制了TCP的性能,它小心翼翼地处理发送的数据量,以尽可能防止重传
  • 不充分使用连接会导致限流阈值降低。如果连接未被充分使用,TCP会将拥塞窗口的大小减小,因为它不确定在上个最优的拥塞窗口之后网络参数有没有发生变化
  • 丢包也会导致TCP的限流阈值降低。TCP认为所有的丢包都是由窗口拥堵造成的,但其实并不是(比如网络不稳定);有的TCP拥塞控制算法会直接将拥塞窗口大小减半,也就是说会将容量减半;
  • 数据包可能被排队。乱序接收到的数据包会被排队,以保证数据是有序的

这些问题在HTTP/2下依然存在,其中一些问题正是为什么在HTTP/2下使用单个TCP连接更好的原因。然而,在某些丢包的情况下,最后两个问题会导致HTTP/2比HTTP/1.1更慢。HTTP/2解决了HTTP层面的队头阻塞问题,但是TCP的队头阻塞问题仍然存在。

怎么办呢?

方法1:提升TCP——这一方法实行起来慢,广泛采用需要时间

  • TCP通常由底层操作系统控制,所以可以升级操作系统
  • 改善拥塞控制算法
  • 禁止重启慢启动

方法2:采用其他协议替代TCP

QUIC(发音同quick):是Google发明的一个基于UDP的协议,特点

  • 大量减少连接创建时间
  • 改善拥塞控制
  • 多路复用
  • 但不要带来队头阻塞
  • 前向纠错
  • 连接迁移

HTTP over QUIC 后来被称为 HTTP/3。

image.png

PS:什么是UDP?

UDP(User Datagram Protocol,用户数据报协议),相比于TCP,UDP是一个更轻量的协议,它们都是基于IP(Internet Protocol,网际协议)构建的。TCP在IP中实现网络的可靠连接,包括重传、拥塞和流量控制。通常这是一些好的必要的功能,但在HTTP/2中,它们会导致效率下降。在HTTP/2下,这些功能不是网络层必要的,它们会导致产生不必要的TCP队头阻塞问题。

四、扩展阅读

# 如何看待 HTTP/3 ?