本文通过一个小例子串一遍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配置如下:
1 | server { |
客户端按如下方式发起请求:
1 | curl -k -I -L https://IP:8443 |
请求解析
客户端请求问题
先思考一个问题,上文配置中使用curl发送请求时,为何直接返回的是http/2,而不是http/1.1(虽然服务端配置了使用http2,但万一客户端未支持http2协议,直接返回http2客户端会解析不了)
因为nginx中http2必须在ssl之上,所以我们首先通过在nginx代码中的ssl握手部分打断点gdb跟一下.
1 | (gdb) b ngx_ssl_handshake_handler //ssl握手函数 |
简单说就是通过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种类型
1 | #define NGX_HTTP_V2_DATA_FRAME 0x0 //body数据 |
frame ID在客户端按奇数递增,例如1,3,5,偶数型id留给服务端推送push时使用,设置连接属性相关的frame id都为0
flags有如下定义:
1 | #define NGX_HTTP_V2_NO_FLAG 0x00 //未设置 |
如下是一个http头类型frame具体的内容格式:
padded和priority由上文头部的flag决定是否有这两字段。接下来占8bit的flag决定header是否需要索引,如果需要,索引号是多少。
huff(1)表明该字段是否使用了huffman编码。header_value_len(7)和header_value是具体头字段的value值
如下是一个设置相关的frame
如下是一个窗口更新的frame
下边我们看一个具体的例子
http2报文解析
新版本的curl有一个–http2参数,可以直接指明使用http2进行通讯。我们将客户端命令修改如下:
1 | curl --http2 -k -I -L https://10.96.79.14:8443 |
通过上边的gdb跟踪,我们看到http2初始化入口函数为ngx_http_v2_init,直接在此处打断点,继续跟踪代码.跟踪过程不再详细描述,当把报文读取进缓存之后,我们直接在gdb中bt查看调用路径,如下:
1 | #0 ngx_http_v2_state_preface (h2c=0x15a9310, pos=0x164b0b0 "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", end=0x164b11e "") |
调用到ngx_http_v2_state_preface这个函数之后,开始处理http2请求,我们将请求内容打印出来看一下:
1 | (gdb) p end-pos |
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就会延迟加载。
Q&A
- HTTP2如果在服务端动态索引header,会使http变成有状态的服务,集群之间如何解决header头缓存的问题?
- 静态资源文件首次请求后会在浏览器端缓存,push如何保证只推送一次(即只有首次请求时才push)?
参考资料
1.https://www.nginx.com/blog/http2-theory-and-practice-in-nginx-stable-13/