前端面试常备03:从HTTP1到HTTP3知识大全(长文)

1,180 阅读41分钟

HTTP概述

Http是无状态(stateless)协议, http协议自身不对请求和响应之间的通信状态进行保存. 也就是说在HTTP这个级别, 协议对于发送过的请求或者响应都不做持久化处理.

使用HTTP协议, 每当有新的请求发送的时候, 就会有对应的新的响应信息.这么设计的原因是为了更快的处理大量的事务, 确保协议的可伸缩性.

然而随着业务的发展, 出现了需要保存状态的需求. 于是引入了Cookie技术, 这部分我们在浏览器的存储中会介绍到.

有了Cookie在使用HTTP通信, 就可以管理状态了.

HTTP 请求方法

  • HTTP1.0支持的方法: GET, POST 和 HEAD 方法
  • HTTP1.1支持的方法(增加的): OPTIONS, PUT, DELETE, TRACE 和 CONNECT

所有类型的请求概览

  • GET: 通常用于请求服务器发送某些资源
  • HEAD: 请求资源的头部信息, 并且这些头部与 HTTP GET 方法请求时返回的一致. 该请求方法的一个使用场景是在下载一个大文件前先获取其大小再决定是否要下载, 以此可以节约带宽资源
  • OPTIONS: 用于获取目的资源所支持的通信选项
  • POST: 发送数据给服务器
  • PUT: 用于新增资源或者使用请求中的有效负载替换目标资源的表现形式
  • DELETE: 用于删除指定的资源
  • PATCH: 用于对资源进行部分修改
  • CONNECT: HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器, CONNECT要求在与代理服务器通信的时候要建立隧道, 使用隧道协议进行TCP通信. 主要使用SSL(Secure Sockets Layer, 安全套接层)和TLS(Transport Layer Security, 传输层安全)协议把通信内容通过加密后经网络隧道传输
  • TRACE: 追踪路径, 让web服务端将之前的请求通信环回给客户端的方法. 发送请求时, 在Max-Forwards首部字段中填入数值, 每经过一个服务器就将该数字减一, 当数值刚好减到0的时候, 就停止继续传输, 最后接收到请求的服务端则返回200状态码

关于Trace的说明

客户端通过Trace方法可以查询发送出去的请求时怎样被加工修改的. 这是因为请求想要连接到源目标服务器可能会通过代理中转, Trace方法就是用来确认连接过程中发生的一些列操作.

但是Trace方法不太常用, 并且容易引发XST(Cross-Site Tracing, 跨站追踪)攻击, 因此通常就更不会用到了.

GET 和 POST 的区别

  • GET在浏览器回退时是无害的, 而POST会再次提交请求
  • GET产生的URL地址可以被保存, 而POST不可以
  • GET请求会被浏览器主动cache, 而POST不会
  • GET请求只能进行url编码, 而POST支持多种编码方式
  • GET请求参数会被完整保留在浏览器历史记录中, 而POST中的参数不会被保留
  • GET请求在URL中传输的参数是有长度限制的, 而POST没有
  • 对参数的数据类型, GET只接受ASCLL字符, 而POST没有限制
  • GET比POST更不安全, 因为参数直接暴露在URL上, 所以不能用来传递敏感信息
  • GET参数通过URL传递, POST放在Request body中

但是在本质上来说, GET和POST是HTTP协议中两种发送请求的方法. HTTP的底层是TCP/IP, 也就是说, 从协议层面上这两种请求的本质是一样的.

我们可以给GET加上request body, 也可以在POST加上url参数. 这在技术上是行的通的.

所以get和post实际上是一种约定的规则. 浏览器和服务器采用这种约定的规则行事, 如果没有依照这种约定, 就不保证服务的可用性.

除此之外, GET/POST还有一个状态的区别:

GET产生一个TCP数据包, POST产生两个TCP数据包.

  • 对于GET请求, 浏览器会把http headerdata一起发送数据, 服务器响应200.
  • 对于POST, 浏览器先发送header, 服务器响应100 continue, 浏览器再发送data, 服务器响应200 ok

从这里看, GET请求的效率比POST要高一些, 但是也不要盲目的用GET去替代POST, 这里是一些理由:

  1. GET/POST 有自己的明确的含义, 不应该随意的混用
  2. 在网络环境好的情况下, 发一次包的时间和发两次包的时间可以基本无视, 而在网络环境差的情况下, 两次包的TCP在验证数据包完整性上, 有非常大的优点.
  3. 并不是所有浏览器都会在POST中发送两次包, FireFox就只发送一次

PUT 和 POST 的区别

PUT 方法是幂等的, 而 POST 的非幂等的.

除此之外, 通常情况下, PUT 的 URI 执行具体单一资源, POST 指向资源集合.

也可以理解为 POST 是创建资源, PUT 是更新资源

幂等: 多次执行统一请求服务器返回的结果是一致的, 我们就说这个请求是幂等的.

PUT 和 PATCH 的区别

PUTPATCH 都是更新资源, PATCH 用来对已知资源进行局部更新.

简单的说, PUT 是重新更新全部信息, PATCH 是更新某部分的信息.

HTTP Keep-Alive

在早期的 HTTP/1.0 中,每次 http 请求都要创建一个连接,而创建连接的过程需要消耗资源和时间,为了减少资源消耗,缩短响应时间,就需要重用连接。在后来的 HTTP/1.0 中以及 HTTP/1.1 中,引入了重用连接的机制,就是在 http 请求头中加入 Connection: keep-alive 来告诉对方这个请求响应完成后不要关闭,下一次咱们还用这个请求继续交流。协议规定 HTTP/1.0 如果想要保持长连接,需要在请求头中加上 Connection: keep-alive.

