运营研发 张仕华
本文通过一个小例子串一遍 nginx 处理 http2 的流程。主要涉及到 http2 的协议以及 nginx 的处理流程。
http2 简介
http2 比较 http1.1 主要有如下五个方面的不同:
二进制协议
http1.1 请求行和请求头部都是纯文本编码, 即可以直接按 ascii 字符解释,而 http2 是有自己的编码格式。并且 nginx 中 http2 必须建立在 ssl 协议之上。
头部压缩
举个例子,HTTP1.1 传一个 header <method: GET>, 需要 11 个字符.http2 中有一个静态索引表,客户端传索引键,例如 1,nginx 通过查表能知道 1 代表 method: GET.nginx 中除了该静态表,还会有一个动态表,保存例如 host 这种变化的头部
多路复用
http1.1 一个连接上只能传输一个请求,当一个请求结束之后才能传输下一个请求。所以对 http1.1 协议的服务发起请求时,一般浏览器会建立 6 条连接,并行的去请求不同的资源。而 http2 的二进制协议中有一个 frame 的概念,每个 frame 有自己的 id, 所以一个连接上可以同时多路复用传输多个不同 id 的 frame
主动 push
http1.1 是请求 - 响应模型,而 http2 可以主动给客户端推送资源
优先级
既然多路复用,所有数据跑在了一条通道上,必然会有优先级的需求
本文的例子主要通过解析报文说明头三个特性
配置环境
NGINX 配置如下:
server {
listen 8443 ssl http2;
access_log logs/host_server2.access.log main;
ssl_certificate /home/xiaoju/nginx-2/nginx-selfsigned.crt;
ssl_certificate_key /home/xiaoju/nginx-2/nginx-selfsigned.key;
ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
location / {
root html;
index index.html index.htm /abc.html;
access_log logs/host_location3.access.log main;
http2_push /favicon.ico;
http2_push /nginx.png;
}
}
客户端按如下方式发起请求:
curl -k -I -L https://IP:8443
HTTP/2 200 // 可以看到,返回是 http/2
server: nginx/1.14.0
date: Tue, 11 Dec 2018 09:20:33 GMT
content-type: text/html
content-length: 664
last-modified: Tue, 11 Dec 2018 04:19:32 GMT
etag: “5c0f3ad4-298”
accept-ranges: bytes
请求解析
客户端请求问题
先思考一个问题,上文配置中使用 curl 发送请求时, 为何直接返回的是 http/2, 而不是 http/1.1(虽然服务端配置了使用 http2, 但万一客户端未支持 http2 协议,直接返回 http2 客户端会解析不了)
因为 nginx 中 http2 必须在 ssl 之上,所以我们首先通过在 nginx 代码中的 ssl 握手部分打断点 gdb 跟一下.
(gdb) b ngx_ssl_handshake_handler //ssl 握手函数
Breakpoint 1 at 0x47ddb5: file src/event/ngx_event_openssl.c, line 1373.
(gdb) c
Continuing.
Breakpoint 1, ngx_ssl_handshake_handler (ev=0x16141f0) at src/event/ngx_event_openssl.c:1373
1373 {
1390 c->ssl->handler(c); // 实际处理逻辑位于 ngx_http_ssl_handshake_handler
(gdb) s
ngx_http_ssl_handshake_handler (c=0x15da400) at src/http/ngx_http_request.c:782
782 {
(gdb) n
805 if (hc->addr_conf->http2) {// 配置 http2 后 hc->addr_conf->http2 标志位为 1
(gdb) n
808 SSL_get0_alpn_selected(c->ssl->connection, &data, &len);// 从 ssl 协议中取出 alpn
(gdb) n
820 if (len == 2 && data[0] == ‘h’ && data[1] == ‘2’) {// 如果为 h2, 说明客户端支持升级到 http2 协议
(gdb) n
821 ngx_http_v2_init(c->read);// 开始进入 http2 的初始化阶段
简单说就是通过 ssl 协议握手阶段获取一个 alpn 相关的配置,如果是 h2,就进入 http2 的处理流程。我们通过 wireshark 抓包可以更直观的看出这个流程
如上图,在 ssl 握手中的 Client Hello 阶段有一个协议扩展 alpn
http2 报文格式
http2 以一个 preface 开头,接着是一个个的 frame, 其中每个 frame 都有一个 header, 如下:
其中 length 代表 frame 内容的长度,type 表明 frame 的类型,flag 给 frame 做一些特殊的标记,sid 代表的就是 frame 的 id.
其中 frame 有如下 10 种类型
#define NGX_HTTP_V2_DATA_FRAME 0x0 //body 数据
#define NGX_HTTP_V2_HEADERS_FRAME 0x1 //header 数据
#define NGX_HTTP_V2_PRIORITY_FRAME 0x2 // 优先级设置
#define NGX_HTTP_V2_RST_STREAM_FRAME 0x3 // 重置一个 stream
#define NGX_HTTP_V2_SETTINGS_FRAME 0x4 // 其他设置项,例如是否开启 push, 同时能够处理的 stream 数量等
#define NGX_HTTP_V2_PUSH_PROMISE_FRAME 0x5 //push
#define NGX_HTTP_V2_PING_FRAME 0x6 //ping
#define NGX_HTTP_V2_GOAWAY_FRAME 0x7 //goaway. 发送此 frame 后会重新建立连接
#define NGX_HTTP_V2_WINDOW_UPDATE_FRAME 0x8 // 窗口更新 流控使用
#define NGX_HTTP_V2_CONTINUATION_FRAME 0x9 // 当一个 frame 发送不完数据时,可以按 continuation 格式继续发送
frame ID 在客户端按奇数递增,例如 1,3,5,偶数型 id 留给服务端推送 push 时使用,设置连接属性相关的 frame id 都为 0
flags 有如下定义:
#define NGX_HTTP_V2_NO_FLAG 0x00 // 未设置
#define NGX_HTTP_V2_ACK_FLAG 0x01 //ack flag
#define NGX_HTTP_V2_END_STREAM_FLAG 0x01 // 结束 stream
#define NGX_HTTP_V2_END_HEADERS_FLAG 0x04 // 结束 headers
#define NGX_HTTP_V2_PADDED_FLAG 0x08 // 填充 flag
#define NGX_HTTP_V2_PRIORITY_FLAG 0x20 // 优先级设置 flag
如下是一个 http 头类型 frame 具体的内容格式:
padded 和 priority 由上文头部的 flag 决定是否有这两字段。接下来占 8bit 的 flag 决定 header 是否需要索引,如果需要,索引号是多少。
huff(1) 表明该字段是否使用了 huffman 编码。header_value_len(7) 和 header_value 是具体头字段的 value 值
如下是一个设置相关的 frame
如下是一个窗口更新的 frame
下边我们看一个具体的例子,来更直观的了解下。
http2 报文解析
新版本的 curl 有一个–http2 参数,可以直接指明使用 http2 进行通讯。我们将客户端命令修改如下:
curl –http2 -k -I -L https://10.96.79.14:8443
通过上边的 gdb 跟踪,我们看到 http2 初始化入口函数为 ngx_http_v2_init,直接在此处打断点,继续跟踪代码. 跟踪过程不再详细描述,当把报文读取进缓存之后,我们直接在 gdb 中 bt 查看调用路径,如下:
#0 ngx_http_v2_state_preface (h2c=0x15a9310, pos=0x164b0b0 “PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n”, end=0x164b11e “”)
at src/http/v2/ngx_http_v2.c:713
#1 0x00000000004bca20 in ngx_http_v2_read_handler (rev=0x16141f0) at src/http/v2/ngx_http_v2.c:415
#2 0x00000000004bcf8a in ngx_http_v2_init (rev=0x16141f0) at src/http/v2/ngx_http_v2.c:328
#3 0x0000000000490a13 in ngx_http_ssl_handshake_handler (c=0x15da400) at src/http/ngx_http_request.c:821
#4 0x000000000047de24 in ngx_ssl_handshake_handler (ev=0x16141f0) at src/event/ngx_event_openssl.c:1390
#5 0x0000000000479637 in ngx_epoll_process_events (cycle=0x1597e30, timer=<optimized out>, flags=<optimized out>)
at src/event/modules/ngx_epoll_module.c:902
#6 0x000000000046f9db in ngx_process_events_and_timers (cycle=0x1597e30) at src/event/ngx_event.c:242
#7 0x000000000047761c in ngx_worker_process_cycle (cycle=0x1597e30, data=<optimized out>) at src/os/unix/ngx_process_cycle.c:750
#8 0x0000000000475c50 in ngx_spawn_process (cycle=0x1597e30, proc=0x477589 <ngx_worker_process_cycle>, data=0x0,
name=0x684922 “worker process”, respawn=-3) at src/os/unix/ngx_process.c:199
#9 0x00000000004769aa in ngx_start_worker_processes (cycle=0x1597e30, n=1, type=-3) at src/os/unix/ngx_process_cycle.c:359
#10 0x0000000000477cb0 in ngx_master_process_cycle (cycle=0x1597e30) at src/os/unix/ngx_process_cycle.c:131
#11 0x0000000000450ea4 in main (argc=<optimized out>, argv=<optimized out>) at src/core/nginx.c:382
调用到 ngx_http_v2_state_preface 这个函数之后,开始处理 http2 请求,我们将请求内容打印出来看一下:
(gdb) p end-pos
$1 = 110
(gdb) p *pos@110
$2 = “PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n\000\000\022\004\000\000\000\000\000\000\003\000\000\000d\000\004@\000\000\000\000\002\000\000\000\000\000\000\004\b\000\000\000\000\000?\377\000\001\000\000%\001\005\000\000\000\001B\004HEAD\204\207A\214\b\027}\305\335}p\265q\346\232gz\210%\266Pë\266\322\340S\003*/*”
nginx 接下来开始处理 http2 请求,处理方法可以按上述方法继续跟踪,我们直接按 http2 协议将上述报文解析一下,如下所示:
注意 gdb 打印出来的是八进制格式
http push 抓包
注意上文 nginx 配置中配置了两条 http2_push 指令,即服务端会在请求 index.html 时主动将 favicon.ico 和 nginx.png 两个图片 push 下去。
wireshark 中抓包如下:
服务端首先发送一个 push_promise 报文,报文中会包括 push 的文件路径和 frame id. 第二个和第三个红框即开始 push 具体的信息,frame id 分别为 2 和 4
我们从浏览器端看一下 push 的请求:
不主动 push 请求如下:
浏览器必须首先将 index.html 加载之后才会知道接着去请求哪些资源,于是 favicon.ico 和 nginx.png 就会延迟加载。
问题
HTTP2 如果在服务端动态索引 header,会使 http 变成有状态的服务,集群之间如何解决 header 头缓存的问题?静态资源文件首次请求后会在浏览器端缓存,push 如何保证只推送一次 (即只有首次请求时才 push)? 参考资料 1.https://www.nginx.com/blog/ht…
2.https://httpwg.org/specs/rfc7540