HTTP协议探究(六):H2帧详解和HTTP优化
一 复习与目标
1 复习
-
HTTP1.1存在的问题
-
HTTP2.0要兼容HTTP1.1
-
HTTP2.0的重要概念
- 分帧层
- 二进制:流 消息 帧
- 流的状态、优先级和并发
- 流量控制
- 服务器推送
- 首部压缩
-
HTTP2.0的流的建立(HEADERS或PUSH_PROMISE)和数据发送(DATA)
2 目标
- 帧定义
- HTTP2.0流量分析
- Chrome插件:HTTP/2 and SPDY
- WireShark
- 对某些帧进行分析
- HTTP优化
二 帧定义
1 HEADERS
(1)定义
- 长度:16位,代表帧净荷最大可达65535字节(64KB),只有当Padded为1时才有效。
- 类型:0x01
- 标志:
- End Stream:位1标识最后报头区块;一个HEADER帧太长时会分帧传输,后续跟着CONTINUATION 帧,如果CONTINUATION帧的END_STREAM标志为1,代表流传输结束。但是CONTINUATION帧并不能用于关闭流。
- End Segment:位2标识当前端的最后一帧。
- End Header:位3标识帧包含了整个的报头块且后面没有延续帧。
- Padded:位4标识Pad Length字段会呈现。
- Priority:位6标识专用标记、流依赖及权重字段
- R:保留字段,1位
- 流标志符:31位,唯一标识 HTTP 2.0 的流,流标识符为奇数(不包含1),1为升级协议使用。
- 首部块:HTTP首部
(2)wireshark抓包
Stream: HEADERS, Stream ID: 25, Length 51, POST /fd/ls/lsp.aspx
Length: 51
Type: HEADERS (1)
Flags: 0x24
.... ...0 = End Stream: False
.... .1.. = End Headers: True
.... 0... = Padded: False
..1. .... = Priority: True
00.0 ..0. = Unused: 0x00
0... .... .... .... .... .... .... .... = Reserved: 0x0
.000 0000 0000 0000 0000 0000 0001 1001 = Stream Identifier: 25
1... .... .... .... .... .... .... .... = Exclusive: True
.000 0000 0000 0000 0000 0000 0000 0000 = Stream Dependency: 0
Weight: 219
Header Block Fragment: 83dd870084b958d33f8b625918a10c508ad71a2bf35c830b...
Header: :method: POST
Header: :authority: cn.bing.com
Header: :scheme: https
#......
2 DATA
(1)定义
- 类型为0x0
注:基本上与HEADERS帧差不多,不过多介绍了。
(2)wireshark抓包
Stream: DATA, Stream ID: 25, Length 1316
Length: 1316
Type: DATA (0)
Flags: 0x01
.... ...1 = End Stream: True
.... 0... = Padded: False
0000 .00. = Unused: 0x00
0... .... .... .... .... .... .... .... = Reserved: 0x0
.000 0000 0000 0000 0000 0000 0001 1001 = Stream Identifier: 25
Data: ......
3 SETTINGS帧
(1)定义
-
类型为0x4
-
ACK标志:位1表示设置帧被接收端接受并应用。当ACK为1时,帧净荷必须为空.
-
帧净荷为帧参数
- SETTINGS_HEADER_TABLE_SIZE(1) :允许发送端通知远端终端解码报头区块的报头压缩表的最大承载量。初始值为4096字节
- SETTINGS_ENABLE_PUSH(2) :服务器推送许可标志,默认开启。
- SETTINGS_MAX_CONCURRENT_STREAMS(3) :最大流并发数,0代表不允许新建流,默认没有限制。
- SETTINGS_INITIAL_WINDOW_SIZE(4):初始窗口大小,默认65535,不超过2 ^ 31-1。
(2)wireshark抓包
# 发送设置帧
202.89.233.101 192.168.1.46 HTTP2 123 SETTINGS[0], WINDOW_UPDATE[0]
Stream: SETTINGS, Stream ID: 0, Length 18
Length: 18
Type: SETTINGS (4)
Flags: 0x00
.... ...0 = ACK: False
0000 000. = Unused: 0x00
0... .... .... .... .... .... .... .... = Reserved: 0x0
.000 0000 0000 0000 0000 0000 0000 0000 = Stream Identifier: 0
Settings - Header table size : 65536
Settings - Max concurrent streams : 1000
Settings - Initial Windows size : 6291456
# 发送设置帧确认
192.168.1.46 202.89.233.101 HTTP2 92 SETTINGS[0]
Stream: SETTINGS, Stream ID: 0, Length 0
Length: 0
Type: SETTINGS (4)
Flags: 0x01
.... ...1 = ACK: True
0000 000. = Unused: 0x00
0... .... .... .... .... .... .... .... = Reserved: 0x0
.000 0000 0000 0000 0000 0000 0000 0000 = Stream Identifier: 0
4 WINDOW_UPDATE帧
(1)定义
- 类型:0x8
- 标志:无
- 帧净荷:
- Reserved:1位保留位
- Window Size Increment:最大值位2^31-1
(2)wireshark抓包
Stream: WINDOW_UPDATE, Stream ID: 0, Length 4
Length: 4
Type: WINDOW_UPDATE (8)
Flags: 0x00
0... .... .... .... .... .... .... .... = Reserved: 0x0
.000 0000 0000 0000 0000 0000 0000 0000 = Stream Identifier: 0
0... .... .... .... .... .... .... .... = Reserved: 0x0
.000 0000 0000 1111 0000 0000 0000 0001 = Window Size Increment: 983041
(3)补充
- 流量控制是能作用某个流或整个连接(流标志符号为0,即根流可以限制依赖流)。
- WINDOW_UPDATE可由一个已经发送带有END_STREAM标记的帧的对等端来发送。这意味着接收端可以在“半封闭(远程)”或者“关闭”的流上接收WINDOW_UPDATE帧。
- 流量控制窗口用于指示发送端被允许传输的字节数(请回想起TCP的阻塞窗口和接受窗口)。
- 流量控制计算不包含帧报头。
- 流量窗口初始值为65535
- 流量窗口为负值代表禁止发送
- SETTING帧无法修改链接状态的流量控制窗口(即只能设置初始值,无法通过SETTING帧进行修改)
例如,如果客户端在建立的连接上立即发送60KB的数据,而终端将初始的窗口大小设置成16KB,客户端将重新计算流量控制窗口的可用空间为-44KB。终端将保持一个负数的流量控制窗口直到窗口更新帧恢复窗口到正数,这个时候客户端才能恢复数据发送。
5 PING帧
(1)定义
- 长度:固定为0x8
- 类型:0x6
- ACK标志:位1用于标识0为请求,1为响应
- 流标志符:固定为0x0
- 帧净荷:ping的唯一标识符
(2)wireshark抓包
# 成对出现
192.168.1.46 202.89.233.100 HTTP2 100 PING[0]
Stream: PING, Stream ID: 0, Length 8
Length: 8
Type: PING (6)
Flags: 0x00
.... ...0 = ACK: False
0000 000. = Unused: 0x00
0... .... .... .... .... .... .... .... = Reserved: 0x0
.000 0000 0000 0000 0000 0000 0000 0000 = Stream Identifier: 0
Ping: 0000000000000001
202.89.233.100 192.168.1.46 HTTP2 100 PING[0]
Stream: PING, Stream ID: 0, Length 8
Length: 8
Type: PING (6)
Flags: 0x00
.... ...1 = ACK: True
0000 000. = Unused: 0x00
0... .... .... .... .... .... .... .... = Reserved: 0x0
.000 0000 0000 0000 0000 0000 0000 0000 = Stream Identifier: 0
Ping: 0000000000000001
6 RST_STREAM帧
(1)定义
- 长度:固定为0x4
- 类型:0x3
- ACK标志:无
- 流标志符:标识哪个流非正常终结
- 帧净荷:错误码
- NO_ERROR(0):没有错误,用于给GOWAY帧平滑关闭
- PROTOCOL_ERROR(1):协议错误
- INTERNAL_ERROR (2) : 终端遇到意外的内部错误。
- FLOW_CONTROL_ERROR (3) : 终端检测到对等端违反了流量控制
- SETTINGS_TIMEOUT (4) : 终端发送了设置帧,但没有及时收到响应。
- STREAM_CLOSED (5) : 终端在流半封闭的时候收到帧。
- FRAME_SIZE_ERROR (6) : 终端收到大小超过最大尺寸的帧(2^14 -1)。
- REFUSED_STREAM (7) : 终端拒绝流在它执行任何应用处理之前.
- CANCEL (8) : 终端使用这个标示某个流不再需要。
- COMPRESSION_ERROR (9) : 终端无法维持报头压缩上下文的连接
- CONNECT_ERROR (10) : 响应某个连接请求建立的连接被服为异常关闭。
- ENHANCE_YOUR_CALM (11) : 终端检测出对等端在表现出可能会产生过大负荷的行为。
- INADEQUATE_SECURITY (12) : 基础传输包含属性不满足文档或者终端申明的最小要求。
(2)wireshark抓包
Stream: RST_STREAM, Stream ID: 5, Length 4
Length: 4
Type: RST_STREAM (3)
Flags: 0x00
0... .... .... .... .... .... .... .... = Reserved: 0x0
.000 0000 0000 0000 0000 0000 0000 0101 = Stream Identifier: 5
Error: CANCEL (8)
7 PUSH_PROMISE
(1)定义
- 长度:非固定
- 类型:0x04
- 标志:
- END_HEADERS:位3标识帧包含了整个的报头块且后面没有延续帧。
- Padded:位4标识Pad Length字段会呈现。
- 流标志符(Stream Identifier)
- 准备推送的流标志符(Promised-Stream-ID)
- 首部块:HTTP首部
(2)wireshark抓包
# 由服务器建立流并发送HTTP头部
Stream: PUSH_PROMISE, Stream ID: 1, Length 166, GET /styles.780f923f35dd43d00653.css
Length: 166
Type: PUSH_PROMISE (5)
Flags: 0x04
.... .1.. = End Headers: True
.... 0... = Padded: False
0000 ..00 = Unused: 0x00
0... .... .... .... .... .... .... .... = Reserved: 0x0
.000 0000 0000 0000 0000 0000 0000 0001 = Stream Identifier: 1
0... .... .... .... .... .... .... .... = Reserved: 0x0
.000 0000 0000 0000 0000 0000 0000 0010 = Promised-Stream-ID: 2
Header Block Fragment: 208244976109f54150bbaf0257c4ccacb7248d332000e36c...
Header: :method: GET
# ......
(3)补充
- PUSH_PROMISE为服务器建立流,HEADERS为客户端建立流。
- 服务器建立的流标识符为偶数(不包含0),0为根流标识符。
- 客户端 -----HEADERS帧(含priority)-----> 服务器
- 服务器 -----PUSH_PROMISE帧(不含priority)-----> 客户端,所以一般后续帧为 客户端 -----PRIORITY帧----> 服务器。
8 PRIORITY帧
(1)定义
- 长度:非固定
- 类型:0x02
- 标志:无
- 保留位和流标志符(Stream Identifier)
- 独占位(Exclusive):1位,指示流的依赖是专有的
- 依赖流标识符(Stream Dependency)
- 优先级权重:1-256的权重值
(2)wireshark抓包
Stream: PRIORITY, Stream ID: 2, Length 5
Length: 5
Type: PRIORITY (2)
Flags: 0x00
0... .... .... .... .... .... .... .... = Reserved: 0x0
.000 0000 0000 0000 0000 0000 0000 0010 = Stream Identifier: 2
1... .... .... .... .... .... .... .... = Exclusive: True
.000 0000 0000 0000 0000 0000 0000 0001 = Stream Dependency: 1
Weight: 109
9 GOWAY帧
(1)定义
- 长度:固定
- 类型:0x07
- 标志:无
- 保留位和流标志符(Stream Identifier)
- 保留位与最后一个流标识符
- 错误码
(2)wireshark抓包
Stream: GOAWAY, Stream ID: 0, Length 8
Length: 8
Type: GOAWAY (7)
Flags: 0x00
0... .... .... .... .... .... .... .... = Reserved: 0x0
.000 0000 0000 0000 0000 0000 0000 0000 = Stream Identifier: 0
0... .... .... .... .... .... .... .... = Reserved: 0x0
.000 0000 0000 0000 0000 0000 0000 0011 = Promised-Stream-ID: 3
Error: NO_ERROR (0)
(3)补充
- 通知远端对等端不要在这个连接上建立新流,即提示抛弃该连接,重新建立一条TCP连接。
- 允许终端优雅的停止接收新的流,但仍可以继续完成之前已经建立的流的处理。
注:CONTINUTION帧省略,CONTINUTION帧比较简单。
三 Chrome插件解析HTTP2流量
88: HTTP2_SESSION
Start Time: 2018-12-03 14:36:45.975
# HEADERS帧
t= 10464 [st= 0] HTTP2_SESSION_SEND_HEADERS
--> exclusive = true
--> fin = false
--> has_priority = true
--> :method: POST
:authority: cn.bing.com
:scheme: https
:path: /fd/ls/lsp.aspx
content-length: 1316
origin: https://cn.bing.com
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36
content-type: text/xml
accept: */*
referer: https://cn.bing.com/
accept-encoding: gzip, deflate, br
accept-language: zh-CN,zh;q=0.9,en;q=0.8,en-US;q=0.7
cookie: _EDGE_V=1; MUID=1EF97277BD3961513FB57ECDBC17605A; MUIDB=1EF97277BD3961513FB57ECDBC17605A; SRCHD=AF=BEHPTB; SRCHUID=V=2&GUID=BD0C76F5F80546D8877C9F6688DDF1AA&dmnchg=1; ENSEARCH=BENVER=1; SRCHUSR=DOB=20181203&T=1543818612000; _EDGE_CD=u=zh-cn; _UR=MC=1; SRCHHPGUSR=CW=1536&CH=711&DPR=1.25&UTC=480&WTS=63679415390&NEWWND=1&NRSLT=-1&SRCHLANG=&AS=1&NNT=1&HAP=0; _EDGE_S=SID=04B092AF518367C83A3F9E1550AD6673; _SS=SID=04B092AF518367C83A3F9E1550AD6673&bIm=978208&HV=1543819002; ULC=P=1D5FE|9:1&H=1D5FE|5:1&T=1D5FE|6:1:8
--> parent_stream_id = 0
--> source_dependency = 259 (HTTP_STREAM_JOB)
--> stream_id = 25
--> weight = 220
# DATA帧
t= 10464 [st= 0] HTTP2_SESSION_SEND_DATA
--> fin = true
--> size = 1316
--> stream_id = 25
# WINDOW_UPDATE帧
t= 10464 [st= 0] HTTP2_SESSION_UPDATE_SEND_WINDOW
--> delta = -1316
--> window_size = 1047260
t= 10501 [st= 37] HTTP2_SESSION_RECV_WINDOW_UPDATE
--> delta = 1316
--> stream_id = 0
t= 10501 [st= 37] HTTP2_SESSION_UPDATE_SEND_WINDOW
--> delta = 1316
--> window_size = 1048576
t= 10541 [st= 77] HTTP2_SESSION_RECV_HEADERS
--> fin = false
--> :status: 204
x-msedge-ref: Ref A: D0007C217F0649D79ED88874FFB61759 Ref B: BJ1EDGE0217 Ref C: 2018-12-03T06:36:55Z
date: Mon, 03 Dec 2018 06:36:55 GMT
--> stream_id = 25
t= 10542 [st= 78] HTTP2_SESSION_RECV_DATA
--> fin = true
--> size = 0
--> stream_id = 25
# PING帧
t= 96465 [st= 86001] HTTP2_SESSION_PING
--> is_ack = false
--> type = "sent"
--> unique_id = 1
t= 96501 [st= 86037] HTTP2_SESSION_PING
--> is_ack = true
--> type = "received"
--> unique_id = 1
# RST_STREAM
t=25836233 [st=1902] HTTP2_SESSION_SEND_RST_STREAM
--> description = ""
--> error_code = "8 (CANCEL)"
--> stream_id = 5
t=222812 [st=212348] HTTP2_SESSION_CLOSE
--> description = "Error 101 reading from socket."
--> net_error = -101 (ERR_CONNECTION_RESET)
t=222813 [st=212349] HTTP2_SESSION_POOL_REMOVE_SESSION
t=222813 [st=212349] -HTTP2_SESSION
四 HTTP优化
(1)基于TCP和TLS优化后
(2)减少DNS查询
- 尽量不用多域名
- 证书链不要太长
(3)减少HTTP重定向
- HTTP 重定向极费时间,特别是不同域名之间的重定向,更加费时
(4)使用CDN(内容分发网络)
- 把数据放到离用户地理位置更近的地方,可以显著减少每次TCP 连接的网络延
迟,增大吞吐量。
(5)在客户端缓存资源
# nginx添加响应头
add_header Cache-Control private,max-age=86400,proxy-revalidate;
- 具体缓存策略参考:HTTP协议探究(二):代理、网关和隧道
(6)传输压缩过的内容
- 速度:brotli > gzip压缩 > 没压缩
# nginx使用ngx_brotli模块
# gzip压缩1.1,只有britli失效才有用
gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
gzip_vary on;
gzip_disable "MSIE [1-6]\.";
gzip_comp_level 6;
gzip_http_version 1.1;
# brotli压缩所有HTTP版本
brotli on;
brotli_comp_level 6;
brotli_buffers 16 8k;
brotli_min_length 20;
brotli_types *;
(7)消除不必要的请求开销
- 升级HTTP2.0即可
- HTTP2.0能够压缩头部,减少重复头部传输
# nginx配置
listen 443 ssl http2 fastopen=3 reuseport default_server;
(8)利用服务器推送
# nginx配置
location / {
root /home/nginx;
index index.html;
http2_push /styles.389ab69b43b87e6d33f0.css;
# ...
}
参考:
- http2详解
- HTTP/2 服务器推送(Server Push)教程
- 《Web性能权威指南》
- RFC 7540