leevis.com icon indicating copy to clipboard operation
leevis.com copied to clipboard

nginx location 的匹配顺序

Open vislee opened this issue 8 years ago • 0 comments

nginx location 的匹配顺序

nginx的官方文档location支持以下几种形式的配置,

location [ = | ~ | ~* | ^~ ] uri { ... }
location @name { ... }

注:在实际配置中符号和后面的uri中间有没有空格效果是一样的。

我们一般也就用三种配置,= 精确匹配,[^~] 前缀匹配,~或~* 正则匹配。 这个顺序也是他们匹配的顺序,前缀匹配会最大限度的匹配,例如 /test/ 和 /test/hello 如果你的url是/test/helloworld 会匹配到location /test/hello。前缀匹配完,如果没有配置^~,则还会匹配正则匹配的,如果有正则匹配则会覆盖前缀匹配的。如果前缀匹配的配置了^~,则不再匹配正则匹配的。

这个匹配规则和你在配置文件中配置的location的先后顺序没有关系,不会因为location /test/ {}location /test/hello {}就让url /test/helloworld 先匹配第一个location。

利用我github ngx_http_hello_world_module 模块测试一下。从github上clone下来以后,编译的时候在configure添加--add-module=./vislee/ngx_http_hello_world_module后编译。

测试配置文件:

        location ^~/test0/ {
            hello_world;
            hello_by "by ^~/test0/";
            
            location ^~/test0/hello/ {
                hello_world;
                hello_by "by nested ^~/test0/hello";
            }
            
            location ^~/test0/world/ {
                hello_world;
                hello_by "by nested ^~/test0/world/";
            }
            
        }

        location ^~/test0/hello/ {
            hello_world;
            hello_by "by ^~/test0/hello";
        }

        location =/test0/ {
            hello_world;
            hello_by "by =/test0/";
        }
        
        location ^~/test {
            hello_world;
            hello_by "by ^~/test";
        }
        
        location ^~/test/ {
            hello_world;
            hello_by "by ^~/test/";
        }

        location ^~/testa {
            hello_world;
            hello_by "by ^~/testa";
        }
      
        

测试结果:

curl -v 'http://127.0.0.1:8080/test0/
hello world by =/test0/%

curl -v 'http://127.0.0.1:8080/test0/test'
hello world by ^~/test0/%

curl -v 'http://127.0.0.1:8080/test0/hello/'
hello world by ^~/test0/hello%

curl -v 'http://127.0.0.1:8080/test0/world/'
hello world by nested ^~/test0/world/%

总结: 先匹配精确匹配的location,也就是配置location = xxx {}的。 其次匹配正则表达式的location,也就是配置location ~ xxx {}的。linux平台下~和~*是有点区别的,带星号的是忽略大小写的,匹配的前后顺序按照配置的先后顺序匹配。 最后匹配前缀匹配的location,也就是配置location [^~] xxx {}的。先匹配前缀较长的,相同长度的先匹配字典顺序较大的。 注:代码实现上,前缀匹配和精确匹配先匹配,只不过精确匹配后直接返回,而前缀匹配后还进行正则匹配,匹配完以后如果有合适的location就覆盖前缀匹配的。例如下面的配置,"/hello/li"会返回333,"/hello/world/" 会返回111。如果下面的配置改为location ^~ /hello/, 正则表达式的location就不起作用了。

        location ~ /hello/\w+ {
            return 200 "333";
        }

        location /hello/ {
            location = /hello/world/ {
                return 200 "111";
            }

            location /hello/li {
                return 200 "222";
            }
        }

源码分析

  • location解析

location的解析是通过函数ngx_http_core_location处理的。首先location也会对应一个ngx_http_conf_ctx_t,并分配location级别的模块的配置结构体。在此这些都不是重点。

配置解析了2个或3个参数,第一个参数是location,第二个参数是符号(= | ^~| ~ )第三个参数是uri。

