面试篇之~HTTP HTTP2 特性对比

759 阅读11分钟

前言:HTTP的每一次升级都是为了提高传输效率

HTTP/ 1、HTTPS、HTTP/ 2协议栈对比图

image.png

HTTP/ 0.9

特点:

  • 客户端只有一个请求行,没有请求头和请求体;
  • 服务器只返回了响应数据,没有头信息;
  • 由于客户端想获取的都是html文档,因此返回的文件内容是以ASCII字符流来传输的;

缺点:

  • 支持的都是HTML文件的下载,过于单一;

过程示意图:

image.png

HTTP/ 1.0

特点:

  • 新增请求头和响应头,方便浏览器和服务器交流;

头信息中包括但不限于压缩方式、编码方式、语言、文件类型等,可支持多种类型文件下载;

  • 引入了状态码。用于告知服务器对请求的处理状态;
  • 提供了Cache机制,缓解服务器压力;

缺点:

  • 每进行一次HTTP通信,都要经历一次完整的TCP连接(三次握手+四次挥手),服务器压力大,耗时长;
  • 一台服务器只支持一个域名(即一个域名只绑定了一个IP地址);
  • 响应头中会通过 Content-Length:xxx 返回完整的数据大小,不能按照动态生成的内容大小返回;

HTTP/ 1.1

特点:

  • 新增持久连接;

原本一个TCP连接只能传输一次HTTP请求,为了提高传输效率,HTTP/1.1新增持久连接,允许在一次TCP连接上传输多个HTTP请求。

  • 持久连接在HTTP/1.1中默认开启;
  • 对于同一个域名,大多数浏览器允许同时创建6个持久连接;
  • 为了跳过6个的限制,建立更多的连接,还出现了域名分片,即根据域名创建其子域名,但其实都指向的是同一台服务器;
  • 支持动态内容;

引入Chunk transfer,将数据分成n个数据块传输。

  • 在消息头中设置:Transfer-Encoding: chunked;
  • 每个非空的分块中包含数据的长度值(十六进制表示) + CRLF(回车及换行)+ 数据本身 + CRLF;
  • 最后以一个分块长度值为 0,对应的分块数据没有内容表示发送结束;
  • 客户端收到消息后会对其进行解码,恢复到编码前的实体主体;
  • 示例如下:

image.png

  • 请求头中新增Host字段用于指定完整主机名或域名的URI,用来区分当一台物理主机上绑定了多个虚拟主机(即同一个IP地址对应多个域名)的时候,要访问的究竟是哪个域名;
  • 引入了Cookie;

缺点:

  • HTTP1.1只对请求体进行了压缩,但有时候请求头的大小甚至大于请求体,因此当请求很多的时候,浪费了带宽,增加了网络延迟。
  • 对带宽的利用率较低;

带宽利用率低的三个原因:

什么是带宽?

带宽是指每秒最大能发送或者接收的字节数。

原因:

  1. TCP启动慢: TCP在建立连接后,刚开始会采用一个非常慢的速度发送数据,之后慢慢加快直到稳定,这是为了减少网络拥塞的一种策略。但是我们的一些关键资源本来就不大,TCP又是慢启动,这会导致页面首次渲染时间较长;
  2. 同时开启了多条 TCP 连接,这些连接会竞争固定的带宽:当建立的TCP连接较多而带宽又不足的时候,各个TCP就会动态减慢接收数据的速度,但是问题在于TCP无法知道哪一些是关键资源需要优先下载,哪一些是普通资源可以延后下载,因此有可能会影响到关键资源;
  3. 队头阻塞: 由于在同一个TCP管道中,同一时刻只能处理一个HTTP请求(即任意一个时间点实际上还是单向的:上行请求时下行空闲,下行响应时上行空闲),每次都需要等待返回响应后才可以发送下一次请求,这个过程中一旦有某个请求被阻塞,就会影响后续的所有请求,从而产生队头阻塞问题;(尝试了管线化但失败了)

过程示意图:

image.png

HTTP/ 2

