ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
# 7 自定义Nginx模块 ##7.1 ngx_command_t 数组 commands 数组用于定义模块的配置文件参数,每一个数组元素都是`ngx_command_t`类型,数组的结尾是用`ngx_numm_command`表示。Nginx在解析配置文件中的一个配置项时首先会遍历所有的模块,对于每一个模块而言,即通过遍历commands数组进行,另外,在数组中检查到ngx_numm_command时,会停止使用当前模块解析该配置项。每一个ngx_command_t结构体定义个如下配置: ```cpp typedef struct ngx_command_s ngx_command_t; struct ngx_command_s { //配置项名称,如"gzip" ngx_str_t name; //配置项类型,type将制定配置项可以出现的位置, //例如:出现在server{}活location{}中,等等 ngx_uint_t type; //出现了name中指定的配置项后,将会调用set方法处理配置项的参数 char *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); //在配置文件中的偏移量 ngx_uint_t conf; //通常用于使用预设的解析方法解析配置项,这是配置模块的一个优秀的设计, //需要与conf配合使用 ngx_uint_t offset; //配置项读取后的处理方法,必须是ngx_conf_post_t结构的指针 void *post; }; ``` ngx_null_command只是一个空的ngx_command_s: ```cpp #define ngx_null_command {ngx_null_string, 0, NULL, 0, 0, NULL} ``` 也就是说,对于我们在nginx.cong 中编写的配置项mytest来说,nginx首先会遍历所有的模块(modules),而对于每个模块,会遍历他所对应的ngx_command_t数组,试图找到关于我们配置项目mytest的解析方式。 ```cpp static ngx_command_t ngx_http_mytest_commands[] = { { ngx_string("mytest"), NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_HTTP_LMT_CONF | NGX_CONF_NOARGS, ngx_http_mytest, NGX_HTTP_LOC_CONF_OFFSET, 0, NULL }, ngx_null_command }; ``` ##7.2 command中用于处理配置项参数的set方法 ```cpp static char * ngx_http_mytest(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_http_core_loc_conf_t *clcf; //首先找到mytest配置项所属的配置块,clcf貌似是location块内的数据 //结构,其实不然,它可以是main、srv或者loc级别配置项,也就是说在每个 //http{}和server{}内也都有一个ngx_http_core_loc_conf_t结构体 clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module); //http框架在处理用户请求进行到NGX_HTTP_CONTENT_PHASE阶段时,如果 //请求的主机域名、URI与mytest配置项所在的配置块相匹配,就将调用我们 //实现的ngx_http_mytest_handler方法处理这个请求 clcf->handler = ngx_http_mytest_handler; return NGX_CONF_OK; } ``` 关于ngx_http_conf_get_module_loc_conf 的定义可以参考: http://lxr.nginx.org/source/src/http/ngx_http_config.h#0065 >本质: 就是设置`ngx_http_mytest_handler`, 匹配项被选中的时候, 应该如何解析。 ##7.3 定义ngx_http_module_t接口 这部分的代码, 是用于http框架的, 相当于http框架的回掉函数, 由于这里并不需要框架做任何操作。 ```cpp static ngx_http_module_t ngx_http_mytest_module_ctx = { NULL, /* preconfiguration */ NULL, /* postconfiguration */ NULL, /* create main configuration */ NULL, /* init main configuration */ NULL, /* create server configuration */ NULL, /* merge server configuration */ NULL, /* create location configuration */ NULL /* merge location configuration */ }; ``` ##7.4 定义处理“mytest”command的handler 我们这里需要定义配置项被匹配之后的处理方法. ```cpp static ngx_int_t ngx_http_mytest_handler(ngx_http_request_t *r) { //必须是GET或者HEAD方法,否则返回405 Not Allowed if (!(r->method & (NGX_HTTP_GET | NGX_HTTP_HEAD))) { return NGX_HTTP_NOT_ALLOWED; } //丢弃请求中的包体 ngx_int_t rc = ngx_http_discard_request_body(r); if (rc != NGX_OK) { return rc; } //设置返回的Content-Type。注意,ngx_str_t有一个很方便的初始化宏 //ngx_string,它可以把ngx_str_t的data和len成员都设置好 ngx_str_t type = ngx_string("text/plain"); //返回的包体内容 ngx_str_t response = ngx_string("Hello World!"); //设置返回状态码 r->headers_out.status = NGX_HTTP_OK; //响应包是有包体内容的,所以需要设置Content-Length长度 r->headers_out.content_length_n = response.len; //设置Content-Type r->headers_out.content_type = type; //发送http头部 rc = ngx_http_send_header(r); if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) { return rc; } //构造ngx_buf_t结构准备发送包体 ngx_buf_t *b; b = ngx_create_temp_buf(r->pool, response.len); if (b == NULL) { return NGX_HTTP_INTERNAL_SERVER_ERROR; } //将Hello World拷贝到ngx_buf_t指向的内存中 ngx_memcpy(b->pos, response.data, response.len); //注意,一定要设置好last指针 b->last = b->pos + response.len; //声明这是最后一块缓冲区 b->last_buf = 1; //构造发送时的ngx_chain_t结构体 ngx_chain_t out; //赋值ngx_buf_t out.buf = b; //设置next为NULL out.next = NULL; //最后一步发送包体,http框架会调ngx_http_finalize_request方法 //结束请求 return ngx_http_output_filter(r, &out); } ``` ##7.5 定义ngx_module_t中的mytest模块 定义HTTP模块方式很简单,例如: ```cpp ngx_module_t ngx_http_mytest_module; ``` 其中ngx_module_t是一个Nginx模块的数据结构,下面来分析一下Nginx模块中的所有成员: ```cpp typedef struct ngx_module_s ngx_module_t; struct ngx_module_s { /* 下面的ctx_index、index、spare0,spare1, spare2,spare3,version变量不需要再定义时候赋值, 可以用Nginx准备好的宏NGX_MODULE_V1来定义, 已经定义好了这7个值 #define NGX_MODULE_V1 0,0,0,0,0,0,1 对于一类模块(由下面的type成员决定类别)而言, ctx_index表示当前模块在这类模块中的序号。 这个成员常常是由管理这类模块的一个Nginx核心模块设置的, 对于所有的HTTP模块而言,ctx_index是由 核心模块ngx_http_module设置的。 ctx_index非常重要,Nginx的模块化设计非常依赖于各个模块的顺序, 他们既用于表达优先级,也用于表明每个模块的位置, 借以帮助Nginx框架快速获得某个模块的数据. */ ngx_uint_t ctx_index; /* index表示当前模块在ngx_module数组中的序号。 注意,ctx_index表示的是当前模块在一类模块中的序号, 而index表示当前模块在所有模块中的序号,同样很关键. Nginx启动时会根据ngx_modules数组设置各模块的index值, 例如: ngx_max_module = 0; for(i = 0; ngx_module[i]; i++) { ngx_modules[i]->index = ngx_max_module++; } */ ngx_uint_t index; //spare系列的保留变量,暂未使用 ngx_uint_t spare0; ngx_uint_t spare1; ngx_uint_t spare2; ngx_uint_t spare3; //模块的版本,便于将来的拓展,目前只有一种,默认为1 ngx_uint_t version; /* ctx用于指向一类模块的上下文结构体, 为什么需要ctx呢?因为前面说过, Nginx模块有许多种类,不同类模块之间的功能差别很大。 例如, 事件类型的模块主要处理I/O事件的相关功能, HTTP类型的模块主要处理HTTP应用层的功能。 这样每个模块都有了自己的特性, 而ctx会指向特定类型模块的公共接口。 例如, 在HTTP模块中,ctx需要指向ngx_http_module_t结构体 */ void *ctx; /* commands处理nginx.conf中的配置项 */ ngx_command_t *commands; /* type表示该模块的类型,它与ctx指针是紧密相关的。 在官方Nginx中,它的取值范围有以下5种: NGX_HTTP_MODULE NGX_CORE_MODULE NGX_CONF_MODULE NGX_EVENT_MODULE NGX_MAIL_MODULE */ ngx_uint_t type; /* 在Nginx的启动、停止过程中,以下7个函数指针表示7个 执行点分别用调用这7种方法。对于任意一个方法而言, 如果不需要Nginx再某个时刻执行它,那么简单地把它设为NULL 空指针即可。 */ /* 虽然从字面上理解应当在master进程启动的时,回调 init_master,但到目前为止,框架代码从来不会调动它, 所以设置为NULL */ ngx_int_t (*init_master)(ngx_log_t *log); /* init_module回调方法在初始化所有模块时候被调用。 在master/worker模式下,这个阶段将在启动worker子进程前完成。 */ ngx_int_t (*init_module)(ngx_cycle_t *cycle); /* init_process 回调方法在正常服务前被调用。 在master/worker模式下,多个worker子进程已经产生。 在每个worker进程的初始化过程会调用所有模块的init_process函数 */ ngx_int_t (*init_process)(ngx_cycle_t *cycle); /* 由于Nginx暂时不支持多线程模式,所以init_thread在框架中 没有被调用过,设置为NULL */ ngx_int_t (*init_thread)(ngx_cycle_t *cycle); /* 同上、exit_thread也不支持,设为NULL */ void (*exit_thread)(ngx_cycle_t *cycle); /* exit_process回调方法在服务停止前被调用。 在/master/worker模式下,worker进程会在退出前调用 */ void (*exit_process)(ngx_cycle_t *cycle); /* exit_master回调方法将在master进程退出前被调用 */ void (*exit_master)(ngx_cycle_t *cycle); /* 一下8个spare_hook变量也是保留字段,目前没有使用, 但可用Nginx提供的NGX_MODULE_V1_PADDING宏来填充 #define NGX_MODULE_V1_PADDING 0,0,0,0,0,0,0,0 */ uintptr_t spare_hook0; uintptr_t spare_hook1; uintptr_t spare_hook2; uintptr_t spare_hook3; uintptr_t spare_hook4; uintptr_t spare_hook5; uintptr_t spare_hook6; uintptr_t spare_hook7; }; ``` 所以在定义一个HTTP模块的时候,务必把type字段设置为NGX_HTTP_MODULE. 对于下列回调方法:init_module、init_process、exit_process、exit_master 调用他们的是Nginx框架代码。换句话说,这4个回调方法与HTTP框架无关。 即使nginx.conf中没有设置http{...}这种开启HTTP功能的配置项,这些 回调方法仍然会被调用。因此,通常开发HTTP模块时候都把他们设置为NULL。 这样Nginx不作为web服务器使用时,不会执行HTTP模块的任何代码。 定义HTTP时候,最重要的是要设置ctx和commands这两个成员。 对于HTTP类型模块来说,ngx_module_中的ctx指针必须指向ngx_http_module_t接口。 所以最终我们定义个ngx_module_t 模块如下: ```cpp ngx_module_t ngx_http_mytest_module = { NGX_MODULE_V1, &ngx_http_mytest_module_ctx, /* module context */ ngx_http_mytest_commands, /* module directives */ NGX_HTTP_MODULE, /* module type */ NULL, /* init master */ NULL, /* init module */ NULL, /* init process */ NULL, /* init thread */ NULL, /* exit thread */ NULL, /* exit process */ NULL, /* exit master */ NGX_MODULE_V1_PADDING }; ``` 这样, mytest 模块在编译的时候, 就可以被加入到ngx_modules的全局数组中了。 ## 7.6 完整代码 ngx_http_mytest_module.c 所以全部的编码工作完毕,最终的代码应该如下: ```cpp #include <ngx_config.h> #include <ngx_core.h> #include <ngx_http.h> #include <sys/types.h> #include <unistd.h> //定义处理用户请求hello world handler static ngx_int_t ngx_http_mytest_handler(ngx_http_request_t *r) { //必须是GET或者HEAD方法,否则返回405 Not Allowed if (!(r->method & (NGX_HTTP_GET | NGX_HTTP_HEAD))) { return NGX_HTTP_NOT_ALLOWED; } //丢弃请求中的包体 ngx_int_t rc = ngx_http_discard_request_body(r); if (rc != NGX_OK) { return rc; } //设置返回的Content-Type。注意,ngx_str_t有一个很方便的初始化宏 //ngx_string,它可以把ngx_str_t的data和len成员都设置好 ngx_str_t type = ngx_string("text/plain"); //返回的包体内容 ngx_str_t response = ngx_string("Hello World!"); //设置返回状态码 r->headers_out.status = NGX_HTTP_OK; //响应包是有包体内容的,所以需要设置Content-Length长度 r->headers_out.content_length_n = response.len; //设置Content-Type r->headers_out.content_type = type; //发送http头部 rc = ngx_http_send_header(r); if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) { return rc; } //构造ngx_buf_t结构准备发送包体 ngx_buf_t *b; b = ngx_create_temp_buf(r->pool, response.len); if (b == NULL) { return NGX_HTTP_INTERNAL_SERVER_ERROR; } //将Hello World拷贝到ngx_buf_t指向的内存中 ngx_memcpy(b->pos, response.data, response.len); //注意,一定要设置好last指针 b->last = b->pos + response.len; //声明这是最后一块缓冲区 b->last_buf = 1; //构造发送时的ngx_chain_t结构体 ngx_chain_t out; //赋值ngx_buf_t out.buf = b; //设置next为NULL out.next = NULL; //最后一步发送包体,http框架会调用ngx_http_finalize_request方法 //结束请求 return ngx_http_output_filter(r, &out); } //定义command用于处理配置项参数的set方法 static char * ngx_http_mytest(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_http_core_loc_conf_t *clcf; //首先找到mytest配置项所属的配置块,clcf貌似是location块内的数据 //结构,其实不然,它可以是main、srv或者loc级别配置项,也就是说在每个 //http{}和server{}内也都有一个ngx_http_core_loc_conf_t结构体 clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module); //http框架在处理用户请求进行到NGX_HTTP_CONTENT_PHASE阶段时,如果 //请求的主机域名、URI与mytest配置项所在的配置块相匹配,就将调用我们 //实现的ngx_http_mytest_handler方法处理这个请求 clcf->handler = ngx_http_mytest_handler; return NGX_CONF_OK; } //定义ngx mytest 配置匹配的command static ngx_command_t ngx_http_mytest_commands[] = { { ngx_string("mytest"), NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_HTTP_LMT_CONF | NGX_CONF_NOARGS, ngx_http_mytest, NGX_HTTP_LOC_CONF_OFFSET, 0, NULL }, ngx_null_command }; //定义ngx_http_module_t接口 static ngx_http_module_t ngx_http_mytest_module_ctx = { NULL, /* preconfiguration */ NULL, /* postconfiguration */ NULL, /* create main configuration */ NULL, /* init main configuration */ NULL, /* create server configuration */ NULL, /* merge server configuration */ NULL, /* create location configuration */ NULL /* merge location configuration */ }; //定义mytest模块 ngx_module_t ngx_http_mytest_module = { NGX_MODULE_V1, &ngx_http_mytest_module_ctx, /* module context */ ngx_http_mytest_commands, /* module directives */ NGX_HTTP_MODULE, /* module type */ NULL, /* init master */ NULL, /* init module */ NULL, /* init process */ NULL, /* init thread */ NULL, /* exit thread */ NULL, /* exit process */ NULL, /* exit master */ NGX_MODULE_V1_PADDING }; ``` ## 7.7 配置文件 config 但是之后我们还需要给该模块相同目录下提供一个配置文件"config"表示在nginx编译的时候的一些编译选项。 ```bash ngx_addon_name=ngx_http_mytest_module HTTP_MODULES="$HTTP_MODULES ngx_http_mytest_module" NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_mytest_module.c" ``` 所以我们的模块编写完毕了,当前目录应该有两个文件 ```bash ls config ngx_http_mytest_module.c ``` ## 7.8 重新编译Nginx 并添加自定义模块 进入Nginx源码目录 执行 ```bash ./configure --prefix=/usr/local/nginx --add-module=/home/ace/openSource_test/nginx_module_http_test ``` --add-module为刚才自定义模块源码的目录 ```bash make sudo make install ``` ## 测试自定义模块 修改nginx.conf文件 ```php server { listen 8777; server_name localhost; location / { mytest;#我们自定义模块的名称 } } ``` 重新启动nginx 打开浏览器输入地址和端口 ![](https://img.kancloud.cn/88/ae/88aec56dd54a781ee152d6c07b2c1a99_692x240.png)