HTTP/2 协议


HTTP/2(HTTP/2.0)即超文本传输协议 2.0,是下一代HTTP协议。相对于HTTP/1.x协议的文本传输格式,HTTP/2以二进制的格式进行数据传输。因此,具有更小的传输体积以及负载,相比于文本解析,二进制解析更加方便、高效。HTTP/2HTTP/1.x相比具有如下特性:

HTTP/2 帧的特性

二进制分帧

HTTP/2在应用层跟传输层 之间增加了一个 二进制分帧层,从而能够达到:在不改动HTTP的语义、HTTP方法、状态码、URI及首部字段的情况下,突破HTTP 1.1的性能限制,改进传输性能,实现低延迟和高吞吐量。

http2.png

如上图所示,在二进制分帧层上, HTTP 2.0会将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码,其中HTTP 1.1的首部信息会被封装到Headers帧,而request body被封装到Data帧里面。然后,HTTP 2.0 通信都在一个连接上完成,这个连接可以承载任意数量的双向数据流。相应地,每个数据流以消息的形式发送,而消息由一或多个帧组成,这些帧可以乱序发送,然后再根据每个帧首部的流标识符重新组装。

header压缩

HTTP 2.0采用HPACK的压缩算法来压缩头部。并且在客户端和服务器端使用 “首部表” 来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送,通信期间几乎不会改变的通用键-值对(用户代理、可接受的媒体类型,等等)只需发送一次。

  • 如果请求中不包含首部(例如对同一资源的轮询请求),那么 首部开销就是零字节。此时所有首部都自动使用之前请求发送的首部。
  • 如果首部发生变化了,那么只需要发送变化了数据在Headers帧里面,新增或修改的首部帧会被追加到“首部表”。首部表在HTTP 2.0的连接存续期内始终存在,由客户端和服务器共同渐进地更新。

header_diff

多路复用

HTTP/1.1中,若干个请求排队串行化单线程处理,后面的请求等待前面请求的返回才能获得执行机会,一旦有某请求超时等,后续请求只能被阻塞。HTTP 2.0 多个请求可同时在一个连接上并行执行。某个请求任务耗时严重,不会影响到其它连接的正常执行。

multi_plexing.png

HTTP 2.0把HTTP协议通信的基本单位缩小为一个一个的帧,这些帧对应着逻辑流中的消息。并行地在同一个TCP连接上双向交换消息。每一个request都是是用作连接共享机制的。一个request对应一个id,这样一个连接上可以有多个request,每个连接的request可以随机的混杂在一起,接收方可以根据request的id将request再归属到各自不同的服务端请求里面。

stream.png

上图中包含了同一个连接上多个传输中的数据流:客户端正在向服务器传输一个DATA帧(stream 5),与此同时,服务器正向客户端乱序发送stream 1stream 3的一系列帧。此时,一个连接上有3个请求/响应并行交换。

服务器推送

HTTP 2.0新增的一个强大的新功能,就是服务器可以对一个客户端请求发送多个响应。服务器向客户端推送资源无需客户端明确地请求。

服务端推送能把客户端所需要的资源伴随着index.html一起发送到客户端,省去了客户端重复请求的步骤。正因为没有发起请求,建立连接等操作,所以静态资源通过服务端推送的方式可以极大地提升速度。这相当于预加载技术,具体如下:

普通的客户端请求过程:

request.png

服务端推送的过程:

server_push.png

更安全的SSL

HTTP2.0使用了TLS的拓展ALPN来做协议升级,除此之外加密这块还有一个改动,HTTP2.0TLS的安全性做了近一步加强,通过黑名单机制禁用了几百种不再安全的加密算法,一些加密算法可能还在被继续使用。

虽然HTTP2.0中的TLS是可选的,但是现在主流的浏览器像chrome,firefox都只支持基于TLS部署的HTTP2.0协议。所以要想站点升级为HTTP2.0首先要将站点先升级为HTTPS

