Nginx 之 Location 的整理

Posted 季末的天堂

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Nginx 之 Location 的整理相关的知识,希望对你有一定的参考价值。

1. Location 的整理

在将配置解析完后,所有的 location 此时都以 tree 的形式组织起来,具体可参考 Nginx之 Location 的生成

此时需要对所有 server 下的 location 进行整理(如把同类型的 location 放置在一起以便于查找等)。

static char *
ngx_http_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ...
    
    /* create location trees */

    for (s = 0; s < cmcf->servers.nelts; s++) {

        /* cscfp 指向当前 http{} 下所属于的 ngx_http_conf_ctx_t 结构体的 main_conf 
         * 数组中 ngx_http_core_module.ctx_index 索引处的上下文结构体 
         * ngx_http_core_main_conf_t 结构体中的 servers 成员数组,该数组保存着当前 http{}
         * 下所有的 server{} 的配置上下文结构体 ngx_http_core_srv_conf_t,每一个该结构体
         * 代表一个 server{} */
        clcf = cscfp[s]->ctx->loc_conf[ngx_http_core_module.ctx_index];

        /* 这里会对所有的 location 进行排序,然后:
         * 1. 先将未命名 location 从 locations 队列中拆分出来;
         * 2. 接着将命名 location 从 locations 队列中拆分出来(拆分之前会将所有的
         *    命名 location 逐一保存到 cscf->named_location 数组中)
         * 3. 最后将 regex location 拆分出来(拆分之前会将所有的 regex location 的配置数据
         *    以数组的形式逐一保存到 clcf->regex_locations 数组中)
         * 经过拆分之后,此时 locations 队列中只剩下被称之为 static 的 location (包括绝对匹配 
         * location 和普通的 location)*/
        if (ngx_http_init_locations(cf, cscfp[s], clcf) != NGX_OK) {
            return NGX_CONF_ERROR;
        }

        if (ngx_http_init_static_location_trees(cf, clcf) != NGX_OK) {
            return NGX_CONF_ERROR;
        }
    }
    
    ...
}

1.1 ngx_http_init_locations

/*
 * @cscf: 代表一个 server{}
 * @pclcf: 当前 server{}(即 cscf)下的一个 location
 */
