HTTP2 即未来

1,263 阅读14分钟
原文链接: mp.weixin.qq.com

现在浏览器里面很大一部分网页还在使用HTTP1.1作为主要的网络通信协议。 但,这傻逼协议是1999年弄出来的. 距今已经有xx年了, 这些年里,美国的IETF 觉得这样不行.我得出来拯救世界了, 在Chrome的倡导下, 借用Chrome的SPDY 来做为HTTP2的前身,即, HTTP2 是SPDY/3 draft的优优化版.

那,HTTP2 为什么要出现,又解决了HTTP1.1不能解决的什么事情呢?

简而言之就是

  • H2是一个二进制协议而,H1是超文本协议.传输的内容都不是一样的

  • H2遵循多路复用即,代替同一host下的内容,只建立一次连接. H1不是(傻逼)

  • H2可以使用HPACK进行头部的压缩,H1则不论什么请求都会发送

  • H2允许服务器,预先将网页所需要的资源PUSH到浏览器的内存当中.

接下来,我们来看看,H2到底有哪些具体的feature

HTTP2的features

首先介绍一下,HTTP2为什么是一种二进制的协议.

HTTP2 binary

说道H2的二进制,首先得介绍一下H1的超文本协议.HTTP1.1每次在发送请求时,都需要找出 开头和结尾的每一帧的位置, 并且,在写入的时候,还需要删除多余的空格,以及选择最优的方式写入, 并且如果是HTTP+TLS的话,那性能损耗就比较呵呵了,因为TLS本身的握手协议,以及加密的方式,在一定程度上会对文本信息的内容进行处理等等. 这些无疑都给HTTP1.1的速度造成了极大的影响.所以,HTTP2 不采用这种方式来,而,干脆直接使用二进制. 那,H2是怎样实现,二进制传输呢? 这里,借Grigorik在velocity 会议上的PPT,来看一看.

没错,H2是安放在应用层的协议,在接受服务器发送的来的请求时,自动将Header 和 Body部分区分开.

HTTP2 多路复用

在H1中,当发送多个请求时, 会有一种head-of-line blocking现象. 也就是我们经常看见的瀑布流式的加载方式,这样的加载方式,只能让资源按照顺序一个一个的加载。 有可能造成如下图的现象:

前面一个资源内容超级多,并且都是一次性加载完,即使后面有更重要的资源,也需要进行等待.但在,H2中就没有这样的限制了. 他直接会将不同的资源,分拆为细小的二进制帧来进行传输.

当然,你也没必要担心,每一次是否会传输错误,因为实际上每一帧里面的格式为:

在传输的每一帧里面,会有如下属性来进行表示Length, Type, Flags, Stream Identifier, and frame payload.

only one Tcp connection

这个特性是建立在二进制传输的多路复用(multiplexed)的机制上的. 简而言之就是一句话:

  • 一个域只需要一个TCP连接

因为在H1的时候,虽然有Connection:keep-alive的特性可以让你的TCP断开的稍微晚一点. 但这并没有什么x用,因为,H1天生自带max-connections数, 没办法, 为了加快更多的资源,你只有多开几个域名来进行连接,这样一方面是域名成本的花销,还有一方面是维护量太大。这就是著名的 Domain Sharding. 不过,这一切在H2中,都变得特别的SB。。。以为,H2本身就可以实现,一个TCP, 资源无上限的特点.最显而易见的特性就是: akamai HTTP2的demo.

前面那一坨绿色的就是HTTP1.1写一下的资源请求,可以看到最多有8个,在红线后面是HTTP2请求的资源数.最多(没有最多) 就一个... 这足以体现HTTP1.1的傻逼特性了。。。那他实际上,是怎么做到在一次TCP中,进行多个资源的请求呢?参考NewCircle Training 讲解的multiplexed video.我们以前发送HTTP1.1的情况是:

在HTTP2中,我们请求的方式改变为:

有同学可能会问: 他这样将多个内容放在一个stream里面进行传输,是怎样保证资源的有序性呢?问得好!HTTP2这个特性确实是建立在stream基础上的, 上面已经提到过,HTTP2将资源划分为最小的frame进行传输,这样可以达到interleave和priority的效果. 每一个frame里面如下图所示:

为了保证order和priority的feature, 所以,HTTP2在每次发送时,需要额外附带上一些信息:

  • a unique stream ID

  • different priority