HTTP/2性能

HTTP 2.0相比于HTTP 1.x,大幅度的提升了web性能。https://http2.akamai.com/demo这个网站可以看到在HTTP 2.0HTTP 1.1的环境下的性能比较。

nginx HTTP/2.0 配置

安装nginx

首先确认当前openssl版本,最低要求1.0.2,如果不满足要求还要下载openssl,nginx也用最新的稳定版。

openssl version -a
wget https://www.openssl.org/source/openssl-1.0.2e.tar.gz
wget http://nginx.org/download/nginx-1.12.2.tar.gz

解压:

tar xzf openssl-1.0.2e.tar.gz
tar xzf nginx-1.12.2.tar.gz

编译安装:

cd nginx-1.12.2

./configure --prefix=/usr/local/nginx --with-http_stub_status_module --with-http_ssl_module --with-http_realip_module --with-http_v2_module --with-openssl=../openssl-1.0.2e

make 
sudo make install

配置运行

配置nginx伪证书、配置文件中开启https即可

到需要放置证书的目录(选在nginx的/usr/local/nginx/conf目录下就可以),建立服务器的私钥(此过程需输入密码短语):

openssl genrsa -des3 -out server.key 1024

创建证书签名请求csr:

openssl req -new -key server.key -out server.csr

生成RSA公钥:

cp server.key server.key.org
openssl rsa -in server.key.org -out server.key

使用上面的私钥和CSR对证书进行签名:

openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt

配置nginx:

server {
    listen       443 ssl http2 default_server;
    server_name  www.heqingliang.com;                                                                            
    ssl_certificate      server.crt;
    ssl_certificate_key  server.key;

    location / { 
        root   html;
        index  index.html index.htm;
    }
}

启动nginx:

cd /usr/local/nginx/sbin
sudo ./nginx

把生成的证书server.crt导入进firxfox浏览器,注意firefox的版本要支持HTTP/2,把浏览器的开发模式打开,输入配置中的域名,看到如下请求,则表示HTTP/2配置成功:

http2_request.png

HTTP/2 帧格式

使用wireshark抓包工具,可以看到在TLS请求包中带有h2,这表示请求使用的是HTTP 2.0

http2_ssl.png

如果服务器支持HTTP 2.0,则客户端会发送一个24个字节(PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n)开始的连接序言。如果不支持HTTP 2.0,则使用HTTP/1.x。关于HTTP 2.0的请求过程可以参考RFC

preface.png

要看到HTTP/2 帧格式,首先要把上面wireshark抓的TLS数据包是解开。可以参考这篇文章,https://ismisepaul.wordpress.com/2015/03/04/http2-traffic-in-wireshark/,解开TLS数据包后,可以看到:

http2_pcap.png

帧头

frame_format.png

固定的9个字节((24+8+8+1+31)/8=9)呈现,变化的为帧的Frame PayloadFrame Payload的内容是由帧类型(Type)定义。

帧长度(Length): 无符号的自然数,24个比特表示,仅表示帧的Frame Payload所占用字节数,不包括帧头所占用的9个字节。 默认大小区间为为0~16,384(2^14),一旦超过默认最大值2^14(16384),发送方将不再允许发送,除非接收到接收方定义的SETTINGS_MAX_FRAME_SIZE(一般此值区间为2^14 ~ 2^24)值的通知。

帧类型(Type): 8个比特表示,定义了帧负载的具体格式和帧的语义,HTTP/2规范定义了10个帧类型。

frame_type.png

帧的标志位(Flags): 8个比特表示,表示具体帧的类型,默认值为0x0。8个比特可以容纳8个不同的标志,比如,PADDED值为0x8,二进制表示为00001000;END_HEADERS值为0x4,二进制表示为00000100;END_STREAM值为0X1,二进制为00000001。可以同时在一个字节中传达三种标志位,二进制表示为00001101,即0x13。因此,后面的帧结构中,标志位一般会使用8个比特表示,若某位不确定,使用问号?替代,表示此处可能会被设置标志位。

