HTTP2 的前世今生

本文转载自HTTP2 的前世今生

导语

作为一名 Web 后端开发工程师,无论是工作中,还是面试时,对于 HTTP 协议的理解都是必不可少的。而 HTTP2 协议的发布更是解决了 HTTP1.1 协议中一系列的问题。这篇文章是根据我在团队的一次技术分享改编而来,里面介绍了 HTTP/1.0 和 HTTP/1.1 的主要区别,以及 HTTP2 相关的新特性。

听说写博客的程序员属于业界良心,而我因为贪玩和拖延,直到今天才开始自己的个人博客之旅,希望借此积累一些经验和技术。如有不足,请多指教。

HTTP 定义

超文本传输协议(HyperText Transfer Protocol,简称:HTTP)是一种用于分布式、协作式和超媒体信息系统的应用层协议。

HTTP 报文由三部分组成:对报文进行描述的起始行(start line)、包含属性的首部(header)块、以及可选的、包含数据的主体(body)部分。

HTTP 报文组成

HTTP 存在以下几个版本:

  • HTTP/0.9:非正式版本,只接受GET一种请求方法,没有在通讯中指定版本号,且不支持请求头。目前已不再使用
  • HTTP/1.0:新增请求方法 GET、POST、HEAD,是使用较广的第一个 HTTP 协议正式版。
  • HTTP/1.1:新增请求方法 PUT、DELETE、OPTIONS、TRACE、CONNECT,支持持久连接、管道化处理等新特性,较之 HTTP/1.0 版本有诸多改进与完善。
  • HTTP/2

HTTP 请求方法

常见的八种方法:GET、HEAD、POST、PUT、DELETE、 TRACE、 OPTIONS 、CONNECT

HTTP 报文方法介绍

HTTP 请求方法的三种属性

  • 安全性(Safe):指的是这个方法在语义上是只读的,它不会对服务器产生任何预期修改。
  • 幂等性(Idempotent):一个请求多次独立执行和只执行一次对服务器产生的预期效果完全相同。
  • 可缓存性(Cacheable):指的是该方法对应的响应消息能够在客户端被存储,并在之后的请求中能被直接使用,而不再需要从服务端重新获取。

HTTP 请求方法的属性

区分安全性方法的意义是让网络爬虫和网页预加载程序能够安心工作,而无需担心会对服务器字眼造成什么危害。
区分幂等性方法的意义是当客户端发送一条请求后,在获取服务器响应前出现连接错误导致无法确定本次请求是否成功时,可以安全的再次重复这次请求,而不用担心会产生什么副作用。
区分可缓存性方法则是为了标记客服端哪些方法的结果是可以被缓存的,哪些方法的结果则不需要考虑缓存,但是具体的缓存策略和缓存实现则非常复杂,暂且略过不谈。有兴趣的同学可以查看《HTTP 缓存策略》一文。

HTTP 请求方法属性的注意事项

  1. 幂等性操作涉及的执行结果指的是对服务端产生的预期结果,并不是客户端得到的报文内容。例如,GET 请求是幂等的,执行任意多次都不会对服务器产生任何预期修改。但这并不是说客户端每次执行 GET 得到的报文内容是一样的。
  2. 幂等性安全性都是是一种从客户端角度来看请求方法的性质,它同样是一种语义上的性质。它并不代表服务端实现上一定是安全幂等的。例如,服务端可以自由的为每条 GET 请求记录日志,也可以为每条 PUT 请求记录修改时间。显然,语义安全的操作可以服务器产生修改,语义幂等的操作在服务端都会产生不同的结果,但这些不同的结果并非是客户端请求时所预期的。

这里需要区分语义安全实现安全,以及语义幂等实现幂等

那么 GET 请求时,服务端可能会记录统计日志导致语义不安全,这个时候如何实现 GET 请求语义上的安全和幂等呢?其实也很简单,只需要拆分为两个请求,一个是语义上安全幂等 GET 请求,一个是 POST 统计日志请求。有兴趣的同事可以看一下 Github 的请求,它的用户操作相关的统计日志总是独立出来请求的。