第二个参数 如果是“=” 则为精确匹配clcf->exact_match = 1。 如果是“^~”,则为非正则匹配(前缀匹配)clcf->noregex = 1,非正则匹配是前缀匹配的一种。 如果是“~”,则为正则匹配,加*的会忽略大小写,仅linux平台。clcf->regex = ngx_http_regex_compile(cf, &rc);

clcf->name = 第三个参数。

location是可以嵌套的,精确和正则匹配的location内不能再嵌套location。前缀匹配的location内嵌套的location的uri(第三个参数)前缀必须是相同的,就是说外层location的uri是uri1,则内层的必需是uri1xxxx。必需以外层location的uri开始。

最后调用ngx_http_add_location函数把clcf添加到父级别(server级别、location级别)clcf的locations双链表中。

http_block 最后会调用ngx_http_init_locations处理每个server下的location。

  1. 首先会对server下的所有location进行排序,排序结果:字母顺序(精确匹配、前缀匹配的)升序(有相同前缀的字符串,长的排在后面)|正则匹配|@匹配的|if location的

  2. 遍历排序后的location。把@符号的location设置到cscf->named_locations。正则的设置到pclcf->regex_locations上。最终locations只剩下了精确匹配的和前缀匹配的了。

  3. 接着调用ngx_http_init_static_location_trees函数把pclcf->locations链表上的location初始化为一棵静态二叉查找树,树根节点付给pclcf->static_locations

  4. ngx_http_init_static_location_trees函数中先调用ngx_http_join_exact_locations函数,把同名的两个location列表上的元素合并在一个元素上。上面对location列表进行了排序,同名的精确匹配的在前缀匹配的前面。就是把这样的两个location合并到一个上。exact指针指向精确匹配的,inclusive指向前缀匹配的。这样减少了元素的个数,降低了二叉树的深度,加快了查找速度。

  5. 调用ngx_http_create_locations_list函数合并有相同前缀的前缀匹配的location链表元素。上述排序把有相同前缀的location,按照长度由小到大排列。例如:/abc /abcd /abcde 这样。该函数的目的就是把有相同前缀的location元素合并到一个元素上。例如,/abcd 和/abcde就会挂到 /abc这个元素的list指针上。这样做的目的也是降低二叉树深度。

  • location查找

调用ngx_http_core_find_location函数。调用ngx_http_core_find_static_location函数从所属server下的location静态二叉查找树,根据uri查找location。

附录

  • 增加debug信息
static void
ngx_http_create_locations_list(ngx_queue_t *locations, ngx_queue_t *q)
{
    u_char                     *name;
    size_t                      len;
    ngx_queue_t                *x, tail;
    ngx_http_location_queue_t  *lq, *lx;

    // 双链表只有一个元素
    if (q == ngx_queue_last(locations)) {
        return;
    }

    lq = (ngx_http_location_queue_t *) q;

    // 如果是完全匹配
    if (lq->inclusive == NULL) {
        ngx_http_create_locations_list(locations, ngx_queue_next(q));
        return;
    }

    len = lq->name->len;
    name = lq->name->data;

    fprintf(stderr, "==1==%.*s\n", (int)len, name);

    // 有相同前缀
    for (x = ngx_queue_next(q);
         x != ngx_queue_sentinel(locations);
         x = ngx_queue_next(x))
    {
        lx = (ngx_http_location_queue_t *) x;
        fprintf(stderr, "==2==%.*s\n", (int)lx->name->len, lx->name->data);

        if (len > lx->name->len
            || ngx_filename_cmp(name, lx->name->data, len) != 0)
        {
            break;
        }
    }

    // 让出相同前缀的第一个元素,也就是q指向第二个
    q = ngx_queue_next(q);

    if (q == x) {    // 没有相同前缀的元素
        ngx_http_create_locations_list(locations, x);
        return;
    }

    fprintf(stderr, "==3==%.*s\n", (int)(((ngx_http_location_queue_t*)q)->name->len), ((ngx_http_location_queue_t*)q)->name->data);
    ngx_queue_split(locations, q, &tail);
    ngx_queue_add(&lq->list, &tail);

    if (x == ngx_queue_sentinel(locations)) {
        ngx_http_create_locations_list(&lq->list, ngx_queue_head(&lq->list));
        return;
    }

    fprintf(stderr, "==4==%.*s\n", (int)(((ngx_http_location_queue_t*)x)->name->len), ((ngx_http_location_queue_t*)x)->name->data);
    ngx_queue_split(&lq->list, x, &tail);
    ngx_queue_add(locations, &tail);

    ngx_http_create_locations_list(&lq->list, ngx_queue_head(&lq->list));

    ngx_http_create_locations_list(locations, x);
}