static ngx_int_t
ngx_http_init_locations(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf,
    ngx_http_core_loc_conf_t *pclcf)
{
    ngx_uint_t                   n;
    ngx_queue_t                 *q, *locations, *named, tail;
    ngx_http_core_loc_conf_t    *clcf;
    ngx_http_location_queue_t   *lq;
    ngx_http_core_loc_conf_t   **clcfp;
#if (NGX_PCRE)
    ngx_uint_t                   r;
    ngx_queue_t                 *regex;
#endif

    /* 获取当前 server{} 下管理的所有 location 的 locations 队列 */
    locations = pclcf->locations;

    /* 若当前 server{} 没有配置 location,则返回 NGX_OK */
    if (locations == NULL) {
        return NGX_OK;
    }

    /* 对该 server 下的 location 进行排序,大致排序如下:
     * 绝对匹配 location,正则 location,命名location,未命名location */
    ngx_queue_sort(locations, ngx_http_cmp_locations);

    named = NULL;
    n = 0;
#if (NGX_PCRE)
    regex = NULL;
    r = 0;
#endif

    for (q = ngx_queue_head(locations);
         q != ngx_queue_sentinel(locations);
         q = ngx_queue_next(q))
    {
        lq = (ngx_http_location_queue_t *) q;

        clcf = lq->exact ? lq->exact : lq->inclusive;

        /* 若该 location 下有嵌套的 location,则同样对其进行排序,否则直接返回 */
        if (ngx_http_init_locations(cf, NULL, clcf) != NGX_OK) {
            return NGX_ERROR;
        }

#if (NGX_PCRE)

        /* 若为正则location,则 r 加 1,同时记录第一个正则 location */
        if (clcf->regex) {
            r++;

            if (regex == NULL) {
                regex = q;
            }

            continue;
        }

#endif

        /* 若为命名 location,则 n 加 1,同时记录第一个命名 location */
        if (clcf->named) {
            n++;

            if (named == NULL) {
                named = q;
            }

            continue;
        }

        /* 若当前 location 为未命名 location,则退出循环 */
        if (clcf->noname) {
            break;
        }
    }

    /* 上面遍历完排序后的 locations 时,假设此时 q 指向一个未命名 location,则
     * 这里会将 locatons 队列中的所有未命名 location 从 locations 队列中拆分
     * 出来,将其保存到 q 和 tail 构成的双向循环链表中;
     * 而此时 locations 剩下的是除未命名 locations 以外的所有 location */
    if (q != ngx_queue_sentinel(locations)) {
        ngx_queue_split(locations, q, &tail);
    }
    
    /* 从这里之后 locations 队列就不存在有未命名 location 了,配置解析时之所以将
     * 未命名 location 也加入到 locations 队列中,是因为在进行配置合并的时候,可以
     * 让它们也可以继承接受来自上层 location 的相关设置值,因此在配置合并完之后,
     * 这里也就把未命名 location 中 locations 队列中移除了
     * 注意,这里虽然没有对拆分后的未命名 location 进行保存,看似这些数据丢失了,
     * 但是,在其他地方的其他字段,如 limit_except_loc_conf 或脚本引擎里已经保存
     * 了对这些 location 数据的引用 */

    if (named) {
        /* 为命名 location 分配内存 */
        clcfp = ngx_palloc(cf->pool,
                           (n + 1) * sizeof(ngx_http_core_loc_conf_t *));
        if (clcfp == NULL) {
            return NGX_ERROR;
        }

        cscf->named_locations = clcfp;
        
        /* named location 有两个限定:
         * 1. named location 只会出现在 server 上下文里
         * 2. named location 配置数据(即 clcf)固定保存在 exact 字段下 */

        /* 将 locations 队列中的所有命名 location 都存放到上面
         * named_locations 数组指针指向的新分配的内存中 */
        for (q = named;
             q != ngx_queue_sentinel(locations);
             q = ngx_queue_next(q))
        {
            lq = (ngx_http_location_queue_t *) q;

            /* 将命名 location 的配置项结构体保存到 named_locations 数组中 */
            *(clcfp++) = lq->exact;
        }

        *clcfp = NULL;

        /* 将命名 location 从 locations 队列中拆分出来 */
        ngx_queue_split(locations, named, &tail);
    }

#if (NGX_PCRE)

    if (regex) {

        /* 这里为所有的正则匹配 location 分配存储空间 */
        clcfp = ngx_palloc(cf->pool,
                           (r + 1) * sizeof(ngx_http_core_loc_conf_t *));
        if (clcfp == NULL) {
            return NGX_ERROR;
        }

        /* 下面的循环中将 locations 队列中所有的正则匹配 location 
         * 存放到 regex_locations 指针数组中 */
        pclcf->regex_locations = clcfp;

        for (q = regex;
             q != ngx_queue_sentinel(locations);
             q = ngx_queue_next(q))
        {
            lq = (ngx_http_location_queue_t *) q;

            *(clcfp++) = lq->exact;
        }

        *clcfp = NULL;

        /* 然后将正则匹配 location 中 locations 队列中移除,
         * 此时 locations 队列只剩下被称为静态(static)的 location
         * 配置(包括绝对匹配 location 和不属于正则、命名、未命名外的
         * location 配置)*/
        ngx_queue_split(locations, regex, &tail);
    }

#endif

    return NGX_OK;
}

1.1.1 ngx_queue_sort

/* the stable insertion sort */

void
ngx_queue_sort(ngx_queue_t *queue,
    ngx_int_t (*cmp)(const ngx_queue_t *, const ngx_queue_t *))
{
    ngx_queue_t  *q, *prev, *next;

    /* 获取该队列中的第一个节点 */
    q = ngx_queue_head(queue);

    /* 若该队列为空,则直接返回 */
    if (q == ngx_queue_last(queue)) {
        return;
    }

    /* 遍历该队列中的所有节点 */
    for (q = ngx_queue_next(q); q != ngx_queue_sentinel(queue); q = next) {

        prev = ngx_queue_prev(q);
        next = ngx_queue_next(q);

        /* 将当前节点从队列中移除 */
        ngx_queue_remove(q);

        do {
            /* 将当前节点与它的上一个节点进行比较
             * 返回 0,则表示插入到 prev 之后 */
            if (cmp(prev, q) <= 0) {
                break;
            }
            
            /* 返回 1,则继续取 prev 的上一个节点,再次进行比较 */
            prev = ngx_queue_prev(prev);

        } while (prev != ngx_queue_sentinel(queue));

        /* 将该 q 插入到 prev 的后面 */
        ngx_queue_insert_after(prev, q);
    }
}

