支撑现有 Web 服务的 HTTP 协议距离其发布时的 1997 年已经有些年月了,随后的 HTTP/1.1 版本发布自 1999 年。随着技术的进步和需求的进化,对于数据快速高效地传输,HTTP/1.1 显得捉襟见肘,大部分优化是在应用层来做的,因为协议是改不了的嘛,比如雪碧图减少请求数,域名共享(Domain sharing)增加同时建立的请求数。

SPDY 与 HTTP/2

早在 2009 年,Google 便尝试推出了一个叫 SPDY 的实验性协议来对 HTTP/1.1 进行升级优化。在一个工作了这么久的协议上进行大的改进,如何做到对现有应用影响最小又收效显著,相信每个对历史代码有重构的开发者来说是能够体会的。所以 Google 在尝试对协议进行优化时,其目标是:

  • 减少 50% 页面加载耗时(PLT/Page Load Time)
  • 做到无须开发者适配网页
  • 简化部署和避免对现有网络设施进行大改动
  • 与开源社区一同推进实施
  • 采集实际运行时的性能数据以评估实施结果

为了达到减少 50% 页面加载耗时的目标,分帧层便是在这时引入的。

在最初的内部实验结果中,页面加载耗时的提升相当显著,达到了 55%。随后该协议在主流浏览器中得以实现,Chrome, Firefix 及 Opera,各大小公司也开始在其服务中部署实施。借着 SPDY 这波势头,HTTP 工作组 (HTTP Working Group/HTTP-WG)以其为基础开始了 HTTP/2 的提案,随后两者并行发展,SPDY 可看成是 HTTP/2 的一个实验分支,在这上面进行新功能开发和测试,然后将稳定的功能提交到 HTTP/2 草案中。

经过长时间的不断测试演进,2015 年 HTTP/2 通过了可发布的标准,是时,Chrome 团队给出了废弃 SPDY 的日程,开始了 HTTP/2 的时代。但其实早在此协议正式通过前,已经有好些浏览器及站点完整地实施了 HTTP/2,享受到了性能上的巨大提升。

HTTP/2 对于现有应用层的东西没有改变,比如 HTTP 动词,状态码,请求响应头。它的变动主要在数据格式及传输上,通过新增的分帧层(Frame layer)将这些变动和应用层隔离,所以对于应用层来说是透明的,现有 Web 应用可以不经任何修改就切到 HTTP/2。

关于版本

为何不是 HTTP/1.2

为了达到预期的性能优化,HTTP/2 中新增了二进制分帧层,这与现有 HTTP/1.1 协议的服务器及客户端是不兼容的,所以版本号来了个大升级。

除非你是服务器的开发者,需要关注这些底层的实现,否则对于应用来说,这些优化是服务器和客户端替你实现的,无须关心。

如何看站点使用的 HTTP 版本

最简单的办法,通过浏览器的开发者工具中的网络面板查看。

Chrome DevTools 网络面板中查看 HTTP 版本

其中 Protocal 一列显示 h2 即为 HTTP/2 协议。

也可使用 curl 查看到 HTTP/2 的返回。因为只需要查看响应头,所以加 -I 参数即可。

-I, --head

(HTTP FTP FILE) Fetch the headers only! HTTP-servers feature the command HEAD which this uses to get nothing but the header of a document. When used on an FTP or FILE file, curl displays the file size and last modification time only.

-- 来自 curl 工具 man 页面对 -I 参数的解释。

$ curl -I <url>

以下是 Bing 中国及百度的返回。