static ngx_http_location_tree_node_t *
ngx_http_create_locations_tree(ngx_conf_t *cf, ngx_queue_t *locations,
    size_t prefix)
{
    size_t                          len;
    ngx_queue_t                    *q, tail;
    ngx_http_location_queue_t      *lq;
    ngx_http_location_tree_node_t  *node;

    // 快慢指针获取链表中间偏右的节点。奇数个节点中间节点唯一,偶数个节点中间2个节点取第二个。
    // 符合构造树的习惯
    q = ngx_queue_middle(locations);

    lq = (ngx_http_location_queue_t *) q;
    len = lq->name->len - prefix;

    node = ngx_palloc(cf->pool,
                      offsetof(ngx_http_location_tree_node_t, name) + len);
    if (node == NULL) {
        return NULL;
    }

    node->left = NULL;
    node->right = NULL;
    node->tree = NULL;
    node->exact = lq->exact;
    node->inclusive = lq->inclusive;

    node->auto_redirect = (u_char) ((lq->exact && lq->exact->auto_redirect)
                           || (lq->inclusive && lq->inclusive->auto_redirect));

    node->len = (u_char) len;
    ngx_memcpy(node->name, &lq->name->data[prefix], len);

    fprintf(stderr, "===node====prefix:%.*s name:%.*s\n", (int)prefix, lq->name->data, (int)node->len, node->name);

    ngx_queue_split(locations, q, &tail);

    if (ngx_queue_empty(locations)) {
        /*
         * ngx_queue_split() insures that if left part is empty,
         * then right one is empty too
         */
        goto inclusive;
    }

    node->left = ngx_http_create_locations_tree(cf, locations, prefix);
    if (node->left == NULL) {
        return NULL;
    }

    ngx_queue_remove(q);

    if (ngx_queue_empty(&tail)) {
        goto inclusive;
    }

    node->right = ngx_http_create_locations_tree(cf, &tail, prefix);
    if (node->right == NULL) {
        return NULL;
    }

inclusive:

    if (ngx_queue_empty(&lq->list)) {
        return node;
    }

    node->tree = ngx_http_create_locations_tree(cf, &lq->list, prefix + len);
    if (node->tree == NULL) {
        return NULL;
    }

    return node;
}

    server {
        listen       8080;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location /ab {
            return 200 "/ab";
        }

        location /abc {
            return 200 "/abc";
        }

        location /abcd {
            return 200 "/abcd";
        }

        location = /b {
            return 200 "= /b";
        }

        location /b {
            return 200 "/b";
        }

        location /bcd {
            return 200 "/abcd";
        }

        location /bce {
            return 200 "/abce";
        }
        location /bcf {
            return 200 "/bcf";
        }
        location /bcg {
            return 200 "/bcg";
        }
    }
➜  nginx ./sbin/nginx -c ./conf/nginx.conf -t
==1==/ab
==2==/abc
==2==/abcd
==2==/b
==3==/abc
==4==/b
==1==/abc
==2==/abcd
==3==/abcd
==1==/b
==2==/bcd
==2==/bce
==2==/bcf
==2==/bcg
==3==/bcd
==1==/bcd
==2==/bce
==1==/bce
==2==/bcf
==1==/bcf
==2==/bcg
===node====prefix: name:/b
===node====prefix: name:/ab
===node====prefix:/ab name:c
===node====prefix:/abc name:d
===node====prefix:/b name:cf
===node====prefix:/b name:ce
===node====prefix:/b name:cd
===node====prefix:/b name:cg