keep-alive的优点:

  • 较少的 CPU 和内存的使用(由于同时打开的连接的减少了)
  • 允许请求和应答的 HTTP 管线化
  • 降低拥塞控制 (TCP 连接减少了)
  • 减少了后续请求的延迟(无需再进行握手)
  • 报告错误无需关闭 TCP

Keep-Alive 在Http1.1是默认开启的, 可以在Response Header中看到: Connection: keep-alive.

在配置该请求头的情况下, 可以配置Keep-Alive请求头的一些参数, 比如:

Keep-Alive: timeout=5, max=100

表示这个TCP的通道可以保持5秒, max则表示这个长连接最多接受100次请求就断开, 默认是没有限制的.

nginx中有两个比较重要的配置:

keepalive_timeout 65 // 保持连接的时间, 也叫超时时间
keepalive_request 100 // 最大连接上限

与TCP的 Keep Alive 的区别

HTTP的Keep-Alive和TCP的Keep Alive 在意图上有些不同, 前者只要是TCP连接的复用, 避免建立过多的TCP连接, 而TCP的Keep Alive的意图在于保持TCP连接的存活, 本质上是在发送心跳包: 每隔一段时间给接受方发送一个探测包, 如果收到回应的ACK, 则认为连接还是存活的, 在超过一定重试次数之后, 还是没有收到对方的回应, 则丢弃该TCP连接.

管线化

持久链接使得多数请求以管线化(pipelining)方式发送成为可能. 从前发送请求后需要等待并受到响应, 才能发送下一个请求. 管线化技术出现后, 不同等待响应也可以直接发送下一个请求.

这样就能做到同时并行的发送多个请求, 而不需要一个接一个的等待响应了.

本质上是将多个HTTP请求整批提交的技术, 而且在传输过程中不需要先等待服务端的回应. 管线化机制必须通过永久连接完成, 并且只有GET和HEAD请求可以进行管线化, 而POST则有所限制. 只能在HTTP1.1中启用.

浏览器将HTTP请求大批的提交可以缩短页面的加载时间. 关键在于把多个HTTP的请求消息同时塞入一个TCP分组中, 所以只要提交一个分组就能同时发出多个请求, 借此减少网络上多余的分组并降低线路负载.

HTTP 请求报文

请求报文由 4 部分组成:

  1. 请求行
  2. 请求头
  3. 空行
  4. 请求体

请求行

请求行包括: 请求方法字段(method), URL 字段, HTTP 协议版本字段. 它们用空格分隔. 例如, GET /index.html HTTP/1.1

请求头

请求头部由关键字/值组成, 每行一对, 关键字和值用英文冒号":"分隔

  1. User-Agent: 产生请求的浏览器类型
  2. Accept: 客户端可识别的内容类型列表
  3. Host: 请求的主机名, 允许多个域名同时使用一个IP地址, 即虚拟主机.

请求体

post,put等请求携带的数据.

HTTP 响应报文

响应报文也由四部分组成:

  1. 响应行: 由协议版本, 状态码和状态码的原因短语组成. 例如HTTP/1.1 200 OK
  2. 响应头: 响应首部组成
  3. 空行
  4. 响应体: 服务器响应的数据

HTTP 编码

HTTP在传输数据的时候可以按照数据原貌直接传输, 但也可以在传输过程中通过编码提升传输速率. 通过在传输的时候编码, 能够有效的处理大量的访问请求. 但是, 编码的操作需要计算机来完成, 会消耗额外的CPU资源.

报文主体和实体主体

  • 报文(message): 是HTTP通信中的基本单位, 由8位组字节流(octet sequence, 其中octet为8个比特)组成, 通过HTTP通信传输
  • 实体(entity): 作为请求或者响应的有效载荷数据(补充项)被传输, 内容由实体首部和实体主体组成.

HTTP报文的主体用于传输请求或响应的实体主体.

通常, 报文主体等于实体主体. 只有当传输中进行编码操作的时候, 实体主体的内容发生变化, 才导致它和报文主体产生差异.

内容编码

HTTP协议中有一种被称为内容编码的功能, 能将数据容量变小.

内容编码指明应用在实体内容上的编码格式, 并保持实体信息原样压缩. 内容编码后的实体由客户端接收并负责解码.

常用的内容编码有以下几种:

  • gzip(GNU zip)
  • compress(UNIX系统的标准压缩)
  • deflate(zlib)
  • identity(不进行编码)

分块传输编码

在Http通信过程中, 请求的编码实体资源尚未全部传输完成之前, 浏览器无法显示请求页面. 在传输大容量数据的时候, 通过把数据分割为多块, 能够让浏览器逐步显示页面

这种把实体主体分块的功能称为分块传输编码(Chunked Transfer Coding).

分块传输编码会将实体分成多个部分(块), 每一块都会用16进制来标记块的大小, 而实体主体的最后一块会使用"0(CR+LF)"来标记.

使用分块传输编码的实体主体会由接受的客户端负责解码, 恢复到编码前的实体主体.

HTTP/1.1 中存在一种叫做传输编码(Transfer Coding)的机制, 它可以在通信时按某种编码方式传输, 但只定义作用于分块传输编码中.

多部分对象集合

在发送邮件的时候, 我们可以在邮件中写入文字并且添加多份的附件. 这是因为采用了MIME(Multipurpose Internet Mail Extensions, 多用途因特网邮件扩展)机制, 它允许邮件处理文本, 图片, 视频等多个不同类型的数据.