对 Bing 中国使用 curl 查看返回的结果
$ curl -I https://cn.bing.com
+ HTTP/2 302
cache-control: private
content-length: 139
content-type: text/html; charset=utf-8
location: https://www4.bing.com/
vary: Accept-Encoding
set-cookie: ...
set-cookie: ...
set-cookie: ...
set-cookie:...
set-cookie: ...
date: Sun, 14 Apr 2019 06:29:36 GMT
对 百度使用 curl 查看返回的结果
$ curl -I https://www.baidu.com
+ HTTP/1.1 200 OK
Accept-Ranges: bytes
Cache-Control: private, no-cache, no-store, proxy-revalidate, no-transform
Connection: Keep-Alive
Content-Length: 277
Content-Type: text/html
Date: Sun, 14 Apr 2019 06:30:43 GMT
Etag: "575e1f5c-115"
Last-Modified: Mon, 13 Jun 2016 02:50:04 GMT
Pragma: no-cache
Server: bfe/1.0.8.18

通过返回可以了解到,Bing 中国站点已经启用 HTTP/2,百度使用的是 HTTP/1.1。

二进制分帧层

HTTP/2 所有优化中,最关键的技术点莫过于二进制分帧层(Binary framing layer)了,它负责 HTTP 消息在服务器与客户端之间封装及传输。

来自 Web Fundamentals 关于二进制分帧层的示意图

从示意图可看出新引入的分帧层位于 HTTP 应用层与套接字接口(socket interface )之间。上层 HTTP API 相关的部分比如 HTTP 动词,headers 并不受影响,所以做到了既有的应用无需做适配。但真实的 HTTP 消息在传输时,经过分帧层的处理,数据被划分成了更小单位的帧,以二进制的形式传送。所以浏览器与服务器需要处理对这种新增二进制数据的解析。

流,消息与帧

进一步了解 HTTP/2 中数据传输前,先来理解一些新的概念。

流/Stream

客户端与服务器建立连接后,两者之间一个双向的数据流,也可理解为管道,一次可传输一个或多个消息(Message)。每个流都有唯一标识和一个可选的优先级。

消息/Message

一序列有序的帧(Frame),对应一个完整请求(Request)或响应(Response)数据。

帧/Frame

HTTP/2 中数据传输的最小单位,每一帧都包含一个头部(frame header)以标识该帧属于哪个流(Stream)。同时帧里还会承载特定数据的传输,比如请求头,请求数据,响应头,响应数据。

来自 Web Fundamentals 关于 HTTP/2 中一个 TCP 链接的示意图

所有通信都通过一个 TCP 链接进行,其中可承载任意数量的流。 来自不同流的帧数据在 TCP 连接中是混合着传输的,这使得帧在传输时是非常高效的,数据到达后会通过其中的头部来重新进行组装,还原到原来所属的流中。上图中可看出可各术语的包含关系。

简单理解,HTTP/2 中将通信数据切分成一小片一小片的帧并以二进制形式传输,在单个 TCP 中多路复用(multiplexed)。这是 HTTP/2 提供的基础特性,为其他性能优化提供了技术上的保障。

请求与响应的多路复用

HTTP/1.1 协议下,客户端如果想并行加载资源,需要建立多个 TCP 连接,每个请求单独一个。这样的效率是不高的,比如 header 中的 cookie 冗余,客户端能够同时发起的链接也有限,建立 TCP 连接成本很大,然而这个建立好的连接却没有被充分利用,只能返回一个响应。

HTTP/2 中通过将请求数据分帧,在同一个 TCP 链接中交错传输,在接收方再重新将分片的数据组装回来,同时数据的传输双向同时并行,提高了连接的利用效率,也避免了建立多个 TCP 连接的资源和时间消耗。

来自 Web Fundamentals 关于 HTTP/2 协议下连接的示意图

上面示例中,服务器通过 stream 1 和 stream 3 向客户端发送数据,同时客户端也在通过 stream 5 向服务器发送数据。

HTTP/2 提供的这种分帧数据传输带来了一系列建立在其之上的优化:

  • 并行发起多个请求和发送多个响应,互不阻塞。
  • 多个请求与响应并行地在一个 TCP 连接中完成。
  • HTTP/1.1 中一些优化可以去掉了(比如 Optimizing for HTTP/1.x 中提到的雪碧图,域名共享)。
  • 通过减少了不必要的连接及优化现有网络机制减少了时延,加快了页面加载速度。
  • ...

流处理的优先级