特点:

  • 多路复用:多个往返通信都复用一个连接来处理,实现资源的并行传输;
  • 使用HPACK算法对请求头和响应头进行了压缩:提高传输速率;
  • 可以设置请求的优先级:HTTP/2中添加了控制帧来管理流,实现了优先级和流量控制;
  • 服务器推送:服务器不再只是被动地接受请求返回响应,而是可以新建'流',主动向客户端发送消息。( 例如:在浏览器请求HTML的时候,服务器除了返回HTML之外,还可以把之后用到的js,css预先返回给浏览器,加快首次打开页面速度。)
  • 语义保持不变:HTTP/2将HTTP分成了语义和语法两部分,语义保持不变,与HTTP/1.1完全一致,这样消除了学习成本,上层应用也不需要做任何的修改;
  • 通常都是使用'https'协议名,跑在 TLS 上面:为了区分'加密'和'明文'这两个不同的版本,HTTP/2 协议定义了两个字符串标识符:'h2'表示加密的 HTTP/2,'h2c'表示明文的 HTTP/2;
  • 默认长连接,不需要“Connection”头字段
  • 安全性更高:要求下层的通信协议必须是TLS1.2 以上,还要支持前向安全和 SNI,并且把几百个弱密码套件列入了“黑名单”;
  • 下载大文件的时候想取消接收不必断开连接:在 HTTP/1 里只能断开 TCP 连接重新'三次握手',成本很高;而在 HTTP/2 里就可以简单地发送一个'RST_STREAM'中断流,而长连接会继续保持;

问题:

  • HTTP/2 并没有将队头阻塞的问题完全解决:因为 HTTP/2 是基于 TCP 协议的,而 TCP 协议依然存在数据包级别的队头阻塞,如果网络连接质量差,发生丢包,那么 TCP 会等待重传,传输速度就会降低;
  • 在移动网络中发生 IP 地址切换后,一切都要'重新开始':下层的 TCP 必须重新建连,要再次'握手',经历'慢启动',而且之前连接里积累的 HPACK 字典也都消失了,必须重新开始计算,导致带宽浪费和时延;
  • HTTP/2全部依靠一个连接,不稳定:HTTP/2 对一个域名只开一个连接,所以一旦这个连接出问题,那么整个网站的体验也就变差了;

HTTP2多路复用的实现:

HTTP/2的帧结构

image.png

报头各字段含义如下:

  • 帧长度:开头的3个字节表示传输数据的长度(长度不包括头的 9 个字节),默认上限是 2^14,最大是 2^24,也就是说 HTTP/2 的帧通常不超过 16K,最大是 16M。
  • 帧类型:可大致分为两类:数据帧和控制帧(其中HEADERS和DATA都属于数据帧,用来存放HTTP报文;SETTINGS、PING、PRIORITY 等则是用来管理流的控制帧。)
  • 标志位:可以保存8个标志位,携带简单地控制信息(如:END_HEADERS表示头数据结束;END_STREAM表示单方向数据发送结束等。)
  • 流标识符:用于表示帧所属的"流",接收方可通过标识符从乱序的帧里识别出具有相同流 ID 的帧序列,按顺序组装起来就实现了虚拟的“流”。

HTTP/2的流

什么是流?

流是二进制帧的双向传输序列,同一个消息往返的帧会分配一个唯一的流 ID

在概念上,一个流就等同于一次HTTP/1.1里的'请求-响应',你可以把它想象成是一个虚拟的“数据流”,在里面流动的是一串有先后顺序的数据帧,这些数据帧按照次序组装起来就是一次HTTP通信。

因为“流”是虚拟的,实际上并不存在,所以 HTTP/2 就可以在一个 TCP 连接上用“流”同时发送多个“碎片化”的消息。

image.png