# 调用ngx_http_create_locations_list后的数据结构
/ab ------------>=/b(/b)
|->/abc             |->/bcd -> /bce -> /bcf -> /bcg
    |->/abcd


// 调用ngx_http_create_locations_tree生成树

image

ngx_http_core_find_location


/*
 * NGX_OK       - exact or regex match
 * NGX_DONE     - auto redirect
 * NGX_AGAIN    - inclusive match
 * NGX_ERROR    - regex error
 * NGX_DECLINED - no match
 */

static ngx_int_t
ngx_http_core_find_location(ngx_http_request_t *r)
{
    ngx_int_t                  rc;
    ngx_http_core_loc_conf_t  *pclcf;
#if (NGX_PCRE)
    ngx_int_t                  n;
    ngx_uint_t                 noregex;
    ngx_http_core_loc_conf_t  *clcf, **clcfp;

    noregex = 0;
#endif

    // 默认server块的默认location
    pclcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);

    // 查找对应的location
    rc = ngx_http_core_find_static_location(r, pclcf->static_locations);

    // 如果是前缀匹配
    if (rc == NGX_AGAIN) {

#if (NGX_PCRE)
        clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
        // 匹配的location是否要继续查找正则匹配的location
        noregex = clcf->noregex;
#endif

        /* look up nested locations */
        // 继续查找嵌套的location
        rc = ngx_http_core_find_location(r);
    }

    // 只有匹配的是前缀匹配的location,才可以继续正则匹配。
    if (rc == NGX_OK || rc == NGX_DONE) {
        return rc;
    }

    /* rc == NGX_DECLINED or rc == NGX_AGAIN in nested location */

#if (NGX_PCRE)

    if (noregex == 0 && pclcf->regex_locations) {

        for (clcfp = pclcf->regex_locations; *clcfp; clcfp++) {

            ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                           "test location: ~ \"%V\"", &(*clcfp)->name);

            // 执行匹配正则表达式
            n = ngx_http_regex_exec(r, (*clcfp)->regex, &r->uri);

            if (n == NGX_OK) {
                r->loc_conf = (*clcfp)->loc_conf;

                /* look up nested locations */
                // 匹配嵌套的location
                rc = ngx_http_core_find_location(r);

                return (rc == NGX_ERROR) ? rc : NGX_OK;
            }

            if (n == NGX_DECLINED) {
                continue;
            }

            return NGX_ERROR;
        }
    }
#endif

    return rc;
}


ngx_http_core_find_static_location


/*
 * NGX_OK       - exact match
 * NGX_DONE     - auto redirect
 * NGX_AGAIN    - inclusive match
 * NGX_DECLINED - no match
 */

static ngx_int_t
ngx_http_core_find_static_location(ngx_http_request_t *r,
    ngx_http_location_tree_node_t *node)
{
    u_char     *uri;
    size_t      len, n;
    ngx_int_t   rc, rv;

    len = r->uri.len;
    uri = r->uri.data;

    rv = NGX_DECLINED;

    for ( ;; ) {

         // 遍历子节点结束 或者 树为空
        if (node == NULL) {
            return rv;
        }

        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                       "test location: \"%*s\"",
                       (size_t) node->len, node->name);

        n = (len <= (size_t) node->len) ? len : node->len;

        rc = ngx_filename_cmp(uri, node->name, n);

        if (rc != 0) {
            node = (rc < 0) ? node->left : node->right;    // 遍历二叉树

            continue;
        }

        // 匹配到二叉树节点

        if (len > (size_t) node->len) {

            if (node->inclusive) {  // 是否配置了前缀匹配的loaction

                // 赋值,已经找到了前缀匹配的一个location,接下来的匹配
                r->loc_conf = node->inclusive->loc_conf;
                rv = NGX_AGAIN;

                // 有多个相同前缀的location,只能有一个会保存在inclusive,其余的都以二叉树的形似保存在tree下,同时为了提升匹配效率,已经比较过的字符串就不再比较了。
                node = node->tree;    // 匹配更多的前缀匹配树
                uri += n;
                len -= n;

                continue;
            }

            /* exact only */

            node = node->right;

            continue;
        }

        if (len == (size_t) node->len) {
            // 有精确匹配的,则直接返回精确匹配的location。
            if (node->exact) {
                r->loc_conf = node->exact->loc_conf;
                return NGX_OK;

            } else {
                r->loc_conf = node->inclusive->loc_conf;
                return NGX_AGAIN;
            }
        }

        /* len < node->len */

        // proxy_pass 的 location 配置的是 /a/ 而请求的path是 /a 注意最后的/,就会命中该if分支
        // 最后给客户端返沪的是301 schema://host:port/a/
        if (len + 1 == (size_t) node->len && node->auto_redirect) {

            r->loc_conf = (node->exact) ? node->exact->loc_conf:
                                          node->inclusive->loc_conf;
            rv = NGX_DONE;
        }

        node = node->left;
    }
}

