要非常感谢nginx,它帮助我更加了解蝙蝠侠这个喜剧角。
蝙蝠侠很快,nginx也很快。蝙蝠侠在与罪恶斗争,nginx在与浪费CPU、内存泄漏等现象做斗争。蝙蝠侠在压力下能保持良好状态,nginx在强大的服务请求压力下表现出。
但是,蝙蝠侠如果没有那根蝙蝠侠万能腰带(batman utility belt),那他就什么都不是。
在任何时候,蝙蝠侠的万能腰带都应该包括一个锁扣、几个batarang(蝙蝠侠的特殊武器)、几个bat-cuff(护腕)、夜视眼镜、bat-tracer(跟踪器?)、几个bat-darts(蝙蝠镖)...或者还包括一个apple iphone。当蝙蝠侠需要使他的敌人失明、失聪、或者晕倒,或者当他需要跟踪他的敌人,或者给他的敌人发个短信,你最好相信他正在他的万能腰带上一个合适的工具。这根腰带对蝙蝠侠的行动如此至关重要,所以,当蝙蝠侠在选择穿裤子还是穿腰带的时候,他肯定会选择穿腰带。事实上他确实选择了腰带,这就是为什么蝙蝠侠穿着紧绷的橡胶衣,而没有穿裤子。
虽然nginx没有这样一条万能腰带,但是nginx有一个模块链(module chain),当nginx需要对应答进行gzip或chunked编码时,它会拿出一个模块来做这个工作。当nginx基于IP或http认证来阻拦对某些资源的访问时,也是由一个模块来做这些工作的。同样,当nginx需要和memcahed或者fastCGI交互时,也分别由对应的模块来做相应的工作。
尽管蝙蝠侠的万能腰带上有很多小玩意,但是有时候他还是需要一个新的小工具。也许他有新的敌人,而他现在的武器(如batarang或bat-cuff)都不足以对付这个敌人。也许他需要有新的技能,比如在水下呼吸。所以他会让Lucius Fox来帮他设计这些小工具。
这个文档会告诉你nginx模块链表(module chain)的一些细节,这样你就可以做到像Lucius Fox一样。当你完成这个课程,你将可以设计并写出高质量的模块,来完成nginx以前做不了的事。Nginx的模块系统有很多需要注意的细节,所以你可能需要经常返回这篇文档阅读。我已尽力将这些内容梳理清楚,但我比较愚钝,写nginx模块仍然是很费力的一件事情。
0 前提条件(prerequisites)
你需要熟悉C。不仅仅是C语言语法;你需要熟悉C结构体相关的知识;不能被指针和函数引用所吓倒;对预编译、宏有一定的认识。如果你需要提高一下C语言方面的知识,没有什么比这更好了:K&R。
对HTTP协议的了解很有用,毕竟你是在一个webserver之上工作。
你也应该对nginx的配置文件非常熟悉。如果你不熟悉它,这里有一个简短的介绍:配置
文件中有4种context(分别叫main,server,upstream,location)。每个context 中可以包含数个拥有一个或
多个参数的指令。在main context中的指令对所有事情生效;在server context中的指令对一个特定的host/port生效;在upstream context
中的指令对后端的一组server生效;在location context中的指令仅对能匹配到的web地址(如"/","/images"等)生效。一个location context会继承包含它的server context的属性,同样,一个server context会继承main context的属性。Upstream context不会继承任何属性,也不会把自己的属性传递给其它;它有自己的特殊的指令,这些指令不会在其它任何地方生效。我比较多地提到了这4个context,所以...别忘记它们。
让我们开始吧。
1 nginx模块概述
Nginx模块中有3个角我们将要关注:
1. Handler:处理请求并且产生输出。
2. Filter:对handler产生的输出进行操作(比如gzip压缩,chunked编码等)
3. Load-balancer:当有多个符合条件的后端server可供选择时,从中选择一个以便将请求转发过去。
模块承担着所有你能想到的和webserver相关的实际工作:当nginx寻一个静态文件,或者将请求代理到另外一台server的时候,这时候有一个handler模块来做这些工作;当nginx对输出进行编码,或者执行有一个服务端的包含(server-side include)操作,它将使用filter模块。Nginx的核心模块很简单,它关心网络通信、应用协议。另外,当处理一个请求时,将所有需要调用的合格模块排好序(以便在处理时依次调用)。这种没有中心控制节点的架构,让你可以写一个非常好的自包含模块来做你想做的事情。
注意:不像apache中的模块,nginx中的模块不是以动态链接库(so)的方式存在。(换句话说,它被编译进nginx这个可执行的bin文件)。
nginx 配置文件一个模块是如何被调用的?一般情况下,当nginx启动时,每个handler都有一个机会将自己和配置文件中的某个location关联起来;如果有2个以上的模块关联到同一个location,那么仅有其中一个能成功(当然,一个好的配置书写人员不会上这种冲突产生)。Handler能以3种方式返回:所有处理成功;有错误产生;拒绝处理请求并将它
传给默认handler处理(默认handler通常返回一个静态文件)。
如果一个handler是一个到一些后端server组的反向代理,那么就需要用到load-balancer。一个load-balancer决定一个被接收到的请求将要被发送到后端server组中的哪一台。Nginx自带2个load-balance
r模块:round-robin(轮询),它像我们在扑克牌游戏中发牌一样,(将请求轮流发送给后端server);iphash,它能保证一个客户端的多次请求可以被同一台后端server处理。
如果handler在处理过程没有产生错误,那么filter将被调用。每个location都可以被关联(hook)多个filter,所以,(举个例子),一个应答可以先被压缩,然后被chunked编码。它们的调用顺序在编译时就决定好了。Filter是一种典型的CHAIN OF RESPONSIBILITY(职责链)设计模式。一个filter被调用,做完它的工作,然后再调用下一个,直到最后一个filter被调用,nginx才完成它对response的处理。
Filter链表中最酷的设计在于,每个filter并不需要等待前一个filter完成后才能开始工作,它可以像unix系统管道一样工作,在前一个filter产生输出的同时处理
Filter在buffer之上操作。Buffer通常的大小为4K(系统页面大小),当然,你可以在f中改变这个值。这就意味着,不必等到后端server上接收到整个应答,也就是说,只要接收到应答的一小部分,这个模块就可以开始压缩应答,并把这小部分先返回给客户端。
总结一下主要的概念,典型的处理流程如下:
客户端发送HTTP请求-> nginx根据配置文件中的location配置选择正确的模块-> (如果需要)load-balancer选择一个后端server -> handler处理完它的事情并把每个输出buffer传递给第一个filter -> 第一个filter传递处理后的buffer给第二个filter -> 第二个filter传递给第三个-> ... -> 最终应答发送给客户端
“”
我用到了典型这个词,因为nginx的模块调用非常可定制化。模块何时被调用,以及如何被调用,都给模块开发人员带来很大的负担。调用通过一系列回调函数来实现,这些回调函数有很多。在以下情况下,你都可以指定一个函数来执行:
1. 在server读取配置文件之前
2. 当每一条配置指令出现在每一个loaction和server context的时候
3. 当nginx初始化main配置
4. 当nginx初始化server配置
5. 当nginx将main配置与server配置合并
6. 当nginx初始化location配置
7. 当nginx将某个location父节点server的配置与这个location节点的配置合并
8. 当nginx master进程启动
9. 当nginx worker进程启动
10. 当nginx master进程退出
11. 处理一个请求
12. 操作应答消息头
13. 操作应答消息体
14. 选择一个后端server
15. 初始化对后端server的一个请求
16. 重新初始化到后端server的请求
17. 处理从后端server接收到的应答
18. 完成与后端server的一次交互
通过这些回调函数你能做很多事情,现在是时候深入研究一下nginx的几个模块了。
2 nginx模块的组成部分
我说过,你有非常大的机动性来做一个nginx模块。这一节将描述一些常见的部分。它是一个帮助你读懂一个模块的指引,也是一本写模块时需要翻阅的手册。
2.1模块配置结构(module configuration
structures)
一个模块可以分别为main、server、location这3种context分别定义3种配置结构。事实上,大部分模块仅仅需要一个location配置结构。这些配置结构的命名习惯一般是这样的:ngx_http_<module name>_(main|srv|loc)_conf_t。这里有一个例子(从dav模块中拿出):
typedef struct {
ngx_uint_t methods;
ngx_flag_t create_full_put_path;
ngx_uint_t access;
} ngx_http_dav_loc_conf_t;
注意,Nginx有一些特殊的数据类型(比如ngx_uin_t、ngx_flag_t)。这些仅仅只是一些元数据类型的别名。
这些结构中的元素由模块指令填充。
2.2 模块指令(module directives)
一个模块的指令出现在一个ngx_command_t数组中,这里有一个例子,来说明他们是如何定义的。这个例子来自于我写的一个小模块
static ngx_command_t ngx_http_circle_gif_commands[] = {
{ ngx_string("circle_gif"),
NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS,
ngx_http_circle_gif,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
{ ngx_string("circle_gif_min_radius"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_num_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_circle_gif_loc_conf_t, min_radius),
NULL },
...
ngx_null_command
};
这里有ngx_command_t(我们定义的结构体)的声明,你能在core/ngx_conf_file.h 中到它。
struct ngx_command_t {
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是指令字符串,不含空格。数据类型是ngx_str_t。举个例子,它经常被这样初始化:ngx_str("pr
oxy_pass")。注意:ngx_str_t是一个包含2个成员变量的结构,data 和len,data为一个字符串,len为该字符串的长度。在大多数需要使用字符串的时候,nginx都使用该结构体。
Type是一组标志位。通过这个标志位,来说明这个指令带几个参数,配置在何处生效。这些标志位可以被按位或,它们包括:
NGX_HTTP_MAIN_CONF:指令在main配置中生效
NGX_HTTP_SRV_CONF:指令在server配置中生效
NGX_HTTP_LOC_CONF:指令在location配置中生效
NGX_CONF_NOARGS:指令不带参数
NGX_CONF_TAKE1:指令带一个参数
NGX_CONF_TAKE2:指令带二个参数
...
NGX_CONF_TAKE7:指令带7个参数
NGX_CONF_FLAG:指令带有一个bool变量参数("on"或"off")
NGX_CONF_1MORE:指令带有一个以上参数(至少一个)
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论