HTTP2-协议
Layers of HTTP/2
HTTP/2可以分为两个部分:framing层和数据或者HTTP层。,framing层多路复用能力的核心。
framing层是通用的,可重复使用的结构,用来传输HTTP。
数据层用来跟HTTP/1.1兼容。
- Binary protocol:h2的framing层是二进制协议。
- Header compression:
- Multiplexed:请求和响应交织在一起
- Encrypted:大部分数据被加密了。
The Connection
任何HTTP/2 session的基本元素是连接。它是由客户端初始化的TCP/IP socket,发送HTTP请求的实体。
和h1没区别。但是,h1是无状态的,h2把所有连接级的元素(连接级的设置和头表header table)捆绑在一起,帧和流都运行在其上。
协议发现
HTTP/2支持两种办法,知道对端支持什么协议:
如果连接没加密,客户端发送Upgrade header,说它希望h2.如果服务器支持h2,返回“101 Switching Protocols”响应。
如果连接在TLS之上,客户端在ClientHello里设置程序级协议握手扩展(Application-Layer Protocol Negotiation(ALPN) extension),SPDY和早期版本的h2使用的是Next Protocol Negotiation (NPN) 扩展协商h2。
在连接上,客户端首先发送一个magic八字节流,这主要适用于客户端从HTTP/1.1升级而来:
0x505249202a20485454502f322e300d0a0d0a534d0d0a0d0a
解码成ASCII:
PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n
这看起来像一个h1消息,h1服务器收到它,将返回错误。
magic字符串后面,是一个SETTINGS帧。支持h2的服务器确认客户端的SETTINGS帧,回复一个它自己的SETTINGS帧。
这样就可以开始h2了。如果客户端在收到SETTINGS之前,收到了其他东西,协商失败。
Frames
Framing是一个方法,Framing把所有重要的材料包装到一起,使消费者很容易读取、分析和增加。
对比一下,h1不是framed,而是用文字分割的。例如:
GET / HTTP/1.1 <crlf>
Host: www.example.com <crlf>
Connection: keep-alive <crlf>
Accept: text/html,application/xhtml+xml,application/xml;q=0.9... <crlf>
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4)... <crlf>
Accept-Encoding: gzip, deflate, sdch <crlf>
Accept-Language: en-US,en;q=0.8 <crlf>
Cookie: pfy_cbc_lb=p-browse-w; customerZipCode=99912|N; ltc=%20;... <crlf>
<crlf>
分析这样的结构,慢而且容易出错。分析h1的请求/响应容易碰到的问题是:
- 一次只有一个请求/响应。接收完才能分析
- 不知道分析会用多少内存。这导致下列问题
- 你把一行读到什么缓存
- 行太长怎么办
- 你重新分配缓存,还是返回400错误
Frames,能让消费者提前知道会发生什么。HTTP/2的帧看上去像这样:
HTTP/2 frame header fields
Name | Length | Description |
---|---|---|
Length | 3 bytes | 帧的载荷(payload)的长度。214到224-1之间。214是默认长度,再大需要发送一个SETTINGS帧 |
Type | 1 bytes | 帧的类型 |
R | 1 bit | 保留位,不要设置 |
Stream Identifier | 31 bits | 流的唯一标识 |
Frame Payload | Variable | 帧的实际内容 |
HTTP/2 frame types
Name | ID | Description |
---|---|---|
DATA | 0x0 | 携带流的核心内容 |
HEADERS | 0x1 | HTTP头和任选的优先级 |
PRIORITY | 0x2 | 说明或者修改流的优先级和依赖项 |
RST_STREAM | 0x3 | 允许一端结束流(通常在发生错误时) |
SETTINGS | 0x4 | 连接级参数 |
PUSH_PROMISE | 0x5 | 告诉客户端,服务器将发消息 |
PING | 0x6 | 连接测试,测量round-trip时间(RTT) |
GOAWAY | 0x7 | 告诉对端,自己已经开始接受新流 |
WINDOW_UPDATE | 0x8 | 告诉对方,自己将接收多少数据(用于流量控制) |
CONTINUATION | 0x9 | 用于扩展HEADER |
extension frames
H2可以增加新的帧类型。新类型不应该影响核心协议。
Streams
流(stream)就是HTTP/2连接内的客户端和服务器端之间的独立的、双向的帧序列。可以理解成一个连接上的独立的请求/响应对的一系列帧。客户端想发送请求时,初始化一个新的流。服务器在同一个流上响应。和H1的请求/响应流(flow)类似,但是,H2的多个请求/响应交织在一起,不会互相阻塞。Stream Identifier(帧头的6-9字节)表示帧属于哪个流。
客户端建立H2连接以后,通过发送一个HEADERS帧,开始一个新的stream,如果头需要多帧,就跟着CONTINUATION帧。HEADERS通常包含请求或者响应。随后的streams初始化一个新的,Stream Identifier增加了的HEADERS帧。
CONTINUATION FRAMES
如果HEADERS帧没有更多的头,就设置帧的Flags属性的END_HEADERS位。CONTINUATION帧是一种特殊的HEADERS帧。HEADERS和CONTINUATION必须是连续的,减少了多路复用的收益。要尽量少用CONTINUATION帧。
Messages
GET请求可能是这样的:
POST请求可能是这样的:
HTTP/1和HTTP/2消息有几点明显的不同:
-
Everything is a header:h1把消息分成request/status行和头。h2把这些行打包成pseudo头。
HTTP/1.1的请求/响应可能是这样的:
GET / HTTP/1.1
Host: www.example.com
User-agent: Next-Great-h2-browser-1.0.0
Accept-Encoding: compress, gzip
HTTP/1.1 200 OK
Content-type: text/plain
Content-length: 2
...
对应的HTTP/2版是:
:scheme: https
:method: GET
:path: /
:authority: www.example.com
User-agent: Next-Great-h2-browser-1.0.0
Accept-Encoding: compress, gzip
:status: 200
content-type: text/plain
现在,请求和状态行被分成了:scheme、:method、:path和:status头。
- No chunked encoding:H2使用frames分块
- No more 101 responses:H1一般用来把协议升级到WebSocket。ALPN提供更明确的协议协商路径,具有更少的往返开销。
Flow Control
H1,客户端消耗多快,服务器就发多快。H2提供了客户端/服务器控制速度的能力。流量控制信息是WINDOW_UPDATE帧,告诉对方,它将接收多少字节。收到WINDOW_UPDATE帧的,也发送WINDOWS_UPDATE帧,说明自己已经更新消费数据的能力。
Priority
使用HEADERS和PRIORITY帧,客户端能说明它需要什么资源,以及期望这些资源以什么顺序发送。它会声明一个依赖树和权重:
- Dependencies:客户端可以告诉服务器,应该优先交付某对象,因为其他对象依赖它。
- Weights:客户端可以告诉服务器,拥有相同依赖关系的对象的优先级。
举一个简单的web站点的例子:
- index.html
- header.jpg
- critical.js
- less_critical.js
- style.css
- ad.js
- photo.jpg
接收完基本的HTML页面,客户端分析生成依赖树,并分配权重。这棵树可能是这个样子的:
- index.html
- style.css
- critical.js
- less_critical.js (weight 20)
- photo.jpg (weight 8)
- header.jpg (weight 8)
- ad.js (weight 4)
- critical.js
- style.css
Server Push
允许服务器向客户端发送数据,容易导致性能和安全问题。
Pushing an Object
服务器决定推送对象时,构造PUSH_PROMISE帧。这种帧的重要属性有:
- PUSH_PROMISE帧头里的流ID是与响应相关联的请求的流ID。一个推送的响应总是关联到客户端已经发送的一个请求。例如,如果浏览器请求一个基本的HTML页,服务器会为页面上的JS对象构造一个基于该请求的流ID的PUSH_PROMISE。
- PUSH_PROMISE帧的头
- 发送的对象是可缓存的
- :method头属性被认为是安全的。安全的方法是幂等的,不能修改状态。比如GET请求是幂等的,不改变状态,而POST就不是幂等的
- 理想情况下,PUSH_PROMISE应该先被推到客户端,然后再接收引用推送对象的DATA帧。如果服务器在发送PUSH_PROMISE之前,发送了完整的HTML,比如客户端在收到PUSH_PROMISE之前,已经请求了某对象,H2协议足够强大,可以处理这种情况,但是浪费了精力。
如果客户端不能安全地处理PUSH_PROMISE的对象,会发送复位新流(RST_STREAM)或者发送一个PROTOCOL_ERROR(在GOAWAY帧内)。
Choosing What to Push
当服务器收到请求,它需要决定推送页面上的对象,还是等客户端请求这些对象。决策应该考虑:
- 对象是否在浏览器缓存中
- 从客户端的角度,假设对象的优先级
- 客户端接受推送对象的能力,受带宽和类似资源的影响
Header Compression (HPACK)
HPACK是一种表查找(table lookup)压缩方案,使用Huffman编码,接近GZIP的压缩率。
一个web页依赖的对象需要多次请求,他们的头都是类似的。
当客户端发送请求时,在header block里说明需要的头和索引。应该增加这样一个表格:
Index | Name | Value |
---|---|---|
62 | Header1 | foo |
63 | Header2 | bar |
64 | Header3 | bat |
在服务器侧,读这些头,生成同样的表格。下次请求时,如果头相同,发送的头就是这样的:62 63 64。
实际的HPACK更复杂:
- 在请求和响应侧,实际上各维护两张表。一个是动态表,很像上面描述的例子。另一个是静态表,由最常用的61个头的名称和值的组成。因为静态表有61个实体,所以例子的索引从62开始
- 头怎么被索引,有很多控制
- 发送文字值和索引
- 发送文字值,不索引(一次性的或者敏感的)
- 发送文字值的索引的头名,不索引(比如:path: /foo.html,内容总变)
- 发送索引的头和值
- 整数压缩
- 利用霍夫曼编码表进一步压缩字符串
On the Wire
h2是二进制,被压缩的。
A Simple GET
HTTP/2 GET请求是这样的:
:authority: www.akamai.com
:method: GET
:path: /
:scheme: https
accept: text/html,application/xhtml+xml,...
accept-language: en-US,en;q=0.8
cookie: sidebar_collapsed=0; _mkto_trk=...
upgrade-insecure-requests: 1
user-agent: Mozilla/5.0 (Macintosh;...
HTTP/2 GET响应头:
:status: 200
cache-control: max-age=600
content-encoding: gzip
content-type: text/html;charset=UTF-8
date: Tue, 31 May 2016 23:38:47 GMT
etag: "08c024491eb772547850bf157abb6c430-gzip"
expires: Tue, 31 May 2016 23:48:47 GMT
link: <https://c.go-mpulse.net>;rel=preconnect
set-cookie: ak_bmsc=8DEA673F92AC...
vary: Accept-Encoding, User-Agent
x-akamai-transformed: 9c 237807 0 pmb=mRUM,1
x-frame-options: SAMEORIGIN
<DATA Frames follow here>
使用nghttp工具,可以看H2的细节:
nghttp -v -n --no-dep -w 14 -a -H "Header1: Foo" https://www.akamai.com
该命令行设置的窗口大小是16 KB(214),加了一个头,请求下载该页的关键资产。输出如下:
[ 0.047] Connected
The negotiated protocol: h2 [^1]
[ 0.164] send SETTINGS frame <length=12, flags=0x00, stream_id=0> [^2]
(niv=2)
[SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
[SETTINGS_INITIAL_WINDOW_SIZE(0x04):16383] [^3]
[ 0.164] send HEADERS frame <length=45, flags=0x05, stream_id=1>
; END_STREAM | END_HEADERS [^4]
(padlen=0)
; Open new stream
:method: GET
:path: /
:scheme: https
:authority: www.akamai.com
accept: */*
accept-encoding: gzip, deflate
user-agent: nghttp2/1.9.2
header1: Foo [^5]
[ 0.171] recv SETTINGS frame <length=30, flags=0x00, stream_id=0> [^6]
(niv=5)
[SETTINGS_HEADER_TABLE_SIZE(0x01):4096]
[SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
[SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535]
[SETTINGS_MAX_FRAME_SIZE(0x05):16384]
[SETTINGS_MAX_HEADER_LIST_SIZE(0x06):16384]
[ 0.171] send SETTINGS frame <length=0, flags=0x01, stream_id=0> [^7]
; ACK
(niv=0)
[ 0.197] recv SETTINGS frame <length=0, flags=0x01, stream_id=0>
; ACK
(niv=0)
[ 0.278] recv (stream_id=1, sensitive) :status: 200 [^8][^9]
[ 0.279] recv (stream_id=1, sensitive) last-modified: Wed, 01 Jun 2016 ...
[ 0.279] recv (stream_id=1, sensitive) content-type: text/html;charset=UTF-8
[ 0.279] recv (stream_id=1, sensitive) etag: "0265cc232654508d14d13deb...gzip"
[ 0.279] recv (stream_id=1, sensitive) x-frame-options: SAMEORIGIN
[ 0.279] recv (stream_id=1, sensitive) vary: Accept-Encoding, User-Agent
[ 0.279] recv (stream_id=1, sensitive) x-akamai-transformed: 9 - 0 pmb=mRUM,1
[ 0.279] recv (stream_id=1, sensitive) content-encoding: gzip
[ 0.279] recv (stream_id=1, sensitive) expires: Wed, 01 Jun 2016 22:01:01 GMT
[ 0.279] recv (stream_id=1, sensitive) date: Wed, 01 Jun 2016 22:01:01 GMT
[ 0.279] recv (stream_id=1, sensitive) set-cookie: ak_bmsc=70A833EB...
[ 0.279] recv HEADERS frame <length=458, flags=0x04, stream_id=1> [^10]
; END_HEADERS
(padlen=0)
; First response heade
[ 0.346] recv DATA frame <length=2771, flags=0x00, stream_id=1> [^11]
[ 0.346] recv DATA frame <length=4072, flags=0x00, stream_id=1>
[ 0.346] recv DATA frame <length=4072, flags=0x00, stream_id=1>
[ 0.348] recv DATA frame <length=4072, flags=0x00, stream_id=1>
[ 0.348] recv DATA frame <length=1396, flags=0x00, stream_id=1>
[ 0.348] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=1>
[ 0.348] send HEADERS frame <length=39, flags=0x25, stream_id=15> [^12]
:path: /styles/screen.1462424759000.css
[ 0.348] send HEADERS frame <length=31, flags=0x25, stream_id=17>
:path: /styles/fonts--full.css
[ 0.348] send HEADERS frame <length=45, flags=0x25, stream_id=19>
:path: /images/favicons/favicon.ico?v=XBBK2PxW74
[ 0.378] recv DATA frame <length=2676, flags=0x00, stream_id=1>
[ 0.378] recv DATA frame <length=4072, flags=0x00, stream_id=1>
[ 0.378] recv DATA frame <length=1445, flags=0x00, stream_id=1>
[ 0.378] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=13>
(window_size_increment=12216)
[ 0.379] recv HEADERS frame <length=164, flags=0x04, stream_id=17> [^13]
[ 0.379] recv DATA frame <length=175, flags=0x00, stream_id=17>
[ 0.379] recv DATA frame <length=0, flags=0x01, stream_id=17>
; END_STREAM
[ 0.380] recv DATA frame <length=2627, flags=0x00, stream_id=1>
[ 0.380] recv DATA frame <length=95, flags=0x00, stream_id=1>
[ 0.385] recv HEADERS frame <length=170, flags=0x04, stream_id=19> [^13]
[ 0.387] recv DATA frame <length=1615, flags=0x00, stream_id=19>
[ 0.387] recv DATA frame <length=0, flags=0x01, stream_id=19>
; END_STREAM
[ 0.389] recv HEADERS frame <length=166, flags=0x04, stream_id=15> [^13]
[ 0.390] recv DATA frame <length=2954, flags=0x00, stream_id=15>
[ 0.390] recv DATA frame <length=1213, flags=0x00, stream_id=15>
[ 0.390] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
(window_size_increment=36114)
[ 0.390] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=15> [^14]
(window_size_increment=11098)
[ 0.410] recv DATA frame <length=3977, flags=0x00, stream_id=1>
[ 0.410] recv DATA frame <length=4072, flags=0x00, stream_id=1>
[ 0.410] recv DATA frame <length=1589, flags=0x00, stream_id=1> [^15]
[ 0.410] recv DATA frame <length=0, flags=0x01, stream_id=1>
[ 0.410] recv DATA frame <length=0, flags=0x01, stream_id=15>
[ 0.457] send GOAWAY frame <length=8, flags=0x00, stream_id=0>
(last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[])
[^1]:协商好的协议
[^2]:发送SETTINGS帧
[^3]:窗口大小设置成16 KB
[^4]:客户端(nghttp)发送END_HEADERS和END_STREAM标记。告诉服务器没有头了,也不发数据。如果是POST,现在不发送END_STREAM
[^5]:nghttp命令行增加的头
[^6]:客户端接收到的服务器的SETTINGS帧
[^7]:发送和接收SETTINGS帧的确认信息
[^8]:stream_id说明相关的请求
[^9]:200状态码,成功了
[^10]:没有END_STREAM,因为DATA来了
[^11]:最后,开始获取数据流,WINDOW_UPDATE帧后面是5个DATA帧。客户端告诉服务器,已经读了10,915字节的DATA帧,并且准备好读取更多数据。请注意,此流还没完成
[^12]:客户端已经有了一些基本HTML,开始请求页面上的对象。新增加了三个流,15、16和19,准备接收css文件和图标
[^13]:流15、16和19的HEADERS帧
[^14]:窗口更新。包括连接级(connection-level)的更新(流0)
[^15]:流1的最后的DATA帧