View on GitHub

从 Nginx 分析 ServerPush

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;
}