帧保留比特位(R): HTTP/2语境下为保留的比特位,固定值为0X0。

流标识符(Stream Identifier): 无符号的31比特表示无符号自然数。0x0值表示为帧仅作用于连接,不隶属于单独的流。

HEADERS 帧

报头(type = 0x1)主要载体,请求头或响应头。

http2_header.png

Pad Length: 受制于PADDED标志(flags标志中第3位)控制是否设置,8个比特表示填充的字节数。

E: 一个比特表示流依赖是否专用,可选项,只在流优先级PRIORITY(flags标志中第5位)被设置时有效。

Stream Dependency: 31个比特表示流依赖,只在流优先级PRIORITY(flags标志中第5位)被设置时有效。

Weight: 8个比特(一个字节)表示无符号的自然数流优先级,值范围自然是(1~256)。只在流优先级PRIORITY(flags标志中第5位)被设置时有效。

Header Block Fragment: 报头块分片,采用HPACK压缩。

Padding: 填充的字节,受制于PADDED标志(flags标志中第3位)标志控制是否设置,长度由Pad Length字段决定。

标志位END_HEADERS(0X4): 表示报头块的最后一个帧,否则后面还会跟随CONTINUATION帧。

如图是未设置流优先级PRIORITYHEADERS帧:

http2_header_not_priority.png

如图是设置流优先级PRIORITYHEADERS帧:

http2_header_priority.png

HPACK

HTTP 2.0的头部使用的是HPACK压缩,关于HPACK可以参考RFC

对于HPACK,可以使用nghttp2库进行压缩和解压缩。把上面的HEADERS帧的导出字节流,可以使用下面程序进行解压缩。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
 #include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <assert.h>

#include <nghttp2/nghttp2.h>

#define ARRAR_SIZE(a) (sizeof(a) / sizeof(a[0]))

/*
* 帧结构体:
* type: 帧的类型
* type_name: 帧的名称
* flags: 帧的标志
* length: 帧的长度
* func: 帧的处理函数
*/

struct frame_info {
int type;
const char* type_name;
int flags;
int length;
int (*func)(uint8_t *buf, struct frame_info* f);
};

static int inflate_header_block(nghttp2_hd_inflater *inflater, uint8_t *in,
size_t in_len, int final) {
ssize_t ret;

for (;;) {
nghttp2_nv nv;
int inflate_flags = 0;
size_t proclen;

if((ret = nghttp2_hd_inflate_hd(inflater, &nv, &inflate_flags, in, in_len, final)) < 0) {
printf("inflate failed with error code %zd\n", ret);
return -1;
}


proclen = (size_t)ret;

in += proclen;
in_len -= proclen;

if(inflate_flags & NGHTTP2_HD_INFLATE_EMIT) {
fwrite(nv.name, 1, nv.namelen, stdout);
fprintf(stdout, ": ");
fwrite(nv.value, 1, nv.valuelen, stdout);
fprintf(stdout, "\n");
}

if(inflate_flags & NGHTTP2_HD_INFLATE_FINAL) {
nghttp2_hd_inflate_end_headers(inflater);
break;
}

if((inflate_flags & NGHTTP2_HD_INFLATE_EMIT) == 0 && in_len == 0) {
break;
}
}

return 0;
}

/*
* data_frame: 处理data帧
* buf: 指向帧数据开始的地方
* f: 帧信息结构体
*/

static int data_frame(uint8_t *buf, struct frame_info* f) {
assert(buf && f);

int offset = 0;
int padded_len = 0;
int data_length = 0;
int length = f->length;
int flags = f->flags;
uint8_t *data = buf;

/* PADDED */
if((flags & (1 << 3)) && (length > 0)) {
offset += 1;
padded_len = data[0];
}

if(offset >= length) {
return -1;
}

data += offset;
data_length = length - offset - padded_len;

if(data_length > 0) {
fwrite(data, 1, data_length, stdout);
}

return 0;
}

