http 模块的初始化个人认为十分复杂,主要数据之间的关系太乱了,那么先从 http 部分的配置解析开始,首先是 http 块的解析,也就是ngx_http_block
函数,由于这些函数都特别特别地长,所以就挑重点看吧
首先保证只有一个 http 块,然后创建一个存放 http 块下所有模块配置信息的ngx_http_conf_ctx_t
结构体
if (*(ngx_http_conf_ctx_t **) conf) {
return "is duplicate";
}
ctx = ngx_pcalloc(cf->pool, sizeof(ngx_http_conf_ctx_t));
if (ctx == NULL) {
return NGX_CONF_ERROR;
}
*(ngx_http_conf_ctx_t **) conf = ctx;
确定 http 模块的数量,创建并初始化相应数量的各模块各级的配置结构体,注意这些都是只用于这个 http 块的,如果下面有 serv 块需要重新创建,也就是分块保存配置,以此来实现不同层级的配置合并
ngx_http_max_module = ngx_count_modules(cf->cycle, NGX_HTTP_MODULE);
ctx->main_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_http_max_module);
if (ctx->main_conf == NULL) {
return NGX_CONF_ERROR;
}
ctx->srv_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_http_max_module);
if (ctx->srv_conf == NULL) {
return NGX_CONF_ERROR;
}
ctx->loc_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_http_max_module);
if (ctx->loc_conf == NULL) {
return NGX_CONF_ERROR;
}
for (m = 0; cf->cycle->modules[m]; m++) {
if (cf->cycle->modules[m]->type != NGX_HTTP_MODULE) {
continue;
}
module = cf->cycle->modules[m]->ctx;
mi = cf->cycle->modules[m]->ctx_index;
if (module->create_main_conf) {
ctx->main_conf[mi] = module->create_main_conf(cf);
if (ctx->main_conf[mi] == NULL) {
return NGX_CONF_ERROR;
}
}
if (module->create_srv_conf) {
ctx->srv_conf[mi] = module->create_srv_conf(cf);
if (ctx->srv_conf[mi] == NULL) {
return NGX_CONF_ERROR;
}
}
if (module->create_loc_conf) {
ctx->loc_conf[mi] = module->create_loc_conf(cf);
if (ctx->loc_conf[mi] == NULL) {
return NGX_CONF_ERROR;
}
}
}
然后调用各 http 模块的preconfiguration
,接着开始解析 http 块下的 main 级配置,这里还是老规矩,先要保存之前 cf 用于解析完后恢复
pcf = *cf;
cf->ctx = ctx;
for (m = 0; cf->cycle->modules[m]; m++) {
if (cf->cycle->modules[m]->type != NGX_HTTP_MODULE) {
continue;
}
module = cf->cycle->modules[m]->ctx;
if (module->preconfiguration) {
if (module->preconfiguration(cf) != NGX_OK) {
return NGX_CONF_ERROR;
}
}
}
cf->module_type = NGX_HTTP_MODULE;
cf->cmd_type = NGX_HTTP_MAIN_CONF;
rv = ngx_conf_parse(cf, NULL);
那么先存个档,准备跳转了,后续部分等到最后再看,这里假设遇到了一个 server 块,跳到ngx_http_core_server
函数
函数开头和解析 http 块时差不多
ctx = ngx_pcalloc(cf->pool, sizeof(ngx_http_conf_ctx_t));
if (ctx == NULL) {
return NGX_CONF_ERROR;
}
http_ctx = cf->ctx;
ctx->main_conf = http_ctx->main_conf;
ctx->srv_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_http_max_module);
if (ctx->srv_conf == NULL) {
return NGX_CONF_ERROR;
}
ctx->loc_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_http_max_module);
if (ctx->loc_conf == NULL) {
return NGX_CONF_ERROR;
}
for (i = 0; cf->cycle->modules[i]; i++) {
if (cf->cycle->modules[i]->type != NGX_HTTP_MODULE) {
continue;
}
module = cf->cycle->modules[i]->ctx;
if (module->create_srv_conf) {
mconf = module->create_srv_conf(cf);
if (mconf == NULL) {
return NGX_CONF_ERROR;
}
ctx->srv_conf[cf->cycle->modules[i]->ctx_index] = mconf;
}
if (module->create_loc_conf) {
mconf = module->create_loc_conf(cf);
if (mconf == NULL) {
return NGX_CONF_ERROR;
}
ctx->loc_conf[cf->cycle->modules[i]->ctx_index] = mconf;
}
}
这里没有重复性判断,因为可以有很多个 server 块,然后就像刚刚说的,创建了自己的ngx_http_conf_ctx_t
结构体,需要注意的是这里的main_conf
指向了 http 块的main_conf
,server 块里没有 main 级配置
接着又是熟悉的节奏,解析 server 块下的配置
cscf = ctx->srv_conf[ngx_http_core_module.ctx_index];
cscf->ctx = ctx;
cmcf = ctx->main_conf[ngx_http_core_module.ctx_index];
cscfp = ngx_array_push(&cmcf->servers);
if (cscfp == NULL) {
return NGX_CONF_ERROR;
}
*cscfp = cscf;
pcf = *cf;
cf->ctx = ctx;
cf->cmd_type = NGX_HTTP_SRV_CONF;
rv = ngx_conf_parse(cf, NULL);
相比之前,这里又多了一些操作,首先明确下ngx_http_core_module
模块的 main 和 srv 级配置结构体
typedef struct {
ngx_array_t servers; /* ngx_http_core_srv_conf_t */
...
} ngx_http_core_main_conf_t;
typedef struct {
...
ngx_http_conf_ctx_t *ctx;
...
} ngx_http_core_srv_conf_t;
首先是设置 srv 级配置结构体中的ctx
,指向当前 server 块的ngx_http_conf_ctx_t
结构体,简单地理解的话就是这个结构体代表了当前 server 块,内含当前 server 块的所有配置信息,然后往ngx_http_core_module
的 main 级结构体的servers
数组中加入当前 server 块的ngx_http_conf_ctx_t
结构体,servers
数组用于保存配置文件中的所有 server 块,用一张图来简单表达下就是
那么接下来又要跳转了,在解析 location 块前,先假设遇到一个listen 0.0.0.0:80 reuseport
,这时候会调用ngx_http_core_listen
函数
ngx_http_core_srv_conf_t *cscf = conf;
ngx_url_t u;
ngx_http_listen_opt_t lsopt;
cscf->listen = 1;
value = cf->args->elts;
ngx_memzero(&u, sizeof(ngx_url_t));
u.url = value[1];
u.listen = 1;
u.default_port = 80;
if (ngx_parse_url(cf->pool, &u) != NGX_OK) {
if (u.err) {
... // ngx_conf_log_error
}
return NGX_CONF_ERROR;
}
首先设置了 server 块的listen
域,表示需要监听,然后来了一个新的结构体ngx_url_t
,里面存放了端口、协议簇、地址等信息,可以看到这里默认端口是 80,然后是ngx_parse_url
函数,这个函数比较长但是逻辑比较简单,按上面那个例子来说,就是从127.0.0.1:80
解析出地址和端口号,然后存放到ngx_url_t
中
接下来的一大串代码都是在设置lsopt
变量,类型为ngx_http_listen_opt_t
,简单来说这个结构体里存放了描述 socket 特性的东西,例如发送接收缓冲区大小、fastopen、reuseport 等,这些值是根据listen
命令后面的可选参数来设定的,然后就来到最后的部分
if (ngx_http_add_listen(cf, cscf, &lsopt) == NGX_OK) {
return NGX_CONF_OK;
}
ngx_http_add_listen
函数里面跳来跳去的,里面涉及到了几个结构体
typedef struct {
...
ngx_array_t *ports; /* array of ngx_http_conf_port_t */
...
} ngx_http_core_main_conf_t;
typedef struct {
ngx_int_t family;
in_port_t port;
ngx_array_t addrs; /* array of ngx_http_conf_addr_t */
} ngx_http_conf_port_t;
typedef struct {
ngx_http_listen_opt_t opt;
...
/* the default server configuration for this address:port */
ngx_http_core_srv_conf_t *default_server;
ngx_array_t servers; /* array of ngx_http_core_srv_conf_t */
} ngx_http_conf_addr_t;
函数的目的就是设置 main 级配置的ports
数组,这个数组根据端口关联各个虚拟主机,也就是 server 块,假设配置文件如下
http {
server {
listen 10.0.0.1:80 default_server;
server_name www.server1.com www.server2.com;
}
server {
listen 10.0.0.1:80;
server_name www.server3.com;
}
server {
listen 10.0.0.2:80;
}
server {
listen 10.0.0.3:8000;
}
}
那么结果大概如下图所示,其中default_server
就是如果不能找到 http 请求中指定的 host 时就会使用该虚拟主机
解析listen
配置结束,假设接下来遇到 location 块,解析函数为ngx_http_core_location
,开头还是一如既往,只是srv_conf
指向上级 server 块的srv_conf
ngx_http_core_loc_conf_t *clcf;
ctx = ngx_pcalloc(cf->pool, sizeof(ngx_http_conf_ctx_t));
if (ctx == NULL) {
return NGX_CONF_ERROR;
}
pctx = cf->ctx;
ctx->main_conf = pctx->main_conf;
ctx->srv_conf = pctx->srv_conf;
ctx->loc_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_http_max_module);
if (ctx->loc_conf == NULL) {
return NGX_CONF_ERROR;
}
...
clcf = ctx->loc_conf[ngx_http_core_module.ctx_index];
clcf->loc_conf = ctx->loc_conf;
这里还将ngx_http_core_loc_conf_t
的loc_conf
域指向了各个模块的 loc 级配置数组
接下来的部分是在判断 location 指令后的参数,设置前缀匹配、精确匹配、正则匹配、命名路径等相关值,其中还处理了嵌套 location 块的情况
函数最后调用了ngx_http_add_location
,首先需要明确一下,server 块下个各个 location 块是通过 server 块的ngx_http_core_loc_conf_t
的locations
队列联系起来的,队列内存放的元素类型为ngx_http_location_queue_t
,定义如下
typedef struct {
ngx_queue_t queue;
ngx_http_core_loc_conf_t *exact; // 路径精确匹配 包括正则
ngx_http_core_loc_conf_t *inclusive; // 路径非精确匹配 例如前缀匹配
ngx_str_t *name; // 路径名
u_char *file_name; // 配置文件名称
ngx_uint_t line; // 配置文件行数
ngx_queue_t list; // 含有相同前缀的节点
} ngx_http_location_queue_t;
假设配置文件为
server {
location = /test1 {
}
location ^~ /test2 {
}
}
则执行完`ngx_http_add_location
后效果如下图所示
接下来便是解析 location 下的配置项,这里就不分析下去了,现在假设解析完了 server 块里的配置项,回到ngx_http_core_server
函数
if (rv == NGX_CONF_OK && !cscf->listen) {
ngx_memzero(&lsopt, sizeof(ngx_http_listen_opt_t));
sin = &lsopt.sockaddr.sockaddr_in;
sin->sin_family = AF_INET;
#if (NGX_WIN32)
sin->sin_port = htons(80);
#else
sin->sin_port = htons((getuid() == 0) ? 80 : 8000);
#endif
sin->sin_addr.s_addr = INADDR_ANY;
lsopt.socklen = sizeof(struct sockaddr_in);
lsopt.backlog = NGX_LISTEN_BACKLOG;
lsopt.rcvbuf = -1;
lsopt.sndbuf = -1;
#if (NGX_HAVE_SETFIB)
lsopt.setfib = -1;
#endif
#if (NGX_HAVE_TCP_FASTOPEN)
lsopt.fastopen = -1;
#endif
lsopt.wildcard = 1;
(void) ngx_sock_ntop(&lsopt.sockaddr.sockaddr, lsopt.socklen,
lsopt.addr, NGX_SOCKADDR_STRLEN, 1);
if (ngx_http_add_listen(cf, cscf, &lsopt) != NGX_OK) {
return NGX_CONF_ERROR;
}
}
解析完 server 块后,如果没有指定listen
,则监听默认地址,这里的函数之前都提到过,和解析listen
配置项时类似
好的,然后假设解析完 http 块下的配置项,回到最开始的ngx_http_block
函数
cmcf = ctx->main_conf[ngx_http_core_module.ctx_index];
cscfp = cmcf->servers.elts;
for (m = 0; cf->cycle->modules[m]; m++) {
if (cf->cycle->modules[m]->type != NGX_HTTP_MODULE) {
continue;
}
module = cf->cycle->modules[m]->ctx;
mi = cf->cycle->modules[m]->ctx_index;
/* init http{} main_conf's */
if (module->init_main_conf) {
rv = module->init_main_conf(cf, ctx->main_conf[mi]);
if (rv != NGX_CONF_OK) {
goto failed;
}
}
rv = ngx_http_merge_servers(cf, cmcf, module, mi);
if (rv != NGX_CONF_OK) {
goto failed;
}
}
首先遍历servers
数组,调用init_main_conf
函数,然后逐级向下合并配置项,合并完后开始构造 location 搜索树
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;
}
}
location 搜索树用于快速查找 http 请求对应的 location,首先遍历servers
数组,调用ngx_http_init_locations
函数,这个函数将之前提到的locations
队列按类型和特定规则排序,处理然后切分出命名路径存放到 srv 级配置的named_locations
中,切分出正则路径放到 loc 级配置的regex_locations
中,其余路径留在 loc 级配置的locations
中
然后调用ngx_http_init_static_location_trees
函数构造静态树,这个函数首先合并了路径名称相同的精确匹配路径和前缀匹配路径,例如location = /test
和location /test
,合并后将前缀匹配的inclusive
存放到精确匹配的inclusive
域中,然后从队列中删除前缀匹配的项
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) {
...
lq->inclusive = lx->inclusive;
ngx_queue_remove(x);
}
ngx_http_init_static_location_trees
函数接着合并具有相同前缀的节点合并到同一个节点的list
队列中,这部分我没仔细看,感觉上最后应该是构造出一个三叉树,左子树是字典序小的节点,右子树是字典序大的节点,中间子树是具有相同前缀的节点,某些部分有点像字典树
然后调用ngx_http_init_phases
为各个 phase 的 handler 数组分配空间,代码比较简单,接下来把ngx_http_headers_in
中定义的 http 头存入headers_in_hash
哈希表,里面指定了遇到这些 http 头是需要执行的处理函数
if (ngx_http_init_phases(cf, cmcf) != NGX_OK) {
return NGX_CONF_ERROR;
}
if (ngx_http_init_headers_in_hash(cf, cmcf) != NGX_OK) {
return NGX_CONF_ERROR;
}
接着调用各个 http 模块的postconfiguration
函数,完后调用ngx_http_variables_init_vars
函数,但是与变量有关的部分我都没有看
for (m = 0; cf->cycle->modules[m]; m++) {
if (cf->cycle->modules[m]->type != NGX_HTTP_MODULE) {
continue;
}
module = cf->cycle->modules[m]->ctx;
if (module->postconfiguration) {
if (module->postconfiguration(cf) != NGX_OK) {
return NGX_CONF_ERROR;
}
}
}
if (ngx_http_variables_init_vars(cf) != NGX_OK) {
return NGX_CONF_ERROR;
}
由于各个模块一般在postconfiguration
函数中加入自己希望介入的 phase 的处理函数,所以接下来就可以调用ngx_http_init_phase_handlers
来初始化各个 handler 数组,这个函数将各个 handler 数组的元素按顺序加入到phases
数组中,此外还指定了next
和checker
,开头首先定义了变量n
来存放 handler 总数
use_rewrite = cmcf->phases[NGX_HTTP_REWRITE_PHASE].handlers.nelts ? 1 : 0;
use_access = cmcf->phases[NGX_HTTP_ACCESS_PHASE].handlers.nelts ? 1 : 0;
n = 1 /* find config phase */
+ use_rewrite /* post rewrite phase */
+ use_access /* post access phase */
+ cmcf->try_files;
for (i = 0; i < NGX_HTTP_LOG_PHASE; i++) {
n += cmcf->phases[i].handlers.nelts;
}
由于以下四个阶段是用户不可以介入的,所以这里需要算上这些阶段可能存在的 handler
NGX_HTTP_POST_REWRITE_PHASE
NGX_HTTP_POST_ACCESS_PHASE
NGX_HTTP_TRY_FILES_PHASE
NGX_HTTP_FIND_CONFIG_PHASE
接着对应各阶段设置不同的next
和checker
,需要注意next
一般指的下一个阶段的首个处理函数(可能跳过了本阶段的处理函数),但是也有特殊情况,比如NGX_HTTP_ACCESS_PHASE
,默认的next
不是指向NGX_HTTP_POST_ACCESS_PHASE
,还有就是这里的 handler 是反向添加的
n += cmcf->phases[i].handlers.nelts;
for (j = cmcf->phases[i].handlers.nelts - 1; j >=0; j--) {
ph->checker = checker;
ph->handler = h[j];
ph->next = n;
ph++;
}
初始化完 phase 处理函数后就来到最后的部分了!也就是ngx_http_optimize_servers
函数,首先遍历ports
数组,并将其中存放的地址排序,然后将地址中存放的所有servers
的以名字为 key 加入 hash 表,这里还分了前置通配符和后置通配符的情况
for (p = 0; p < ports->nelts; p++) {
ngx_sort(port[p].addrs.elts, (size_t) port[p].addrs.nelts,
sizeof(ngx_http_conf_addr_t), ngx_http_cmp_conf_addrs);
addr = port[p].addrs.elts;
for (a = 0; a < port[p].addrs.nelts; a++) {
if (addr[a].servers.nelts > 1
#if (NGX_PCRE)
|| addr[a].default_server->captures
#endif
) {
if (ngx_http_server_names(cf, cmcf, &addr[a]) != NGX_OK) {
return NGX_ERROR;
}
}
}
if (ngx_http_init_listening(cf, &port[p]) != NGX_OK) {
return NGX_ERROR;
}
}
循环的最后,调用了ngx_http_init_listening
,设置了监听对象,函数开头判断了是否含有通配地址
addr = port->addrs.elts;
last = port->addrs.nelts;
if (addr[last - 1].opt.wildcard) {
addr[last - 1].opt.bind = 1;
bind_wildcard = 1;
} else {
bind_wildcard = 0;
}
若端口有含有通配地址,也就是*:port
形式的,则那些没有设置 socket 属性的地址,统一bind
通配地址即可,由于addr
是排过序的,所以直接根据最后一个来元素判断,函数接着循环遍历addr
数组,为每个需要绑定的地址添加监听对象
if (bind_wildcard && !addr[i].opt.bind) {
i++;
continue;
}
ls = ngx_http_add_listening(cf, &addr[i]);
if (ls == NULL) {
return NGX_ERROR;
}
ngx_http_add_listening
函数根据地址的opt
设置ngx_listening_t
的各项属性,需要注意的是,其中还为其指定了handler
,也就是在处理 accept 事件是回调用的函数,具体以后有机会再说
ngx_listening_t *ls;
...
ls = ngx_create_listening(cf, &addr->opt.sockaddr.sockaddr, addr->opt.socklen);
...
ls->handler = ngx_http_init_connection;
接着ngx_http_init_listening
的循环中,还为刚刚创建的ngx_listening_t
设置了servers
,这是一个ngx_http_port_t
,里面存放了该端口的其他地址,在ngx_http_init_connection
中会使用到,例如使用通配地址的情况,可能需要与前面的请求地址比较
循环最后调用了ngx_clone_listening
函数,该函数只有在对地址开启了reuseport
时才有作用,也就是对同一ngx_listening_t
复制与进程数量等量的份,它们绑定相同的地址,然后每个进程使用一个以此来解决惊群问题
if (ngx_clone_listening(cf, ls) != NGX_OK) {
return NGX_ERROR;
}
最后,看完这些东西我已经晕了