当然除了这些基本的优化外,HTTP2在HEADER方面的优化也是下血本的.

HEADER Compression

HEADER的优化,主要还是由于HTTP1.1的头部机制--在每次请求时,都需要将一大堆头部带上,甚至带上cookie这灰常大的内容. 所以,SPDY 觉得这样不行,然后就是用了GZIP的压缩方式,但这样很容易的就被破解并且劫持,导致安全性问题. HTTP2吸取了这次教训,决定自己开发一套优化方案,即,因为头部的更替不是很频繁,那我就在Server端做个缓存呗,在你这次连接有效的时间里面, client就用重复的发送请求头了. 这就是HTTP2的HPACK压缩方式. HPACK压缩会经过两步:

  • 传输的value,会经过Huffman coding. 一遍来节省资源.

  • 为了server和client同步, 两边都需要保留一份Header list, 并且,每次发送请求时,都会检查更新

ok, 那这样就有一个问题, 第一次的请求,肯定是最慢的.因为他所有的list都需要进行一份初始化操作. 但这是真没办法。。。 如果你靠猜Header的方式进行发送的话,就有可能造成相应错误的情况. 我们在具体细分一下list, 实际上,每一个list里面还分为static list 和 dynamic list. 两者的区别具体就是:

  • static: 主要用来存储common header. 比如 method,path等

  • dynamic: 主要用来存储自定义的协议头. 比如: custom-name,custom-method等

整个流程,就可以用下图来进行表述:

可以看到,req/res的Header都会存在同一份表里面,这样做可能有点伤内存,不过,速度上还是非常棒的。

这里,还有几个额外的点需要提及一下:

  • 所有头的协议在HTTP2中都没有发生改变, 缓存还是 cache-control, etag,last-modifier

  • response Header 全部是小写.比如 server,status.

  • request Header 也是全部是小写,不过有几个特殊情况. :method, :scheme, :authority, and :path这几个基本的头前面需要:作为pseudo-header fields.

HTTP2 priority

前面说过了,HTTP2的每一帧上带有一定的相关信息,比如说权重--priority. 另外还有一个叫做依赖--dependence. 即, 假如某个client想要请求 index.html的资源,那么server会一并返回index.js和index.css的资源回去. 减少client发送更多的请求,相当于一种Server Push的技术,和现在的SSE挺像的.想要实现这个feature,有两个基本的标准:

  • 每个stream需要有一个1~256的数字来表示权重

  • 每个stream都应该清晰的标明他的依赖有哪些

实际的一个图就是这样:

我们就按照上图的情况来说明吧. 如果一个C资源依赖于D资源,那么D则作为C的父节点. 然后按照这样的顺序继续排下去.如果存在在一个根节点下面存在两个节点,比如第一个A,B。 那应该怎么分呢? 这时候,就用到上文提到的priority. 主要A和B上面的数字. A-12,B-4. 将网络资源--实际上就是带宽和CPU,化成一块蛋糕,那么,在此时,A可以分到3/4的资源,而B只能分到1/4的资源. 分配 ( allocation ) 好了之后,则便返回数据.( Ps: 在HTTP2中,分数不分数这并不重要,因为HTTP2传的是二进制,所以,资源不完整是肯定的.只是说,那些文件传的快一些.)我们这里就按照第三个图来进行解释一下吧:

  1. 首先D占用100%的资源进行发送

  2. D发送完了C同样占用100%的资源进行发送

  3. 这里,由于A占3/4而B只占1/4所以,资源按照权重进行分配,然后继续发送文件直到结束

HTTP2 Server PUSH

这个机制算是 HTTP2 第二大 feature , 即, one-to-many 的机制去请求资源.因为考虑到以前,前端请求资源是通过 document 的解析来实现资源的 fetch . 这种方式有点傻逼... 就是,我知道这个资源是需要加载的,但是我不能一开始在一次请求中发给你,我需要等你要,我才给. 这样,就造成了一种沟通上的麻烦.所以, HTTP2 为了解决这个 bug , 决定开发出一套,可以实现 Server Push 资源的机制.

这里,我们之请求了 page.html ,但实际上通过 push promise. server 自动 push 给我们了 script.js 和 style.css 两个文件. 这样就省去的两个 request 的开销.这种方式,也就是我们经常看到的 inlining css 和 inlining script. 不过, 使用 HTTP2 这种机制的话,有一下几个优于 inlining 的特点:

  • push 的资源能够缓存在浏览器中

  • 不同的网页能够使用该缓存,而不用重新发起

  • push 的资源是通过 multiplexed 进行传输的

  • push 的资源能够进行 priority 标识

  • client 有权取消push 资源的加载

  • push 的资源必须同域