流的特点

  • 流是可并发的,一个 HTTP/2 连接上可以同时发出多个流传输数据,也就是并发多请求,实现'多路复用';
  • 客户端和服务器都可以创建流,双方互不干扰;
  • 流是双向的,一个流里面客户端和服务器都可以发送或接收数据帧,也就是一个'请求- 应答'来回;
  • 流之间没有固定关系,彼此独立,但流内部的帧是有严格顺序的;
  • 流可以设置优先级,让服务器优先处理,比如先传 HTML/CSS,后传图片,优化用户体验;
  • 流 ID 不能重用,只能顺序递增,客户端发起的 ID 是奇数,服务器端发起的 ID 是偶数;
  • 在流上发送 'RST_STREAM' 帧可以随时终止流,取消接收或发送;
  • 第 0 号流比较特殊,不能关闭,也不能发送数据帧,只能发送控制帧,用于流量控制。
  • 客户端在一个连接里最多只能发出 2^30请求,ID 用完了这个时候可以再发一个控制帧 'GOAWAY',真正关闭 TCP 连接。

流与帧的关系

在HTTP/2的连接上,发送的数据都会经过处理转换成一个个带有流ID编号的帧,帧是乱序收发的,但只要他们都拥有相同的流ID,就都属于同一个流,而且流里的帧是有严格的先后顺序的。

多路复用传输过程

image.png

  1. 在正式收发数据前,成功建立TCP、TLS连接;
  2. TLS握手成功后,客户端发送'连接前言',用于确认建立HTTP/2连接;

这个“连接前言”是标准的 HTTP/1 请求报文,使用纯文本的 ASCII 码格式,请求方法是特别注册的一个关键字“PRI”,全文只有 24 个字节:PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n

  1. 浏览器准备好请求数据(请求行、请求头、请求体),在请求发送前,会使用HPACK算法来压缩头部数据;
  2. 请求数据经过二进制分帧层处理,会被转换成一个个带有流ID编号的帧,用 'HEADERS' 帧存放头数据、'DATA' 帧存放实体数据;
  3. 客户端发送 'HEADERS' 帧后,就有了流ID,流就进入了'打开'状态,此时两端都可以收发数据;
  4. 然后客户端发送一个带 'END_STREAM' 标志位的帧,表示请求数据已经发送完毕,流就进入了'半关闭'状态;

这个'半关闭'状态很重要,意味着客户端的请求数据已经发送完了,需要接受响应数据,而服务器端也知道请求数据接收完毕,之后就要内部处理这个请求,再发送响应数据。

  1. 服务器收到所有的帧后,会将ID相同的帧合并成一条完整的请求信息;
  2. 服务器处理该请求信息,并返回响应;响应数据发完了之后,也要带上 'END_STREAM' 标志位,表示数据发送完毕,这样流两端就都进入了'关闭'状态,流就结束了。流关闭就是一次通信结束
  3. 返回的响应也会发送至二进制分帧层进行转换处理,然后再发送至浏览器;
  4. 浏览器收到响应后根据流ID编号将数据还原;
  5. 下一次再发请求就要开一个新流(而不是新连接),流 ID 不断增加,直到到达上限,发送 'GOAWAY' 帧开一个新的 TCP 连接,流 ID 就又可以重头计数。

HTTP/2头部压缩算法—— HPACK

  • 客户端和服务器两端各自维护一份 "key-value" 索引表,压缩和解压缩就是查表和更新表的操作;
  • 这份索引表分为了:动态表静态表
    • 常用头字段在只读的静态表里;
    • 如果是静态表里没有的 key-value,那么就需要用到动态表,它被添加在静态表之后,可以随时更新;

[静态表如下图所示:]

image.png

  • HTTP/2 废除了原有的起始行概念,把起始行里面的请求方法、URI、状态码等统一转换成了头字段的形式,并且给这些“不是头字段的头字段”起了个特别的名字—“伪头字段”,并废除了起始行里的版本号和错误原因;
  • "伪头字段"会在名字前加一个":";

[动态表如下图所示:]

  • 例如:第一次发送请求时的 'user-agent' 字段长是一百多个字节,用哈夫曼压缩编码发送之后,客户端和服务器都更新自己的动态表,添加一个新的索引号'65'。那么下一次发送的时候就不用再重复发那么多字节了,只要用一个字节发送编号就好。

image.png

随着越来越多的请求发送,表里的字段会越来越完善,以前上千字节的头现在只用几十个字节就可以表示,压缩效果显著。