来自不同请求的数据都在同一个连接中双向并行传输,这些帧的顺序及优先级该如何设置才能让信息处理得高效,这成了一个问题的关键。所以 HTTP/2 中为流提供了权重(weight)和依赖两个属性:

  • 每个流都可设置一个介于 1~256 之间的权重。
  • 第个流都可设置其依赖于另一个流。

结合以上两个因素可以分析形成一个对流数据的优先级树(prioritization tree),根据它客户端可做出如何接收数据的最佳决策,服务器也可以根据它来决定如何优先处理哪些请求,进行合理的 CPU ,内存及其他资源的分配。

来自 Web Fundamentals 关于优先级树的示意图

前面介绍流的时候有提到其身上有个唯一的身分标识,通过这个标识,一个流可指定依赖于另一个流,形成父子关系。如果流没有指定依赖,默认依赖于根节点(root stream)。这个依赖关系很好理解,就是被依赖的流数据应该优先被处理及返回,想象一下页面中 JS 资源的相互依赖。

多个流数据所依赖的流相同,即拥有同一个父级,则根据他们的权重来决定处理的优先级和资源分配。

比如上图中第一个情况,权重值为 12 流 A 和权重为 4 的流 B 都没有显式指定依赖,他们隐式依赖于 root stream,在处理他们优先级时 A 为 12/(12+4),B 为 4/(12+4)。

第二个情况下,D 先于 C 拥有全部优先级,处理完成后才是 C。其他情况推而广之同样的逻辑。

更重要的是,这些优先级可随时根据情况而动态调整。比如浏览器可根据用户的交互动态调整资源的优先级,重新分配,以达到资源加载的最优效果。

单个源对应一个持久的连接

基于 HTTP/2 将所有请求分片后在一个连接中传输处理的特点,对于一个域(origin),只需要建立一个长久的连接即可。大多数 HTTP 通信是瞬时的,而 TCP 是为长连接批量数据传输而设计。通过始终复用该连接,可以达到减少重复建立连接的不必要资源消耗,极大地减少网络时延,同时也会减少持有多个连接带来的内存等资源的消耗。

流的控制

流的控制是这么一种机制,它防止在接收方已经无暇接收或疲于处理时,发送方仍不断发送数据。比如服务器压力过大,无法处理更多请求时;比如浏览器中请求视频资源,但用户点击了暂停,不想再继续加载后续的数据;或链路中有多层代理时,不同节点处理能力不一致,需要平衡一下数据传输量。

TCP 中是有流控制机制的,但无法满足 HTTP/2 中分帧情况下的控制需求,因此 HTTP/2 提供了一些基础设施(building blocks)让浏览器和服务器可实现自己的控制逻辑,或更加底层的控制:

  • 流控制是定向的,每个接收方可根据需求设置其接收的阈值。
  • 流控制基于约定。每个接收方在连接之处告之它的流控制阈值(以字节为单位),每当接收到 DATA 帧时减少而发送 WINDOW_UPDATE 帧时增大。
  • 流控制不能被关闭。通过 SETTING 帧客户端与服务器建立连接时,就初始化了两端的流控制阈值(默认为 65,535 字节)。但接收方可自行设置这个初始值,然后在每次接收到数据后发送 WINDOW_UPDATE 来维护这个阈值。
  • 流控制不是端对端的控制,链路上不同节点可根据自身情况来进行。

服务端推送

另一个 HTTP/2 带来的特性便是允许服务器向单个请求发送多次响应。典型的场景便是 Web 网页。浏览器请求网页后分析其中的资源,然后再请求相应的其他资源。这一过程完全可以省掉,转而由服务器一并发送。因为文档在服务器这边,服务器是知道该页面还需要哪些资源的。下图展示了对于客户端的请求,服务器推送了额外资源的情况。

来自 Web Fundamentals 的插图展示了服务器推送

服务器通过发送 PUSH_PROMISE 帧描述即将主动推送的资源,里面包含的只是资源的响应头 HEADER,而不是实际的响应 DATA。浏览器收到后可根据情况发送 RST_STREAM 帧来取消(比如该资源已经加载过在本地有缓存)。