上面具体的介绍了,关于HTTP2具体的feature. 可以说,上面都是一些理论上的东西,没有涉及到一些具体的实操. 不过,一旦你深入过后,你就会发现,实操都是在理论相对完善的时候才做的. 都是一些工程化上的内容, 记一下就ok了.那具体涉及请求是怎样的呢?简单来说,在 HTTP1.1 进行请求时,如图:

而添加 HTTP2.0 server push 之后为:

它会将相关联的资源放到缓存中,当下次有对指定资源进行请求时,直接从缓存中获取。

所以,这里,我们继续深入的看一下具体的HTTP2的协议--frame的内容

HTTP2 frame 内容

先看一张图吧:

这和上面那张图的内容一样,只是更加清楚了。HTTP2 就是凭借他来进行所有的信息交流的,地位差不多和TCP的 frame内容一样的. HTTP2通过设定了length,type,Flags,R,Stream Identifier来标识一个frame. 这些一共占用了9B的大小. 具体的为:

  • 用24-bit 的大小来表示 Length --该 frame 承载数据量的多少, 最多可以放2^24B (~16MB) 大小. 但在具体实践中,一般的上线设置为 16KB。 当然,你也可以手动进行修改.不过,这样就不能体现小文件,流式传输的特点

  • 8-bit 的 type字段 用来表示该frame的类型

  • 8-bit 的 Flags 字段用来表明,该次 frame 包括哪些 type

  • 1-bit 的 R 就是个保留字段,永远设置为0. 实际 没啥用

  • 31-bit 的 identifier 来表明该stream的unique ID.很有用,该 flag 就是用来确保有序性的 flag.

根据 HTTP2 官方的解释说, 俺这样的安排其实很有深意的,你知道我为什么会把Length放在开头吗? 就是为了让 parser 解析的更快, 因为当 parser 开始解析时, 首先就知道了,你这次会传多大的 frame, 并且也知道了你的 type , 那么其他的我就把该 frame 分给特定的引擎进行解析就可以了. 然后我就 skip 一下,调到一下一个 frame 继续解析. 然后, 完成最终的数据接受.

既然, 不同的 type 能够被不同的引擎所解析,那么 type一共有多少种呢?懒得数了...就直接说吧.(从上到下 重要性降低哈~)

  • DATA: 相当于 message body内容. 即, 返回的响应体内容

  • HEADERS: 就是 相应头呗...

  • PRIORITY: 和前面内容提到的 priority 一样, 用来标识该次 frame 的优先顺序.

  • RST_STREAM: 用来结束该资源的 signal

  • PUSH_PROMISE: 这个就比较重要了. 这个上文所说的 server PUSH 有很大的关系. 该是用来设置 server 自己发送的相关资源的 flag.

  • SETTINGS: 用来设置 client 和 server 之间 connection 的 相关配置

  • GOAWAY : 用来告诉 server , 停止发送相关资源

  • CONTINUATION: 和 GOWAY 相反,继续发送相关资源

  • WINDOW_UPDATE: 使用 flow control 对流进行控制.

HTTP2 传输过程

HTTP2 同样是建立在 TCP 连接上的, 他同样也需要发送请求,并且获得响应. 那他第一次发送的内容到底是什么呢?是资源请求吗? HTML? JS ? CSS ?actually, No~HTTP2 在第一次请求的过程当中,发送的内容实际是 HEADERS,因为需要在两端建立一个 virtually list 来存储头部,进行HPACK 压缩. 如下图:

请仔细查看他的 Type 可以发现,就是一个 HEADERS。 这也就是上面所说的存储两个不同的 header table -- static table && dynamic table.

另外, 还有一个点需要补充一下,就是, client 和 server 为了防止 stream ID 的重复, 做了一个规定: client-initiated stream 只能为奇数 stream-ID, 而 server-initiated stream 只能为偶数的 stream-ID.

HTTP2 实践过程

首先一个协议的出现, 必定是 >=2 之间的沟通. 那针对于 HTTP2 就是 server 和 browser 之间的通信协议. 所以, 这就要求, HTTP2 的成功实践, 不仅仅 server 支持, 你的浏览器也必须支持才行. 不过,就目前来说, 已经很不错了: can i use