HTTP 状态码、媒体类型

常用的状态码和媒体类型还是需要有所了解,在这里我就不一一列出,可以参考《HTTP常见状态码》来巩固一下。

HTTP/1.0 和 HTTP/1.1 的主要区别

  1. 缓存处理
    HTTP/1.0 中主要使用 header 里的 Pragma,Expires 来做为缓存判断的标准,HTTP/1.1 则引入了更多的缓存控制策略例如 Cache-Control,Last-Modified,Entity tag,If-Modified-Since, If-Match 等更多可供选择的缓存头来控制缓存策略。
  2. 带宽优化及网络连接的使用
    HTTP/1.0 中,即使客户端只是需要某个对象的一部分,服务器还是会将整个对象全部返回,造成带宽的浪费,此外也不支持断点续传功能。HTTP/1.1 针对此在请求头引入了 range 头域,它允许只请求资源的某个部分,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。
  3. 错误通知的管理
    在 HTTP1.1 中新增了 24 个错误状态响应码,如 409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。
  4. 增加 Host 头部
    在 HTTP/1.0 中认为每台服务器都绑定一个唯一的 IP 地址,因此,请求消息中的 URL 并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个 IP 地址。HTTP/1.1 的请求消息和响应消息都应支持 Host 头域,且请求消息中如果没有 Host 头域会报告一个错误(400 Bad Request)。
  5. keep-alive 连接和持久连接
    HTTP/1.0 可以通过设置 header 头(Connection: keep-alive)来创建 keep-alive 连接,避免每个请求都需要创建 TCP 连接。HTTP/1.1 支持持久连接(Persistent Connection)管道化处理(Pipelining),在一个 TCP 连接上可以传送多个 HTTP 请求和响应,减少了建立和关闭连接的消耗和延迟。

注意:keep-alive 连接仅仅是复用 TCP 连接,并不支持多个请求同时发送。

HTTP 持久连接和管道化处理

持久连接有两种类型:

  1. HTTP/1.0 + keep-alive 连接
    在 HTTP/1.0 中,keep-alive 并不是默认使用的。客户端必需发送一个 Connection:Keep-Alive 请求首部来激活 keep-alive 连接。
    如果服务器愿意为下一条请求将连接保持在打开状态,就在响应中包含相同的首部,如果响应中没有 Connection : Keep-Alive 首部,客户端就认为服务器不支持 keep-live,会在发回响应报文后关闭连接。
    只有在无需检测到连接的关闭就可以确定报文实体主体部分长度的情况下,才能将连接保持在打开状态–也就是说实体的主体部分必需有正确的 Content-Length。
  2. HTTP/1.1 + 持久连接 Persistent Connection
    只有当连接所有的报文都有正确的、自定义报文长度时,也就是说,实体主体部分的长度都和相应的 Content-Length 一致,或者用分块传输编码方式编码的,连接诶才能持久保持。
    如果客户端想要关闭连接,只需要在最后一条请求中加上Connection:close请求首部。

HTTP-串行连接、持久连接和管道化连接的比较

管道化连接可以克服同域并行请求限制带来的阻塞,是把所有请求一并发给服务器,但是服务器需要按照顺序一个一个响应,而不是等到一个响应回来才能发下一个请求,这样就节省了很多请求到服务器的时间。不过,HTTP 管道化连接仍旧有阻塞的问题,若上一响应迟迟不回,后面的响应都会被阻塞到。下面是关于管道化连接使用时需要注意的地方:

  1. 如果 HTTP 客户端无法确认连接是持久的,就不应该使用管道。
  2. 必须按照与请求相同的顺序回送 HTTP 响应。
  3. HTTP 客户端必须做好连接会在任意时刻关闭的准备,还要准备好重发所有未完成管道化的请求。
  4. 出错的时候,管道连接会阻碍客户端了解服务器执行的是一些列管道化请求中的哪一些。由于无法安全地重试 POST 这样的非幂等请求,所以出错时,就存在某些方法永远不会被执行的风险。