头部信息的压缩

每次 HTTP 的数据传输都包含了大量头部以描述请求的目的和传送的资源的属性,这些 header 通过普通字符串的形式发送,带上 cookie 时其大小可达几 Kb 。HTTP/2 中使用 HPACK 格式来对这些头信息进行压缩优化:

  • 对头信息使用哈夫曼编码(Huffman code)进行压缩。
  • 要求客户端及服务器同时维护一个指向上一次请求头中字段的索引列表,这样在后续传输中可高效地对之前传输过的字段进行编码。

来自 Web Fundamentals 的插图展示 header 压缩的工作流程

从对上图的理解可以看出,新发送的头部只需要包含变更的部分,而剩余的部分可通过本地索引找出,无须再次发送。

一个本地示例

是时候 show me the code 了。下面的示例来自 Node.js 中 HTTP/2 的文档

服务端代码:

server.js

const http2 = require('http2');
const fs = require('fs'); const server = http2.createSecureServer({

key: fs.readFileSync('localhost-privkey.pem'),

cert: fs.readFileSync('localhost-cert.pem')

});

server.on('error', (err) => console.error(err)); server.on('stream', (stream, headers) => {

// stream is a Duplex

stream.respond({

'content-type': 'text/html',

':status': 200

});

stream.end('<h1>Hello World</h1>');

}); server.listen(8443);

因为浏览器不支持未加密的 HTTP/2,所以需要创建本地证书来启用 HTTPS。

通过如下命令创建本地证书。

$ openssl req -x509 -newkey rsa:2048 -nodes -sha256 -subj '/CN=localhost' \
-keyout localhost-privkey.pem -out localhost-cert.pem

然后启动服务器就可以在浏览器中访问查看了。

$ node node.js

打开浏览器访问 https://localhost:8443。注意一定是带 https 访问,protocal 省略的情况下会以 HTTP/1.1 协议访问,这样是访问不到页面的,因为 server 端代码中没有使用 allowHTTP1 开启对 HTTP/1.1 回退的支持。这里开启也没有意义,因为我们在测试 HTTP/2 不是么。

本地 Node.js 搭建的 HTTP/2 示例

也可通过 curl 工具访问查看:

$ curl -ki  https://localhost:8443

HTTP/2 200

content-type: text/html

date: Sun, 14 Apr 2019 08:44:34 GMT <h1>Hello World</h1>⏎

这里 -k--insecure)参数表示允许不安全的连接,本地测试时需要加上,不然访问不了。

相关资源