对于 locations 队列中的所有节点(即location),进行比较的函数为 ngx_http_cmp_locations:

static ngx_int_t
ngx_http_cmp_locations(const ngx_queue_t *one, const ngx_queue_t *two)
{
    ngx_int_t                   rc;
    ngx_http_core_loc_conf_t   *first, *second;
    ngx_http_location_queue_t  *lq1, *lq2;

    lq1 = (ngx_http_location_queue_t *) one;
    lq2 = (ngx_http_location_queue_t *) two;

    /* 获取该location挂载在 exact 或 inclusive 字段下的  
     * ngx_http_core_loc_conf_t 配置结构体 */
    first = lq1->exact ? lq1->exact : lq1->inclusive;
    second = lq2->exact ? lq2->exact : lq2->inclusive;

    /* 若前一个 location 为未命名 location,且当前 location 不为
     * 未命名 location 时,则将一个 location(即 first)后移 */
    if (first->noname && !second->noname) {
        /* shift no named locations to the end */
        return 1;
    }

    /* 未命名 location 排到后面 */
    if (!first->noname && second->noname) {
        /* shift no named locations to the end */
        return -1;
    }

    /* 都为未命名 location 则,排序不变 */
    if (first->noname || second->noname) {
        /* do not sort no named locations */
        return 0;
    }

    /* 命名 location 后移 */
    if (first->named && !second->named) {
        /* shift named locations to the end */
        return 1;
    }

    if (!first->named && second->named) {
        /* shift named locations to the end */
        return -1;
    }

    /* 若两个 location 都为命名 location,则按它们名称的字符序进行排序,
     * 名称字符序大的排到后面 */
    if (first->named && second->named) {
        return ngx_strcmp(first->name.data, second->name.data);
    }

#if (NGX_PCRE)

    /* 正则 location 排到后面 */
    if (first->regex && !second->regex) {
        /* shift the regex matches to the end */
        return 1;
    }

    if (!first->regex && second->regex) {
        /* shift the regex matches to the end */
        return -1;
    }

    if (first->regex || second->regex) {
        /* do not sort the regex matches */
        return 0;
    }

#endif

    /* 其他情况,按名称的字符序进行排序 */
    rc = ngx_filename_cmp(first->name.data, second->name.data,
                          ngx_min(first->name.len, second->name.len) + 1);

    /* 但有一个特殊处理,即对于出现比较的两个 location 名称相同的情况,
     * 如果存在有绝对匹配 location,则将其放到前面 */
    if (rc == 0 && !first->exact_match && second->exact_match) {
        /* an exact match must be before the same inclusive one */
        return 1;
    }

    return rc;
}

对 locations 队列中任意两个 location 进行排序的规则如下:

  1. 两个比较 location 中的未命名 location(即 noname 为 1)排到后面。
  2. 如果比较的两个 location 都为未命名 location,那么保持原定次序,即保持用户在配置文件里书写的先后顺序(按文件从头到尾)。
  3. 两个比较 location 中的命名 location(即 named 为 1)排到后面。
  4. 如果比较的两个 location 都为命名 location,那么按它们名称的字符序进行排序,即通过函数 strcmp() 比较它们的名称,名称字符序大的排到后面。
  5. 两个比较 location 中的正则匹配 location(即 regex 字段不为空)排到后面。
  6. 如果比较的两个 location 都为正则匹配 location,那么保持原定次序,即保持用户在配置文件里书写的先后顺序(按文件从头到尾)。
  7. 其他情况,按名称的字符序进行排序。但是有一个特别处理,即对于出现比较的两个 loaction 名称相同的情况,如果存在有绝对匹配 location,那么要把它放在前面。

1.1.2 ngx_http_init_static_location_trees