在Http中也是类似的. 主要的对象包含:

  • multipart/form-data: 在web表单上传时使用
  • multipart/byteranges: 状态码206(Partial Content, 部分内容)响应报文包含了多个范围的内容时使用.

使用这些参数需要在后部字段中使用Content-Type进行指定.

使用boundary字符串来划分多部分对象集合指明的各类实体. 在boundary字符串指定的各类实体的其实行插入--标记, 而在多部分对象集合对应的字符串的最后插入--标记作为结束.

多部分对象集合的每个部分类型中, 都可以含有首部字段, 另外, 可以在某个部分中嵌套使用多部分对象集合. 有关多部分对象集合更详细的解释, 请参考RFC2046.

范围请求

指定范围发送的请求就是范围请求(Range Request).

对于一份10000字节大小的资源, 如果使用范围请求, 可以只请求5001~10 000字节内的资源.

<!-- 5001 ~ 10 000 -->
Range: bytes=5001-10000

<!-- 5001 ~ -->
Range: bytes=5001-

<!-- 0~3000, 5000~7000 -->
Range: bytes=-3000, 5000-7000

内容协商返回最合适的内容

当浏览器的默认语言为英文或者中文, 访问相同URL的web页面时会显示对应的英文版本或者中文版本. 这样的机制叫做内容协商(Content Negotiation)机制.

内容协商机制是指客户端和服务器端就响应的资源内容进行协商, 然后提供给客户端最合适的资源. 内容协商会以响应资源的语言, 字符集, 编码方式等作为判断的基准.

主要参考:

  • accept
  • accept-charset
  • accept-encoding
  • accept-language
  • content-language

内容协商技术有三种类型:

  • 服务器驱动协商(Server-driven Negotiation)
  • 客户端驱动协商(Agent-driven Negotiation)
  • 透明协商(Transparent Negotiation)

HTTP 响应状态码

2XX 成功:

  • 200 OK, 表示从客户端发来的请求在服务器端被正确处理 ✨
  • 201 Created, 请求已经被实现,而且有一个新的资源已经依据请求的需要而建立
  • 202 Accepted, 请求已接受,但是还没执行,不保证完成请求
  • 204 No content, 表示请求成功,但响应报文不含实体的主体部分
  • 206 Partial Content, 进行范围请求 ✨

3XX 重定向:

  • 301 moved permanently,永久性重定向,表示资源已被分配了新的 URL
  • 302 found,临时性重定向,表示资源临时被分配了新的 URL ✨
  • 303 see other,临重定向, 表示资源存在着另一个 URL,应使用 GET 方法定向获取资源(会转换请求方法)
  • 304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况
  • 307 temporary redirect,临时重定向,和 302 含义相同, 但是不会改变请求方式

4XX 客户端错误:

  • 400 bad request,请求报文存在语法错误 ✨
  • 401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息 ✨
  • 403 forbidden,表示对请求资源的访问被服务器拒绝 ✨
  • 404 not found,表示在服务器上没有找到请求的资源 ✨
  • 408 Request timeout, 客户端请求超时
  • 409 Confict, 请求的资源可能引起冲突

5XX 服务器错误:

  • 500 internal sever error,表示服务器端在执行请求时发生了错误 ✨
  • 501 Not Implemented 请求超出服务器能力范围,例如服务器不支持当前请求所需要的某个功能,或者请求是服务器不支持的某个方法
  • 503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求
  • 505 http version not supported 服务器不支持http协议版本,或者拒绝支持在请求中使用的 HTTP 版本

307, 303, 302 的区别

302 是 http1.0 的协议状态码,在 http1.1 版本的时候为了细化 302 状态码又出来了两个 303 和 307。

303 明确表示客户端应当采用 get 方法获取资源,他会把 POST 请求变为 GET 请求进行重定向。 307 会遵照浏览器标准,不会从 post 变为 get。

302 与 301 对搜索引擎的影响

302 与 301 都是重定向状态码, 不同的是 301 是永久重定向, 302 是临时重定向.

301 重定向是网页更改地址后对搜索引擎友好的最好方法,只要不是暂时搬移的情况,都建议使用 301 来做转址。 如果我们把一个地址采用 301 跳转方式跳转的话,搜索引擎会把老地址的PageRank等信息带到新地址,同时在搜索引擎索引库中彻底废弃掉原先的老地址。旧网址的排名等完全清零

302 代表暂时性转移(Temporarily Moved ),在前些年,不少 Black Hat SEO 曾广泛应用这项技术作弊,目前,各大主要搜索引擎均加强了打击力度,像 Google 前些年对 Business.com 以及近来对 BMW 德国网站的惩罚。即使网站客观上不是 spam,也很容易被搜寻引擎容易误判为 spam 而遭到惩罚。

:::tip spam spam, 搜索引擎垃圾技术, 利用不道德的技巧提高搜索引擎上的排名. 可能会导致搜索引擎把你的网站从它的数据库中永久删除.

常见的搜索引擎垃圾技术:

  1. 隐藏文本
  2. 重复关键字
  3. 使用无关关键字
  4. 隐藏标签
  5. 相同或者相似的页面
  6. 页面交换技术
  7. 无内容
  8. 过渡提交
  9. 链接搜索引擎垃圾技术 :::

HTTP 头部

HTTP的请求分为请求报文和响应报文.

在请求报文中, 由方法, URI, HTTP版本, HTTP首部字段等部分组成.

在相应报文中, 有HTTP版本, 状态码, HTTP首部字段3部分组成.

其中首部字段的内容是最为丰富的.

