Nginx源码阅读笔记-查询HTTP配置流程
概述
前面已经分析过nginx解析配置文件的整体流程,接下来看查询HTTP配置的流程。
HTTP属于nginx的core顶层模块,下面又包括了三部分:
- main部分配置:即在HTTP块但是又不在任何server、location块中的配置,如下图中的sendfile配置指令。
- server块:在server块内部的配置。
- location块:在location块内部分配置。
解析HTTP模块的入口函数是ngx_http_block,这一点可以从http指令相关的配置看出:
{ ngx_string("http),
NGX_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS,
ngx_http_block,
0,
0,
NULL }
在这个解析函数的开始,就创建了ngx_http_conf_ctx_t结构体,所以看的出来这个结构体是HTTP模块的第一级配置,它的定义如下:
typedef struct {
void **main_conf;
void **srv_conf;
void **loc_conf;
} ngx_http_conf_ctx_t;
下面列举出来这几部分相关的函数以及数据结构:
块 | 入口函数 | 数据结构 |
---|---|---|
http | ngx_http_block | ngx_http_conf_ctx_t |
main | ngx_http_core_main_conf_t | |
server | ngx_http_core_server | ngx_http_core_srv_conf_t |
location | ngx_http_core_location | ngx_http_core_loc_conf_t |
另外,由于HTTP块内的一些配置,作用域可以在多种块中,因此需要涉及到合并配置的流程,即:
- 如果子作用域某配置项在解析过程中未被赋值,则将父作用域的 相同的配置项值拷贝至此配置项里;
- 如果子作用域配置项在解析过程中被赋值了,则保留原 样;如果子作用域配置项和父作用域配置项都没有被初始化,则填入代码中预设的默认值。
相关的合并配置函数列举如下:
块 | 合并函数 |
---|---|
server | ngx_http_merge_servers |
location | ngx_http_merge_locations |
以下具体看看一次HTTP请求如何查找到相关HTTP配置的流程,分为两步:
- 根据Host查找server块
- 根据URI查找location块
根据Host查找server块流程
前面分析nginx接收HTTP请求流程中分析到,nginx在接收HTTP请求流程中,将调用ngx_http_process_request_headers函数来处理请求头。
nginx使用一个ngx_http_header_t结构体,定义了哪些请求头需要进行特定的函数回调处理,函数ngx_http_process_request_headers会根据这个表来查询接收到的请求头都需要哪些回调函数来处理:
ngx_http_header_t ngx_http_headers_in[] = {
{ ngx_string("Host"), offsetof(ngx_http_headers_in_t, host),
ngx_http_process_host },
{ ngx_string("Connection"), offsetof(ngx_http_headers_in_t, connection),
ngx_http_process_connection },
....
}
可以看到,针对Host这个header,会调用ngx_http_process_host函数,这个函数最终会调用ngx_http_set_virtual_server函数来根据Host头确定对应的server块。
nginx中,不同的server块可以监听同一个地址端口,只要对应的server_name不一样就可以了。
而相同的地址端口,在nginx中对应的是ngx_http_addr_conf_t,内部将同样地址端口的多个不同server_name再组织到一起来:
typedef struct {
ngx_hash_combined_t names;
ngx_uint_t nregex;
ngx_http_server_name_t *regex;
} ngx_http_virtual_names_t;
struct ngx_http_addr_conf_s {
/* the default server configuration for this address:port */
ngx_http_core_srv_conf_t *default_server;
ngx_http_virtual_names_t *virtual_names;
unsigned ssl:1;
unsigned http2:1;
unsigned proxy_protocol:1;
};
显然,如果相同地址端口的server如果使用链表组织在一起,每一次都是线性时间的查找复杂度,这就太慢了。因此nginx定义了ngx_hash_combined_t这个数据结构,将相同地址端口的server_name组织到一起来:
typedef struct {
ngx_hash_t hash;
ngx_hash_wildcard_t *wc_head;
ngx_hash_wildcard_t *wc_tail;
} ngx_hash_combined_t;
该结构体中有三个成员,区分不同的server_name格式:
- ngx_hash_t hash:精确匹配的哈希表,用于存储没有使用通配符的虚拟主机名,如”www.example.com“。
- ngx_hash_wildcard_t wc_head:前置通配符哈希表,用于存储如”.example.org“和”.example.org“这样的前置通配符虚拟主机名。
- ngx_hash_wildcard_t wc_tail:后置通配符哈希表,用于存储如”example.“这样的后置通配符虚拟主机名。
具体这个支持通配符的hash表,不在这里讲解,只谈host的查找顺序:
- 首先查找精确匹配hash表,查找到则返回;
- 接着查找前置通配符hash表,查找到则返回;
- 最后查找后置通配符hash表,查找到则返回;
- 如果以上都没有查找到,落到default_server的server块进行处理。
根据URI查找location块流程
根据Host查找到了server块,紧跟着就是根据URI来查找location块了。
location区分几种格式:
location = / {
[ configuration A ]
}
location / {
[ configuration B ]
}
location /documents/ {
[ configuration C ]
}
location ^~ /images/ {
[ configuration D ]
}
location ~* \.(gif|jpg|jpeg)$ {
[ configuration E ]
}
在上面的配置例子中:
- 配置A:精确匹配"/" URI,主机名后面不能带任何字符串。
- 配置B:因为所有的地址都以 / 开头,所以这条规则将匹配到所有请求,但是正则和最长字符串会优先匹配。
- 配置C:匹配任何以 /documents/ 开头的地址,匹配符合以后,还要继续往下搜索,只有后面的正则表达式没有匹配到时,这一条才会采用这一条。
- 配置D:匹配任何以 /images/ 开头的地址,匹配符合以后,停止往下搜索正则,采用这一条。
- 配置E:匹配所有以 gif,jpg或jpeg 结尾的请求,然而,所有请求 /images/ 下的图片会被 config D 处理,因为 ^~ 到达不了这一条正则。
具体根据URI匹配location的流程如下:
- 首先先检查使用前缀字符定义的location,选择最长匹配的项并记录下来;
- 如果找到了精确匹配的location,也就是使用了=修饰符的location,结束查找,使用它的配置。
- 然后按顺序查找使用正则定义的location,如果匹配则停止查找,使用它定义的配置。
- 如果没有匹配的正则location,则使用前面记录的最长匹配前缀字符location。
可以看到:
- 不包含正则的 location 在配置文件中的顺序不会影响匹配顺序。而包含正则表达式的 location 会按照配置文件中定义的顺序进行匹配。
- 设置为精确匹配 (with = prefix) 的 location 如果匹配请求 URI 的话,此 location 被马上使用,匹配过程结束。
- 在其它只包含普通字符的 location 中,找到和请求 URI 最长的匹配。如果此 server {} 没有包含正则的 location 或者该 location 启用了 ^~ 的话,这个最 长匹配的 location 会被使用。如果此 server {} 中包含正则的 location,则先在 这些正则 location 中进行匹配,如果找到匹配,则使用匹配的正则 location,如果 没找到匹配,依然使用最大匹配的 location。
有了以上的准备,开始看具体的代码实现。
ngx_http_core_loc_conf_s结构体
ngx_http_core_loc_conf_s结构体对应一个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_core_loc_conf_s中使用了几个成员named、noname、exact_match、noregex区分了以上的情况。
ngx_http_location_queue_t
结构体ngx_http_location_queue_t用于临时保存location的队列:
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;
} ngx_http_location_queue_t;
ngx_http_location_tree_node_t
ngx_http_location_tree_node_t结构体是最终存储location的结构体,将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; // 精确匹配的location配置
ngx_http_core_loc_conf_t *inclusive; // inclusive匹配的location配置
u_char auto_redirect;
u_char len;
u_char name[1];
};
构建location查找树的流程
在函数ngx_http_block中(该函数即HTTP块的入口函数),将调用两个函数进行location的初始化:
- ngx_http_init_locations:用于完成location的排序以及分类存放。
- ngx_http_init_static_location_trees:用于将exact以及inclusive类型的location进一步处理,构造出可以快速访问的树状结构。
ngx_http_init_locations
- 首先调用ngx_queue_sort(locations, ngx_http_cmp_locations)函数对location队列进行排序,排序的结果为:exact(sorted) -> inclusive(sorted) -> regex -> named -> noname,这里说明一下inclusive,它表示URI之间的包含关系,即”/abc/a“这个URI是包含”/abc“的。
- 遍历排序过后的location队列,将其中的noname类型的location分离出队列。
- 将named类型的location分离出来,放到配置的named_locations中。
- 将含有正则的location分离出来,放到配置的regex_locations中。
可以看到,以上流程完成之后,原先的location队列就只剩下exact以及inclusive类型的location了。接着调用ngx_http_init_static_location_trees函数做进一步的处理。
ngx_http_init_static_location_trees
有以下几个流程:
- ngx_http_join_exact_locations:将当前虚拟主机中 uri 字符串完全一致的 exact 和 inclusive 类型的 location 进行合并。
- ngx_http_create_locations_list:将前缀一致的location放到list链表中。
- ngx_http_create_locations_tree:构造location的树结构。
ngx_http_create_locations_list
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) {
// 如果不是inclusive类型的location,直接跳过,继续队列中下一个location的处理
ngx_http_create_locations_list(locations, ngx_queue_next(q));
return;
}
len = lq->name->len;
name = lq->name->data;
// 从该location的下一个元素开始遍历队列
for (x = ngx_queue_next(q);
x != ngx_queue_sentinel(locations);
x = ngx_queue_next(x))
{
lx = (ngx_http_location_queue_t *) x;
// 找到第一个不以q的location做为前缀的location就退出循环
// 比如当前队列location为:/a /ab /abc /b
// 这里的q就是/a,x就是/b,中间的/ab和/abc都是以/a为前缀的,不会终止循环
if (len > lx->name->len
|| ngx_filename_cmp(name, lx->name->data, len) != 0)
{
break;
}
}
q = ngx_queue_next(q);
if (q == x) { // 如果x就是q的下一个元素,说明没有找到前缀匹配的,那么直接进入x进行下次递归调用
ngx_http_create_locations_list(locations, x);
return;
}
// 到了这里说明前面找到有前缀匹配的location了
// 这里将与q相同前缀的节点,分离出队列
ngx_queue_split(locations, q, &tail);
// 然后加入到q的list链表中
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;
}
// 将x从队列中分离出来
ngx_queue_split(&lq->list, x, &tail);
// 放回到location队列中
ngx_queue_add(locations, &tail);
// 对lq->list做相同的操作
ngx_http_create_locations_list(&lq->list, ngx_queue_head(&lq->list));
// 对从x开始的剩余节点做相同的操作
ngx_http_create_locations_list(locations, x);
}
对该函数的几个说明:
- 由于存在递归调用,所以函数开始要做q == ngx_queue_last(locations)的判断,做为递归的终止条件。
- 对于非 inclusive 类型 (此时 locations 队列中也只包含 exact 和 inclusive 类型的 location 节点) 的 location 节点,直接跳过,不做任何整理。
- 从lq开始遍历队列,直到查找到第一个不以q做为前缀的location才退出循环,退出循环时保存当前位置为x。比如当前队列location为:/a /ab /abc /b,这里的q就是/a,x就是/b,中间的/ab和/abc都是以/a为前缀的,不会终止循环。
- 将与lq前缀匹配的队列元素,放到lq的list中,同时针对这个list递归调用ngx_http_create_locations_list函数。
- 继续针对x开始的剩余队列节点递归调用ngx_http_create_locations_list函数。
如下图所示就是ngx_http_create_locations_list调用前后的效果:
ngx_http_create_location_trees
ngx_http_create_location_trees在上面的基础上构造location查找树
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;
// 快速确定中间节点的位置,保存到q中
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);
// 从中间节点将location分为两部分
ngx_queue_split(locations, q, &tail);
// 如果分离完毕location队列为空
if (ngx_queue_empty(locations)) {
/*
* ngx_queue_split() insures that if left part is empty,
* then right one is empty too
*/
// 直接跳到构造inclusive类型的子树
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:
// 到这里构造inclusive类型的树保存到tree成员中
// list为空说明没有inclusive类型的location了
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;
}
说明:
- 调用ngx_queue_middle快速确定locaiton队列的中间节点。
- 从中间节点将location分为两部分。
- 分别构造左右子树放到成员left和right中。
- 将inclusive类型的location放入到成员tree中。
如下图所示就是ngx-location-create-locations-tree调用前后的效果:
查找location流程
请求的 location 匹配,在请求处理的 FIND_CONFIG 阶段相对应的 checker ngx_http_core_find_config_phase 函数中完成。ngx_http_core_find_config_phase 函数调用 ngx_http_core_find_location 函数完成实际的匹配工作。
本质上就是根据前面构建好的树结构,进行二分查找,不再阐述。