static ngx_int_t
ngx_http_init_static_location_trees(ngx_conf_t *cf,
    ngx_http_core_loc_conf_t *pclcf)
{
    ngx_queue_t                *q, *locations;
    ngx_http_core_loc_conf_t   *clcf;
    ngx_http_location_queue_t  *lq;

    locations = pclcf->locations;

    if (locations == NULL) {
        return NGX_OK;
    }

    if (ngx_queue_empty(locations)) {
        return NGX_OK;
    }

    for (q = ngx_queue_head(locations);
         q != ngx_queue_sentinel(locations);
         q = ngx_queue_next(q))
    {
        lq = (ngx_http_location_queue_t *) q;

        clcf = lq->exact ? lq->exact : lq->inclusive;

        /* 这里是处理 location 嵌套的情况 */
        if (ngx_http_init_static_location_trees(cf, clcf) != NGX_OK) {
            return NGX_ERROR;
        }
    }

    /* 该函数把同一层次里的名称相同的不同 location 合并在一起。
     * "名称相同,但又是不同的 location"?
     * 这主要是因为 location 有绝对匹配(对应 exact 字段)和包含匹配(也就是前缀
     * 匹配,以指定前缀开头的都匹配上,所以是包含匹配,对应 inclusive 字段)。若
     * 这两个 location 的名称相同(如都为 "/"),则可以把它们共用在一个队列节点里 */
    if (ngx_http_join_exact_locations(cf, locations) != NGX_OK) {
        return NGX_ERROR;
    }

    /* nginx 对包含匹配的查找采用的是最佳匹配原则,也就是说,如果有两个 location 
     * A 和 B 的包含匹配串分别为 "/" 和 "/images/",那么对于 uri 地址 "/images/top.jpg"
     * 在这两个 location 里的查找将匹配到 location B。为了提高这种查找速度,所以
     * 下面两个函数是把队列转换成 tree */
    /*  */
    ngx_http_create_locations_list(locations, ngx_queue_head(locations));

    pclcf->static_locations = ngx_http_create_locations_tree(cf, locations, 0);
    if (pclcf->static_locations == NULL) {
        return NGX_ERROR;
    }

    return NGX_OK;
}

1.1.3 ngx_http_join_exact_locations

static ngx_int_t
ngx_http_join_exact_locations(ngx_conf_t *cf, ngx_queue_t *locations)
{
    ngx_queue_t                *q, *x;
    ngx_http_location_queue_t  *lq, *lx;

    q = ngx_queue_head(locations);

    while (q != ngx_queue_last(locations)) {

        x = ngx_queue_next(q);

        lq = (ngx_http_location_queue_t *) q;
        lx = (ngx_http_location_queue_t *) x;

        /* 比较名称是否相同 */
        if (lq->name->len == lx->name->len
            && ngx_filename_cmp(lq->name->data, lx->name->data, lx->name->len)
               == 0)
        {
            /* 若都为绝对匹配或都为包含匹配,则表示出错 */
            if ((lq->exact && lx->exact) || (lq->inclusive && lx->inclusive)) {
                ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
                              "duplicate location \\"%V\\" in %s:%ui",
                              lx->name, lx->file_name, lx->line);

                return NGX_ERROR;
            }

            /* 将名称相同的两个 location 公用同一个节点 */
            lq->inclusive = lx->inclusive;

            /* 移除后一个名称相同的节点*/
            ngx_queue_remove(x);

            continue;
        }

        /* 若名称不同,则继续取下一个进行比较 */
        q = ngx_queue_next(q);
    }

    return NGX_OK;
}

经过上面的一系列整理后,所有的 location 都被恰当地整理后放置在对应的字段下:

  • 未命名 location 被 limit_except_loc_conf 或脚本引擎引用
  • 命名 location 被放置在 server 配置的 cscf->named_locations 字段下
  • 正则匹配 location 被放置在 server 或 location 配置的 pclcf->regex_locations 字段下
  • 静态匹配(包括绝对匹配和前缀匹配)location 被放置在 server 或 location 配置的 pclcf->static_locations 字段下

以上是关于Nginx 之 Location 的整理的主要内容,如果未能解决你的问题,请参考以下文章

❤️爆肝万字整理的综合架构web服务之nginx详解❤️,附建议收藏

[整理] Nginx Location 匹配规则

Nginx之 Location 的生成

Nginx系列Nginx之location

nginx之location

Nginx之location 匹配规则详解