HTTP/2 简介的更多相关文章

  1. ASP.NET Core 1.1 简介

    ASP.NET Core 1.1 于2016年11月16日发布.这个版本包括许多伟大的新功能以及许多错误修复和一般的增强.这个版本包含了多个新的中间件组件.针对Windows的WebListener服 ...

  2. MVVM模式和在WPF中的实现(一)MVVM模式简介

    MVVM模式解析和在WPF中的实现(一) MVVM模式简介 系列目录: MVVM模式解析和在WPF中的实现(一)MVVM模式简介 MVVM模式解析和在WPF中的实现(二)数据绑定 MVVM模式解析和在 ...

  3. Cassandra简介

    在前面的一篇文章<图形数据库Neo4J简介>中,我们介绍了一种非常流行的图形数据库Neo4J的使用方法.而在本文中,我们将对另外一种类型的NoSQL数据库——Cassandra进行简单地介 ...

  4. REST简介

    一说到REST,我想大家的第一反应就是“啊,就是那种前后台通信方式.”但是在要求详细讲述它所提出的各个约束,以及如何开始搭建REST服务时,却很少有人能够清晰地说出它到底是什么,需要遵守什么样的准则. ...

  5. Microservice架构模式简介

    在2014年,Sam Newman,Martin Fowler在ThoughtWorks的一位同事,出版了一本新书<Building Microservices>.该书描述了如何按照Mic ...

  6. const,static,extern 简介

    const,static,extern 简介 一.const与宏的区别: const简介:之前常用的字符串常量,一般是抽成宏,但是苹果不推荐我们抽成宏,推荐我们使用const常量. 执行时刻:宏是预编 ...

  7. HTTPS简介

    一.简单总结 1.HTTPS概念总结 HTTPS 就是对HTTP进行了TLS或SSL加密. 应用层的HTTP协议通过传输层的TCP协议来传输,HTTPS 在 HTTP和 TCP中间加了一层TLS/SS ...

  8. 【Machine Learning】机器学习及其基础概念简介

    机器学习及其基础概念简介 作者:白宁超 2016年12月23日21:24:51 摘要:随着机器学习和深度学习的热潮,各种图书层出不穷.然而多数是基础理论知识介绍,缺乏实现的深入理解.本系列文章是作者结 ...

  9. Cesium简介以及离线部署运行

    Cesium简介 cesium是国外一个基于JavaScript编写的使用WebGL的地图引擎,一款开源3DGIS的js库.cesium支持3D,2D,2.5D形式的地图展示,可以自行绘制图形,高亮区 ...

  10. 1.Hibernate简介

    1.框架简介: 定义:基于java语言开发的一套ORM框架: 优点:a.方便开发;           b.大大减少代码量;           c.性能稍高(不能与数据库高手相比,较一般数据库使用者 ...

随机推荐

  1. Jquery浅克隆与深克隆

    Jquery浅克隆与深克隆 JavaScript部分 $("div").on('click', function() {//执行操作}) //clone处理一 $("di ...

  2. python笔记:#007#变量

    变量的基本使用 程序就是用来处理数据的,而变量就是用来存储数据的 目标 变量定义 变量的类型 变量的命名 01. 变量定义 在 Python 中,每个变量 在使用前都必须赋值,变量 赋值以后 该变量 ...

  3. 异步任务spring @Async注解源码解析

    1.引子 开启异步任务使用方法: 1).方法上加@Async注解 2).启动类或者配置类上@EnableAsync 2.源码解析 虽然spring5已经出来了,但是我们还是使用的spring4,本文就 ...

  4. Java学习导航

    由于最近在系统的重新学习Java,为了便于日后复习,给个人博客中Java内容做一个目录. Java基础:Java虚拟机(JVM) Java基础:内存模型 Java基础:JVM垃圾回收算法 Java基础 ...

  5. 万网主机使用wordpress发送邮件的方法

    今天弄了一下午总算明白了,这里写一下具体过程. 首先是邮箱,万网主机是不支持mail()函数的,所以默认的不可用,如果你想发送邮件的话,只能使用fsockopen()函数.首先进入万网主机管理平台,启 ...

  6. 洛谷 P1430 解题报告

    P1430 序列取数 题目描述 给定一个长为\(n\)的整数序列\((n<=1000)\),由\(A\)和\(B\)轮流取数(\(A\)先取).每个人可从序列的左端或右端取若干个数(至少一个), ...

  7. 图灵程序设计丛书(SQL必知必会)笔记

    SQL必知必会 第二课:检索数据 1.分页 (1).SQL Server 栗子 : select top 2 columns from tableName (2).Oracle 栗子 :select ...

  8. java通过反射获取字段的类型

    import java.lang.reflect.Field;  //这是需要引入的包 Field[] f = 类名.class.getDeclaredFields(); //获取该类的字段for(F ...

  9. synchronized(this) 与synchronized(class) 之间的区别

    一.概念 synchronized 是 Java 中的关键字,是利用锁的机制来实现同步的. 锁机制有如下两种特性: 互斥性:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程中的协调机 ...

  10. 第一章——机器学习总览(The Machine Learning Landscape)

    本章介绍了机器学习的一些基本概念,已经应用场景.这部分知识在其它地方也经常看到,不再赘述. 这里只记录一些作者提到的,有趣的知识点. 回归(regression)名字的来源:这是由Francis Ga ...