HTTP的首部字段根据实际的通途分为四类:

  • 通用首部字段: 请求报文和响应报文都会使用的首部字段
  • 请求首部字段: 客户端向服务器发送请求报文时使用的首部字段, 补充了请求的附加内容, 客户端信息, 响应内容相关优先级信息
  • 响应首部字段: 从服务端向客户端返回响应报文时使用的首部, 补充了响应的附加内容, 也会要求客户端附加额外的内容信息
  • 实体首部字段: 针对请求报文和响应报文的实体部分使用的首部, 补充了资源内容更新时间和实体有关的信息

通用首部字段(General Header Fields)

请求报文和响应报文两方都会使用的首部:

  • Cache-Control 控制缓存 ✨
  • Connection 连接管理、逐跳首部 ✨
  • Upgrade 可以用来检测HTTP协议以及其他协议是否可使用, 并用更高的版本进行通信.
  • via 代理服务器的相关信息, 可以用来追踪客户端与服务器之间的请求和响应报文的传输路径. 也可以用于避免请求回环的发生, 常常和trace请求一起使用.
  • Wraning 告知用户一些与缓存相关的问题的警告, 格式如下: Warning: [警告码] [警告的主机:端口号] "[警告内容]" ([日期时间])
  • Transfor-Encoding 报文主体的传输编码格式 ✨
  • Trailer 会事先说明在报文主体后面记录了哪些首部字段, 可以应用在分块传输编码的时候.
  • Pragma 报文指令, 是历史遗留字段, 仅仅为了兼容而存在.
  • Date 创建报文的日期

Cache-Control 控制缓存

该指令的参数是可选的, 多个指令之间通过,进行分隔. 首部字段Cache-Control可以用于请求以及响应时.

具体的参数内容可以参照浏览器/缓存策略

Connection 连接管理、逐跳首部

该首部具有两个作用:

  • 控制不再转发给代理的首部字段: Connection: 不再转发的首部字段名称
  • 管理持久链接: Connetion: close HTTP/1.1版本是默认持久连接的. 只要TCP连接不断开, 就可以一直发送HTTP请求, 持续不断, 没有上限. 在1.0版本中有连接的限制和规则. 这里不展开叙述. 可以通过执行close明确关闭连接. 只有设置Connection: Keep-Alive, 后面的keep-alive字段才会生效.

请求头部字段

客户端向服务器发送请求的报文时使用的首部

  • Accept 客户端或者代理能够处理的媒体类型 ✨
  • Accept-Encoding 优先可处理的编码格式
  • Accept-Language 优先可处理的自然语言
  • Accept-Charset 优先可以处理的字符集
  • Authorization web 的认证信息 ✨
  • Host 请求资源所在服务器, 是HTTP/1.1规范中唯一一个必须被包含在请求内的首部字段
  • Expect 期待服务器的特定行为
  • If-Match 比较实体标记Etag, 只有两个值匹配一致的时候, 服务器才会接受请求
  • If-None-Match 比较实体标记(ETage)与 If-Match 相反 ✨
  • If-Modified-Since 比较资源更新时间(Last-Modified)✨
  • If-Unmodified-Since 比较资源更新时间(Last-Modified),与 If-Modified-Since 相反 ✨
  • If-Ranges 告知服务器若指定的if-range字段值(ETag的值或者时间)与请求资源的ETag值或者时间相一致的时候, 作为范围请求处理. 反之, 返回全体资源
  • Range 实体的字节范围请求 ✨
  • Proxy-Authorization 代理服务器要求 web 认证信息
  • From 用户的邮箱地址
  • User-Agent 客户端程序信息 ✨
  • Max-Forwrads 最大的逐跳次数, 每经过一个代理服务器就减1, 可以用来检查请求路径的通信情况.
  • TE 传输编码的优先级
  • Referer 请求原始方的url, 可以知道URI是从哪个web页面发起的

响应首部字段

从服务器向客户端响应式使用的字段:

  • Accept-Ranges 能接受的字节范围, 当不能处理范围请求时, 可以发送Accept-Ranges: none
  • Age 告知客户端, 源服务器在多久之前创建了响应, 字段值单位为秒
  • ETag 能够表示资源唯一资源的字符串 ✨
  • Location 令客户端重定向的 URI, 基本用在配合30x的重定向请求时提供 ✨
  • Proxy-Authenticate 代理服务器要求客户端的验证信息
  • Retry-After 和状态码 503 一起使用的首部字段,表示下次请求服务器的时间
  • Server 服务器的信息 ✨
  • Vary 代理服务器的缓存信息控制, 比如Vary: Accept-Language表示只能对相同自然语言的请求返回缓存
  • WWW-Authenticate 服务器要求客户端的验证信息

实体首部字段

针对请求报文和响应报文的实体部分使用首部

  • Allow 资源可支持 http 请求的方法 ✨
  • Content-Encoding 实体的编码格式, 主要有gzip,compress, deflate, identity四种
  • Content-Language 实体的资源语言
  • Content-Location 代替资源的uri, 与Location不同, 该字段表示的是报文主题返回资源对应的URI
  • Content-Length 实体的大小(字节)
  • Content-Type 实体媒体类型
  • Content-MD5 实体报文的摘要, 该字段无法校验内容是否被篡改
  • Content-Range 针对范围请求, 能告知客户端作为响应返回的实体的哪个部分符合范围请求
  • Last-Modified 资源最后的修改资源 ✨
  • Expires 实体主体的过期资源 ✨

非HTTP/1.1首部字段

除了这些字段, 还有比较常用的cookie, set-cookie以及content-disposition等等在其他RFC中定义的首部字段, 使用的频率也是很高的.

