在 Nginx 的配置文件中,一个虚拟主机 (server {}) 中可以配置多个 location {}。
location 有什么作用呢?
This directive allows different configurations depending on the URI. It can be configured using both literal strings and regular expressions.
location 就是从 URI 层对 HTTP request 进行区分,从而进行不同处理的方法。比
如,有些对有些 URI 返回静态内容;另有些 URI 分发到后端应用服务器后,返回由这
些应用服务器生成的动态内容。
总而言之,location 实现了对 HTTP request 的细分处理。
location 匹配
一个 server {} 能够配置多个 location,请求到达时,如何从这些 location 中选
择一个合适的 location 呢?
The order in which
locationdirectives are checked as follows:
- Directives with the "=" prefix that match the query exactly (literal string). If found, searching stops.
- All remaining directives with conventional strings. If this match used the `^~" prefix, searching stops.
- Regular expressions, in the order they are defined in the configuration file.
- If #3 yielded a match, that result is used. Otherwise, the match from #2 is used.
To determine which
locationdirective matches a particular query, the literal strings are checked first. Literal strings match the beginning portion of the query - the most specific match will be used. Afterwards, regular expressions are checked in the order defined in the configuration file. The first regular expression to match the query will stop the search. If no regular expression matches are found, the result from the literal search is used.
总结下来:
-
不包含正则的
location在配置文件中的顺序不会影响匹配顺序。而包含正则表达式的location会按照配置文件中定义的顺序进行匹配。 -
设置为精确匹配 (with
=prefix) 的location如果匹配请求 URI 的话,此location被马上使用,匹配过程结束。 -
在其它只包含普通字符的
location中,找到和请求 URI 最长的匹配。如果此server {}没有包含正则的location或者该location启用了^~的话,这个最 长匹配的location会被使用。如果此server {}中包含正则的location,则先在 这些正则location中进行匹配,如果找到匹配,则使用匹配的正则location,如果 没找到匹配,依然使用最大匹配的location。 -
^~,=,~,~*这些修饰符和后面的 URI 字符串中间可以不使用空格隔开。@修饰符必须和 URI 字符串 (实际上应该叫 “命名”) 直接连接。 -
location可以嵌套定义。但是要符合以下几条规则:=修饰的location中不能再嵌套其它location@修饰的location中不能再嵌套其它location@修饰的location不能嵌套到其它location中- 父
location的 URI 字符串必须是子location的 URI 字符串的前缀 (子location启用了正则的情况除外)
接下来的篇幅,分析一下 Nginx 是如何组织存储 location,以及上面的匹配规则是如何
实现的。
存储结构
对 location 指令的配置解析,在 配置解析 一文中
已经进行了简单描述。先来介绍一下,这部分涉及到的主要数据类型。
-
ngx_http_core_loc_conf_t-location作用域结构体。这个结构体也负责在其它高 层作用域中存储能同时定义于多个作用域的location配置项,还负责连接高层作用域和location作用域。struct ngx_http_core_loc_conf_s { ngx_str_t name; /* URI 部分字符串 */ ngx_http_regex_t *regex; /* 正则引擎编译过的 正则表达式对象 */ ... unsigned named:1; /* @ 修饰符 */ unsigned noname:1; /* if () {} */ unsigned exact_match:1; /* = 修饰符 */ unsigned noregex:1; /* ^= 修饰符 */ ... ngx_http_location_tree_node_t *static_location; ngx_http_core_loc_conf_t **regex_location; void **loc_conf; ... ngx_queue_t *locations; /* 连接 `location` 作用域,由 ngx_http_location_queue_t 强制转 换而来 */ }; -
ngx_http_location_queue_t- 用于临时保存location的ngx_queue_t封装结 构。即作为queue head,又可作用queue node。typedef struct { ngx_queue_t queue; ngx_http_core_loc_conf_t *exact; /* exact_match, regex, named, noname */ ngx_http_core_loc_conf_t *inclusive; /* 非 exact 的 location */ ngx_str_t *name; u_char *file_name; ngx_uint_t *line; ngx_queue_t list; } ngx_http_location_queue_t; -
ngx_http_location_tree_node_t- 运行时location的树状存储结构节点。便于location的快速查找。struct ngx_http_location_tree_node_s { ngx_http_location_tree_node_t *left; ngx_http_location_tree_node_t *right; ngx_http_location_tree_node_t *tree; ngx_http_core_loc_conf_t *exact; ngx_http_core_loc_conf_t *inclusive; u_char auto_redirect; u_char len; u_char name[1]; };
在配置解析初步完成后,一个server {} location 指令的临时存储结构可以参考
配置解析 中的图例。
ngx_http_block 完成配置读取和存储后,调用 ngx_init_locations 函数和
ngx_http_init_static_location_tree 函数对每个虚拟主机 (server {}) 中定义的
location 进行重新整理。
------------http/ngx_http.c:114------------
static char *
ngx_http_block(ngx_conf_t *cf, ngx_combined_t *cmd, void *conf)
{
...
cmcf = ctx->main_conf[ngx_http_core_module.ctx_index];
cscfp = cmcf->servers.elts;
...
for (s = 0; s < cmcf->servers.nelts; s++) {
clcf = cscfp[s]->ctx->loc_conf[ngx_http_core_module.ctx_index];
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;
}
}
...
}
对上述代码的补充说明:
clcf最终指向用于连接每个server {}和其中定义的各location的ngx_http_loc_conf_t的结构体 (参见 配置解析)。
其中,ngx_http_init_locations 负责将 location 排序,并且分类存放。函数处理完
成后,locations 队列中只剩下了 exact 和 inclusive 类型的 location。而后,
ngx_http_init_static_location_trees 再将 exact 和 inclusive 类型的
location 进一步处理,构造出更容访问的数据结构。
先看一下 ngx_http_init_locations 是如何将 location 分类存放的。
-----------http/ngx_http.c:626-------------
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)
{
...
locations = pclcf->locations;
...
ngx_queue_sort(locations, ngx_http_cmp_locations);
named = NULL;
n = 0;
regex = NULL;
r = 0;
for (q = ngx_queue_head(locations);
q != ngx_queue_sentinel(locations);
q = ngx_queue_next(q))
{
clcf = lq->exact ? lq->exact : lq->inclusive;
if (ngx_http_init_locations(cf, NULL, clcf) != NGX_OK) {
return NGX_ERROR;
}
if (clcf->regex) {
r++;
if (regex == NULL) {
regex = q;
}
continue;
}
if (clcf->named) {
n++;
if (named == NULL) {
named = q;
}
continue;
}
if (clcf->noname) {
break;
}
}
if (q != ngx_queue_sentinel(locations)) {
ngx_queue_split(locations, q, &tail);
}
if (named) {
clcfp = ngx_palloc(cf->pool,
(n + 1) * sizeof(ngx_http_core_loc_conf_t **));
...
cscf->named_locations = clcfp;
for (q = named;
q != ngx_queue_sentinel(locations);
q = ngx_queue_next(q))
{
lq = (ngx_http_location_queue_t *) q;
*(clcfp++) = lq->exact;
}
*clcfp = NULL;
ngx_queue_split(locations, named, &tail);
}
if (regex) {
...
pclcf->regex_locations = clcfp;
...
}
return NGX_OK;
}
对上面代码的补充说明:
-
ngx_queue_sort将所有的location按其类型进行排序。操作完成后, 各类location的顺序如下 (inclusive表示这个location配置的URI如果出现于request URI的前部,就认为此location是匹配的):exact(sorted) -> inclusive(sorted) -> regex -> named -> noname -
ngx_http_init_locations递归调用了自己。这是为了处理location嵌套的情况。 -
noname类型的location被丢弃。 -
named类型的location加入到了ngx_http_srv_conf_t成员named_locations指向的数组中。named类型的location不能嵌套到其它location或被嵌套其它location的,所以存储到了ngx_http_srv_cont_t结构体中。 -
regex类型的location加放到了起关联作用的ngx_http_loc_conf_t成员regex_locations指向的数组中。 -
exact和inclusive类型的location依然保留在locations队列中。它们都 属于static location。
接下来, exact 和 inclusive 类型的 location 被
ngx_http_init_static_location_trees 函数进一步处理:
--------------http/ngx_http.c:755---------------
static ngx_int_t
ngx_http_init_static_location_trees(ngx_conf_t *cf,
ngx_http_core_loc_conf_t *pclcf)
{
locations = pclcf->locations;
...
for (q = ngx_queue_head(locations);
q != ngx_queue_sentinal(locations);
q = ngx_queue_next(q))
{
lq = (ngx_http_location_queue_t *) q;
clcf = lq->exact ? lq->exact : lq->inclusive;
ngx_http_init_static_location_trees(cf, clcf);
}
ngx_http_join_exact_locations(cf, locations);
ngx_http_create_locations_list(locations, ngx_queue_head(locations));
pclcf->static_locations = ngx_http_create_locations_tree(cf, locations, 0);
...
}
对以上代码的补充说明:
-
ngx_http_init_static_location_trees函数会被嵌套调用,为了处理location嵌套定义的情况。 -
ngx_http_join_exact_locations将当前虚拟主机中 uri 字符串完全一致的exact和inclusive类型的location进行合并。 -
ngx_http_create_locations_list和ngx_http_create_locations_tree用于构造 相对平衡的二叉查找树。详细实现代码,接下来进行分析。
由于 inclusive 类型的 location 在 ngx_http_init_locations 函数中已经被按字
典顺序进行了排序。也就是说,有共同前缀的 inclusive 类型的 location 会被集中
存放。 如果一个 location 的 URI 是后面一个或多个 location URI 的前缀,那
么这几个 location 节点会被 ngx_http_create_locations_list 整理合并。
为了使分析更具体化,下面使用示例 locations (只列出 URI,即节点 name 部分)
/a /ab /abc /abd /b /bc 对代码进行举例说明。
exact 类型的节点依然保留在 locations 队列中。
--------------http/ngx_http.c:959--------------------
static void
ngx_http_create_locations_list(ngx_queue_t *locations, ngx_queue_t *q)
{
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;
for (x = ngx_queue_next(q);
x != ngx_queue_sentinel(locations);
x = ngx_queue_next(x))
{
lx = (ngx_http_location_queue_t *) x;
if (len > lx->name->len
|| (ngx_strncmp(name, lx->name->data, len) != 0))
{
break;
}
}
...
ngx_queue_split(locations, q, &tail);
ngx_queue_add(&lq->list, &tail);
...
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);
}
对上面代码的补充说明:
-
有些边界操作上面摘录中被省略掉了。
-
参数
locations指向第一级的location节点 (其它级别的location节点已经 被追加到上级location节点的list变量中),q指向locations中还未被整理 过的location节点。 -
由于这个函数存在递归调用,
q == ngx_queue_last(locations)是递归的退出条件。 -
对于非
inclusive类型 (此时locations队列中也只包含exact和inclusive类型的location节点) 的location节点,直接跳过,不做任何整理。if (lq->inclusive == NULL) { ngx_http_create_locations_list(locations, ngx_queue_next(q)); return; } -
lq指向第一个待整理的location节点。lx指向第一个URI不以lq指向 的节点URI为前缀的节点。例如,以上面的示例locations为例,如果lq指向/a,那么lx最终将指向/b。那么/a /ab /abc /abd就是可以被整理合并到第一 级节点/a的节点。 -
将可以合并的节点,通过
ngx_queue_split和ngx_queue_add操作,追加到/a节点的list变量中。从lx开始的剩余节点使用ngx_queue_list和ngx_queue_add操作,放回locations队列中。 -
最后对
lq->list中的节点和从lx开始的剩余节点进行相同的操作。 -
ngx_http_create_locations_list函数完成后,示例locations的结构体如下:
ngx_http_create_location_list 将某个 location 节点和其后以其 URI 为 URI
前缀的一个或多个节点进行整理合并,并且在 ngx_http_create_location_trees 转化成
二叉查找树 (location 节点在树中是有序排列的) 后,可以加快针对某 request URI
进行 location 查找匹配的速度。
二叉查找树构造在 ngx_http_create_location_trees 函数中完成。
------------http/ngx_http.c:1023------------------
static ngx_http_location_tree_node_t *
ngx_http_create_locations_tree(ngx_conf_t *cf, ngx_queue_t *locations)
{
...
q = ngx_queue_middle(location);
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);
...
node->len = (u_char) len;
ngx_memcpy(node->name, &lq->name->data[prefix], len);
ngx_queue_split(locations, q, &tail);
...
node->left = ngx_http_create_locations_tree(cf, locations, prefix);
...
ngx_queue_remove(q);
...
node->right = ngx_http_create_locations_tree(cf, &tail, prefix);
...
inclusive:
if (ngx_queue_empty(&lq->list)) {
return node;
}
node->tree = ngx_http_create_locations_tree(cf, &lq->list, prefix + len);
...
return node;
}
对上述代码的补充说明:
-
有些边界操作上面摘录中被省略掉了。
-
值得一提的是
ngx_queue_middle函数使用了快慢指针,单次遍历就确定了中间节点的 位置。虽然,复杂度上没有任何提高,但是技巧很赞。queue中节点数为奇数时,函数返 回正中间位置的节点;节点数为偶数时,函数返回后半部分的首个节点。 -
locations队列里,中间节点q前的元素成为q的左子树节点;中间节点q后面的元素,变成了q的右子树节点。随后,在这两部分节点上再重复建左右子树的过 程。 -
然后,再对
q的list里包含的由ngx_http_create_locations_list函数合并 的节点递归构建二叉查找树。需要注意的一点是ngx_http_create_locations_list生成 的分级别链表中,上级的URI总是下级的URI前缀。这样一来,下级节点就不需要再 存储完整URI了,它只用存储多上级多出的部分。这么做,在节省的空间的同时,也能 提高查找效率 (已经比较过的前缀,再对下级进行查找时,就不用再次比较了)。 -
针对
q的下级节点构造的二叉查找树由node->tree成员变量记录。 -
最终,
locations队列节点全部转化成了二叉树的节点。临时存储结构ngx_http_location_queue_t的list成员中的节点,全部转化成了ngx_http_location_tree_node_t结构体tree成员中的节点。 -
对上面的示例
locations队列调用ngx_http_create_locations_tree后,结构图 如下:
到此为止,location 相关的数据结构算是构造完成了。剩下的就是如何定位每个请求对
应的 location 作用域了。
请求匹配
在开始定位 request 应由哪个 location 处理之前,Nginx 已经确定了 request 所
属的虚拟主机。
请求的 location 匹配,在请求处理的 FIND_CONFIG phase (可参见
模块分类 中对 handler 模块的介绍) 相对应的 checker
ngx_http_core_find_config_phase 函数中完成。ngx_http_core_find_config_phase
函数调用 ngx_http_core_find_location 函数完成实际的匹配工作。
------------http/ngx_http_core_module.c:1427----------
static ngx_int_t
ngx_http_core_find_location(ngx_http_request_t *r)
{
...
pclcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
rc = ngx_http_core_find_static_location(r, pclcf->static_locations);
if (rc == NGX_AGAIN) {
...
rc = ngx_http_core_find_location(r);
}
if (rc == NGX_OK || rc == NGX_DONE) {
return rc;
}
/* rc == NGX_DECLINED or rc == NGX_AGAIN in nested location */
if (noregex == 0 && pclcf->regex_locations) {
for (clcfp = pclcf->regex_locations; *clcfp; clcfp++) {
...
n = ngx_http_regex_exec(r, (*clcfp)->regex, &r->uri);
...
}
}
return rc;
}
对上面代码的补充说明:
-
ngx_http_core_find_static_location函数的返回值决定是否再次尝试正则查找。其 允许的返回值如下:NGX_OK- exact matchNGX_DONE- auto redirectNGX_AGAIN- inclusive matchNGX_DECLINED- no match
-
返回值为
NGX_AGAIN时,也就是inclusive match时,根据预先设定的逻辑,接下 来需要完成正则匹配了。但是,由于嵌套location的存在,还要在完成嵌套查找后再决 定是否开始正则匹配。 -
但是关于嵌套
location查找一个疑问:假如一个inclusive类型的location A被匹配上了,那么第一次ngx_http_core_find_static_location函数调用,返回值为NGX_AGAIN,接下来,开始检查A有没有嵌套locaton。如果A没有嵌套,那么 用于检测嵌套的对ngx_http_core_find_location函数的递归调用返回NGX_DECLINED。 再如果并没有设置和A并列的正则调用时,整个查找过程会返回NGX_DECLINED。 这 和函数ngx_http_core_find_static_location返回值的定义不符啊?
关于函数 ngx_http_core_find_location 的代码逻辑,和上一节的树结构紧密相关。主
要逻辑就不再分析了。唯一一个需要注意的地方是,在我使用的 0.8.34 的代码中,对
exact 类型 location 的检查逻辑和当前 Nginx 的使用文档描述不符:
if (len == (size_t) node->len) {
r->loc_conf = (node->exact) ? node->exact->loc_conf:
node->inclusive->loc_conf;
return NGX_OK;
}
根据此函数的返回值定义和代码中看到的,不论该 location 类型是否是 exact,只要
出现了完全匹配,Nginx 都认为是发生了 exact match。
幸运的是,在较新版本 1.4.1 中,这段代码已经变为:
if (len == (size_t) node->len) {
if (node->exact) {
r->loc_conf = node->exact->loc_conf;
return NGX_OK;
} else {
r->loc_conf = node->inclusive->loc_conf;
return NGX_AGAIN;
}
}
并且,从 CHANGES 文件中,找到了这个行为变动的版本号和时间:
1353 Changes with nginx 0.8.42 21 Jun 2010 1354 1355 *) Change: now nginx tests locations given by regular expressions, if 1356 request was matched exactly by a location given by a prefix string. 1357 The previous behavior has been introduced in 0.7.1.
Comments
不要轻轻地离开我,请留下点什么...