HTTP2即未来

现在浏览器里面很大一部分网页还在使用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 上面套一个.

原文发布于微信公众号 - 前端小吉米(villainThr)

原文发表时间:2017-09-12

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏转载gongluck的CSDN博客

cocos2dx 打灰机

#include "GamePlane.h" #include "PlaneSprite.h" #include "BulletNode.h" #include...

5226
来自专栏张善友的专栏

LINQ via C# 系列文章

LINQ via C# Recently I am giving a series of talk on LINQ. the name “LINQ via C...

2605
来自专栏大内老A

The .NET of Tomorrow

Ed Charbeneau(http://developer.telerik.com/featured/the-net-of-tomorrow/) Exciti...

30510
来自专栏跟着阿笨一起玩NET

c#实现打印功能

2572
来自专栏我和未来有约会

Silverlight第三方控件专题

这里我收集整理了目前网上silverlight第三方控件的专题,若果有所遗漏请告知我一下。 名称 简介 截图 telerik 商 RadC...

3945
来自专栏Ceph对象存储方案

Luminous版本PG 分布调优

Luminous版本开始新增的balancer模块在PG分布优化方面效果非常明显,操作也非常简便,强烈推荐各位在集群上线之前进行这一操作,能够极大的提升整个集群...

3035
来自专栏张善友的专栏

Miguel de Icaza 细说 Mix 07大会上的Silverlight和DLR

Mono之父Miguel de Icaza 详细报道微软Mix 07大会上的Silverlight和DLR ,上面还谈到了Mono and Silverligh...

2657
来自专栏java 成神之路

使用 NIO 实现 echo 服务器

4437
来自专栏落花落雨不落叶

canvas画简单电路图

57011
来自专栏闻道于事

js登录滑动验证,不滑动无法登陆

js的判断这里是根据滑块的位置进行判断,应该是用一个flag判断 <%@ page language="java" contentType="text/html...

6498

扫码关注云+社区