End-to-end / Hop-by-hop

HTTP首部字段将定义成缓存代理和非缓存代理的行为, 分成2种类型.

  • 端到端首部(End-to-end Header): 分在此类别中的首部会转发给请求/响应对一个的最终接受目标, 且必须保存在由缓存生成的响应中, 另外规定它必须被转发.
  • 逐跳首部(Hop-by-hop Header): 分在此类别中的首部只对单次转发有效, 会因通过缓存或代理而不再转发. HTTP/1.1和之后版本中, 如果要使用该首部, 需要提供Connection首部

逐跳首部字段有:

  • Connection
  • Keep-Alive
  • Proxy-Authenticate
  • Proxy-Authorization
  • Trailer
  • TE
  • Transfer-Encoding
  • Upgrade

Cookie 相关的首部字段

  • set-cookie: 开始状态管理所使用的cookie信息, 响应首部字段
  • cookie: 服务器接收到的Cookie信息, 请求首部字段

这部分内容参考浏览器存储文章内容

其他首部字段

  • X-frame-Options: 属于HTTP响应首部, 用于控制网站内容在其他Web网站的Frame标签内的显示问题. 其主要目的是为了防止点击劫持(clickjacking)攻击. 其中有两个可以指定的字段:
    • DENY: 拒绝
    • SAMEORIGIN: 仅同源域名下的页面匹配的时候许可
  • X-XSS-Protection: 针对跨站脚本攻击的一种策略, 用于控制浏览器XSS防护机制的开关
    • 0: 将XSS过滤设置为无效状态
    • 1: 将XSS过滤设置为有效状态
  • DNT: Do Not Track. 表示拒绝被精准广告追踪的一种方法
    • 0: 同意被追踪
    • 1: 拒绝被追踪
  • P3P(The Platform for Privacy Preferences, 在线隐私偏好平台)技术, 可以让web网站上的个人隐私变成一种仅供程序可理解的形式, 以达到保护用户隐私的目的.

要设置P3P, 需要:

  1. 创建P3P隐私
  2. 创建P3P隐私对照文件, 命名保存在/w3c/p3p.xml
  3. 从P3P隐私中新建Compact policies后, 输出到HTTP响应中.

HTTP2

HTTP2 相比较于 HTTP1.X 大幅度的提升了 web 性能, 在于 HTTP1.1 完全兼容的基础上, 进一步减少了网络延迟, 而对于前端开发人员来说, 无疑减少了在前端方面的优化工作.

具体的文档在这里: HTTP/2: the Future of the Internet

技术方案在这里: RFC 7541

多路复用

众所周知,在 HTTP/1.1 协议中, 浏览器客户端在同一时间. 针对同一域名下的请求有一定数量限制。超过限制数目的请求会被阻塞.

HTTP2 的多路复用则允许通过单一的 HTTP2 链接发起多重的请求-响应的消息.

因此 HTTP2 可以很容易的实现多流并行而不用依赖建立多个 TCP 链接, HTTP/2 把 HTTP 协议通信的基本单位缩小为一个一个的帧, 这些帧对应逻辑流中的消息, 并行的在同一个 TCP 连接上双向交换消息.

二进制分帧

HTTP2 在兼容 HTTP1.X 的情况下, 在应用层(HTTP/2)和传输层(TCP/UDP)之间增加了一个二进制分帧层.

在这个二进制分帧层中, HTTP2 会将所有传输的信息分割为更小的消息和帧(frame), 并对他们采用二进制格式的编码, 其中 HTTP1.x 的首部信息会被封装到HEADER frame, 而相应的 Request Body 则封装到 DATA frame 里面.

HTTP/2 通信都在一个连接上完成, 这个连接可以承载任意数量的双向数据流.

在 1.X 版本中, HTTP 性能优化的关键并不在于高带宽, 而是低延迟. TCP 连接会随着时间进行自我调节, 期初会限制连接的最大速度, 如果数据传输成功, 会随着时间的推移提高传输的速度. 这种调谐被称为 TCP 慢启动.

HTTP2 通过让所有数据流共用一个连接, 可以更有效的使用 TCP 连接, 让高带宽服务于 HTTP 的性能提升.

总之:

  1. 单连接多资源的方式, 减少了服务端的压力, 内存占用更少, 连接吞吐量更大
  2. 由于 TCP 的连接的减少而使用网络拥塞状况得以改善, 同时慢启动时间的减少, 使得拥塞和丢包恢复速度更快.

请求优先级

把HTTP消息分为很多独立帧之后, 就可以通过优化这些帧的交错和传输顺序进一步优化性能. 每一个流都可以带一个31比特的优先值: 0表示最高优先级, 2^31 - 1表示最低优先级.

权重: 服务器可以根据流的优先级, 控制资源分配(CPU, 内存, 带宽), 而在响应数据准备好之后, 优先级将最高优先级的帧发送给客户端. 高优先级的流会优先发送. 但也不是绝对的, 因为会引入首队阻塞的问题: 高优先级的请求导致阻塞其他资源的交付.

分配处理资源和客户端与服务器之间的带宽, 不同优先级的混合也是必须的. 客户端可以指定哪个流是重要的.

流依赖关系: 每个流都可以明确的依赖另一个流, 客户端会使用权重和流依赖关系的组合信息, 向服务端构造和传递"优先级树". 这个树表明了其希望如何接受响应. 即我们期望优先级越高的请求越快得到响应.

浏览器中有一个默认的优先级。浏览器基于自身对资源重要性的判读:

  • 优先级最高: html
  • 优先级高: css
  • 优先级中: js
  • 优先级低: 图片等其他资源

