现在浏览器里面很大一部分网页还在使用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传的是二进制,所以,资源不完整是肯定的.只是说,那些文件传的快一些.)
我们这里就按照第三个图来进行解释一下吧:
首先D占用100%的资源进行发送
D发送完了C同样占用100%的资源进行发送
这里,由于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了. 所以,这里,我们继续深入的看一下具体的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 上面套一个.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。