nginx源码分析之设计之美
大千世界,任何东西都有共通之处。当我们讨论一个东西时,首先要给它定义个边界, 在这边界里有两个东西:内核(Kernel)和用户(User)。nginx作为http服务器(其实远不止),我们给它定义边界:实现http服务器提 供服务功能。项目名称为nginx,代号(或简称)为ngx,前缀为ngx_。
一、一切从命名谈起如果有人认为不具可读性的代码是可接受的,那他就是个'天才'。
刚提到任何东西在我们讨论的边界里,都有两个东西:内核(Kernel)和用户(User)。 Kernel作为基础设施存在,天生存在,User则是码农自定义并创造出来的。 比如函数 printf,这个是Kernel的一部分。ngx_write_console是nginx里自定义的一个函数。 Kernel和User的东西一定要区分,这非常有利于提高代码的可读性。
如何区分呢? 就是给User的东西加上项目的前缀ngx_。为什么这样设计呢?比如log_error,这函数能确认是C提供的,还是自己自定义的呢?但是,换成 ngx_log_error,一目了然,肯定是nginx源码里的一个自定义的函数。
所有的接口(全局和静态函数,全局和静态变量,自定义类型)应该遵守这一原则。在nginx里,自定义结构体看起来非常舒服,比如 ngx_command_t,t是typedef的代号。struct: command vs struct: ngx_command_t,您觉得呢?好的命名应该是在头脑里不假思索的就直取其意,而不用再经过一次智商运算,头脑风暴。
二、模块化思想
nginx的整个代码像流水线一样工作着,这流水线上布满着各种模块,他们协同工作,共同完成提供服务。比 如 ngx_core_module, ngx_epoll_module, ngx_http_core_module, ngx_http_static_module等,ngx_string.h(c), ngx_times.h(c),在C世界里,文件即模块,你可以当它是基础设施,或工具,只是有的文件有变量,相当于文件访问入口,比如 ngx_http_core_module.h(c)里的ngx_http_core_module全局变量。
没有任何独立存在,不跟任何人打交道的模块,因为那样,它就没有存在的意义,所以模块是有依赖关系的。 比如ngx_event.h(c)依赖于 ngx_string.h,谁维护着这些关系呢,Makefile。所以能掌握一个项目的人,肯定能手写Makefile文件。每个模块有出场顺序的,直 到main函数return。nginx作者给模块设计了一个类型成员,可以是core, event, http的一种,很明显的会是这是 core(核心模块) -> event(事件机制) -> http(http业务处理) 这么一个流程。后面的依赖前面的,非常明了。
三、OO面向对象
面向对象和面向过程之争从来没停过。我一向认为有争议的设计不应该融入语言里,语言应该假设程序员能做最正确的事,而不应该去约束程序员如何犯错误。比如goto是否应该存在,全局变量应该怎么样。C以最简洁的语法提供了程序员能秀的平台。废话点到即止。
nginx里有非常多的结构体,不知某大师曾说程序就是算法+数据结构,这里的数据结构不仅是是数组,列表,队列这些经典的,还包括用户自定义的,或封装 的,我们称它为抽象是不是更好呢。结构体让某业务概念更具血肉,比如 ngx_listening_t, ngx_connection_t, ngx_event_t,非常高明的封装和命名。读过DDD书的,如果结合nginx源码去看,会发现OO最强烈的表达就是抽象(封装?多态?继承?)。
一个结构体或类应该表达某个主题,比如ngx_connection_t抽象了连接这个业务,里面的成员应该表达两种属性:显性和隐性。很多人忽略了隐性 的属性,以 ngx_url_t 为例,url里有 addr, port 这种显而易见的成员,大家都会。但是应该还包括 err,这个表示,一个url解析后的结果,是不是有点像冗余字段,是的,这就是隐性的属性,会让整个结构体更具表达力。读nginx源码,从结构体出现 的顺序去理清是个好的方向。作者在设计时极具功力和细腻,比如 ngx_http_rewrite_module里对rewrite的处理,ngx_http_upstream里对upstream的处理。
结构体代表了业务概念的一个方向,nginx在行为表现方向也设计的很精致,可以细看下日志是如何处理的,其中ngx_log_t的handler和ctx两个成员的设计。还有很明显的责任链模式,让人眼前一亮,参考ngx_http_core_run_phases
四、生命周期里秘密
有两个东西是一直在整个项目代码里游荡的,日志和内存池。简单的讲,有3个重要概念:
cycle : 代表了整个生命周期,只要进程还在,它就一直存活。
connection : 代表连接的生命周期,一个客户连接过来,它就开始诞生,连接结束,它就跟着终结。
request : 代表请求,请求一发过来,它开始诞生,请求结束,它也就消亡。
可能你认为connection和request很像,connection比request生命周期更长,request挂了,connection不 一定会挂。keepalive就是最好的证明,有了keepalive,客户端刷新时,connection的fd还一直保持用着,服务端的socket 是不会close的。
cycle有自己的pool,connection有自己的pool, request有自己的pool,除了cycle外,其余两个在消亡前,要释放内存。
log的表现也很活跃,从最开始的ngx_cycle有自己的log,然后设置成配置文件里指定的error_log,然后从listen开始分支,每个 listen自己复制一份log,然后listen connection用了listen对应的fd, 继续再给 connection的两个event: rev, wev。
五、配置文件为何存在
是先有项目代码,还是先有配置文件呢?我觉得,配置文件是因项目代码存在而存在的,这样讲似乎有点空白。
项目之初,代码是可以硬编码的,比如实现守护进程。但是呢?这样缺少灵活性,所以用配置文件里的配置选项控制这个行为,也正因为如此,配置选项一定要依附,挂钩于某个模块,然后它的值应该解析到这个模块携带的配置结构体。
现在还是单一的行为,由核心行为引起的。但是到了用户决定行为的时候,配置文件应该出现分支。你想到server, location了吗,不同的server有不同的配置,这也是虚拟主机的实现机制。
这个分支非常的重要,原来的ngx_cycle有个void ****conf_ctx;很酷吧,4层指针。获取配置是这样的:
ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);
其中,ngx_get_conf这样预定义:
#define ngx_get_conf(conf_ctx, module) conf_ctx[module.index]
但是到了分支这里,从cycle->conf_ctx变成了r
cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);
ngx_http_get_module_main_conf这样定义:
#define ngx_http_get_module_main_conf(r, module) \
(r)->main_conf[module.ctx_index]
当分析完用户的请求行为后,又会将分析完的配置定下来,比如虚拟主机如何实现的:
ngx_http_find_virtual_server(r, r->headers_in.server.data, r->headers_in.server.len) {
cscf = ngx_hash_find_combined(&r->virtual_names->names,
ngx_hash_key(host, len), host, len);
if (cscf) {
r->srv_conf = cscf->ctx->srv_conf; // 以后直接找r要src_conf获取。
r->loc_conf = cscf->ctx->loc_conf;
}
}