HTTP/1.x 现存缺陷

HTTP/1.x 存在的问题主要以下方面:安全、带宽、性能。

  1. 安全因素:HTTP/1.x 中传输的内容都是明文,客户端和服务端双方无法验证身份。
  2. 协议带宽开销大:HTTP/1.x 中 header 内容过大(每次请求 header 基本不怎么变化),增加了传输的成本。
  3. 队首阻塞(Head-Of-Line Blocking):导致带宽无法被充分利用,以及后续健康请求被阻塞。
    HTTP/1.x 中,服务器必须按接受请求的顺序发送响应,意味着浏览器在一个 TCP 连接上发送了两个请求,那么服务器必须等第一个请求响应完毕才能发送第二个响应。
    现代浏览器允许每个 origin 建立 6 个 connection,但大量网页动辄几十个资源,HOLB 依然是主要问题。
  4. 过多连接导致性能压力:针对浏览器每个 origin 建立连接的限制,出现域名分片等优化方案。但域名分片需要建立更多 keep-alive connection,这样会给服务端带来大量的性能压力。

HTTP 协议中的队首阻塞

队首阻塞(Head-of-line blocking 或缩写为 HOL blocking): 任务处理使用了队列模型,队首的事情处于处理中时,后面的事情只能等待。

  • http1.0 的队首阻塞
    对于同一个 TCP 连接,所有的 http1.0 请求放入队列中,只有前一个请求的响应收到了,然后才能发送下一个请求。
    可见,http1.0 的队首组塞发生在客户端
  • http1.1 的队首阻塞
    对于同一个 TCP 连接,http1.1 持久连接 和 管道化请求 允许一次发送多个 http1.1 请求,也就是说,不必等前一个响应收到,就可以发送下一个请求,这样就解决了 http1.0 的客户端的队首阻塞。但是,http1.1 规定,服务器端的响应的发送要根据请求被接收的顺序排队,也就是说,先接收到的请求的响应也要先发送。这样造成的问题是,如果最先收到的请求的处理时间长的话,响应生成也慢,就会阻塞已经生成了的响应的发送。也会造成队首阻塞。
    可见,http1.1 的队首阻塞发生在服务端

后续我们会讲到 HTTP2 是如何解决队首阻塞。

HTTPS 应声而出:保证 HTTP 请求传输数据的安全性

HTTPS 其实就是在 HTTP 请求的 TCP 层之上加了 TLS (传输层安全性协议,英语:Transport Layer Security)层,为 HTTP 请求加密数据包,提供对网站服务器的身份认证,保护交换数据的隐私与完整性。如下图示:

HTTPS 协议层说明

当然安全的代价是牺牲性能, TLS 握手就需要消耗两个 RTT (Round-Trip Time,往返时间),加上 TCP 3次握手需要一个 RTT,即建立连接需要三个 RTT。

HTTPS 建立连接握手过程

HTTP2的前身:SPDY 方案

2012年 google 如一声惊雷提出了 SPDY 的方案,大家才开始从正面看待和解决老版本 HTTP 协议本身的问题,SPDY 可以说是综合了 HTTPS 和 HTTP 两者优点于一体并有所改进的传输协议。

HTTP2 可以说是 SPDY 的升级版(其实原本也是基于 SPDY 设计的),但是,HTTP2.0 跟 SPDY 仍有不同的地方,主要是以下两点:

  • HTTP2 支持明文 HTTP 传输,而 SPDY 强制使用 TLS 加密层
  • HTTP2 消息头的压缩算法采用 HPACK,而非 SPDY 采用的 DEFLATE

HTTP2 概念解释