首部压缩

HTTP1.X 不支持首部压缩, 因此 SPDY 和 HTTP2 应运而生, SPDY 使用的是 DEFLATE 算法, HTTP2 则使用了专门的 HPACK 算法. 减少了 header 的大小。并在两端维护了索引表,用于记录出现过的 header ,后面在传输过程中就可以传输已经记录过的 header 的键名,对端收到数据后就可以通过键名找到对应的值。

为什么要压缩

在HTTP/1 中, HTTP请求和响应都是由状态行, 请求/响应头部, 消息主题 三部分组成的. 一般而言, 消息主体都会经过gzip压缩, 或者本身传输的就是压缩过后的二进制文件(比如图片, 视频), 但状态行和头部却没有经过任何压缩, 而直接以纯文本传输

随着web功能越来越复杂, 每个页面产生的请求数量也越来越多. 根据HTTP Archive的统计, 当前平均每个页面都会产生上百个请求. 越来越多的请求导致消耗的头部的流量越来越多.

在HTTP/1时代, 为了减少头部消耗的流量, 有很多优化方案可以尝试, 例如合并请求, 启用Cookie-Free域名等等, 但是这些方案多少有点问题.

技术原理

通俗的来说, 头部压缩需要在支持HTTP/2的浏览器和服务端之间:

  • 维护一份相同的静态字典(Static Table), 包含常见的头部名称, 以及特别常见的头部名称和值的组合
  • 维护一份相同的动态字典(Dynamic Table), 可以动态的添加内容
  • 支持基于静态哈夫曼码表的哈夫曼编码(Huffman Coding)

静态字典的作用有两个:

  1. 对于完全匹配的头部键值对, 例如: method: GET, 可以直接使用一个字符表示
  2. 对于头部名称可以匹配的键值对, 例如cookie: xxxx, 可以将名称使用一个字符表示.

HTTP/2中的静态字典大致如下(httpwg.org/specs/rfc75…):

IndexHeader NameHeader Value
1:authority
2:methodGET
3:methodPOST
4:path/
5:path/index.html
6:schemehttp
7:schemehttps
8:status200
32cookie
60via
61www-authenticate

同时, 浏览器可以告知服务端, 将cookie: xxx添加到动态字典中, 这样后续整个键值对就可以使用一个字段表示了. 类似的, 服务端也可以更新对方的动态字典. 需要注意的是, 动态字典是上下文有关的, 需要为每个HTTP/2连接维护不同的字典.

使用字典可以极大的提升压缩效果, 其中静态字典在首次请求中就可以使用. 对于静态, 动态字典不存在的内容, 还可以使用哈夫曼编码来减小体积. HTTP/2使用了一份静态哈夫曼码表, 也需要内置在客户端和服务端之中.

此外,HTTP/1 的状态行信息(Method、Path、Status 等),在 HTTP/2 中被拆成键值对放入头部(冒号开头的那些),同样可以享受到字典和哈夫曼压缩。另外,HTTP/2 中所有头部名称必须小写

服务端推送

在 HTTP 2.0 中,服务端可以在客户端某个请求后,主动推送其他资源。

可以想象以下情况,某些资源客户端是一定会请求的,这时就可以采取服务端 push 的技术,提前给客户端推送必要的资源,这样就可以相对减少一点延迟时间。当然在浏览器兼容的情况下你也可以使用prefetch

服务端推送还有一个比较大的优势: 可以缓存. 也就是说可以在遵循同源策略的情况下, 不同页面之间共享缓存资源.

注意, 服务端推送有两点:

  1. 推送是遵循同源策略的
  2. 这种服务端的推送是基于客户端的请求响应来决定的.

当服务端需要主动推送某个资源的时候, 就会发送一个Frame TypePUSH_PROMISEFrame, 里面携带了PUSH需要新建的Stream ID. 意思是告诉客户端: 接下来我要用这个ID给你发送信息. 客户端解析Frame的时候, 发现它是一个PUSH_PROMISE类型, 就会接受服务端要推送的流.

性能瓶颈

启用HTTP/2会带来很大的性能提升, 但是也会带来新的性能瓶颈. 因为现在所有的压力都集中在底层一个TCP的连接上, TCP很可能就是下一个性能瓶颈, 比如TCP分组的队首阻塞问题, 单个TCP packet丢失导致的整个连接阻塞, 无法避免, 此时所有消息都会受到印象.

nginx 升级 HTTP2

  1. nginx版本高于1,9,5
  2. 安装了--with-http_ssl_module--with-http_v2_module
  3. 配置了HTTPS(启用HTTP2的前提条件)
  4. 配置listen 443 ssl http2
  5. nginx restart(注意不要直接nginx -s reload)

HTTP3

HTTP3是在保持QUIC稳定性的同事使用UDP来实现高速度, 同时又不会牺牲TLS的安全性.

QUIC 协议概览

QUIC(Quick UDP Internet Connections, 快速UDP网络连接)是基于UDP的协议, 利用了UDP的速度和效率, 同时整合TCP, TLS和HTTP/2的优点并加以优化. 用一张图可以清晰的表示他们之间的关系.

QUIC是用来替代TCP, SSL/TLS的传输层协议, 在传输层之上还有应用层. 我们熟知的应用层协议有HTTP, FTP, IMAP等, 这些协议理论上都可以运行在QUIC上, 其中运行在QUIC之上的协议被称为HTTP/3, 这就是HTTP over QUICHTTP/3的含义