/*
* headers_frame: 处理headers帧
* buf: 指向帧数据开始的地方
* f: 帧信息结构体
*/

static int headers_frame(uint8_t *buf, struct frame_info* f) {
assert(buf && f);

int ret = 0;
int offset = 0;
int padded_len = 0;
int header_len = 0;
int length = f->length;
int flags = f->flags;
uint8_t *head_block = buf;

/* PADDED */
if((flags & (1 << 3)) && (length > 0)) {
offset += 1;
padded_len = head_block[0];
}

/* priority */
if(flags & (1 << 5)) {
offset += 5;
}

if(offset >= length) {
return -1;
}

head_block += offset;
header_len = length - offset - padded_len;

nghttp2_hd_inflater *inflater = NULL;

do {
/* 使用nghttp2 解压缩headers */
if((ret = nghttp2_hd_inflate_new(&inflater)) != 0) {
printf("nghttp2_hd_inflate_init failed with error: %s\n", nghttp2_strerror(ret));
break;
}

if((ret = inflate_header_block(inflater, head_block, header_len, 1)) != 0) {
printf("inflate_header_block error\n");
break;
}
} while(0);

if(inflater) {
nghttp2_hd_inflate_del(inflater);
}

return ret;
}

static int priority_frame(uint8_t *buf, struct frame_info* f) {
assert(buf && f);

return 0;
}

static int rst_stream_frame(uint8_t *buf, struct frame_info* f) {
assert(buf && f);

return 0;
}

static int settings_frame(uint8_t *buf, struct frame_info* f) {
assert(buf && f);

return 0;
}

static int push_promise_frame(uint8_t *buf, struct frame_info* f) {
assert(buf && f);

return 0;
}

static int ping_frame(uint8_t *buf, struct frame_info* f) {
assert(buf && f);

return 0;
}

static int goaway_frame(uint8_t *buf, struct frame_info* f) {
assert(buf && f);

return 0;
}

static int window_update_frame(uint8_t *buf, struct frame_info* f) {
assert(buf && f);

return 0;
}

static int continuation_frame(uint8_t *buf, struct frame_info* f) {
assert(buf && f);

return 0;
}

/*
* 帧结构体:
* type: 帧的类型
* type_name: 帧的名称
* flags: 帧的标志
* length: 帧的长度
* func: 帧的处理函数
*/

struct frame_info frame[] = {
{0x0, "data", 0, 0, data_frame },
{0x1, "headers", 0, 0, headers_frame },
{0x2, "priority", 0, 0, priority_frame },
{0x3, "rst_stream", 0, 0, rst_stream_frame },
{0x4, "settings", 0, 0, settings_frame },
{0x5, "push_promise", 0, 0, push_promise_frame },
{0x6, "ping", 0, 0, ping_frame },
{0x7, "goaway", 0, 0, goaway_frame },
{0x8, "window_update", 0, 0, window_update_frame},
{0x9, "continuation", 0, 0, continuation_frame }
};

/*
* get_file_size: 获得输入文件的大小
* fd: 打开文件的描述符
*/

static size_t get_file_size(int fd) {
struct stat file_stat;

if(fstat(fd, &file_stat) < 0) {
printf("stat error");
return -1;
}

return file_stat.st_size;
}

/*
* print_hex: 以十六进制,打印原始文件的内容
* buf: 文件的内容
* buf_len: 文件的长度
*/

static void print_hex(uint8_t *buf, size_t buf_len) {
assert(buf && buf_len > 0);

size_t i;

for (i = 0; i < buf_len; ++i) {
if ((i & 0x0fu) == 0) {
printf("%08zX: ", i);
}

printf("%02X ", buf[i]);

if (((i + 1) & 0x0fu) == 0) {
printf("\n");
}
}
printf("\n");
}