在HTTP/2中,有两个非常重要的概念:帧(frame)和流(stream)。

  • 帧(frame):HTTP/2 中数据传输的最小单位。
  • 流(stream):HTTP/2 中是一个逻辑上的概念,方便对应于 HTTP1.x 中的一个请求。

HTTP/2 规范定义了 10 个帧类型,这里不包括实验类型帧和扩展类型帧,如下:

  1. DATA (数据)
  2. HEADERS (首部)
  3. PRIORITY(设置流的优先级)
  4. RST_STREAM(终止流)
  5. SETTINGS(设置此连接的参数)
  6. PUSH_PROMISE(服务器推送)
  7. PING(测量RTT)
  8. GOAWAY(终止连接)
  9. WINDOW_UPDATE(流量控制)
  10. CONTINUATION(继续传输头部数据)

下图所示的即是 HTTP2 二进制帧的通用格式。

HTTP2 帧的二进制格式

帧头固定为 9 个字节,长度变化的为帧的负载(Frame Payload),负载内容是由帧类型(Type)定义。length 定义了整个 frame 的大小,type 定义 frame 的类型,flags 用 bit 位定义一些重要的参数,stream id 用作流控制,payload 就是 request 的正文。

HTTP2 新特性

  1. 新的二进制格式(Binary Format)
    HTTP1.x 的解析是基于文本。基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认 0 和 1 的组合。基于这种考虑 HTTP2.0 的协议解析决定采用二进制格式,实现方便且健壮。
  2. 首部压缩(Header compression)
    HTTP1.x 的 header 带有大量信息,而且每次都要重复发送,HTTP2.0 使用 encoder 来减少需要传输的 header 大小,通讯双方各自 cache 一份 header fields 表,既避免了重复 header 的传输,又减小了需要传输的大小。
  3. 多路复用(Multiplexing)
    即连接共享。一个 request 对应一个 id,这样一个连接上可以有多个 request,每个连接的 request 可以随机的混杂在一起,接收方可以根据 request 的 id 将 request 再归属到各自不同的服务端请求里面。多路复用通过多个请求 stream 共享一个 TCP 连接的方式,解决了 队首阻塞(HOL blocking)的问题,降低了延迟同时提高了带宽的利用率。
  4. 请求优先级(Request Prioritization)
    多路复用带来一个新的问题是,在连接共享的基础之上有可能会导致关键请求被阻塞。HTTP2 允许给每个 request 设置优先级,这样重要的请求就会优先得到响应。
    比如浏览器加载首页,首页的 html 内容应该优先展示,之后才是各种静态资源文件,脚本文件等加载,这样可以保证用户能第一时间看到网页内容。
  5. 流量控制(Flow Control)
    由于 TCP 流控制既不够精细,也无法提供必要的应用级 API 来调节各个数据流的传输。例如,客户端可能请求了一个具有较高优先级的大型视频流,但是用户已经暂停视频,客户端现在希望暂停或限制从服务器的传输,以免提取和缓冲不必要的数据。
  6. 服务端推送(Server Push)
    HTTP/2 新增的另一个强大的新功能是,服务器可以对一个客户端请求发送多个响应。 换句话说,除了对最初请求的响应外,服务器还可以向客户端推送额外资源,而无需客户端明确地请求。

HTTP2 如何解决队首阻塞

想知道 HTTP2 是如何解决队首阻塞问题,要先思考为什么 HTTP1.1 会出现队首阻塞?其实是因为 HTTP 的每个请求没有唯一标识。在管道化连接上,多个 HTTP 请求出现在同一个 TCP 连接上时,如果响应乱序返回,浏览器便无法知晓服务器返回的响应是对应哪个请求。

这是 HTTP 协议自身缺陷导致的

而HTTP2 的多路复用使得 HTTP2 的每个请求对应一个 stream,每个 stream 都有唯一标识,这就使得无论在客户端还是在服务器端都不需要排队等待,在同一个 tcp 连接上,各个 steam 相互独立,互不阻塞,从而可以做到乱序响应,避免了 HTTP 协议层面 的队首阻塞问题。

