Nginx扩展开发初探
一、初探Nginx架构
Nginx 作为一款面向性能设计的HTTP服务器,相较于Apache、lighttpd具有占有内存少,稳定性高等优势。其流行度越来越高,应用也越来越广泛,常见的应用有:
- 网页服务器
- 反向代理服务器
- 电子邮件(IMAP/POP3)代理服务器
高并发大流量站点常用来做接入层的负载均衡,还有非常常见的用法是作为日志采集服务器等。
Nginx 整体采用模块化设计,有丰富的模块库和第三方模块库,配置灵活。其中模块化设计是nginx的一大卖点,甚至http服务器核心功能也是一个模块。要注意的是:nginx的模块是静态的,添加和删除模块都要对nginx进行重新编译,这一点与Apache的动态模块完全不同。
0x1:master-slave架构
nginx在启动后,在unix系统中会以daemon的方式在后台运行,后台进程包含一个master进程和多个worker进程。我们也可以手动地关掉后台模式,让nginx在前台运行,并且通过配置让nginx取消master进程,从而可以使nginx以单进程方式运行(很显然,生产环境下我们肯定不会这么做,所以关闭后台模式,一般是用来调试用的)。
一般来说,nginx是以多进程的方式来工作的,当然nginx也是支持多线程的方式的,只是我们主流的方式还是多进程的方式,也是nginx的默认方式。
nginx在启动后,会有一个master进程和多个worker进程:
- master进程主要用来管理worker进程,包含:
- 接收来自外界的信号
- 向各worker进程发送信号
- 监控worker进程的运行状态
- 当worker进程退出后(异常情况下),会自动重新启动新的worker进程
- woker进程:基本的网络事件,则是放在worker进程中来处理了
- 多个worker进程之间是对等的,他们同等竞争来自客户端的请求,各进程互相之间是独立的
- 一个请求,只可能在一个worker进程中处理,一个worker进程,不可能处理其它进程的请求
- worker进程的个数是可以设置的,一般我们会设置与机器cpu核数一致,这里面的原因与nginx的进程模型以及事件处理模型原理有关
从上图中我们可以看到,master来管理worker进程,所以我们只需要与master进程通信就行了。master进程会接收来自外界发来的信号,再根据信号做不同的事情。
所以我们要控制nginx,只需要通过kill向master进程发送信号就行了。比如kill -HUP pid,则是告诉nginx,从容地重启nginx,我们一般用这个信号来重启nginx,或重新加载配置,因为是从容地重启,因此服务是不中断的,整个重启过程如下:
- 首先master进程在接到信号后,会先重新加载配置文件
- 然后再启动新的worker进程,并向所有老的worker进程发送信号,告诉他们可以光荣退休了
- 新的worker在启动后,就开始接收新的请求(并获取新的配置文件)
- 老的worker在收到来自master的信号后,就不再接收新的请求,并且在当前进程中的所有未处理完的请求处理完成后,再退出
当然,直接给master进程发送信号,这是比较老的操作方式,nginx在0.8版本之后,引入了一系列命令行参数,来方便我们管理。比如
- ./nginx -s reload:重启nginx: 执行命令时,我们是启动一个新的nginx进程,而新的nginx进程在解析到reload参数后,就知道我们的目的是控制nginx来重新加载配置文件了,它会向master进程发送信号,然后接下来的动作,就和我们直接向master进程发送信号一样了
- ./nginx -s stop:停止nginx的运行
另一方面,worker进程之间是平等的,每个进程,处理请求的机会也是一样的。当我们提供80端口的http服务时,一个连接请求过来,每个进程都有可能处理这个连接,整个work处理过程如下:
- 每个worker进程都是从master进程fork过来
- 在master进程里面,先建立好需要listen的socket(listenfd)之后,然后再fork出多个worker进程
- 所有worker进程的listenfd会在新连接到来时变得可读(子进程和父进程通过fork共享文件句柄,这使得所有worker进程能够拥有同等的机会处理本次请求),为保证只有一个进程处理该连接,所有worker进程在注册listenfd读事件前抢accept_mutex,抢到互斥锁的那个进程注册listenfd读事件,在读事件里调用accept接受该连接(connfd)。注意,所有worker进程都会去争夺listenfd的读权限,但只有一个worker能最终获得,并调用accept获得connfd,进行后续的动作,而其他未争夺到本次listenfd的worker则继续等待下一次连接并争夺listenfd
- 当一个worker进程在accept这个连接之后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开连接,这样一个完整的请求就是这样的了
我们可以看到,一个请求,完全由worker进程来处理,而且只在一个worker进程中处理。
0x2:Nginx Master & Worker架构的优势
- 对于每个worker进程来说,独立的进程,不需要加锁,所以省掉了锁带来的开销
- 在编程以及问题查找时,也会方便很多
- 采用独立的进程,可以让互相之间不会影响,一个进程退出后,其它进程还在工作,服务不会中断,master进程则很快启动新的worker进程
0x3:nginx的模块化体系结构
nginx的模块根据其功能基本上可以分为以下几种类型:
event module: | 搭建了独立于操作系统的事件处理机制的框架,及提供了各具体事件的处理。包括ngx_events_module, ngx_event_core_module和ngx_epoll_module等。nginx具体使用何种事件处理模块,这依赖于具体的操作系统和编译选项。 |
---|---|
phase handler: | 此类型的模块也被直接称为handler模块。主要负责处理客户端请求并产生待响应内容,比如ngx_http_static_module模块,负责客户端的静态页面请求处理并将对应的磁盘文件准备为响应内容输出。 |
output filter: | 也称为filter模块,主要是负责对输出的内容进行处理,可以对输出进行修改。例如,可以实现对输出的所有html页面增加预定义的footbar一类的工作,或者对输出的图片的URL进行替换之类的工作。 |
upstream: | upstream模块实现反向代理的功能,将真正的请求转发到后端服务器上,并从后端服务器上读取响应,发回客户端。upstream模块是一种特殊的handler,只不过响应内容不是真正由自己产生的,而是从后端服务器上读取的。 |
load-balancer: | 负载均衡模块,实现特定的算法,在众多的后端服务器中,选择一个服务器出来作为某个请求的转发服务器。 |
参考链接:
http://tengine.taobao.org/book/chapter_02.html# https://www.kancloud.cn/kancloud/master-nginx-develop/51805
二、Nginx编译、安装、配置
0x1:编译安装
1. wget https://codeload.github.com/nginx/nginx/zip/master
2. cd /usr/local/nginx/nginx-master
3. ./auto/configure --prefix=/usr/local/nginx
make
make install
//启动nginx
/usr/local/nginx/sbin/nginx
//Nginx默认以Deamon进程启动
curl -i http://localhost/
//停止Nginx
/usr/local/nginx/sbin/nginx -s stop
0x2:Nginx配置文件
配置文件可以看做是Nginx的灵魂,Nginx服务在启动时会读入配置文件,而后续几乎一切动作行为都是按照配置文件中的指令进行的。
#user nobody;
worker_processes 1;
error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root /usr/local/nginx/html;
index index.html index.htm;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}
# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
# listen 8000;
# listen somename:8080;
# server_name somename alias another.alias;
# location / {
# root html;
# index index.html index.htm;
# }
#}
# HTTPS server
#
#server {
# listen 443 ssl;
# server_name localhost;
# ssl_certificate cert.pem;
# ssl_certificate_key cert.key;
# ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 5m;
# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;
# location / {
# root html;
# index index.html index.htm;
# }
#}
}
每个层级可以有自己的指令(Directive),例如worker_processes是一个main层级指令,它指定Nginx服务的Worker进程数量。有的指令只能在一个层级中配置,如worker_processes只能存在于main中,而有的指令可以存在于多个层级,在这种情况下,子block会继承父block的配置,同时如果子block配置了与父block不同的指令,则会覆盖掉父block的配置,指令的格式是:
指令名 参数1 参数2 … 参数N; //注意参数间可用任意数量空格分隔,最后要加分号
在开发Nginx HTTP扩展模块过程中,需要特别注意的是main、server和location三个层级,因为扩展模块通常允许指定新的配置指令在这三个层级中。
最后要提到的是配置文件是可以包含的,如上面配置文件中"include mime.types"就包含了mine.types这个配置文件,此文件指定了各种HTTP Content-type。
一般来说,一个server block表示一个Host,而里面的一个location则代表一个路由映射规则,这两个block可以说是HTTP配置的核心。
参考链接:
http://tengine.taobao.org/book/chapter_03.html
三、nginx扩展开发
作为第三方开发者最可能开发的就是以下几种类型的模块
- nginx handler:Handler模块就是接受来自客户端的请求并产生输出的模块。配置文件中使用location指令可以配置content handler模块,当Nginx系统启动的时候,每个handler模块都有一次机会把自己关联到对应的location上(如果有多个handler模块都关联了同一个location,那么实际上只有一个handler模块真正会起作用)。handler模块处理的结果通常有三种情况
- 处理成功
- 处理失败(处理的时候发生了错误)
- 拒绝去处理。在拒绝处理的情况下,这个location的处理就会由默认的handler模块来进行处理。例如,当请求一个静态文件的时候,如果关联到这个location上的一个handler模块拒绝处理,就会由默认的ngx_http_static_module模块进行处理,该模块是一个典型的handler模块
- nginx filter:
- load-balancer:
- nginx lua:
0x1:nginx lua
虽然 Nginx 有如此强大的性能以及众多的三方模块支持,但每次重新编译以及寻找三方模块对生产环境来说还是不可接受的,为了解决这个问题,Nginx 它是支持客户自己 Lua 脚本编程扩展相应的功能的,而且可以热加载,这就给生产环境带来了无限可能。
1、nginx执行步骤中的ngx_lua指令
nginx在处理每一个用户请求时,都是按照若干个不同的阶段依次处理的。
- post-read:读取请求内容阶段,nginx读取并解析完请求头之后就立即开始运行。
- server-rewrite:server请求地址重写阶段。
- find-config:配置查找阶段,用来完成当前请求与location配重块之间的配对工作。
- rewrite:location请求地址重写阶段,当ngx_rewrite指令用于location中,就是再这个阶段运行的。
- post-rewrite:请求地址重写提交阶段,当nginx完成rewrite阶段所要求的内部跳转动作,如果rewrite阶段有这个要求的话。
- preaccess:访问权限检查准备阶段,ngx_limit_req和ngx_limit_zone在这个阶段运行,ngx_limit_req可以控制请求的访问频率,ngx_limit_zone可以控制访问的并发度。
- access:权限检查阶段,ngx_access在这个阶段运行,配置指令多是执行访问控制相关的任务,如检查用户的访问权限,检查用户的来源IP是否合法。
- post-access:访问权限检查提交阶段。
- try-files:配置项try_files处理阶段。
- content:内容产生阶段,是所有请求处理阶段中最为重要的阶段,因为这个阶段的指令通常是用来生成HTTP响应内容的。
- log:日志模块处理阶段。
ngx_lua属于nginx的一部分,它的执行指令都包含在nginx的11个步骤之中了,相应的处理阶段可以做插入式处理,即可插拔式架构,不过ngx_lua并不是所有阶段都会运行的。
另外ngx_lua指令可以在http、server、server if、location、location if几个范围进行配置:
指令 |
所处处理阶段 |
使用范围 |
解释 |
---|---|---|---|
init_by_lua init_by_lua_file |
loading-config |
http |
nginx Master进程加载配置时执行; 通常用于初始化全局配置/预加载Lua模块 |
init_worker_by_lua init_worker_by_lua_file |
starting-worker |
http |
每个Nginx Worker进程启动时调用的计时器,如果Master进程不允许则只会在init_by_lua之后调用; 通常用于定时拉取配置/数据,或者后端服务的健康检查 |
set_by_lua set_by_lua_file |
rewrite |
server,server if,location,location if |
设置nginx变量,可以实现复杂的赋值逻辑;此处是阻塞的,Lua代码要做到非常快; |
rewrite_by_lua rewrite_by_lua_file |
rewrite tail |
http,server,location,location if |
rrewrite阶段处理,可以实现复杂的转发/重定向逻辑; |
access_by_lua access_by_lua_file |
access tail |
http,server,location,location if |
请求访问阶段处理,用于访问控制 |
content_by_lua content_by_lua_file |
content |
location,location if |
内容处理器,接收请求处理并输出响应 |
header_filter_by_lua header_filter_by_lua_file |
output-header-filter |
http,server,location,location if |
设置header和cookie |
body_filter_by_lua body_filter_by_lua_file |
output-body-filter |
http,server,location,location if |
对响应数据进行过滤,比如截断、替换。 |
log_by_lua log_by_lua_file |
log |
http,server,location,location if |
log阶段处理,比如记录访问量/统计平均响应时间 |
一个nginx+lua测试的例子如下:
user www www; worker_processes auto; error_log /www/wwwlogs/nginx_error.log crit; pid /www/server/nginx/logs/nginx.pid; worker_rlimit_nofile 51200; stream { log_format tcp_format '$time_local|$remote_addr|$protocol|$status|$bytes_sent|$bytes_received|$session_time|$upstream_addr|$upstream_bytes_sent|$upstream_bytes_received|$upstream_connect_time'; access_log /www/wwwlogs/tcp-access.log tcp_format; error_log /www/wwwlogs/tcp-error.log; include /www/server/panel/vhost/nginx/tcp/*.conf; } events { use epoll; worker_connections 51200; multi_accept on; } http { server { listen 80; set $idTest "idTest_11111111111" ; location ~ /gzip/(.*) { default_type "text/html"; set $resp_body ""; content_by_lua ' --zlib 解码 post gzip 数据 local zlib = require "zlib" local encoding = ngx.req.get_headers()["Content-Encoding"] if encoding == "gzip" then ngx.req.read_body() local body = ngx.req.get_body_data() ngx.say("++++++++++++++++++++++++++++body data:") ngx.print(body) if body then --ngx.var.resp_body = "55555555555555" local stream = zlib.inflate() ngx.var.resp_body = stream(body) end end '; access_log on; } location ~ /post/(.*) { default_type "text/html"; lua_need_request_body on; set $resp_body ""; content_by_lua ' ngx.var.resp_body = ngx.var.request_body '; access_log on; } location ~ /lua/(.*) { default_type "text/html"; set $ref1 "Hello,Nginx & Lua !"; #设置nginx变量 set $a $1; set $b $host; content_by_lua ' --nginx变量 local var = ngx.var ngx.say("ngx.var.a : ", var.a, "<br/>") ngx.say("ngx.var.b : ", var.b, "<br/>") ngx.say("ngx.var[2] : ", var[2], "<br/>") ngx.var.b = 2; ngx.say("<br/>") --请求头 ngx.say(ngx.var.httpRef, "<br/>") local headers = ngx.req.get_headers() for k,v in pairs(headers) do if type(v) == "table" then ngx.say(k, " : ", table.concat(v, ","), "<br/>") else ngx.say(k, " : ", v, "<br/>") end end ngx.say("------------headers end-----------", "<br/><br/><br/>") --get请求uri参数 ngx.say("uri args begin", "<br/>") local uri_args = ngx.req.get_uri_args() for k, v in pairs(uri_args) do if type(v) == "table" then ngx.say(k, " : ", table.concat(v, ", "), "<br/>") else ngx.say(k, ": ", v, "<br/>") end end ngx.say("uri args end", "<br/>") ngx.say("a: ",ngx.var.arg_a, "<br/>") ngx.say("b: ",ngx.var.arg_b, "<br/>") --未经解码的请求 uri local request_uri = headers["Host"] .. "/" .. ngx.var.request_uri; ngx.say("request_uri : ", request_uri, "<br/>"); --解码后的 uri local decode_request_uri = headers["Host"] .. "/" .. ngx.unescape_uri(ngx.var.request_uri); ngx.var.ref1 = decode_request_uri; ngx.say("decode request_uri : ", decode_request_uri, "<br/>"); --MD5 ngx.say("ngx.md5 : ", ngx.md5("123"), "<br/>") --http time ngx.say("ngx.http_time : ", ngx.http_time(ngx.time()), "<br/>") --ngx.var.http_referer = "*********************" '; access_log on; } } }
需要注意的是,如果nginx未编译进lua模块,修改conf文件重启后会报如下错误:
参考链接:
https://cloud.tencent.com/developer/tools/blog-entry?target=http%3A%2F%2Fwww.mrhaoting.com%2F%3Fp%3D157&source=article&objectId=1043931 https://cloud.tencent.com/developer/tools/blog-entry?target=http%3A%2F%2Fwww.mrhaoting.com%2F%3Fp%3D165&source=article&objectId=1043931 https://cloud.tencent.com/developer/article/1043931
0x2:nginx filter
filter 处理handler生成的内容。
filter模块又分为
- header filter:header filter处理nginx response的header
- body filter:body filter处理response的body
参考链接:
https://haobook.readthedocs.io/zh-cn/latest/periodical/201608/haoyankai.html
0x3:nginx load-balancer
0x4:nginx handler
Handler模块就是接受来自客户端的请求并产生输出的模块。在配置文件中可以使用location指令可以配置content handler模块,当Nginx系统启动的时候,每个handler模块都有一次机会把自己关联到对应的location上。如果有多个handler模块都关联了同一个location,那么实际上只有一个handler模块真正会起作用。
handler模块处理的结果通常有三种情况:
- 处理成功
- 处理失败(处理的时候发生了错误)
- 拒绝去处理。在拒绝处理的情况下,这个location的处理就会由默认的handler模块来进行处理。例如,当请求一个静态文件的时候,如果关联到这个location上的一个handler模块拒绝处理,就会由默认的ngx_http_static_module模块进行处理,该模块是一个典型的handler模块。
1、模块的基本数据结构
1)模块配置结构
基本上每个模块都会提供一些配置指令,以便于用户可以通过配置来控制该模块的行为。这些配置信息的存储就需要定义该模块的配置结构来进行存储。
Nginx的配置信息分成了几个作用域(scope,有时也称作上下文),
- main
- server
- location
每个模块提供的配置指令也可以出现在这几个作用域里。那对于这三个作用域的配置信息,每个模块就需要定义三个不同的数据结构去进行存储,有一点需要特别注意的就是,在模块的开发过程中,我们最好使用nginx原有的命名习惯。这样跟原代码的契合度更高,对于模块配置信息的定义,命名习惯是
ngx_http_<module name>_(main|srv|loc)_conf_t
例如,
typedef struct { ngx_str_t hello_string; ngx_int_t hello_counter; }ngx_http_hello_loc_conf_t;
2)模块配置指令
一个模块的配置指令是定义在一个静态数组中的,src/core/ngx_conf_file.h
struct ngx_command_s { //配置指令的名称 ngx_str_t name; /* 该配置的类型,其实更准确一点说,是该配置指令属性的集合。nginx提供了很多预定义的属性值(一些宏定义),通过逻辑或运算符可组合在一起,形成对这个配置指令的详细的说明 1. NGX_CONF_NOARGS:配置指令不接受任何参数 2. NGX_CONF_TAKE1:配置指令接受1个参数 3. NGX_CONF_TAKE2:配置指令接受2个参数 4. NGX_CONF_TAKE3:配置指令接受3个参数 5. NGX_CONF_TAKE4:配置指令接受4个参数 6. NGX_CONF_TAKE5:配置指令接受5个参数 7. NGX_CONF_TAKE6:配置指令接受6个参数 8. NGX_CONF_TAKE7:配置指令接受7个参数 可以组合多个属性,比如一个指令即可以不填参数,也可以接受1个或者2个参数。那么就是NGX_CONF_NOARGS|NGX_CONF_TAKE1|NGX_CONF_TAKE2 1. NGX_CONF_TAKE12:配置指令接受1个或者2个参数 2. NGX_CONF_TAKE13:配置指令接受1个或者3个参数 3. NGX_CONF_TAKE23:配置指令接受2个或者3个参数 4. NGX_CONF_TAKE123:配置指令接受1个或者2个或者3参数 5. NGX_CONF_TAKE1234:配置指令接受1个或者2个或者3个或者4个参数 6. NGX_CONF_1MORE:配置指令接受至少一个参数 7. NGX_CONF_2MORE:配置指令接受至少两个参数 8. NGX_CONF_MULTI: 配置指令可以接受多个参数,即个数不定 1. NGX_CONF_BLOCK:配置指令可以接受的值是一个配置信息块。也就是一对大括号括起来的内容。里面可以再包括很多的配置指令。比如常见的server指令就是这个属性的 2. NGX_CONF_FLAG:配置指令可以接受的值是”on”或者”off”,最终会被转成bool值 3. NGX_CONF_ANY:配置指令可以接受的任意的参数值。一个或者多个,或者”on”或者”off”,或者是配置块 值得注意的是,无论如何,nginx的配置指令的参数个数不可以超过NGX_CONF_MAX_ARGS个。目前这个值被定义为8,也就是不能超过8个参数值 下面介绍一组说明配置指令可以出现的位置的属性。 1. NGX_DIRECT_CONF:可以出现在配置文件中最外层。例如已经提供的配置指令daemon,master_process等 2. NGX_MAIN_CONF: http、mail、events、error_log等 3. NGX_ANY_CONF: 该配置指令可以出现在任意配置级别上 对于我们编写的大多数模块而言,都是在处理http相关的事情,也就是所谓的都是NGX_HTTP_MODULE,对于这样类型的模块,其配置可能出现的位置也是分为直接出现在http里面,以及其他位置 1. NGX_HTTP_MAIN_CONF: 可以直接出现在http配置指令里 2. NGX_HTTP_SRV_CONF: 可以出现在http里面的server配置指令里 3. NGX_HTTP_LOC_CONF: 可以出现在http server块里面的location配置指令里 4. NGX_HTTP_UPS_CONF: 可以出现在http里面的upstream配置指令里 5. NGX_HTTP_SIF_CONF: 可以出现在http里面的server配置指令里的if语句所在的block中 6. NGX_HTTP_LMT_CONF: 可以出现在http里面的limit_except指令的block中 7. NGX_HTTP_LIF_CONF: 可以出现在http server块里面的location配置指令里的if语句所在的block中。 */ ngx_uint_t type; /* set是一个函数指针,当nginx在解析配置的时候,如果遇到这个配置指令,将会把读取到的值传递给这个函数进行分解处理。因为具体每个配置指令的值如何处理,只有定义这个配置指令的人是最清楚的 char *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); 1. cf: 该参数里面保存从配置文件读取到的原始字符串以及相关的一些信息。特别注意的是这个参数的args字段是一个ngx_str_t类型的数组 1) 该数组的首个元素是这个配置指令本身 2) 第二个元素是指令的第一个参数 3) 第三个元素是第二个参数,依次类推 2. cmd: 这个配置指令对应的ngx_command_t结构 3. conf: 就是定义的存储这个配置值的结构体。用户在处理的时候可以使用类型转换,转换成自己知道的类型,再进行字段的赋值 为了更加方便的实现对配置指令参数的读取,nginx已经默认提供了对一些标准类型的参数进行读取的函数,可以直接赋值给set字段使用。下面来看一下这些已经实现的set类型函数 1. ngx_conf_set_flag_slot: 读取NGX_CONF_FLAG类型的参数 2. ngx_conf_set_str_slot:读取字符串类型的参数 3. ngx_conf_set_str_array_slot: 读取字符串数组类型的参数 4. ngx_conf_set_keyval_slot: 读取键值对类型的参数 5. ngx_conf_set_num_slot: 读取整数类型(有符号整数ngx_int_t)的参数 6. ngx_conf_set_size_slot:读取size_t类型的参数,也就是无符号数 7. ngx_conf_set_off_slot: 读取off_t类型的参数 8. ngx_conf_set_msec_slot: 读取毫秒值类型的参数 9. ngx_conf_set_sec_slot: 读取秒值类型的参数 10. ngx_conf_set_bufs_slot: 读取的参数值是2个,一个是buf的个数,一个是buf的大小。例如: output_buffers 1 128k; 11. ngx_conf_set_enum_slot: 读取枚举类型的参数,将其转换成整数ngx_uint_t类型 12. ngx_conf_set_bitmask_slot: 读取参数的值,并将这些参数的值以bit位的形式存储。例如:HttpDavModule模块的dav_methods指令 */ char *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); /* 该字段被NGX_HTTP_MODULE类型模块所用(我们编写的基本上都是NGX_HTTP_MOUDLE,只有一些nginx核心模块是非NGX_HTTP_MODULE),该字段指定当前配置项存储的内存位置。实际上是使用哪个内存池的问题 因为http模块对所有http模块所要保存的配置信息,划分了main, server和location三个地方进行存储,每个地方都有一个内存池用来分配存储这些信息的内存。这里可能的值为 1. NGX_HTTP_MAIN_CONF_OFFSET 2. NGX_HTTP_SRV_CONF_OFFSET 3. NGX_HTTP_LOC_CONF_OFFSET 4. 0(NGX_HTTP_MAIN_CONF_OFFSET) */ ngx_uint_t conf; /* 指定该配置项值的精确存放位置,一般指定为某一个结构体变量的字段偏移。因为对于配置信息的存储,一般我们都是定义个结构体来存储的 那么比如我们定义了一个结构体A,该项配置的值需要存储到该结构体的b字段。那么在这里就可以填写为offsetof(A, b) 对于有些配置项,它的值不需要保存或者是需要保存到更为复杂的结构中时,这里可以设置为0 */ ngx_uint_t offset; //该字段存储一个指针。可以指向任何一个在读取配置过程中需要的数据,以便于进行配置读取的处理。大多数时候,都不需要,所以简单地设为0即可 void *post; }; //需要注意的是,就是在ngx_http_hello_commands这个数组定义的最后,都要加一个ngx_null_command作为结尾 #define ngx_null_command { ngx_null_string, 0, NULL, 0, 0, NULL }
3)模块上下文结构
这是一个ngx_http_module_t类型的静态变量。这个变量实际上是提供一组回调函数指针,这些函数有在创建存储配置信息对象时被调用的函数,也有在创建前和创建后会调用的函数。这些函数都将被nginx在合适的时间进行调用
typedef struct { //在创建和读取该模块的配置信息之前被调用 ngx_int_t (*preconfiguration)(ngx_conf_t *cf); //在创建和读取该模块的配置信息之后被调用 ngx_int_t (*postconfiguration)(ngx_conf_t *cf); //调用该函数创建本模块位于http block的配置信息存储结构。该函数成功的时候,返回创建的配置对象。失败的话,返回NULL void *(*create_main_conf)(ngx_conf_t *cf); //调用该函数初始化本模块位于http block的配置信息存储结构。该函数成功的时候,返回NGX_CONF_OK。失败的话,返回NGX_CONF_ERROR或错误字符串 char *(*init_main_conf)(ngx_conf_t *cf, void *conf); //调用该函数创建本模块位于http server block的配置信息存储结构,每个server block会创建一个。该函数成功的时候,返回创建的配置对象。失败的话,返回NULL void *(*create_srv_conf)(ngx_conf_t *cf); //因为有些配置指令既可以出现在http block,也可以出现在http server block中。那么遇到这种情况,每个server都会有自己存储结构来存储该server的配置,但是在这种情况下http block中的配置与server block中的配置信息发生冲突的时候,就需要调用此函数进行合并,该函数并非必须提供,当预计到绝对不会发生需要合并的情况的时候,就无需提供。当然为了安全起见还是建议提供。该函数执行成功的时候,返回NGX_CONF_OK。失败的话,返回NGX_CONF_ERROR或错误字符串 char *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf); //调用该函数创建本模块位于location block的配置信息存储结构。每个在配置中指明的location创建一个。该函数执行成功,返回创建的配置对象。失败的话,返回NULL void *(*create_loc_conf)(ngx_conf_t *cf); //与merge_srv_conf类似,这个也是进行配置值合并的地方。该函数成功的时候,返回NGX_CONF_OK。失败的话,返回NGX_CONF_ERROR或错误字符串 char *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf); } ngx_http_module_t;
Nginx里面的配置信息都是上下一层层的嵌套的,对于具体某个location的话,对于同一个配置,如果当前层次没有定义,那么就使用上层的配置,否则使用当前层次的配置(就近原则)
这些配置信息一般默认都应该设为一个未初始化的值,针对这个需求,Nginx定义了一系列的宏定义来代表各种配置所对应数据类型的未初始化值,如下
#define NGX_CONF_UNSET -1 #define NGX_CONF_UNSET_UINT (ngx_uint_t) -1 #define NGX_CONF_UNSET_PTR (void *) -1 #define NGX_CONF_UNSET_SIZE (size_t) -1 #define NGX_CONF_UNSET_MSEC (ngx_msec_t) -1
4)模块的定义
对于开发一个模块来说,我们都需要定义一个ngx_module_t类型的变量来说明这个模块本身的信息,这是这个模块最重要的一个信息,它告诉了nginx这个模块的一些信息,上面定义的配置信息,还有模块上下文信息,都是通过这个结构来告诉nginx系统的,也就是加载模块的上层代码,都需要通过定义的这个结构,来获取这些信息
typedef struct ngx_module_s ngx_module_t; struct ngx_module_s { ngx_uint_t ctx_index; ngx_uint_t index; ngx_uint_t spare0; ngx_uint_t spare1; ngx_uint_t abi_compatibility; ngx_uint_t major_version; ngx_uint_t minor_version; void *ctx; ngx_command_t *commands; ngx_uint_t type; 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); 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; }; #define NGX_NUMBER_MAJOR 3 #define NGX_NUMBER_MINOR 1 #define NGX_MODULE_V1 0, 0, 0, 0, \ NGX_DSO_ABI_COMPATIBILITY, NGX_NUMBER_MAJOR, NGX_NUMBER_MINOR #define NGX_MODULE_V1_PADDING 0, 0, 0, 0, 0, 0, 0, 0
参考链接:
http://tengine.taobao.org/book/chapter_03.html
2、nginx handler模块的挂载
handler模块真正的处理函数通过两种方式挂载到处理过程中,
- 一种挂载方式是按处理阶段挂载
- 一种挂载方式是按需挂载
1)按处理阶段挂载
为了更精细地控制对于客户端请求的处理过程,nginx把这个处理过程划分成了11个阶段。他们从前到后,依次列举如下:
- NGX_HTTP_POST_READ_PHASE:读取请求内容阶段
- NGX_HTTP_SERVER_REWRITE_PHASE:Server请求地址重写阶段
- NGX_HTTP_FIND_CONFIG_PHASE:配置查找阶段
- NGX_HTTP_REWRITE_PHASE:Location请求地址重写阶段
- NGX_HTTP_POST_REWRITE_PHASE:请求地址重写提交阶段
- NGX_HTTP_PREACCESS_PHASE:访问权限检查准备阶段
- NGX_HTTP_ACCESS_PHASE:访问权限检查阶段
- NGX_HTTP_POST_ACCESS_PHASE:访问权限检查提交阶段
- NGX_HTTP_TRY_FILES_PHASE:配置项try_files处理阶段
- NGX_HTTP_CONTENT_PHASE:内容产生阶段
- NGX_HTTP_LOG_PHASE:日志模块处理阶段
一般情况下,我们自定义的模块,大多数是挂载在NGX_HTTP_CONTENT_PHASE阶段的。
挂载的动作一般是在模块上下文调用的postconfiguration函数中。
挂载的示例代码如下,
static ngx_int_t ngx_http_hello_init(ngx_conf_t *cf) { ngx_http_handler_pt *h; ngx_http_core_main_conf_t *cmcf; cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module); h = ngx_array_push(&cmcf->phases[NGX_HTTP_CONTENT_PHASE].handlers); if (h == NULL) { return NGX_ERROR; } *h = ngx_http_hello_handler; return NGX_OK; }
使用这种方式挂载的handler也被称为 content phase handlers。
2)按需挂载
以这种方式挂载的handler也被称为 content handler。
当一个请求进来以后,nginx从NGX_HTTP_POST_READ_PHASE阶段开始依次执行每个阶段中所有handler。执行到 NGX_HTTP_CONTENT_PHASE阶段的时候,如果这个location有一个对应的content handler模块,那么就去执行这个content handler模块真正的处理函数。否则继续依次执行NGX_HTTP_CONTENT_PHASE阶段中所有content phase handlers,直到某个函数处理返回NGX_OK或者NGX_ERROR。
换句话说,当某个location处理到NGX_HTTP_CONTENT_PHASE阶段时,如果有content handler模块,那么NGX_HTTP_CONTENT_PHASE挂载的所有content phase handlers都不会被执行了。但是使用这个方法挂载上去的handler有一个特点是必须在NGX_HTTP_CONTENT_PHASE阶段才能执行到。如果你想自己的handler在更早的阶段执行,那就不要使用这种挂载方式。
那么在什么情况会使用这种方式来挂载呢?一般情况下,某个模块对某个location进行了处理以后,发现符合自己处理的逻辑,而且也没有必要再调用NGX_HTTP_CONTENT_PHASE阶段的其它handler进行处理的时候,就动态挂载上这个handler。
挂载的示例代码如下,
static char * ngx_http_circle_gif(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_http_core_loc_conf_t *clcf; clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module); clcf->handler = ngx_http_circle_gif_handler; return NGX_CONF_OK; }
3、Hello World nginx handler模块开发
回顾一下实现一个handler的步骤:
- 编写模块基本结构。包括模块的定义,模块上下文结构,模块的配置结构等。
- 实现handler的挂载函数。根据模块的需求选择正确的挂载方式。
- 编写handler处理函数。模块的功能主要通过这个函数来完成。
我们接下来学习一个简单的Nginx模块开发全过程,我们开发一个叫echo的handler模块,这个模块功能非常简单,它接收"echo"指令,指令可指定一个字符串参数,模块会输出这个字符串作为HTTP响应。例如,做如下配置
location /echo { echo "hello nginx"; } //nginx根据conf配置文件来指导其自身的行为
1)定义模块配置结构
首先我们需要一个结构用于存储从配置文件中读进来的相关指令参数,即模块配置信息结构。
根据Nginx模块开发规则,这个结构的命名规则为ngx_http_[module-name]_[main|srv|loc]_conf_t。其中main、srv和loc分别用于表示同一模块在三层block中的配置信息。这里我们的echo模块只需要运行在loc层级下,需要存储一个字符串参数,因此我们可以定义如下的模块配置
typedef struct { ngx_str_t ed; } ngx_http_echo_loc_conf_t;
2)定义指令
一个Nginx模块往往接收一至多个指令,echo模块接收一个指令“echo”。
Nginx模块使用一个ngx_command_t数组表示模块所能接收的所有模块,其中每一个元素表示一个条指令。
ngx_command_t是ngx_command_s的一个别称(Nginx习惯于使用"_s"后缀命名结构体,然后typedef一个同名"_t"后缀名称作为此结构体的类型名)
下面是echo模块的定义
static ngx_command_t ngx_http_echo_commands[] = { { ngx_string("echo"), NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, ngx_http_echo, NGX_HTTP_LOC_CONF_OFFSET, offsetof(ngx_http_echo_loc_conf_t, ed), NULL }, ngx_null_command }; //指令数组的命名规则为ngx_http_[module-name]_commands,注意数组最后一个元素要是ngx_null_command结束
参数转化函数(ngx_http_echo)的代码为
static char * ngx_http_echo(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_http_core_loc_conf_t *clcf; clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module); //修改了核心模块配置(也就是这个location的配置),将其handler替换为我们编写的handler:ngx_http_echo_handler。这样就屏蔽了此location的默认handler,使用ngx_http_echo_handler产生HTTP响应 clcf->handler = ngx_http_echo_handler; //调用ngx_conf_set_str_slot转化echo指令的参数 ngx_conf_set_str_slot(cf,cmd,conf); return NGX_CONF_OK; }
3)创建合并配置信息
接下来继续学习定义模块Context,这里首先需要定义一个ngx_http_module_t类型的结构体变量,命名规则为ngx_http_[module-name]_module_ctx,这个结构主要用于定义各个Hook函数。下面是echo模块的context结构
static ngx_http_module_t ngx_http_echo_module_ctx = { NULL, /* preconfiguration */ NULL, /* postconfiguration */ NULL, /* create main configuration */ NULL, /* init main configuration */ NULL, /* create server configuration */ NULL, /* merge server configuration */ ngx_http_echo_create_loc_conf, /* create location configration */ ngx_http_echo_merge_loc_conf /* merge location configration */ };
一共有8个Hook注入点,分别会在不同时刻被Nginx调用,由于我们的模块仅仅用于location域,这里将不需要的注入点设为NULL即可
- create_loc_conf用于初始化一个配置结构体,如为配置结构体分配内存等工作
- merge_loc_conf用于将其父block的配置信息合并到此结构体中,也就是实现配置的继承
这两个函数会被Nginx自动调用。注意这里的命名规则:ngx_http_[module-name]_[create|merge]_[main|srv|loc]_conf
下面是echo模块这个两个Hook函数的代码
static void *ngx_http_echo_create_loc_conf(ngx_conf_t *cf) { ngx_http_echo_loc_conf_t *conf; //ngx_pcalloc用于在Nginx内存池中分配一块空间,是pcalloc的一个包装。使用ngx_pcalloc分配的内存空间不必手工free,Nginx会自行管理,在适当是否释放 conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_echo_loc_conf_t)); if (conf == NULL) { return NGX_CONF_ERROR; } conf->ed.len = 0; conf->ed.data = NULL; //create_loc_conf新建一个ngx_http_echo_loc_conf_t,分配内存,并初始化其中的数据,然后返回这个结构的指针 return conf; } static char *ngx_http_echo_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) { ngx_http_echo_loc_conf_t *prev = parent; ngx_http_echo_loc_conf_t *conf = child; ngx_conf_merge_str_value(conf->ed, prev->ed, ""); //merge_loc_conf将父block域的配置信息合并到create_loc_conf新建的配置结构体中 return NGX_CONF_OK; }
4)编写Handler
handler可以说是模块中真正实现功能业务逻辑的代码,它主要有以下四项职责
- 读入模块配置
- 处理功能业务
- 产生HTTP header
- 产生HTTP body
/* * Copyright (C) Eric Zhang */ #include <ngx_config.h> #include <ngx_core.h> #include <ngx_http.h> /* Module config */ typedef struct { ngx_str_t ed; } ngx_http_echo_loc_conf_t; static char *ngx_http_echo(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); static void *ngx_http_echo_create_loc_conf(ngx_conf_t *cf); static char *ngx_http_echo_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child); /* Directives */ static ngx_command_t ngx_http_echo_commands[] = { { ngx_string("echo"), NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, ngx_http_echo, NGX_HTTP_LOC_CONF_OFFSET, offsetof(ngx_http_echo_loc_conf_t, ed), NULL }, ngx_null_command }; /* Http context of the module */ static ngx_http_module_t ngx_http_echo_module_ctx = { NULL, /* preconfiguration */ NULL, /* postconfiguration */ NULL, /* create main configuration */ NULL, /* init main configuration */ NULL, /* create server configuration */ NULL, /* merge server configuration */ ngx_http_echo_create_loc_conf, /* create location configration */ ngx_http_echo_merge_loc_conf /* merge location configration */ }; /* Module */ //完成了Nginx模块各种组件的开发下面就是将这些组合起来了。一个Nginx模块被定义为一个ngx_module_t结构 ngx_module_t ngx_http_echo_module = { NGX_MODULE_V1, &ngx_http_echo_module_ctx, /* module context */ ngx_http_echo_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 }; /* Handler function */ //handler会接收一个ngx_http_request_t指针类型的参数,这个参数指向一个ngx_http_request_t结构体,此结构体存储了这次HTTP请求的一些信息 static ngx_int_t ngx_http_echo_handler(ngx_http_request_t *r) { ngx_int_t rc; ngx_buf_t *b; ngx_chain_t out; ngx_http_echo_loc_conf_t *elcf; //获取模块配置信息 elcf = ngx_http_get_module_loc_conf(r, ngx_http_echo_module); if(!(r->method & (NGX_HTTP_HEAD|NGX_HTTP_GET|NGX_HTTP_POST))) { return NGX_HTTP_NOT_ALLOWED; } //设置response header r->headers_out.content_type.len = sizeof("text/html") - 1; r->headers_out.content_type.data = (u_char *) "text/html"; r->headers_out.status = NGX_HTTP_OK; r->headers_out.content_length_n = elcf->ed.len; if(r->method == NGX_HTTP_HEAD) { rc = ngx_http_send_header(r); if(rc != NGX_OK) { return rc; } } b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t)); if(b == NULL) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "Failed to allocate response buffer."); return NGX_HTTP_INTERNAL_SERVER_ERROR; } out.buf = b; out.next = NULL; b->pos = elcf->ed.data; b->last = elcf->ed.data + (elcf->ed.len); b->memory = 1; b->last_buf = 1; //使用ngx_http_send_header就可以将头信息输出 rc = ngx_http_send_header(r); if(rc != NGX_OK) { return rc; } /* 最后一步也是最重要的一步是输出Response body,Nginx允许handler一次产生一组输出,可以产生多次,Nginx将输出组织成一个单链表结构 struct ngx_chain_s { ngx_buf_t *buf; ngx_chain_t *next; }; 其中ngx_chain_t是ngx_chain_s的别名,buf为某个数据缓冲区的指针,next指向下一个链表节点,可以看到这是一个非常简单的链表 ngx_buf_t的定义比较长而且很复杂,这里就不贴出来了,请自行参考core/ngx_buf.h。ngx_but_t中比较重要的是pos和last,分别表示要缓冲区数据在内存中的起始地址和结尾地址,这里我们将配置中字符串传进去,last_buf是一个位域,设为1表示此缓冲区是链表中最后一个元素,为0表示后面还有元素。因为我们只有一组数据,所以缓冲区链表中只有一个节点,如果需要输入多组数据可将各组数据放入不同缓冲区后插入到链表 缓冲数据准备好后,用ngx_http_output_filter就可以输出了(会送到filter进行各种过滤处理) */ return ngx_http_output_filter(r, &out); } static char * ngx_http_echo(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_http_core_loc_conf_t *clcf; clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module); clcf->handler = ngx_http_echo_handler; ngx_conf_set_str_slot(cf,cmd,conf); return NGX_CONF_OK; } static void * ngx_http_echo_create_loc_conf(ngx_conf_t *cf) { ngx_http_echo_loc_conf_t *conf; conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_echo_loc_conf_t)); if (conf == NULL) { return NGX_CONF_ERROR; } conf->ed.len = 0; conf->ed.data = NULL; return conf; } static char * ngx_http_echo_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) { ngx_http_echo_loc_conf_t *prev = parent; ngx_http_echo_loc_conf_t *conf = child; ngx_conf_merge_str_value(conf->ed, prev->ed, ""); return NGX_CONF_OK; }
5)Nginx模块的编译、安装
Nginx不支持动态链接模块,所以安装模块需要将模块代码与Nginx源代码进行重新编译。安装模块的步骤如下
1. cd /usr/local/nginx/nginx-master/src/http/modules/ 2. mkdir ngx_http_echo_module 3. cd ngx_http_echo_module 4. vim /usr/local/nginx/nginx-master/src/http/modules/ngx_http_echo_module/config /* ngx_addon_name=ngx_http_echo_module HTTP_MODULES="$HTTP_MODULES ngx_http_echo_module" NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_echo_module.c" */ 5. cd /usr/local/nginx/nginx-master 6. ./auto/configure --prefix=/usr/local/nginx/ --add-module=/usr/local/nginx/nginx-master/src/http/modules/ngx_http_echo_module/ 7. make 8. make install 9. vim /usr/local/nginx/conf/nginx.conf //编辑增加 location /echo { echo "hello nginx"; } 10. 重启 /usr/local/nginx/sbin/nginx -s stop /usr/local/nginx/sbin/nginx 11. 访问 http://121.40.254.73/echo
参考链接:
http://blog.codinglabs.org/articles/intro-of-nginx-module-development.html http://blog.csdn.net/poechant/article/details/7627828 http://bg.biedalian.com/2013/08/09/nginx-hello-world.html