/*
* process_frame: 调用帧类型的处理函数
* buf: 指向帧数据开始的地方
* f: 帧信息结构体
*/

static int process_frame(uint8_t *buf, struct frame_info* f) {
assert(buf && f);

printf("\n------------- %s frame -------------\n", f->type_name);
return f->func(buf, f);
}

int main(int argc, char *argv[]) {
uint8_t *buf;
size_t buf_len;
FILE *fp;

if(argc != 2) {
printf("usage: %s <input_file>\n", argv[0]);
return -1;
}

if((fp = fopen(argv[1], "r")) == NULL) {
printf("fopen error");
return -1;
}

buf_len = get_file_size(fileno(fp));
if((buf = malloc(buf_len)) == NULL) {
printf("malloc error");
return -1;
}

do {
int data_offset = 0;
if(fread(buf, sizeof(uint8_t), buf_len, fp) != buf_len) {
printf("fread error");
break;
}

print_hex(buf, buf_len);

uint8_t *tmp_buf = buf;

while(buf_len > 0) {

/* 帧头包含9个字节 */
if(!tmp_buf || buf_len <= 9) {
printf("frame format error\n");
break;
}

int length = 0;
int type = 0;
int flags = 0;
int i = 0;

/* 帧的类型 */
type = tmp_buf[3];

/* 帧的数据长度 */
length = ((tmp_buf[0] << 16) | (tmp_buf[1] << 8) | tmp_buf[2]);

/* 帧的标志 */
flags = tmp_buf[4];

if(length + 9 > buf_len) {
printf("frame length error\n");
break;
}

tmp_buf += 9;
buf_len -= 9;

for(i = 0; i < ARRAR_SIZE(frame); ++i) {
if(frame[i].type == type) {
frame[i].length = length;
frame[i].flags = flags;
break;
}
}

if(i == ARRAR_SIZE(frame)) {
printf("frame type error\n");
break;
}

/* 处理当前帧 */
if(process_frame(tmp_buf, &frame[i]) < 0) {
break;
}

/* 下一个帧,如:服务器回应的数据包括headers帧和data帧 */
tmp_buf += length;
buf_len -= length;
}
} while(0);

free(buf);
fclose(fp);

return 0;
}

编译程序:

gcc inflate.c -lnghttp2

运行程序:

./a.out header.bin

输出:

00000000: 00 00 E3 01 25 00 00 00 0D 00 00 00 0B 1F 82 05 
00000010: 85 62 72 D1 41 D8 41 8E F1 E3 C2 F3 97 B1 AA 9A 
00000020: 83 0E A9 97 21 E9 87 7A BA D0 7F 66 A2 81 B0 DA 
00000030: E0 53 FA FC 08 7E D4 E1 1D B5 26 DF B5 33 9A AB 
00000040: 7C A9 E5 E7 22 71 AF B5 2C EF 71 B0 2E 0F ED 4C 
00000050: 45 27 53 B0 20 04 00 08 02 A6 13 58 59 4F E5 86 
00000060: C0 B8 3F 53 B0 49 7C A5 89 D3 4D 1F 43 AE BA 0C 
00000070: 41 A4 C7 A9 8F 33 A6 9A 3F DF 9A 68 FA 1D 75 D0 
00000080: 62 0D 26 3D 4C 79 A6 8F BE D0 01 77 FE BE 58 F9 
00000090: FB ED 00 17 7B 51 8B 2D 4B 70 DD F4 5A BE FB 40 
000000A0: 05 DB 50 8D 9B D9 AB FA 52 42 CB 40 D2 5F A5 23 
000000B0: B3 40 92 B6 B9 AC 1C 85 58 D5 20 A4 B6 C2 AD 61 
000000C0: 7B 5A 54 25 1F 81 0F 68 96 DF 3D BF 4A 04 4A 43 
000000D0: 5D 8A 08 01 79 40 BD 71 A6 6E 08 0A 62 D1 BF 69 
000000E0: 8B FE 5B 19 25 1B C4 79 60 8C 7F CF 00 00 04 08 
000000F0: 00 00 00 00 0D 00 BE 00 00 

