本文目的是学习HTTP 2.0的原理并研究其通信的详细细节。
二进制分帧层,是HTTP 2.0性能增强的核心。
HTTP 1.x在应用层以纯文本的形式进行通信,而HTTP 2.0将所有的传输信息分割为更小的消息和帧,并对它们采用二进制格式编码。这样,客户端和服务端都需要引入新的二进制编码和解码的机制。
如下图所示,HTTP 2.0并没有改变HTTP 1.x的语义,只是在应用层使用二进制分帧方式传输。
因此,也引入了新的通信单位:
HTTP 2.0通信的最小单位,包括帧首部、流标识符、优先值和帧净荷等。
其中,帧类型又可以分为:
标志位用于不同的帧类型定义特定的消息标志。比如DATA帧就可以使用End Stream: true表示该条消息通信完毕。流标识位表示帧所属的流ID。优先值用于HEADERS帧,表示请求优先级。R表示保留位。
下面是Wireshark抓包的一个DATA帧:
消息是指逻辑上的HTTP消息(请求/响应)。一系列数据帧组成了一个完整的消息。比如一系列DATA帧和一个HEADERS帧组成了请求消息。
流是连接中的一个虚拟信道,可以承载双向消息传输。每个流有唯一整数标识符。为了防止两端流ID冲突,客户端发起的流具有奇数ID,服务器端发起的流具有偶数ID。
所有HTTP 2. 0 通信都在一个TCP连接上完成, 这个连接可以承载任意数量的双向数据流Stream。 相应地, 每个数据流以 消息的形式发送, 而消息由一 或多个帧组成, 这些帧可以乱序发送, 然后根据每个帧首部的流标识符重新组装。
二进制分帧层保留了HTTP的语义不受影响,包括首部、方法等,在应用层来看,和HTTP 1.x没有差别。同时,所有同主机的通信能够在一个TCP连接上完成。
基于二进制分帧层,HTTP 2.0可以在共享TCP连接的基础上,同时发送请求和响应。HTTP消息被分解为独立的帧,而不破坏消息本身的语义,交错发送出去,最后在另一端根据流ID和首部将它们重新组合起来。
我们来对比下HTTP 1.x和HTTP 2.0,假设不考虑1.x的pipeline机制,双方四层都是一个TCP连接。客户端向服务度发起三个图片请求/image1.jpg,/image2.jpg,/image3.jpg。
HTTP 1.x发起请求是串行的,image1返回后才能再发起image2,image2返回后才能再发起image3。
HTTP 2.0建立一条TCP连接后,并行传输着3个数据流,客户端向服务端乱序发送stream1~3的一系列的DATA帧,与此同时,服务端已经在返回stream 1的DATA帧
性能对比,高下立见。HTTP 2.0成功解决了HTTP 1.x的队首阻塞问题(TCP层的阻塞仍无法解决),同时,也不需要通过pipeline机制多条TCP连接来实现并行请求与响应。减少了TCP连接数对服务器性能也有很大的提升。
流可以带有一个31bit的优先级:
客户端明确指定优先级,服务端可以根据这个优先级作为依据交互数据,比如客户端优先级设置为.css>.js>.jpg(具体可参见《高性能网站建设指南》), 服务端按优先级返回结果有利于高效利用底层连接,提高用户体验。
然而,也不能过分迷信请求优先级,仍然要注意以下问题:
HTTP 2.0增加了服务端推送功能,服务端可以根据客户端的请求,提前返回多个响应,推送额外的资源给客户端。如下图所示,客户端请求stream 1,/page.html。服务端在返回stream 1消息的同时推送了stream 2(/script.js)和stream 4(/style.css)。
PUSH_PROMISE帧是服务端向客户端有意推送资源的信号。
Apache mod_headers example
<Location /index.html>
Header add Link "</css/site.css>;rel=preload"
Header add Link "</images/logo.jpg>;rel=preload"
</Location>12345
HTTP 1.x每一次通信(请求/响应)都会携带首部信息用于描述资源属性。HTTP 2.0在客户端和服务端之间使用“首部表”来跟踪和存储之前发送的键-值对。首部表在连接过程中始终存在,新增的键-值对会更新到表尾,因此,不需要每次通信都需要再携带首部。
另外,HTTP 2.0使用了首部压缩技术,压缩算法使用HPACK。可让报头更紧凑,更快速传输,有利于移动网络环境。
需要注意的是,HTTP 2.0关注的是首部压缩,而我们常用的gzip等是报文内容(body)的压缩。二者不仅不冲突,且能够一起达到更好的压缩效果。
考虑一个问题,客户端如何知道服务端是否支持HTTP 2.0?是否支持对二进制分帧层的编码和解码?所以,在两端使用HTTP 2.0通信之前,必然存在协议协商的过程。
支持HTTP 2.0的浏览器可以在TLS会话层自发完成和服务端的协议协商以确定是否使用HTTP 2.0通信。其原理是TLS 1.2中引入了扩展字段,以允许协议的扩展,其中ALPN协议(Application Layer Protocol Negotiation, 应用层协议协商, 前身是NPN)用于客户端和服务端的协议协商过程。
服务端使用ALPN,监听443端口默认提高HTTP 1.1,并允许协商其他协议,比如SPDY和HTTP 2.0。
比如,客户端在TLS握手Client Hello阶段表明自身支持HTTP 2.0
服务端收到后,响应Server Hello,表示自己也支持HTTP 2.0。双方开始HTTP 2.0通信。
然而,HTTP 2.0一定是HTTPS(TLS 1.2)的特权吗?
当然不是,客户端使用HTTP也可以开启HTTP 2.0通信。只不过因为HTTP 1. 0和HTTP 2. 0都使用同一个 端口(80), 又没有服务器是否支持HTTP 2. 0的其他任何 信息,此时 客户端只能使用HTTP Upgrade机制(OkHttp, nghttp2等组件均可实现,也可以自己编码完成)通过协调确定适当的协议:
HTTP Upgrade request
GET / HTTP/1.1
host: nghttp2.org
connection: Upgrade, HTTP2-Settings
upgrade: h2c /*发起带有HTTP2.0 Upgrade头部的请求*/
http2-settings: AAMAAABkAAQAAP__ /*客户端SETTINGS净荷*/
user-agent: nghttp2/1.9.0-DEV
HTTP Upgrade response
HTTP/1.1 101 Switching Protocols /*服务端同意升级到HTTP 2.0*/
Connection: Upgrade
Upgrade: h2c
HTTP Upgrade success /*协商完成*/1234567891011121314
TCP连接建立:
TLS握手和HTTP 2.0通信过程:
另外,在chrome中通过chrome://net-internals/#http2命令也能捕获HTTP 2.0通信过程:
42072: HTTP2_SESSION
textlink.simba.taobao.com:443 (PROXY 10.19.110.55:8080)
Start Time: 2017-04-05 11:39:11.459
t=370225 [st= 0] +HTTP2_SESSION [dt=32475+]
--> host = "textlink.simba.taobao.com:443"
--> proxy = "PROXY 10.19.110.55:8080"
t=370225 [st= 0] HTTP2_SESSION_INITIALIZED
--> protocol = "h2"
--> source_dependency = 42027 (PROXY_CLIENT_SOCKET_WRAPPER)
t=370225 [st= 0] HTTP2_SESSION_SEND_SETTINGS
--> settings = ["[id:3 flags:0 value:1000]","[id:4 flags:0 value:6291456]","[id:1 flags:0 value:65536]"]
t=370225 [st= 0] HTTP2_STREAM_UPDATE_RECV_WINDOW
--> delta = 15663105
--> window_size = 15728640
t=370225 [st= 0] HTTP2_SESSION_SENT_WINDOW_UPDATE_FRAME
--> delta = 15663105
--> stream_id = 0
t=370225 [st= 0] HTTP2_SESSION_SEND_HEADERS
--> exclusive = true
--> fin = true
--> has_priority = true
--> :method: GET
:authority: textlink.simba.taobao.com
:scheme: https
:path: /?name=tbhs&cna=IAj9EOy3fngCAXBQ5kJ9yusH&nn=&count=13&pid=430266_1006&_ksTS=1491363551394_94&callback=jsonp95
user-agent: Mozilla/5.0 (windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
accept: */*
referer: https://www.taobao.com/
accept-encoding: gzip, deflate, sdch, br
accept-language: zh-CN,zh;q=0.8
cookie: [382 bytes were stripped]
--> parent_stream_id = 0
--> stream_id = 1
--> weight = 147
t=370256 [st= 31] HTTP2_SESSION_RECV_SETTINGS
--> host = "textlink.simba.taobao.com:443"
t=370256 [st= 31] HTTP2_SESSION_RECV_SETTING
--> flags = 0
--> id = 3
--> value = 128
t=370256 [st= 31] HTTP2_SESSION_UPDATE_STREAMS_SEND_WINDOW_SIZE
--> delta_window_size = 2147418112
t=370256 [st= 31] HTTP2_SESSION_RECV_SETTING
--> flags = 0
--> id = 4
--> value = 2147483647
t=370256 [st= 31] HTTP2_SESSION_RECV_SETTING
--> flags = 0
--> id = 5
--> value = 16777215
t=370256 [st= 31] HTTP2_SESSION_RECEIVED_WINDOW_UPDATE_FRAME
--> delta = 2147418112
--> stream_id = 0
t=370256 [st= 31] HTTP2_SESSION_UPDATE_SEND_WINDOW
--> delta = 2147418112
--> window_size = 2147483647
t=370261 [st= 36] HTTP2_SESSION_RECV_HEADERS
--> fin = false
--> :status: 200
date: Wed, 05 Apr 2017 03:39:11 GMT
content-type: text/html; charset=ISO-8859-1
vary: Accept-Encoding
server: Tengine
expires: Wed, 05 Apr 2017 03:39:11 GMT
cache-control: max-age=0
strict-transport-security: max-age=0
timing-allow-origin: *
content-encoding: gzip
--> stream_id = 1
t=370261 [st= 36] HTTP2_SESSION_RECV_DATA
--> fin = false
--> size = 58
--> stream_id = 1
t=370261 [st= 36] HTTP2_SESSION_UPDATE_RECV_WINDOW
--> delta = -58
--> window_size = 15728582
t=370261 [st= 36] HTTP2_SESSION_RECV_DATA
--> fin = true
--> size = 0
--> stream_id = 1
t=370295 [st= 70] HTTP2_STREAM_UPDATE_RECV_WINDOW
--> delta = 58
--> window_size = 15728640
t=402700 [st=32475]
备注:
这篇文章摘抄来自网络。我打算总结一些列架构师需要的优秀文章,由于自己写会花太多时间,我决定做一个搬运工,为大家筛选优秀的文章,最后我会做成索引方便大家查找。