因此想要了解HTTP/3, QUIC是绕不过去的, 下面是几个重要的QUIC特性.

0 RTT建立连接

RTT: round-trip time, 仅包括请求访问来回的时间

HTTP/2的连接建立需要3 RTT, 如果考虑会话复用, 即把第一次握手计算出来的对称密钥缓存起来, 那也需要2 RTT. 更进一步的, 如果TLS升级到1.3, 那么HTTP/2连接需要2RTT, 考虑会话复用需要1RTT. 如果HTTP/2不急于HTTPS, 则可以简化, 但实际上几乎所有浏览器的设计都要求HTTP/2需要基于HTTPS.

HTTP/3首次连接只需要1RTT, 后面的链接只需要0RTT, 意味着客户端发送给服务端的第一个包就带有请求数据, 其主要连接过程如下:

  1. 首次连接, 客户端发送Inchoate Client Hello, 用于请求连接;
  2. 服务端生成g, p, a, 根据g, p, a算出A, 然后将g, p, A放到Server Config中在发送Rejection消息给客户端.
  3. 客户端接收到g,p,A后, 自己再生成b, 根据g,p,a算出B, 根据A,p,b算出初始密钥K, B和K算好后, 客户端会用K加密HTTP数据, 连同B一起发送给服务端.
  4. 服务端接收到B后, 根据a,p,B生成与客户端同样的密钥, 再用这密钥解密收到的HTTP数据. 为了进一步的安全(前向安全性), 服务端会更新自己的随机数a和公钥, 在生成新的密钥S, 然后把公钥通过Server Hello发送给客户端. 连同Server Hello消息, 还有HTTP返回数据.

这里使用DH密钥交换算法, DH算法的核心就是服务端生成a,g,p3个随机数, a自己持有, g和p要传输给客户端, 而客户端会生成b这1个随机数, 通过DH算法客户端和服务端可以算出同样的密钥. 在这过程中a和b并不参与网络传输, 安全性大大提升. 因为p和g是大数, 所以即使在网络传输中p, g, A, B都被劫持, 靠现在的计算力算力也无法破解.

连接迁移

TCP连接基于四元组(源IP, 源端口, 目的IP, 目的端口), 切换网络时至少会有一个因素发生变化, 导致连接发送变化. 当连接发送变化是, 如果还是用原来的TCP连接, 则会导致连接失败, 就得等到原来的连接超时后重新建立连接, 所以我们有时候发现切换到一个新的网络时, 即使网络状况良好, 但是内容还是需要加载很久. 如果实现的好, 当检测到网络变化时, 立即建立新的TCP连接, 即使这样, 建立新的连接还是需要几百毫秒时间.

QUIC不受四元组的影响, 当这四个元素发生变化时, 原连接依然维持. 原理如下:

QUIC不以四元素作为表示, 而是使用一个64位的随机数, 这个随机数被称为Connection ID, 即使IP或者端口发生变化, 只要Connection ID没有变化, 那么连接依然可以维持.

队头阻塞/多路复用

HTTP/1.1HTTP/2都存在队头阻塞的问题(Head Of Line blocking).

TCP是个面向连接的协议, 即发送请求后需要收到ACK消息, 以确认对象已接受数据. 如果每次请求都要在收到上次请求的ACK消息后再请求, 那么效率无疑很低. 后来HTTP/1.1提出了Pipeline技术, 允许一个TCP连接同时发送多个请求. 这样就提升了传输效率.

在这样的背景下, 队头阻塞发生了. 比如, 一个TCP连接同时传输10个请求, 其中1,2,3个请求给客户端接收, 但是第四个请求丢失, 那么后面第5-10个请求都被阻塞. 需要等第四个请求处理完毕后才能被处理. 这样就浪费了带宽资源.

因此, HTTP一般又允许每个主机建立6个TCP连接, 这样可以更加充分的利用带宽资源, 但每个连接中队头阻塞的问题还是存在的.

HTTP/2的多路复用解决了上述的队头阻塞问题. 在HTTP/2中, 每个请求都被拆分为多个Frame通过一条TCP连接同时被传输, 这样即使一个请求被阻塞, 也不会影响其他的请求.

但是, HTTP/2虽然可以解决请求这一粒度下的阻塞, 但HTTP/2的基础TCP协议本身却也存在队头阻塞的问题. HTTP/2的每个请求都会被拆分成多个Frame, 不同请求的Frame组合成Stream, Stream是TCP上的逻辑传输单元, 这样HTTP/2就达到了一条连接同时发送多个请求的目标, 其中Stram1已经正确送达, Stram2中的第三个Frame丢失, TCP处理数据是有严格的前后顺序, 先发送的Frame要先被处理, 这样就会要求发送方重新发送第三个Frame, Steam3和Steam4虽然已到达但却不能被处理, 那么这时整条链路都会被阻塞.

不仅如此, 由于HTTP/2必须使用HTTPS, 而HTTPS使用TLS协议也存在队头阻塞问题. TLS基于Record组织数据, 将一对数据放在一起加密, 加密完成后又拆分成多个TCP包传输. 一般每个Record 16K, 包含12个TCP包, 这样如果12个TCP包中有任何一个包丢失, 那么整个Record都无法解密.

队头阻塞会导致HTTP/2在更容易丢包的弱网络环境下比HTTP/1.1更慢.

QUIC是如何解决队头阻塞的问题的? 主要有两点:

  • QUIC的传输单位是Packet, 加密单元也是Packet, 整个加密, 传输, 解密都基于Packet, 这就能避免TLS的阻塞问题.
  • QUIC基于UDP, UDP的数据包在接收端没有处理顺序, 即使中间丢失一个包, 也不会阻塞整条连接. 其他的资源会被正常处理.