在 Server 端, 支持 http2 其实,要求也很简单:

  • nginx 版本 >1.10

  • openssl >1.0.2h 即可.

  • 有一个自己的CA证书.

so, 我们先从 CA 证书说起, 这里,先安利一下各大云平台, 只要你在他那买了一台服务器, 他那自动回给你提供免费而且正规的 CA 证书, 我的证书就是在腾讯云送的. 一年换一次即可. 这里,我就按 TX(腾讯) 上来说. 当你申请成功时, 他会给你一个 zip 文件. 解压之后,会得到两个文件:

  • 证书文件: 1_www.domain.com_cert.crt

  • 私钥文件: 2_www.domain.com.key

这个两个文件先放着, 后面有用.对于, nginx 和 openssl 来说. 一般 server 自带的版本都是比较低的, 所以, 这里我们采取手动编译的方式.

# 假设当前目录在 /usr/local/src
// 下载 1.1.0 openssl
wget -O openssl.tar.gz -c https://www.openssl.org/source/openssl-1.1.0.tar.gz
// 解压
tar zxf openssl.tar.gz
// 改名
mv openssl-1.1.0/ openssl

// 下载 nginx 1.11.4的源码
wget -c https://nginx.org/download/nginx-1.11.4.tar.gz
tar zxf nginx-1.11.4.tar.gz
cd nginx-1.11.4/

// 设置编译过后文件的路径和启用的模块
./configure  --prefix=/usr/local/nginx \
--conf-path=/etc/nginx/nginx.conf \
--with-openssl=../openssl \
--with-http_v2_module \
--with-http_ssl_module \
--with-http_gzip_static_module

// 开始编译
make && sudo make install

这里, 我直接整理到 gist 里. 可以直接下载下来, 使用 . ./http2.sh 执行即可.

设置环境变量

等 nginx 编译完, 我们进入 /usr/local/nginx/sbin 里面. 将 nginx 软连接到 /usr/sbin里面.

ln -s /usr/local/nginx/sbin/nginx /usr/sbin/nginx

现在, 我们就可以在全局当中使用 nginx 命令了.

配置 nginx conf

通过上面的配置, 我们接着进到 /etc/nginx/nginx.conf 中. 我直接 paste 配置代码吧:

# 整体的 nginx 配置
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 2048;

    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;
    gzip on;
    gzip_min_length 1k;
    gzip_buffers 4 16k;
    gzip_comp_level 5;
    gzip_types text/plain application/x-javascript text/css application/xml text/javascript application/x-httpd-php;

    include /etc/nginx/sites-available/*.conf;
}

上面的不多说, 主要内容还在 server 里面. 我这里使用的是 nginx + nodeJS. 所以, 后面有一层 proxy.

server {
    listen 80;
    # 重定向以前的 http协议
    server_name villainhr.com www.villainhr.com;
    return 301 https://www.villainhr.com$request_uri;
}
server {
    listen 443 ssl http2;
    server_name www.villainhr.com;
    root /var/www/myblog/app;
    ssl on;
    # 设置上面给出的证书文件
    ssl_certificate         /etc/nginx/private/1 _www.villainhr.com_cert.crt;
    ssl_certificate_key     /etc/nginx/private/2 _www.villainhr.com.key;
    # 设置 ssl 连接属性
    ssl_session_timeout 5m;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    # 设置 ciphers 套件
    ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
    ssl_prefer_server_ciphers on;

    add_header Strict-Transport-Security max-age=15768000;

    ssl_stapling on;
    ssl_stapling_verify on;
    location / {
          proxy_pass              http://localhost:8000;
        proxy_set_header        Host $host;
        proxy_set_header        X-Forwarded-Proto $scheme;
        proxy_set_header        X-Real-IP $remote_addr;
        proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
    }
    location ~.(js|css|gif|jpg|jpeg|ico|png|bmp|swf|GIF|JPG|JPEG|ICO|PNG|BMP|SWF) $ {
        root /
          root /var/www/myblog/app/public;
          expires 2d;
          add_header  Cache-Control "no-cache";
          add_header  Pragma no-cache;
          log_not_found off;
    }
}

然后, 使用 nginx 直接运行. 如果上面顺利的话, 你的 http2 server 也就大功告成了.如果, 上面有配置错误神马的. 不放心,可以直接去 mozilla 上面套一个.