导语
本文对nginx http模块开发需要掌握的一些关键点进行了提炼,同时以开发一个简单的日志模块进行讲解,让nginx的初学者也能看完之后做到心里有谱。本文只是一个用作入门的概述。
目录
- 背景
- 主线
- 认识nginx
- http请求处理流程
- 日志模块开发准备
- 配置解析流程
- 挂载处理函数
- 获取日志数据
- 模块编译
- 完整源码
- 结尾
背景
网上已经有很多介绍nginx http模块开发入门的文章,但是大多以hello_word程序为例,简单讲解一下代码,看完之后可能只能够依葫芦画瓢,做不了其它事情,更不要谈实际工程需要了。当然网上也有很多系统介绍nginx的系列文章,不过学习成本较高,需要一定时间成本才能开始"动手"。这是我初学nginx时的感受:网上很少有拿捏比较准的,既能屏蔽掉底层细节,又让读者清楚大致原理,做到心中不慌的文章,当自己对nginx有了一定了解后,决定也写一篇关于http模块开发入门的文章。当然由于自己也是半桶水,初衷虽好,但文章水平未必如意。以下都是以linux平台为准。
主线
认识nginx
需要看本文的同学,想必对nginx多少都有一定了解了。nginx是web服务器程序(一般都是作为反向代理服务器)。传统的apache服务器每当收到一个http请求就会创建一个进程,这种方式在高并发情况下,对系统资源(CPU,内存)的消耗十分浪费,与之相比,nginx具备如下优势
1 多worker监听处理模式(类比单Proxy多Worker的服务模型,请求量较大的时候单Proxy容易成为IO瓶颈),同时worker底
层依靠epoll进行IO调度(每个worker能同时处理多个请求),以及每个请求的低内存消耗(事实上对于现在的机器配置而言
,CPU,网卡往往才是性能瓶颈),单机10万QPS也是不在话下
2 通过良好的分层次/功能的模块化设计,各模块间耦合度极低,具备良好的扩展性及灵活性,受益于此,nginx有大量的
第三方模块。
3 在热部署方面,nginx的单Master-多worker进程模型,可让nginx在不停止服务的情况下,进行二进制文件的升级,更
新配置,切割日志文件等操作。
这里还有一个有趣的话题,nginx为什么要叫反向代理服务器呢?因为它是将外部请求转发到内部服务。
http请求处理流程
要开发第三方http模块,必须清楚nginx的http框架处理http请求的流程,才能知道自己的模块应该挂载(作用)在哪个流程。http框架对请求的处理分为11个流程,其中的7个流程可以挂载第三方模块。对于http请求的主要处理流程可归纳为如下5步:
1 完整读取HTTP头部
2 Uri查找及重写阶段
3 访问控制阶段
4 处理请求内容阶段
5 日志阶段
其完整的请求流程如下:
1 NGX_HTTP_POST_READ_PHASE
读取到完整的请求头后进行处理的阶段
2 NGX_HTTP_SERVER_REWRITE_PHASE
还未进行URI的Location匹配前的URL重写阶段
3 NGX_HTTP_FIND_COFIG_PHASE
Location匹配阶段(不可挂载)
4 NGX_HTTP_REWRITE_PHASE
URI匹配之后的URL重写阶段
5 NGX_HTTP_POST_REWRITE_PHASE
重写提交阶段,用于在重写URL之后,再次跳到NGX_HTTP_FIND_COFIG_PHASE阶段(不可挂载)
6 NGX_HTTP_PREACCESS_PHASE
访问控制阶段前
7 NGX_HTTP_ACCESS_PHASE
访问控制阶段
8 NGX_HTTP_POST_ACCESS_PHASE
访问控制提交阶段,如不允许访问,此阶段将构造拒绝访问的回包(不可挂载)
9 NGX_HTTP_TRY_FILES_PHASE
为try-files配置项设立的,用于处理静态文件,可以不关注(不可挂载)
10 NGX_HTTP_CONTENT_PHASE
核心阶段,处理请求内容
11 NGX_HTTP_LOG_PHASE
日志阶段
通过将请求处理划分为十一个流程,每个流程由各自挂接的模块进行处理,使得http框架具备良好的扩展性和灵活性,另外除去上述主流程之外,还有一个经常碰到的流程是content_filter流程,该流程可以视为是NGX_HTTP_CONTENT_PHASE的一个子流程,常用的gzip模块就是作用在这一流程中,由于是入门,此块暂且不提。
另外我们可以看到,在1-9阶段其实都是http服务器的基础功能,如uri重写/匹配,访问权限校验等,一般我们都是在NGX_HTTP_CONTENT_PHASE挂载我们自定义的模块来对特定请求进行处理,或者是在NGX_HTTP_LOG_PHASE,进行一些信息收集类的工作。
日志模块开发准备
通过上面的了解,我们已经清楚了nginx的http请求处理流程,很明显,我们要开发的日志模块,肯定是挂在请求处理的最后阶段,即NGX_HTTP_LOG_PHASE阶段,挂载该阶段还有两个理由
1 因为在上一个阶段,已经处理了回包,该阶段不会影响请求的耗时
2 在该阶段,我们可以获取到该次请求的所有信息(包括请求信息以及回包信息)
下面开始设计该模块,首先需要明白一点,http模块由两部分构成:
1 配置项命令,即nginx配置文件中的各个配置项
2 http模块上下文,上下文中将设置配置解析过程中的各个回调函数
下面我们分别介绍。
配置项命令
首先我们需要增加两个配置项命令,分别用于日志开关和日志格式(控制打印哪些信息),通过定义ngx_command_t来添加配置项命令,先看ngx_command_t结构的定义:
typedef struct ngx_command_t ngx_command_s;
struct ngx_command_s {
ngx_str_t name;
ngx_uint_t type;
char *(*set)(ngx_conf_t *cf,ngx_command_t *cmd,void *conf);
ngx_uint_t conf;
ngx_uint_t offset;
void *post;
};
各成员的含义:
name
name是配置项名称
type
type是配置项类型,该值会指定该配置项可出现的位置(http,server,location等三个),以及可以指定携带多少个参数,nginx预定义了一些宏,这里抽取部分进行介绍
NGX_HTTP_MAIN_CONF
该配置项可出现在http块内(不包含server块)
NGX_HTTP_SRV_CONF
该配置项可出现在server块内(不包含location块)
NGX_HTTP_LOC_CONF
该配置项可出现在location块内
NGX_CONF_NOARGS
该配置项没有参数
NGX_CONF_TAKE1
该配置项携带1个参数
NGX_CONF_TAKE2
该配置项携带2个参数
NGX_CONF_TAKE3
该配置项携带3个参数
...
NGX_CONF_TAKE12
该配置项携带1或者2个参数
NGX_CONF_TAKE13
该配置携带1或者3个参数
set
在解析配置时,碰到了该配置项名后,将会调用该方法来解析其后面的参数,nginx预设了一些解析函数,最常用到的有以下3个(还有很多,包括数组解析,kv对解析,甚至是枚举,这些需要用到的话google一下就好):
1 ngx_conf_set_flag_slot
如果配置项的参数是on/off时(即希望这个参数像个开关一样,打开或者关闭某项功能),可以使用这个预设函数,当参数为
on时,会把对应的变量设置为1,同理,为off时,会设置为0
2 ngx_conf_set_str_slot
如果该配置项只有一个参数,并且希望以ngx_str_t类型(nginx里面的字符串类型)进行存储的话,可使用这个预设函数
3 ngx_conf_set_num_slot
如果该配置项只有一个参数,并且这个参数是数字,而且存储该参数的变量是整形时,可以使用该预设函数
conf
该参数决定存储该配置项内容的内存偏移,在这里很难讲清,具体将会在下文提到。
offset
一般来说,我们会定义一个结构体,用于存放我们自定义模块对应配置项解析出来的值,offset是指存储该配置项的成员在该结构体中的内存偏移量。如果使用的set函数是nginx预设的,那么offset参数必须要设置,一般我们使用这个宏:offsetof(struct,member)。如果使用的set函数是自定义的,那么该值随便你填(一般填0,避免误解)都没关系,它只是nginx提供的解析函数需要使用。
post
该参数一般置为NULL,由于是入门,这里简单提一下,如果上面的set函数是自定义方法,那么post指针完全可以随意发挥。如果set函数用的是nginx预设的,那么需要根据使用的预设set方法来对post进行相应的设定或置为NULL。
了解了ngx_command_t结构后,我们可以先定义我们的配置项命令,以及存储配置项值的结构体了。在这里我们定义了两个配置命令,分别是mylog_switch,以及mylog_format,其中mylog_switch是作为是否打开mylog日志模块的flag使用,mylog_format用于设置日志格式,在这里我们简单处理,只用于存放需要打印的nginx内部变量。
//用于存储解析出来的配置项值
typedef struct{
ngx_str_t name; //变量名
ngx_int_t index; //该变量的下标
}ngx_http_mylog_var_t;
typedef struct{
ngx_flag_t mylog_switch;
ngx_array_t mylog_formats;//数组,在这里用来存储ngx_http_mylog_var_t
}ngx_http_mylog_conf_t;
/*
*声明配置项解析函数
* cf: 该变量保存着当前的配置内容
* cmd: 对应的上文提到的cmd指令
* conf:通过create_main/srv/loc_conf(下文会提到) 创建出来的结构体
*/
static char* ngx_http_mylog_set_switch(ngx_conf_t *cf,ngx_command_t *cmd,void *conf);
static char* ngx_http_mylog_set_format(ngx_conf_t *cf,ngx_command_t *cmd,void *conf);
//定义我们需要新增的配置项,该变量必须为数组类型,且以ngx_null_command结尾
static ngx_command_t ngx_http_mylog_commands[] = {
{
ngx_string("mylog_switch"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_http_mylog_set_swtich,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mylog_conf_t,switch),
NULL
},
{
ngx_string("mylog_format"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_1MORE,
ngx_http_mylog_set_format,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL
},
ngx_null_command
};
解释一下ngx_http_mylog_commands数组中各command定义的含义
mylog_switch
name
ngx_string是nginx预定义好的宏,用于快速创建一个ngx_string_t变量,我们增加了一项名为"mylog_switch"的配置项
type
首先我们设置了该配置项可出现的配置块,它可出现在http块,srv块,loc块等配置中,同时定义了它只能携带一个参数,type参数与conf参数的设置是有一定关联性的,待会讲解conf参数时会提
set
这里我们使用自定义的解析函数,具体实现我们在下文会讲解
conf
conf参数决定存储该配置项内容的内存偏移(具体来由在下文还会提),这个参数我们使用哪个值,需要根据type来决定,简单来说该值需要填type中的最低级别对应的值,比如我们的配置项可出现在http块,srv块,loc块中,那么我们的conf参数理应填NGX_HTTP_LOC_CONF_OFFSET,如果我们的配置项只可出现在http块和srv块中,那么就填NGX_HTTP_SRV_CONF_OFFSET,当然如果只能出现在http块的话,那就填NGX_HTTP_MAIN_CONF_OFFSET即可(如果读者去读nginx的http模块源码的话,可能会发现有些模块传了0进去,NGX_HTTP_MAIN_CONF_OFFSET的值其实就是0)
offset
mylog_format由于使用的自定义解析函数,直接填0,mylog_switch虽然也使用的我们的自定义函数,但是在下文的示例代码里该函数会使用预定义解析函数ngx_conf_set_flag_slot,因此使用offsetof宏,设置一下偏移
post
由于使用的自定义解析函数,该值可以随便我们使用,由于我们并不想搞事,填NULL即可
mylog_format
各参数值与mylog_switch基本一致,这里就不再赘述了,除了type参数中指定配置项参数个数用的是NGX_CONF_1MORE,该参数表示至少有一个参数。
Note:
对于刚刚定义的两个配置项命令,我们可以有如下使用方式:
mylog_switch on;
mylog_format ‘$remote_addr $http_user_agent $http_cookie‘
‘$status $body_bytes_sent‘;
含义是打开mylog日志模块,需要打印remote_addr,http_user_agent ,http_cookie,status,
body_bytes_sent等nginx内置变量
模块上下文
有了配置项命令后,接下来我们需要定义模块上下文,模块上下文用于设置该模块在解析配置文件时的各个阶段的回调函数。我们看一下http模块上下文结构体的定义,其由一堆函数指针组成,这些函数会在配置解析的对应阶段中被调用。
typedef struct {
ngx_int_t (*preconfiguration)(ngx_conf_t *cf);/*配置解析前*/
ngx_int_t (*postconfiguration)(ngx_conf_t *cf);/*配置解析完毕后*/
void* (*create_main_conf)(ngx_conf_t *cf);/*当碰到http块时的回调函数,用于创建http块配置项*/
char* (*init_main_conf)(ngx_conf_t *cf, void *conf);/*初始化http块配置项,该函数会在配置项合并前调用*/
void* (*create_srv_conf)(ngx_conf_t *cf);/*当碰到server块时的回调函数,用于创建server块配置项*/
char* (*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);/*用于合并Main级别和Srv级别的server块配置项*/
void* (*create_loc_conf)(ngx_conf_t *cf);/*当碰到location块时的回调函数,用于创建location块的配置项*/
char* (*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf);/*用于合并Main,Srv级别和Loc级别的location块配置项*/
} ngx_http_module_t;
Note:
create_main_conf,create_srv_conf,create_loc_conf三个函数分别创建不同配置块对应的结构体,因此它们是可以返回
不同类型的结构体的,开发者可根据自己的需求来决定
下面定义我们的日志模块的上下文结构
//函数声明
static ngx_int_t ngx_http_mylog_post_init(ngx_conf_t *cf);
static void* ngx_http_mylog_create_main_conf(ngx_conf_t *cf);
static void* ngx_http_mylog_create_srv_conf(ngx_conf_t *cf);
static char* ngx_http_mylog_merge_srv_conf(ngx_conf_t *cf,void *prev,void *conf);
static void* ngx_http_mylog_create_loc_conf(ngx_conf_t *cf);
static char* ngx_http_mylog_merge_loc_conf(ngx_conf_t *cf,void *prev,void *conf);
//该函数用于挂载到日志阶段,执行mylog模块的逻辑
static ngx_int_t ngx_http_mylog_handler(ngx_http_request_t *r);
//定义日志模块上下文结构
static ngx_http_module_t ngx_http_mylog_module_ctx = {
NULL,
ngx_http_mylog_post_init,
ngx_http_mylog_create_main_conf,
NULL,
ngx_http_mylog_create_srv_conf,
ngx_http_mylog_merge_srv_conf,
ngx_http_mylog_create_loc_conf,
ngx_http_mylog_merge_loc_conf
};
Note:
没错,配置项命令和模块上下文都是在围绕着nginx配置文件在转,可以说nginx配置文件是第三方模块与nginx的桥梁
为什么我们这里声明的函数和日志模块上下文结构都加上了static修复符呢? 全局变量和全局静态变量有什么区别呢?
这一点交给读者自己解决。
上面声明的函数的具体实现我们先放在一边,下文会具体讲解。我们来解释下为何我们的ngx_http_mylog_module_ctx如此定义
1 ngx_http_mylog_post_init
这是我们设置的配置解析完毕后的回调函数,我们将在这个回调函数中,将我们的自定义逻辑处理函数挂载到nginx的日志阶段中去
2 ngx_http_mylog_create_main_conf
当nginx解析配置文件碰到http块时会调用该函数生成对应的main级别的http块配置项结构体,注意!如果create_srv/loc_con
f函数指针不为空的话,也将调用生成main级别的server和location块配置项结构体。
3 ngx_http_mylog_create_srv_conf
同上,在碰到server块时会调用该函数生成srv级别的server块结构体,同时如果create_loc_conf函数指针不为空的话,也
将调用生成srv级别的location块结构体
4 ngx_http_mylog_merge_srv_conf
用于合并main级别的server块结构体和srv级别的server块结构体,当然如果某一方为NULL的话,也就不用合并了
5 ngx_http_mylog_create_loc_conf
同3,在碰到location块时会调用该函数生成loc级别的location块结构体
6 ngx_http_mylog_merge_loc_conf
用于合并不同级别的location块结构体
Note:
上下文结构的定义跟定义ngx_command_t时各command的赋值是有关系的。举个例子,如果说有command它的conf参数是NGX_HTT
P_MAIN_CONF_OFFSET的话,那么create_main_conf不能为空,否则就没有main块结构体供它存储配置内容了。同理,如果有co
mmand的conf参数是NGX_HTTP_SRV_CONF_OFFSET的话,那么create_srv_conf不能为空,否则就没有servr块结构体供它存储配
置内容。至于两个merge函数,如果说可能出现不同级别的server块或者location块结构体时,那么就需要对应的merge函数来
合并配置项,当然事实上你不实现merge函数也不会导致nginx运行错误,只是这可能不符合设计规范
实际上由于我们的mylog日志模块的两个配置项命令的内存偏移都使用的是NGX_HTTP_LOC_CONF_OFFSET,我们其实并不需要定义create_main_conf,create_srv_conf,以及merge_srv_conf,因此我们可以把这三个函数指针置为NULL,当然对应的那三个函数声明也可以删掉了,前面之所以写出来,就是为了在这里说明其实可以删掉它们→_→
现在mylog日志模块的配置项命令以及模块上下文都定义好了,我们可以定义mylog日志模块了,只需要定义一个ngx_module_t变量即可。我们先看ngx_module_t结构的定义,再定义我们的模块变量。
typedef struct ngx_module_s ngx_module_t;
... /*省略号部分的变量我们不需要关注(它们中有版本号,保留字段等,以及两个很核心,但是是由框架来赋值的变量)
,在我们定义变量时基本都是用nginx的预定义宏NGX_MODULE_V1来赋值,因此这里略过*/
void *ctx; //变量名即注释,传入模块上下文
ngx_command_t *command; //传入配置项命令数组
ngx_uint_t type; /*模块类型,nginx预定义了一些类型(可自定义新的
类型),其中就包括我们现在在定义的Http模块,因此填入NGX_HTTP_MODULE*/
/*以下7个函数,是nginx在启动停止时的回调函数,由于nginx还不支持多线程模式,所以init/exit_thread不会被调用
,设置为NULL即可,其中init/exit_master为master进程调用,其余均为work进程调用,参数ngx_cycle_t
*cycle其实是nginx的核心变量,但是由于这7个函数,大多数模块都不会用到,作为入门,这里就不展开了*/
ngx_int_t (*init_master)(ngx_log_t *log);
ngx_int_t (*init_module)(ngx_cycle_t *cycle);
ngx_int_t (*init_process)(ngx_cycle_t *cycle);
ngx_int_t (*init_thread)(ngx_cycle_t *cycle);
void (*exit_thread)(ngx_cycle_t *cycle);
void (*exit_process)(ngx_cycle_t *cycle);
void (*exit_master)(ngx_cycle_t *cycle);
.../*同上面的省略号,以下变量均为保留字段,在我们定义变量时基本都是用nginx的预定义宏NGX_MODULE_V1_PADDING
来填充,因此这里略过*/
接下来定义我们的nginx模块变量
ngx_module_t ngx_http_mylog_module = {
NGX_MODULE_V1,
&ngx_http_mylog_module_ctx,
ngx_http_mylog_commands,
NGX_HTTP_MODULE,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NGX_MODULE_V1_PADDING
};
定义完模块变量已经是万事具备了,接下来的工作只需要将模块挂载到对应的http请求阶段中即可生效,那么在哪里挂载?如何挂载?挂载的是什么?还记得我们定义的ngx_command_t变量中它的set函数(解析到对应配置项时的回调函数)以及ngx_http_module_t中的各回调函数么?第三方模块可以说全是在这些回调函数中进行挂载的。至于如何挂载在讲解完配置解析流程后会提。挂载的自然是我们的自定义逻辑处理函数(通过函数指针赋值,因此函数的参数列表是需要固定的,只不过里面的逻辑由我们决定)。到这里,是不是觉得开发一个第三方http模块十分轻松?
接下来了解一下nginx的配置解析流程,清楚了配置解析流程后,读者就能把上面的碎片化讲解给串起来了。
配置解析流程
nginx是靠配置来驱动的,每一份配置文件就是一个定制化的nginx服务器。nginx在启动过程中会对配置内容进行解析,在解析配置的过程中会回调相应的回调函数,对第三方模块来说我们可以在这些回调函数中将自己的模块挂接到相关的请求处理阶段中。这里我们只关注http配置项,而http配置项中最关键的几个配置块就是http配置块,server配置块和location配置块,这里简要介绍一下nginx的http模块配置解析流程。
nginx配置解析过程中,有一个核心结构必须了解,我们先看一下这个核心结构的定义:
//配置上下文结构,由三个二级指针构成,分别指向http/server/location块配置项结构体
typedef struct{
void **main_conf;
void **srv_conf;
void **loc_conf;
}ngx_http_conf_ctx_t;
下面简述nginx在解析配置文件时遇到http,server,location块及配置项命令时的处理过程:
1 遇到http块时,将调用各模块的create\_main\_conf函数,同时还会调用create\_srv\_conf,create\_loc\_conf
2 调用各模块的http模块上下文的preconfiguration回调函数
3 解析http块内的配置项命令,具体见#
4 遇到server块时,将调用各模块的create\_srv\_conf函数,以及create\_loc\_conf函数
5 解析server块内的配置项命令,具体见#
6 遇到location块时,将调用各模块的create\_loc\_conf函数,由于location内可嵌套location,此过程可递归
7 解析location块内的配置项命令,具体见#
8 调用各模块的init\_main\_conf方法
9 合并各模块main,srv,loc级别下的srv和loc配置项
10 调用各模块的http模块上下文的postconfiguration回调函数
# 在配置解析过程中,当遇到配置项命令时,nginx将寻找该配置项(根据配置项名称,即定义ngx\_command\_t中的name
成员)对应的回调函数(即定义ngx\_command\_t变量时的set函数指针),并将配置内容以及nginx的上下文保存在ngx\_con
f\_t结构中,传入set函数。
由上面可知,正常情况下:
1 create\_main\_conf方法只会被调用一次(http块在nginx配置文件中只能出现一次)
2 2 create\_srv\_conf方法至少会被调用两次(碰到http块时将调用一次,而一个http块下至少会有一个server块)
3 3 create\_loc\_conf方法至少会被调用三次(碰到http块,server块和location块时都将被调用)
为什么nginx需要生成不同级别的server块和location块配置变量呢?这是因为我们需要解决配置项合并的问题。配置项合并的规则也是由我们来决定的,比如你可以完全采用上一级别的配置,也可以完全采用当前级别的配置,或者两者各取一部分。
配置解析过程中,当碰到http块时,将会创建一个ngx_http_conf_ctx_t变量,也就是上面我们提到的配置上下文结构,之后按顺序调用各http模块的create_main/srv/loc_conf方法,然后将生成的http/server/location块的配置项结构体添加到创建好ngx_http_conf_ctx_t变量中(如果某个http模块的create_main_conf方法为NULL呢?那对应位置就为NULL)。那么是按怎样的顺序调用各http模块呢?在我们定义ngx_module_t变量时,曾提到过有两个变量是框架来赋值的,其中的一个就是index变量,每个第三方模块都会被分配一个递增的下标,对第三方模块而言,其index值由它编译进nginx的顺序决定,而nginx正是通过index值从小到大的顺序来进行调用。
index值的大小跟configure脚本的--add-module=Path命令添加进去时的顺序相关,原因是该脚本执行之后将生成一个ngx\_modules.c文件,该文件中会定义一个ngx\_modules数组,其index值与其在数组中的下标相关,下标越大,index值越大,则越靠后被调用
当碰到server块时(每次碰到都会),nginx又会创建一个ngx_http_conf_ctx_t变量,同时按顺序调用各模块的create_srv/loc_conf方法,然后将对应的srv/loc块的结构体添加到创建好的ngx_http_conf_ctx_t变量中,同时将该变量的main_conf指针指向其所属的http块的ngx_http_conf_ctx_t的main_conf指针指向的值(以此保证能寻找到它所属的http块配置)。
当碰到location块时(每次碰到都会,要注意一点是location块是可以嵌套的,不过子location块依然也是这样的逻辑,因此在合并loc级别的配置项时,还可能合并父location与子location块的配置),nginx又会创建一个ngx_http_conf_ctx_t变量,同时按顺序调用各模块的create_loc_conf方法,然后将生成的location块的结构体添加到创建好的ngx_http_conf_ctx_t变量中,同时将该变量的main_conf变量和srv_conf变量分别指向其所属http块的ngx_http_conf_ctx_t的main_conf指针和所属server块的ngx_http_conf_ctx_t的srv_conf指针指向的值(以此保证能寻找到它所属的http块和server块配置项)。
Note:
现在我们再回顾一下定义http模块上下文时的三个函数指针,create_main/srv/loc/_conf,这里我们以mylog日志模块的c
reate_loc_conf函数指针为例,我们看一下它对应的函数声明:
static void* ngx_http_mylog_create_loc_conf(ngx_conf_t *cf);
ngx_conf_t *cf变量中会存储当前的配置项内容,同时它还有一个重要的成员是void *ctx,这个ctx指针指向着的就是当前的配置上下文结构。
这里还忽略了一个问题,上面一共生成了3种级别的ngx_http_conf_ctx_t变量,分别是main级别,srv级别和loc级别,如果说我们拿到了srv级别的ngx_http_conf_ctx_t变量,那么我们能查找到它所属的main级别的ngx_http_conf_ctx_t变量,可如果我们拿到的是main级别的ngx_http_conf_ctx_t变量,却无法对其下属的server块和location块中的配置进行管理。那么nginx是如何处理的呢?
在定义ngx_module_t变量时,第二个由框架来赋值的变量是ctx_index变量,该变量的值表示其所属模块在同一模块类型中的位置,其中ctx_index为0的模块是ngx_http_core_module(nginx的http框架的核心模块),因此在http模块初始化过程中,它是最先被调用的,我们看一下该模块定义的http/server/location块配置项结构体:
//http块配置项结构体的部分定义
typedef struct {
ngx_array_t servers; /* ngx_http_core_srv_conf_t */
...
ngx_http_phase_t phases[NGX_HTTP_LOG_PHASE + 1];//这是我们挂载第三方模块的关键变量,保存了各个阶段的回调函数
} ngx_http_core_main_conf_t;
//server块配置项结构体的部分定义
typedef strcut {
...
ngx_http_conf_ctx_t *ctx;
} ngx_http_core_srv_conf_t;
//location块配置项结构体的部分定义
typedef struct ngx_http_core_loc_conf_s ngx_http_core_loc_conf_t;
struct ngx_http_core_loc_conf_s {
...
ngx_http_handler_pt handler;//这是我们在NGX_HTTP_CONTENT_PHASE阶段挂载第三方模块的关键变量
void **loc_conf;
ngx_queue_t *locations;
};
在ngx_http_core_main_conf_t结构中,servers数组里面存储的元素正是ngx_http_core_srv_conf_t,而ngx_http_core_srv_conf_t中的ctx变量保存了当前server块的配置上下文,因此main级的ngx_http_conf_ctx_t变量将其下的所有server块的配置都管理起来了。
在ngx_http_core_loc_conf_t结构中,locations队列里面最终会存放的依然是ngx_http_core_loc_conf_t变量,而ngx_http_core_loc_conf_t中的loc_conf保存了当前location块的配置上下文(因为碰到location块时,只会调用create_loc_conf方法),所以srv级别的ngx_http_conf_ctx_t变量成功将其下的location块的配置管理起来了,原因是srv级别的ngx_http_conf_ctx_t的ngx_http_core_loc_conf_t变量(由ngx_http_core_module的create_loc_conf生成)会初始化locations队列,该server块下的直属location块配置项都将添加进该locations队列中。另外我们提到过location块是可以嵌套的,比如下面这种结构
...
location L1{
...
location L2{
...
}
location L3{
...
}
...
}
这种结构下在解析location块L1时生成的loc级ngx_http_conf_ctx_t里的ngx_http_core_loc_conf_t变量中的locations将会初始化,L2块和L3块的配置项将添加进该locations队列中。也就是说,location块配置项会添加到上一级的ngx_http_core_loc_conf_t中的locations队列中。
到这里,我们基本理清了nginx对于http块的配置解析流程以及配置管理。
ctx_index和index值有什么联系呢?比如说同一个模块类型的模块A和B,A的ctx_index值 < B的ctx_index值,那么A的index值 < B的index值。那么为什么有了index变量后,还需要一个ctx_index变量呢?因为我们可以根据ctx_index值和http配置上下文结构,直接找到它对应的配置项结构体。
Note:
此处我们再回顾一下ngx_command_t变量中的set函数指针,先看它的声明
char *(*set)(ngx_conf_t *cf,ngx_command_t *cmd,void *conf);
在这里讲解一下各个参数的意义
cf:
上面已经提到了,它存储着该command对应当前的配置内容,以及当前的配置nginx的上下文
cmd:
即查找到的对应配置项的ngx_command_t结构
conf:
通过http模块上下文中的create_main/srv/loc_conf等函数创建出来的结构体,那么这里到底使用的是create_main_
conf创建出来的结构体还是create_srv_conf,或者create_loc_conf创建出来的结构体呢?这一点其实通过上文的了解已
经能够知道了,这里就交给读者自己思考
挂载处理函数
首先,我们需要把我们的逻辑处理函数挂载至日志阶段,nginx的第三方模块挂载一共有两种方式:
1 当需要介入的阶段为NGX_HTTP_CONTENT_PHASE阶段时,如
//在配置命令的set回调函数中,进行挂载
static char* ngx_http_example_set_func(ngx_conf_t *cf,ngx_command_t *cmd,void *conf){
ngx_http_core_loc_conf_t clcf = ngx_http_conf_get_module_loc_conf(cf,ngx_http_core_module);
clcf->handler = ngx_http_example_handler;
return NGX_CONF_OK;
}
此种挂载方法只能用于NGX_HTTP_CONTENT_PHASE阶段,且只对匹配了该location的请求生效。使用此种方法进行挂载时,我们一般都是在对应的ngx_command_t的set回调方法中添加处理方法。
2 获取需要介入阶段的逻辑处理函数数组对象,将我们的逻辑处理函数添加进去,如将mylog日志模块的处理函数挂载上去:
//在postConfiguration函数中进行挂载
//以我们的mylog模块为例
static ngx_int_t ngx_http_mylog_post_init(ngx_conf_t *cf){
ngx_http_handler_pt *h;
ngx_http_core_main_conf_t *cmcf = ngx_http_conf_get_module_main_conf(cf,ngx_http_core_module);
/*
*上文我们已经提过了ngx_http_core_main_conf_t的phases成员保存了各个
*阶段的回调函数
*/
h = ngx_array_push(&cmcf->phases[NGX_HTTP_LOG_PHASE].handlers);
if(!h){
return NGX_ERROR;
}
//挂载mylog模块的处理函数
*h = ngx_http_mylog_handler;
return NGX_OK;
}
此种挂载方法,可适用于所有阶段,且将会对所有请求生效。那么对于NGX_HTTP_CONTENT_PHASE阶段其实是有两种挂载方法的,当同时使用时,只会调用第一种方式挂载的函数。
另外在不同阶段挂载的函数,其函数的返回值也会有各种含义,这里我们只讲解一下NGX_HTTP_LOG_PHASE阶段,其余阶段如果需要的话可google
/* Return:
* NGX_OK:
执行下一阶段中的第一个ngx_http_handler_pt处理方法,即日志阶段的其余方法将被跳过
* NGX_DECLINED:
* 按顺序执行下一个ngx_http_handler_pt方法
* NGX_AGAIN/NGX_DONE:
* 这两个返回值表示当前处理方法还未结束,一般是函数将控制权交给事件模块,当事件发生时再次执行该方法,这里不展开
* NGX_ERROR:
* 将调用ngx_http_finalize_request结束请求
*/
static ngx_int_t ngx_http_mylog_handler(ngx_http_request_t *r);
通过第二种方法,将自定义函数Push进逻辑处理函数数组对象时,该阶段的函数的执行顺序是怎样的?当然这里既然提出
来了,读者想必也猜到了是逆序执行的。
因此mylog日志模块如果不想影响到nginx http框架的日志模块的话,返回值应该用NGX_DECLINED。
如果说想直接跳过的话,那返回值设为NGX_OK即可。在这里我们用NGX_DECLINED,即不影响nginx http框架的日志模块。
获取日志数据
在上文中我们定义了mylog_format配置命令用于设置日志格式(一些内置变量),我们将在我们的自定义处理函数ngx_http_mylog_handler函数中获取mylog_format里设置的各变量的值。
在mylog_format里存储了mylog模块需要打印的nginx内部变量值,nginx的变量机制如果要展开讲的话,需要很多笔墨,
由于是入门篇,这里就不详细说明了。但是有一点需要提一下,对变量进行索引时,必须在配置解析过程中的postConfig
uration函数调用前(强行要在这个函数里进行其实也是可以的,但对于此函数我们一般都是用于挂载第三方模块),因为在配置解析过
程的末尾阶段还会对索引过的变量进行初始化操作,因此我们一般就是在对应的配置项命令的set函数中对需要的变量进行索引。
在讲解ngx_http_mylog_handler函数的实现前,我们先看一下定义ngx_command_t时,mylog_format命令对应的set函数的实现。该函数用于解析mylog_format配置命令,并将相应的内部变量索引化(提升访问速度),供ngx_http_mylog_handler函数使用。
static char* ngx_http_mylog_set_format(ngx_conf_t *cf,ngx_command_t *cmd,void *conf){
ngx_str_t *fmt;
ngx_str_t value;
ngx_str_t var;
ngx_uint_t i,j,k;
ngx_int_t index;
ngx_http_mylog_var_t *mylog_var;
ngx_http_mylog_conf_t *mlcf = conf;
//初始化formats数组
if(mlcf->mylog_formats == NGX_CONF_UNSET_PTR){
//数组初始化大小为2
mlcf->mylog_formats = ngx_array_create(cf->pool,2,sizeof(ngx_http_mylog_var_t));
}
fmt = cf->args->elts;
for(i = 0; i < cf->args->nelts; i++){
value = fmt[i];
j = 0;
while(j < value.len){
if(value.data[j] == ‘$‘){
k = ++j;
var.data = &value.data[k];
while(j < value.len && value.data[j] != ‘ ‘
&& value.data[j] != ‘$‘) j++;
var.len = j - k;
if(var.len == 0){
ngx_conf_log_error(NGX_LOG_EMERG,cf,0,"mylog_format can‘t has empty variable");
return NGX_CONF_ERROR;
}
//获取变量索引
index = ngx_http_get_variable_index(cf,&var);
if(index == NGX_ERROR){
ngx_conf_log_error(NGX_LOG_EMERG,cf,0,"mylog_format get variable index error,name:%V",&var);
}
mylog_var = ngx_array_push(mlcf->mylog_formats);
mylog_var->name = var;
mylog_var->index = index;
ngx_conf_log_error(NGX_LOG_EMERG,cf,0,"mylog_format add variable:%V success",&var);
continue;
}
j++;
}
}
return NGX_CONF_OK;
}
接下来我们在ngx_http_mylog_handler函数中获取对应变量的值,并将其打印出来,其代码如下:
static ngx_int_t ngx_http_mylog_handler(ngx_http_request_t *r){
ngx_log_error(NGX_LOG_EMERG,r->connection->log,0,"ngx_http_mylog_handler");
ngx_http_mylog_conf_t *lccf = ngx_http_get_module_loc_conf(r,ngx_http_mylog_module);
//开启了开关的才会执行mylog日志模块的逻辑,loc conf里mylog_switch只可能为0或者1,不可能为NGX_CONF_UNSET
if(lccf->mylog_switch == 1){
//loc conf里mylog_switch只可能为NULL或者正常初始化,不可能为NGX_CONF_UNSET_PTR
if(lccf->mylog_formats){
ngx_uint_t i;
ngx_http_mylog_var_t var;
ngx_http_variable_value_t *value;
ngx_http_mylog_var_t *vars = lccf->mylog_formats->elts;
for(i = 0; i < lccf->mylog_formats->nelts; i++){
var = vars[i];
value = ngx_http_get_indexed_variable(r,var.index);
if(!value || value->not_found){
continue;
}
ngx_log_error(NGX_LOG_EMERG,r->connection->log,0,"variable:%V , value:%s",&var.name,value->data);
}
}
}
return NGX_DECLINED;
}
模块编译
nginx的源码包提供了一个名为configure的脚本,我们可以通过--add-module=Path的方式将我们的第三方模块设置进去,configure脚本将会去寻找Path路径下的config文件(没错,这个文件的文件名就叫config),根据config文件生成对应的编译指令至Makefile文件中。因此,我们需要先创建我们的config文件,对于mylog日志模块,其config文件内容如下:
ngx_addon_name=ngx_http_mylog_module
HTTP_MODULES="$HTTP_MODULES ngx_http_mylog_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_mylog_module.c"
接下来解释一下,在config文件中常用的变量及含义
ngx_addon_name
该变量一般设置为模块名,并没有什么特殊的用处,不管它也没关系,标识作用
ngx_addon_dir
该变量保存着模块所在的路径,跟我们使用configure脚本的--add-module=Path命令中的Path一致
HTTP_MODULES
该变量保存着所有的模块名称,不能直接覆盖,在这里需要把我们的mylog模块添加进去。另外如果开发的是过滤模块的话需要使用的是HTTP_FILTER_MODULE或者HTTP_FILTER_HEADERS_MODULE
NGX_ADDON_SRCS
第三方模块的源代码文件
NGX_ADDON_DEPS
第三方模块依赖的文件
CORE_INCS
可在此添加依赖路径,注意不能直接覆盖,需要像HTTP_MODULES变量一样append进去
CORE_LIBS
可在此添加生成nginx时的链接指令,比如需要链接第三方库,可在此添加对应的链接指令,同样不能直接覆盖,需要append进去
如果config文件不能满足你的需求,你也可以直接修改configure脚本生成的Makefile文件。比如对于我们的mylog模块,在执行过configure脚本后的nginx目录中找到obs/Makefile文件,可以看到我们的目标文件的相关编译指令:
//写过makefile文件的同学一眼就能理解,就不赘述了
objs/addon/ngx_http_mylog_module/ngx_http_mylog_module.o: $(ADDON_DEPS) ../ngx_http_mylog_module//ngx_http_mylog_module.c
$(CC) -c $(CFLAGS) $(ALL_INCS) -o objs/addon/ngx_http_mylog_module/ngx_http_mylog_module.o ../ngx_http_mylog_module//ngx_http_mylog_module.c
不建议不通过在configure脚本中添加第三方模块的方法来添加第三方模块。初学者可忽略这句话。
完整源码
下面给出mylog日志模块的完整源码,以及对应的config文件,以及将其编译进nginx所需的配置命令。
//ngx_http_mylog_module.c源码
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>
//用于存储解析出来的配置项值
typedef struct{
ngx_str_t name; //变量名
ngx_int_t index; //该变量的下标
}ngx_http_mylog_var_t;
typedef struct{
ngx_flag_t mylog_switch;
ngx_array_t *mylog_formats;//数组,在这里用来存储ngx_http_mylog_var_t
}ngx_http_mylog_conf_t;
/*
*声明配置项解析函数
* cf: 该变量保存着当前的配置内容
* cmd: 对应的上文提到的cmd指令
* conf:通过create_main/srv/loc_conf(下文会提到) 创建出来的结构体
*/
static char* ngx_http_mylog_set_switch(ngx_conf_t *cf,ngx_command_t *cmd,void *conf);
static char* ngx_http_mylog_set_format(ngx_conf_t *cf,ngx_command_t *cmd,void *conf);
//模块上下文函数声明
static ngx_int_t ngx_http_mylog_post_init(ngx_conf_t *cf);
static void* ngx_http_mylog_create_loc_conf(ngx_conf_t *cf);
static char* ngx_http_mylog_merge_loc_conf(ngx_conf_t *cf,void *prev,void *conf);
//该函数用于挂载到日志阶段,执行mylog模块的逻辑
static ngx_int_t ngx_http_mylog_handler(ngx_http_request_t *r);
//定义我们需要新增的配置项,该变量必须为数组类型,且以ngx_null_command结尾
static ngx_command_t ngx_http_mylog_commands[] = {
{
ngx_string("mylog_switch"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_http_mylog_set_switch,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mylog_conf_t,mylog_switch),
NULL
},
{
ngx_string("mylog_format"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_1MORE,
ngx_http_mylog_set_format,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL
},
ngx_null_command
};
//定义日志模块上下文结构
static ngx_http_module_t ngx_http_mylog_module_ctx = {
NULL,
ngx_http_mylog_post_init,
NULL,
NULL,
NULL,
NULL,
ngx_http_mylog_create_loc_conf,
ngx_http_mylog_merge_loc_conf
};
ngx_module_t ngx_http_mylog_module = {
NGX_MODULE_V1,
&ngx_http_mylog_module_ctx,
ngx_http_mylog_commands,
NGX_HTTP_MODULE,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NGX_MODULE_V1_PADDING
};
static char* ngx_http_mylog_set_switch(ngx_conf_t *cf,ngx_command_t *cmd,void *conf){
ngx_str_t *value = cf->args->elts;
ngx_conf_log_error(NGX_LOG_EMERG,cf,0,"ngx_http_mylog_set_switch:%V",&value[1]);
return ngx_conf_set_flag_slot(cf,cmd,conf);
}
static char* ngx_http_mylog_set_format(ngx_conf_t *cf,ngx_command_t *cmd,void *conf){
ngx_str_t *fmt;
ngx_str_t value;
ngx_str_t var;
ngx_uint_t i,j,k;
ngx_int_t index;
ngx_http_mylog_var_t *mylog_var;
ngx_http_mylog_conf_t *mlcf = conf;
//初始化formats数组
if(mlcf->mylog_formats == NGX_CONF_UNSET_PTR){
//数组初始化大小为2
mlcf->mylog_formats = ngx_array_create(cf->pool,2,sizeof(ngx_http_mylog_var_t));
}
fmt = cf->args->elts;
for(i = 0; i < cf->args->nelts; i++){
value = fmt[i];
j = 0;
while(j < value.len){
if(value.data[j] == ‘$‘){
k = ++j;
var.data = &value.data[k];
while(j < value.len && value.data[j] != ‘ ‘
&& value.data[j] != ‘$‘) j++;
var.len = j - k;
if(var.len == 0){
ngx_conf_log_error(NGX_LOG_EMERG,cf,0,"mylog_format can‘t has empty variable");
return NGX_CONF_ERROR;
}
//获取变量索引
index = ngx_http_get_variable_index(cf,&var);
if(index == NGX_ERROR){
ngx_conf_log_error(NGX_LOG_EMERG,cf,0,"mylog_format get variable index error,name:%V",&var);
}
mylog_var = ngx_array_push(mlcf->mylog_formats);
mylog_var->name = var;
mylog_var->index = index;
ngx_conf_log_error(NGX_LOG_EMERG,cf,0,"mylog_format add variable:%V success",&var);
continue;
}
j++;
}
}
return NGX_CONF_OK;
}
//配置块结构体创建
static void* ngx_http_mylog_create_loc_conf(ngx_conf_t *cf){
ngx_conf_log_error(NGX_LOG_EMERG,cf,0,"ngx_http_mylog_create_loc_conf");
ngx_http_mylog_conf_t *conf = ngx_pcalloc(cf->pool,sizeof(ngx_http_mylog_conf_t));
if(!conf)
return NULL;
//由于要使用预定义解析函数,此处必须初始化为NGX_CONF_UNSET,否则ngx_conf_set_flag_slot会认为已经赋值过,而不处理
conf->mylog_switch = NGX_CONF_UNSET;
//初始化formats数组
conf->mylog_formats = NGX_CONF_UNSET_PTR;
return conf;
}
//配置合并函数
static char* ngx_http_mylog_merge_loc_conf(ngx_conf_t *cf, void *prev, void *conf){
//child为子块的配置,parent为父块的配置,这里我们的合并规则是,优先用子块自己的配置,如果子块没设置,则用父块的配置,默认为空
ngx_http_mylog_conf_t *parent = prev;
ngx_http_mylog_conf_t *child = conf;
ngx_conf_merge_value(child->mylog_switch,parent->mylog_switch,0);
ngx_conf_merge_ptr_value(child->mylog_formats,parent->mylog_formats,NULL);
return NGX_CONF_OK;
}
//在postConfiguration函数中进行挂载
static ngx_int_t ngx_http_mylog_post_init(ngx_conf_t *cf){
ngx_http_handler_pt *h;
ngx_http_core_main_conf_t *cmcf = ngx_http_conf_get_module_main_conf(cf,ngx_http_core_module);
h = ngx_array_push(&cmcf->phases[NGX_HTTP_LOG_PHASE].handlers);
if(!h){
return NGX_ERROR;
}
//挂载mylog模块的处理函数
*h = ngx_http_mylog_handler;
return NGX_OK;
}
static ngx_int_t ngx_http_mylog_handler(ngx_http_request_t *r){
ngx_log_error(NGX_LOG_EMERG,r->connection->log,0,"ngx_http_mylog_handler");
ngx_http_mylog_conf_t *lccf = ngx_http_get_module_loc_conf(r,ngx_http_mylog_module);
//开启了开关的才会执行mylog日志模块的逻辑,loc conf里mylog_switch只可能为0或者1,不可能为NGX_CONF_UNSET
if(lccf->mylog_switch == 1){
//loc conf里mylog_switch只可能为NULL或者正常初始化,不可能为NGX_CONF_UNSET_PTR
if(lccf->mylog_formats){
ngx_uint_t i;
ngx_http_mylog_var_t var;
ngx_http_variable_value_t *value;
ngx_http_mylog_var_t *vars = lccf->mylog_formats->elts;
for(i = 0; i < lccf->mylog_formats->nelts; i++){
var = vars[i];
value = ngx_http_get_indexed_variable(r,var.index);
if(!value || value->not_found){
continue;
}
ngx_log_error(NGX_LOG_EMERG,r->connection->log,0,"variable:%V , value:%s",&var.name,value->data);
}
}
}
return NGX_DECLINED;
}
//config文件内容
ngx_addon_name=ngx_http_mylog_module
HTTP_MODULES="$HTTP_MODULES ngx_http_mylog_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_mylog_module.c"
//将其加入nginx的编译指令所需的配置命令,由于我存放的mylog模块所在目录与nginx同级,因此使用../ngx_http_mylog_module
./configure ...(编译nginx需要的其它命令) --add-module=../ngx_http_mylog_module
//如果需要使用,可以在nginx配置文件中加上如下两条指令
mylog_switch on;
mylog_format ‘$status $body_bytes_sent $upstream_response_time‘;
发起请求后,查看error log文件,可通过日志观察mylog模块的执行情况
结尾
在背景部分已经提过了,写这篇文章的初衷,是因为在网上很少看到关于nginx http模块开发入门的拿捏的比较准的文章,但自己也开始动手时,才发现要写下来确实很不容易,很多地方可能描述的不够清晰,主要还是受限自己的水平和文笔。另外也因为在网上看过很多介绍tcp的文章,发现很多的文章描述都不准确,而且缺乏对tcp协议的思考,有感于此,自己也写了一篇tcp相关的文章,里面融入了我对于tcp协议的部分设计的理解,有兴趣的读者可查看本博客中的tcp随笔一文。