注意:TCP 协议层面的队首阻塞是 HTTP/2 无法解决的(HTTP 只是应用层协议,TCP 是传输层协议),TCP 的阻塞问题是因为传输阶段可能会丢包,一旦丢包就会等待重新发包,阻塞后续传输,这个问题虽然有 滑动窗口(Sliding Window)这个方案,但是只能增强抗干扰,并没有彻底解决。

基于 HTTP/2 的 Web 优化

尽管 HTTP/2 相比 HTTP/1.x 有很大的不同,但 HTTP/1.x 的一些优化规则仍然适用的:

  1. 减少 DNS 查询。DNS 查询需要时间,没有 resolved 的域名会阻塞请求。
  2. 减少 TCP 连接。HTTP/2 只使用一个 TCP 连接。
  3. 使用 CDN。使用 CDN 分发资源可以减少延迟。
  4. 减少 HTTP 跳转。特别是非同一域名的跳转,需要 DNS,TCP,HTTP 三种开销。
  5. 消除不必要的请求数据。HTTP/2 压缩了 Header。
  6. 压缩传输的数据。gzip 压缩很高效。
  7. 客户端缓存资源。缓存是必要的。
  8. 消除不必要的资源。激进的提前获取资源对客户端和服务端都开销巨大。

在 HTTP/1.x 里推荐而 HTTP/2 禁止的优化:

  1. 域名分片(Domain Sharding)
    HTTP/1.x 中浏览器一般每个域名最多同时使用 6 个连接。每个连接都会经历 3 次握手和慢启动。然而 1.x 中我们仍然使用域名分片来突破连接数的限制(提高并行加载能力)。但是多少域名合适,每个连接的资源消耗,带宽竞争,DNS 查询时间等等都是问题。在 HTTP/2 里,多路复用完美解决问题。所以请不要在 HTTP/2 里使用域名分片。

  2. 文件合并(Concatenation)

    1.x 中我们经常合并文件来减少请求。但是,

    • 大文件会延迟(delay)客户端的处理执行(必须等到整个文件下载完)。
    • 缓存失效的开销昂贵:少量数据更新会导致整个大文件失效,从而需要重新下载。
    • 文件合并也需要额外的构建处理(build step),增加项目复杂度。
      所以在 HTTP/2 里,请避免合并文件。使用小的颗粒化的资源,优化缓存政策。
  3. 内联资源(Inline Resource)

    内联资源也是 1.x 中常用的优化手段,可以减少请求。但是,内联资源无法独立缓存,且使用了父资源的优先级,也没法被客户端拒绝,从而破坏了 HTTP/2 的多路复用和优先级策略。

    HTTP/2 中不要再使用内联资源,直接利用 Server Push:

    • 颗粒化的资源可以被独立缓存。
    • 颗粒化的资源可以被正确地利用多路复用传输和设置优先级。
    • 允许客户端更灵活地控制资源的下载和使用。

资料参考链接

  1. HTTP,HTTP2.0,SPDY,HTTPS你应该知道的一些事
  2. HTTP2学习(五)—HTTP2 VS SPDY
  3. HTTP请求方法:GET、HEAD、POST、PUT、DELETE、CONNECT、OPTIONS、TRACE
  4. HTTP协议漫谈 - HTTP协议请求方法及其属性
  5. HTTP 缓存策略
  6. HTTP常见状态码 200 301 302 404 500
  7. http协议的队首阻塞
  8. HTTP/1.1 持久连接 persistent connection
  9. HTTP2 帧基础知识以及Header、CONTINUATION、DATA帧相关资料
  10. Google Developers HTTP/2 简介
  11. HTTP2简介和基于HTTP2的Web优化
原文地址:https://www.cnblogs.com/yungyu16/p/13200710.html