Server Push是 HTTP2 的一个特性,其定义在 RFC7540。下面通过分析 Nginx 代码了解它的工作流程。
Nginx 处理 Server Push 的请求
使用前需要先打开 server_push
的配置,参考文档 Module ngx_http_v2_module。
支持的类型大概分成 Nginx 中配送的资源,又或者反代结果 header 中 preload 的资源。
Nginx 主动 push 资源
通过在配置文件中配置uri实现把本地的文件主动推送给客户端。
http2_push /static/css/main.css;
Nginx 的代码详见这里:
if (h2lcf->pushes) {
pushes = h2lcf->pushes->elts;
for (i = 0; i < h2lcf->pushes->nelts; i++) {
if (ngx_http_complex_value(r, &pushes[i], &path) != NGX_OK) {
return NGX_ERROR;
}
...
rc = ngx_http_v2_push_resource(r, &path, binary);
...
}
}
从反代回包 header 中读取 preload
解析 preload 配置需要开启 http2_push_preload。开启后 Nginx 会解析回包 header 中所有 link,解析出 url 并且执行 push流程。 详见代码。
h = r->headers_out.link.elts;
for (i = 0; i < r->headers_out.link.nelts; i++) {
next_link:
... // 解析Link中"<"和">"之间的字符串作为path
path.len = last - start;
path.data = start;
...
while (start < end && *start == ' ') { start++; }
...
// 如果遇到逗号,认为是多个资源,重新解析新的path
if (*start == ',') {
start++;
goto next_link;
}
// 如果遇到分号,就认为解析结束
if (*start++ != ';') {
continue;
}
// 本条url只截取到逗号的位置。
last = ngx_strlchr(start, end, ',');
if (last == NULL) {
last = end;
}
push = 0;
for ( ;; ) {
while (start < last && *start == ' ') { start++; }
// 如果有nopush,则不进行push
if (last - start >= 6 && ngx_strncasecmp(start, (u_char *) "nopush", 6) == 0) {
start += 6;
if (start == last || *start == ' ' || *start == ';') {
push = 0;
break;
}
goto next_param;
}
// 如果有rel=preload,才进行push
if (last - start >= 11 && ngx_strncasecmp(start, (u_char *) "rel=preload", 11) == 0) {
start += 11;
if (start == last || *start == ' ' || *start == ';') {
push = 1;
}
goto next_param;
}
// 兼容rel="preload", rel= "preload"等等这种情况。
if (last - start >= 4 && ngx_strncasecmp(start, (u_char *) "rel=", 4) == 0) {
start += 4;
while (start < last && *start == ' ') { start++; }
if (start == last || *start++ != '"') {
goto next_param;
}
for ( ;; ) {
while (start < last && *start == ' ') { start++; }
if (last - start >= 7 && ngx_strncasecmp(start, (u_char *) "preload", 7) == 0) {
start += 7;
if (start < last && (*start == ' ' || *start == '"')) {
push = 1;
break;
}
}
...
start++;
}
}
next_param:
start = ngx_strlchr(start, last, ';');
if (start == NULL) {
break;
}
start++;
}
...
// 需要push资源,且push的路径不是//开头
if (push && path.len && !(path.len > 1 && path.data[0] == '/' && path.data[1] == '/')) {
rc = ngx_http_v2_push_resource(r, &path, binary);
...
}
if (last < end) {
start = last + 1;
goto next_link;
}
}
推送资源
无论是配置中写好的,还是 header 中 link 标签的都会调用 ngx_http_v2_push_resource
方法,它会重新构造请求请求反向代理获取资源,
代码参考。
static ngx_int_t ngx_http_v2_push_resource(ngx_http_request_t *r, ngx_str_t *path, ngx_str_t *binary)
{
...
// 不是/开头,即不是绝对路径的url不进行处理。
if (!ngx_path_separator(path->data[0])) {
ngx_log_error(NGX_LOG_WARN, fc->log, 0,
"non-absolute path \"%V\" not pushed", path);
return NGX_DECLINED;
}
// 同时push的资源有限制数量,配置是http2_max_concurrent_pushes
if (h2c->pushing >= h2c->concurrent_pushes) {
return NGX_ABORT;
}
...
// 请求时候的header只传:method, :path, :schema, :authority, host, accept-encoding
// accept-language, user-agent。
ph = ngx_http_v2_push_headers;
for (i = 0; i < NGX_HTTP_V2_PUSH_HEADERS; i++) {
h = (ngx_table_elt_t **) ((char *) &r->headers_in + ph[i].offset);
if (*h == NULL) {
continue;
}
ngx_log_debug2(NGX_LOG_DEBUG_HTTP, fc->log, 0,
"http2 push header: \"%V: %V\"",
&ph[i].name, &(*h)->value);
pos = ngx_cpymem(pos, binary[i].data, binary[i].len);
}
frame = ngx_http_v2_create_push_frame(r, start, pos);
if (frame == NULL) {
return NGX_ERROR;
}
ngx_http_v2_queue_blocked_frame(h2c, frame);
stream->queued++;
// 真正发起请求
stream = ngx_http_v2_push_stream(stream, path);
if (stream) {
stream->request->request_length = pos - start;
return NGX_OK;
}
return NGX_ERROR;
}