原来你是这样的http2……

61次阅读

共计 6972 个字符,预计需要花费 18 分钟才能阅读完成。

欢迎大家前往腾讯云 + 社区,获取更多腾讯海量技术实践干货哦~
本文由 mariolu 发表于云 + 社区专栏

序言
目前 HTTP/2.0(简称 h2)已经在广泛使用(截止 2018 年 8 月根据 Alexa 流行度排名的头部 1 千万网站中,h2 占比约 29%,https://w3techs.com/technolog…)。写此文章的目的是:h2 作为较新的技术,并逐渐占有率广泛,虽然目前有更新的 QUIC,但其实现思路类似于 h2。颠覆以往的 HTTP/1.x,H2 的创造性的技术值得我们细细品味。此篇文章根据笔者在 h2 开发经验和思考,向你介绍全面的 h2 知识以及是非功过。本篇更注重于帮助读者理解 h2 的设计思路、亦可作为一篇 RFC 导读或者总结。
第一话、追踪溯源

图 1、HTTP 年鉴图
早在 1991 年,伴随 WWW 诞生之初,HTTP/0.9 协议已经提出。HTTP0.9 是简单且应用受限的协议。支持去网络主机获取对应路径的资源。但是没有扩展属性。其协议之简单甚至只用下面一个访问谷歌主机的例子概括了 HTTP/0.9 的全部。如下所示,协议只支持 GET,没有 http 头;响应只能是超文本。
telnet google.com 80
Connected to x.x.x.x
GET /about
(Hyper text)
(Conection closed)
随着人们对富媒体信息的渴望以及浏览器的普及,HTTP/1.0 在 1996 年被提出来。HTTP/1.0 的很多特性目前还被广泛使用,但是仍然像 HTTP/0.9 一样一次请求需要创建一次的 tcp 连接。随即短短几年时间内,HTTP/1.1 以 RFC 标准形式再次展现在人们眼前。此时的 HTTP 协议 1.1 版本已经重新设计了长连接、options 请求方法、cache 头、upgrade 头、range 头、transfer-encoding 头, 以及 pieline(in order)等概念。
而我们另一个所熟知的 HTTPS 的 SSL/TLS 技术各个版本差不多在后来的十年间逐渐被提出。出于安全考虑,互联网通信间的防火墙路由交换机等设备,这些设备一般仅会开发有限的端口(如 80 和 443)。各种版本的通信协议只能复用这些端口。HTTP1.1 的 80 端口设计了 upgrade 请求头升级到更高级的协议,而 443 端口为了避免多消耗个网络 RTT,在 tls 握手过程中使用了 NPN/ALPN 技术直接在通信之前保持 CS 两端的协议一致。NPN/ALPN 是是 TLS 协议扩展,其中 NPN 是 Google 为实现 spdy 提出的。由服务端提供可支持的协议,供客户端选择。ALPN 则是更接近于 HTTP 交互的方式,由客户端先发出使用某种协议的请求,由服务端确认是否支持协议。ALPN 为了 HTTP2 诞生做铺垫。
另外一个不得不提的是 spdy 协议。Spdy 旨在解决 HTTP1.1 的线头阻塞问题(后面章节有详细讨论)于 2009 被 google 提出。同时分别于 2012 提出了 spdy3.0 实现了流控制,2013-2014 期间提出了流优先级,server push 等概念。Spdy 的存在意义更像是 http2.0 的体验服。它为探索 HTTP 继续演进道路做了铺垫。
第二话、人机交互
汇编是效率最高的语言之一,但是又是最晦涩难懂的语言之一。而人脑易懂的编程语言往往牺牲性能作为折衷。简单说,HTTP/1.x 协议就是为了人类语言习惯所设计的协议,但是转换成机器执行协议并不是高效的。让我们在回顾下计算机执行解析 HTTP1.x 的流程。
GET / HTTP/1.1<crlf>
Host: xxx.aa.com<crlf>
<crlf>
对应的解析伪代码是
loop
while(! CRLF)
read bytes
end while
if line 1:
parse line as Request-Line
else if empty line:
Break out and We have done
else If start with non-whitespace
parse header
else if space
continue with last heade
end if
end loop
在伪代码解析流程可以看到,肉眼看起来简洁的协议解析起来是这么的费劲。而且在 HTTP 服务器中还要考虑这种问题:字节行的长度是未知的,也不知道预先分配多大内存。
HTTP/2.0 使用了计算机易懂的二进制编码信息,而且得向上兼容 HTTP 的涵义。具体我们来看下他是如何做到的。像大多数通信协议一样,桢是传输最小单位。桢分为数据帧和控制桢。数据帧作为数据的载体,控制桢控制信道的信令。h2 桢的通用格式为首部 9 字节 + 额外的字符。正如你能想到的那样,桢的第一个部分是描述长度,第二个部分描述了桢的类型,第三个部分描述了标志 Flag,第四个部分是唯一序列号。这是所有桢的通用头。通用头紧接的是桢的实体。图 4 展示了桢的结构。

图 2、通用桢的格式
这样设计有什么好处呢。再来看一下桢的解析流程,你就会发现对计算机来说更简洁。
loop
read 9 byte
payload_Length=first 3 bytes
read payload
swith type:
Take action
end loop
HTTP2.0 使用 header 桢表达 HTTP header+request line,data 桢表达 Body。header 桢和 data 桢使用相同的 stream id 组成一个完整的 HTTP 请求 / 响应包。这里的 stream 描述了一次请求和响应,相当于完成了一次 HTTP/1.x 的短连接请求和响应。
第三话、并行不悖
上节讲到我们用 h2 桢完整表达了 HTTP/1.x。但是 h2 协议抱负远不止于此。它的真正目的是解决之前 HTTP1.x 的线头阻塞问题、改善网络延迟和页面加载时间。
我们知道一个完整的网页包含了主页请求和数次或数十次的子请求。HTTP/1.1 已经可以并行发出所有请求. 但是 HTTP 本身是无状态的协议,它依赖于时间的顺序来识别请求和响应直接的对应关系。先来的请求必须先给响应。那么如果后面的响应资源对浏览器构建 DOM 或者 CSSOM 更重要。那它必须阻塞等待前者完成。当然这也难不倒我们,我们可以多开几条 tcp 连接(浏览器规定一个 origin(协议 +host+port)最多 6 个)或者合并资源来减少不必要的阻塞。这是有代价的。首先 tcp 建连的开销,其实合并资源带来一小块子资源过期导致整个合并资源的缓存过期。对此,h2 有一揽子的解决方案,接下来一一道来。
h2 在一个 tcp 连接创建多个流。每个流可以有从属关系,比如说根据浏览器加载的优先级顺序(主请求 >CSS> 能改变 DOM 结构的 JS 文件 > 图片和字体资源文件)建立一条依赖关系链。处于同一等级的依赖关系中可以设置权重。权重用于分配传输信道资源多少。
图 6 例子说明了有一次主页请求 index.html、一次 main.css,一次 jq.js 以及一些 image 文件和字体文件 qq.tff

图 3、h2 请求的依赖树
HTML 的优先级最高,在 HTML 传输完成之前,其他文件不会被传输。HTML 传输完成后,JS 和 CSS 根据其分配的权重占比分配信息传输资源。如果 CSS 传输完成后,TFF 和 PNG 如果是相同权重,那么他们将占有 1 / 4 的信道资源。
这里抛出 3 个问题和答案。

如果 CSS 被阻塞了, 那么 JS 得到本属于 CSS 的通信资源
如果 CSS 传输完成但没有被移依赖树, TFF 和 PNG 继承 CSS 的通信份额 (假设 TFF 和 PNG 权重一样,那么各分得 1 / 4 通信资源).
如果 CSS 在依赖数被移除,JS, TFF, PNG 平分通信资源(假设 3 个权重一样,那么三者各分得 1 / 3 通信资源)

第四话、众星捧月
HTTP2 还设计了一系列方案来改善网络的性能、包括流量控制,HPack 压缩,Server Push。
什么你说 TCP 已经有流量控制了,HTTP 不是多此一举吗?没错,但是在单条 TCP 内部,各个流可是没有流量控制。流量控制使用了 Update Frame 不断告知发送方更新发送的窗口大小(上限)。流量控制一个现实用途是阻塞不重要的请求,以腾出更大的通信资源给重要的请求使用。流量控制是不可以被关闭的,流量大小可以设置 2 的 31 次方 -1(2GB)。不同的中间网络设备有不一样的吞吐能力。流控的另一个用途在于同步所有的中间设备交换机最小的上限。流量窗口初始大小为 65535(2 的 16 次方 -1)。
就像世界上的大多数财富聚集在少数人身上一样。在一份对 48,452,989 个请求的统计中,以下 11 个头占据了 99% 的数量,依名次递减分别是 user-agent、accetp-encoding、accept-language、accept、referer、host、connection、cookie、origin、upgrade-inseure-request、content-type。http header 的值也有很大相似性,比如说”/index.html“,“gzip, deflate”。Cookie 也携带冗余的信息。
这些都组成了 http header 大量可以压缩的内容。
而在一份 GET 请求和一个 304 响应或者 content-length 很少的响应中,这些头占据了很大比例的通信资源。2016 年发布的一份 HTTP 报告中,请求头大约在 460bytes,对一个通常的网页,平均会有 140 个请求对象。这些头总共需要 63KB。这些量很有可能会是首屏和页面加载时间优化的瓶颈。
可能你会说用 gzip 等压缩算法这些请求头,不就完了吗?的确 spdy 就这样干过,直到 2013 年 BREACH 攻击暴露了 gzip 压缩在 https 应用的安全性,这种攻击让攻击者很容易获得 session cookie 等数据。于是才有了 HPACK。
HPACK 简单来说就是索引表,包括静态表和动态表。静态表由 RFC 定义,从不改变,静态表预留了 62 个表项。每个连接的通信双方维护着动态表。
H2 协议使用索引号代表 http 中的 name、value 或者 name-value。假设被索引的是 name,value 没有索引,那么 value 还可以用霍夫曼编码压缩。
在预定的头字段静态映射表 中已经有预定义的 Header Name 和 Header Value 值,这时候的二进制数据格式如图 4,第一位固定为 1,后面 7 位为映射的索引值。图 8 的 83 就是这样的,83 的二进制字节标示 1000 0011,抹掉首位就是 3,对应的静态映射表中的 method:POST。

图 4、index 索引 name 和 value

图 5、抓包示意
预定的头字段静态映射表中有 name,需要设置新值。图 6 所示例子,一个指定 path 的 Header,首字符 为 44,对应的二进制位 0100 0100。前两个字符为 01,Index 为 4,即对应静态映射表中的 path 头。第二个字符为 95 对应的二进制位 1001 0101,排除首字符对应的 Value Length 为 十进制的 21。即 算上 44,一共 23 个字符来记录这个信息。

图 6、index 索引 name 和自定义 value

图 7、抓包示意
预定的头字段静态映射表中没有 name,需要设置新 name 和新值。40 的二进制是 0100 0000,02 的二进制是 0000 0010,后七位的十进制值是 2,86 的二进制是 1000 0110,后 7 位的十进制值是 6。

图 8、index 索引自定义 name 和自定义 value

图 9、抓包示意
明确要求该请求头不做 hpack 的 index
HTTP2.0 还有个大杀器是 Server Push,Server Push 利用闲置的带宽资源可以向浏览器预推送页面展示的关键资源,Server push 有效得降低了页面加载时间。具体详情参考笔者的另一篇文章 https://cloud.tencent.com/dev…。
第五话、庖丁解牛
HTTP2 相比 HTTP1 更适合计算机执行。但是其二进制特性不易于人脑理解。这一话我们专门来讲讲关于 http2 的调试工具。
Chrome(qq 浏览器)可以按住 F12 查看 h2 协议。图 13 所示为浏览器网络时序图,列出了具体协议名称。chrome 还可以在地址栏敲入 chrome://net-internals/#http2 查看到 h2 协议细节,如图 11 所示。点击相应的 host 就可以看到 h2 协商过程,如图 12 所示。

图 10、浏览器的网络时序图
chrome://net-internals/#http2

图 11、chrome 调试 h2

图 12、h2 协商过程
wireshark(需和 chrome 或 firefox 搭配使用)。设置环境变量 SSLKEYLOGFILE=c:tempsslkeylog.log。然后在 Wireshark->Preferences->Protocols->SSL 配置 key 所在路径。

图 13、wireshark 配置

图 14、wireshark 抓 h2 包
Nghttp2 是一个完整的 http2 协议实现的组件。作者也参与过 spdy 实现。目前 nghttp2 库被很多知名软件作为 h2 协议实现库使用。另外 nghttp2 也自带了 h2 协议的分析工具。图 18 展示了在明文状态使用 upgrade 头升级到 h2c。图 19 展示了在 https 基础上升级到 h2。

图 15、明文状态使用 upgrade 头升级到 h2c

图 16、展示了在 https 基础上升级到 h2
Curl 的—http2 选项(需要和 nghttp2 一起编译)

图 17、支持 h2 的 curl 客户端调试
Github 还有些实用的 http2 工具组件,诸如 chrome-http2-log-parser、http2-push-manifest 等组件,笔者后续会专门开篇文章介绍这些工具。对于移动端的调试,ios 可以用 charles proxy 做代理,android 需要开发者模式使用移动端的 chrome,笔者在移动端使用较少,这里就不做展开。
第六话、雕栏玉砌
H2 怎么部署呢,目前主流服务端像 nginx、apache 都已经支持 http2,主流的客户端 curl 和各种浏览器(包括移动端 safari 和 chrome-android)基本也支持 http2。代理服务器如 ATS、Varnish,Akamai、腾讯云等 CDN 服务也支持 http2。那么怎么把一套网站部署到 h2。或者说部署 h2 网站和之前 h1 网站有什么不一样?
如果是自己的源站,那么请确保服务器支持 TLS1.2 已经 RFC7540 所要求的加密套件,h2 需要保证支持 alpn。你可以使用 ssllabs 等网站检查。对于 h2 服务器的要求是 h2 必须了解如何设置流的优先级,h2 服务器需要支持 server push。h2 客户端需要尽量多的发送请求。
如果你的网站是从 http1.x 迁移过来的,那么之前对于 http1.x 所做的优化可能无任何帮助甚至更差。合并小文件不在需要,因为额外的小文件请求在 h2 看来只是开销很少。并且如果大文件的局部更改使得整个大文件缓存失效。在 http1.0 时代使用多个域名来并发 http 连接,在 http2 也毫无必要,因为 http2 天生就是并发的。http1.x 做的优化比如说图片资源文件不使用 cookie 来减少请求大小,http2 的 header 压缩功能也减少了这种影响。即使不做这种优化也亦可。像合并 css、小图片带来的增益在 http2.0 也是可忽略的。
如果网页使用第三方网站组件,那么请尽可能减少使用第三方网站组件。第三方网站不能保证支持 h2,所以它可能成为木桶理论的最大短板。
谨慎使用 2.0-1.x 的部署方案,h2 流转化成 h1 请求。因为这样无法发挥 h2 性能。

图 18、2.0-1.x 的部署方案
CDN 代理服务器的 h2 支持,可以屏蔽 h2 强制走 tls 的代理服务器。如图 19,代理可以在与各种协议客户端的网络环境下,切断和客户端的 tls 连接,和服务器新建连接。也可以作为 load balancer,相当于 HTTP2.0 用户和 HTTP2.x 服务器直接通信。

图 19、带 tls 客户端功能的代理
图 20 列举如果绕过 proxy 到达 h2 服务器。此时的 proxy 相当于 tcp 转发的 load balance 功能的设备。如果该 proxy 支持 tls 的 alpn 协议,那么它也可以选择 HTTP 代理功能,和 h2 服务器可以建立加密连接。如果即不支持 alpn,也不支持 tcp 转发。那么 proxy 只能用 upgrade 升级成 h2 协议。

图 20、经过代理服务器的 H2 部署方案
第七话、十全九美
HTTP2.0 是建立在 TCP 之上,所以 TCP 的所有缺点他都有,所以 H2 能发挥最大性能得益于调优的 tcp 协议栈。TCP 的慢启动特性,决定 h2 一开始的并发流量不会太大,TCP 以及 SSL 的握手连接也会拖慢 h2 的首包网络耗时。QUIC 则完全地抛弃 TCP,在 UDP 基础上实现了 HTTP2 的一系列特性。同时做了应用层的如 TCP 的可靠性保障。同时这些 TLS1.3 传输更快更简洁。这些都为 HTTP2.0 进化到 HTTP3.0 提供了一些思路。
总结
以上内容都来源于笔者的实践经验和理论总结。篇幅所限不能涵盖各个细节。具体可以继续参考 RFC7540 和 RFC7541 协议。

问答没有“http | https”的网址怎么实现?相关阅读我是怎么一步步用 go 找出压测性能瓶颈 HTTP/ 2 之服务器推送 (Server Push) 最佳实践低于 0.01% 的极致 Crash 率是怎么做到的?【每日课程推荐】新加坡南洋理工大学博士,带你深度学习 NLP 技术

正文完
 0