------------- headers frame -------------
:method: GET
:path: /hello/
:authority: www.heqingliang.com
:scheme: https
user-agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:50.0) Gecko/20100101 Firefox/50.0
accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
accept-language: en-US,en;q=0.5
accept-encoding: gzip, deflate, br
upgrade-insecure-requests: 1
if-modified-since: Thu, 12 Apr 2018 18:43:20 GMT
if-none-match: "5acfa8c8-1aa"

------------- window_update frame -------------

DATA帧

一个或多个DATA帧(type = 0x0)作为请求、响应内容载体:

http2_data.png

Pad Length: 一个字节表示填充的字节长度,取决于PADDED(flags标志中第3位)标志是否被设置。

Data: 应用数据,真正大小需要减去其他字段(比如填充长度和填充内容)长度。

Padding: 填充内容为若干个0x0字节,受PADDED(flags标志中第3位)标志控制是否设置。接收端处理时可忽略验证填充内容。

标志位END_STREAM (0x1): 标志此帧为对应标志流最后一个帧,流进入了半关闭/关闭状态。

如图是服务器返回的数据:

http2_data_frame.png

把上面的服务器返回的数据导出字节流,运行上面的程序,输出以下结果:

00000000: 00 00 6B 01 04 00 00 00 11 88 76 89 AA 63 55 E5 
00000010: 80 AE 11 2E 2F 61 96 E4 59 3E 94 0B CA 43 5D 8A 
00000020: 08 01 79 40 0A E3 40 B8 16 94 C5 A3 7F 5F 87 49 
00000030: 7C A5 89 D3 4D 1F 5C 03 31 31 39 6C 96 DD 6D 5F 
00000040: 4A 01 E5 21 AE C5 04 00 BC A0 03 70 0F 5C 13 CA 
00000050: 62 D1 BF 00 83 2A 47 37 8B FE 5B 19 1F 72 37 88 
00000060: B3 AE FF 3F 00 89 19 08 5A D2 B5 83 AA 62 A3 84 
00000070: 8F D2 4A 8F 00 00 77 00 01 00 00 00 11 3C 68 74 
00000080: 6D 6C 3E 0D 0A 3C 68 65 61 64 3E 0D 0A 20 20 20 
00000090: 20 3C 74 69 74 6C 65 3E 48 6F 6D 65 3C 2F 74 69 
000000A0: 74 6C 65 3E 0D 0A 3C 2F 68 65 61 64 3E 0D 0A 3C 
000000B0: 62 6F 64 79 3E 0D 0A 20 20 20 20 3C 68 31 20 73 
000000C0: 74 79 6C 65 3D 22 66 6F 6E 74 2D 73 74 79 6C 65 
000000D0: 3A 69 74 61 6C 69 63 22 3E 48 6F 6D 65 3C 2F 68 
000000E0: 31 3E 0D 0A 3C 2F 62 6F 64 79 3E 0D 0A 3C 2F 68 
000000F0: 74 6D 6C 3E 

------------- headers frame -------------
:status: 200
server: nginx/1.12.2
date: Wed, 18 Apr 2018 02:40:14 GMT
content-type: text/html
content-length: 119
last-modified: Sun, 08 Apr 2018 01:08:28 GMT
etag: "5ac96b8c-77"
accept-ranges: bytes

------------- data frame -------------
<html>
<head>
    <title>Home</title>
</head>
<body>
    <h1 style="font-style:italic">Home</h1>
</body>
</html>

其他数据帧的格式可以参考RFC