ngx_http_core_find_config_phase


ngx_int_t
ngx_http_core_find_config_phase(ngx_http_request_t *r,
    ngx_http_phase_handler_t *ph)
{
    u_char                    *p;
    size_t                     len;
    ngx_int_t                  rc;
    ngx_http_core_loc_conf_t  *clcf;

    r->content_handler = NULL;
    r->uri_changed = 0;

    rc = ngx_http_core_find_location(r);

    if (rc == NGX_ERROR) {
        ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
        return NGX_OK;
    }

    clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
    // location 配置了internal。
    // 这个地方也是个坑
    if (!r->internal && clcf->internal) {
        ngx_http_finalize_request(r, NGX_HTTP_NOT_FOUND);
        return NGX_OK;
    }

    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                   "using configuration \"%s%V\"",
                   (clcf->noname ? "*" : (clcf->exact_match ? "=" : "")),
                   &clcf->name);

    // 主要更新一些locaiton的配置,复制到r结构体。
    ngx_http_update_location_config(r);

    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                   "http cl:%O max:%O",
                   r->headers_in.content_length_n, clcf->client_max_body_size);

    if (r->headers_in.content_length_n != -1
        && !r->discard_body
        && clcf->client_max_body_size
        && clcf->client_max_body_size < r->headers_in.content_length_n)
    {
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                      "client intended to send too large body: %O bytes",
                      r->headers_in.content_length_n);

        r->expect_tested = 1;
        (void) ngx_http_discard_request_body(r);
        ngx_http_finalize_request(r, NGX_HTTP_REQUEST_ENTITY_TOO_LARGE);
        return NGX_OK;
    }

    // 301 跳转,设置Location响应头,如果有参数会复制参数。
    if (rc == NGX_DONE) {
        ngx_http_clear_location(r);

        r->headers_out.location = ngx_list_push(&r->headers_out.headers);
        if (r->headers_out.location == NULL) {
            ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
            return NGX_OK;
        }

        r->headers_out.location->hash = 1;
        ngx_str_set(&r->headers_out.location->key, "Location");

        if (r->args.len == 0) {
            r->headers_out.location->value = clcf->name;

        } else {
            len = clcf->name.len + 1 + r->args.len;
            p = ngx_pnalloc(r->pool, len);

            if (p == NULL) {
                ngx_http_clear_location(r);
                ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
                return NGX_OK;
            }

            r->headers_out.location->value.len = len;
            r->headers_out.location->value.data = p;

            p = ngx_cpymem(p, clcf->name.data, clcf->name.len);
            *p++ = '?';
            ngx_memcpy(p, r->args.data, r->args.len);
        }

        ngx_http_finalize_request(r, NGX_HTTP_MOVED_PERMANENTLY);
        return NGX_OK;
    }

    r->phase_handler++;
    return NGX_AGAIN;
}

vislee avatar Feb 20 '17 06:02 vislee