拥塞控制

拥塞控制的目的是避免过多的数据一下子涌入网络, 导致网络超出最大负荷. QUIC的拥塞控制与TCP类似, 并在此基础上做了改进. 先来看看TCP的拥塞控制.

  • 慢启动: 发送方像接收方发送一个单位的数据, 收到确认后发送2个单位, 然后是4个, 8个依次指数增长, 这个过程中不断试探网络的拥塞程度.
  • 避免拥塞: 指数增长到某个限制之后, 指数增长变为线性增长
  • 快速重传: 发送方每一次发送都会设置一个超时计时器, 超时后认为丢失, 需要重发
  • 快速恢复: 在上面快速重传的基础上, 发送方重新发送数据时, 也会启动一个超时定时器, 如果收到确认消息则进入拥塞避免阶段, 如果仍然超时, 则回到慢启动阶段.

QUIC重新实现了TCP协议中的Cubic算法进行拥塞控制, 下面是QUIC改进的拥塞控制的特性:

1. 热插拔

TCP中如果要修改拥塞控制策略, 需要在系统层面今次那个操作, QUIC修改拥塞控制策略只需要在应用层操作, 并且QUIC会根据不同的网络环境, 用户来动态选择拥塞控制算法.

2. 前向纠错 FEC

QUIC使用前向纠错(FEC, Forword Error Correction)技术增加协议的容错性. 一段数据被切分为10个包后, 一次对每个包进行异或运算, 运算结果会作为FEC包与数据包一起被传输, 如果传输过程中有一个数据包丢失, 那么就可以根据剩余9个包以及FEC包推算出丢失的那个包的数据, 这样就大大增加了协议的容错性.

这是符合现阶段网络传输技术的一种方案, 现阶段带宽已经不是网络传输的瓶颈, 往返时间才是, 所以新的网络传输协议可以适当增加数据冗余, 减少重传操作.

3. 单调递增的Packer Number

TCP为了保证可靠性, 使用Sequence Number和ACK来确认消息是否有序到达, 但这样的设计存在缺陷.

超时发生后客户端发起重传, 后来接受到了ACK确认消息, 但因为原始请求和重传请求接受到的ACK消息一样, 所以客户端就不知道这个ACK对应的是原始请求还是重传请求. 这就会造成歧义.

  • RTT: Round Trip Time, 往返事件
  • RTO: Retransmission Timeout, 超时重传时间

如果客户端认为是重传的ACK, 但实际上是右图的情形, 会导致RTT偏小, 反之会导致RTT偏大.

QUCI解决了上面的的歧义问题, 与Sequence Number不同, Packet Number严格单调递增, 如果Packet N丢失了, 那么重传时Packet的标识就不会是N, 而是比N大的数字, 比如N+M, 这样发送方接收到确认消息时, 就能方便的知道ACK对应的原始请求还是重传请求.

4. ACK Delay

TCP计算RTT时没有考虑接收方接受到数据发发送方确认消息之间的延迟, 如下图所示, 这段延迟即ACK Delay. QUIC考虑了这段延迟, 使得RTT的计算更加准确.

5. 更多的ACK块

一般来说, 接收方收到发送方的消息后都应该发送一个ACK恢复, 表示收到了数据. 但每收到一个数据就返回一个ACK恢复实在太麻烦, 所以一般不会立即回复, 而是接受到多个数据后再回复, TCP SACK最多提供3个ACK block. 但在有些场景下, 比如下载, 只需要服务器返回数据就好, 但按照TCP的设计, 每收到三个数据包就要返回一个ACK, 而QUIC最多可以捎带256个ACK block, 在丢包率比较严重的网络下, 更多的ACK可以减少重传量, 提升网络效率.

浏览控制

TCP 会对每个TCP连接进行流量控制, 流量控制的意思是让发送方不要发送太快, 要让接收方来得及接受, 不然会导致数据溢出而丢失, TCP的流量控制主要通过滑动窗口来实现的. 可以看到, 拥塞控制主要是控制发送方的发送策略, 但没有考虑接收方的接收能力, 流量控制是对部分能力的不起.

QUIC只需要建立一条连接, 在这条连接上同时传输多条Stream, 好比有一条道路, 量都分别有一个仓库, 道路中有很多车辆运送物资. QUIC的流量控制有两个级别: 连接级别(Connection Level)和Stream 级别(Stream Level).

对于单条的Stream的流量控制: Stream还没传输数据时, 接收窗口(flow control recevice window)就是最大接收窗口, 随着接收方接收到数据后, 接收窗口不断缩小. 在接收到的数据中, 有的数据已被处理, 而有的数据还没来得及处理. 如下图, 蓝色块表示已处理数据, 黄色块表示违背处理数据, 这部分数据的到来, 使得Stream的接收窗口缩小.

随着数据不断被处理, 接收方就有能力处理更多数据. 当满足(flow control receivce offset - consumed bytes) < (max receive window/2)时, 接收方会发送WINDOW_UPDATE frame告诉发送方你可以再多发送数据, 这时候flow control receive offset就会偏移, 接收窗口增大, 发送方可以发送更多数据到接收方.

Stream级别对防止接收端接收过多数据作用有限, 更需要借助Connection级别的流量控制. 理解了Stream流量那么也很好理解Connection的流控. Stream中,

接收窗口=最大接受窗口 - 已接收数据

而对于Connection来说:

接收窗口 = Stream1 接收窗口 + Stream2 接收窗口 + ... + StreamN 接收窗口

参考链接