http://tengine.taobao.org/book/chapter_02.htmlphp
众所周知,nginx性能高,而nginx的高性能与其架构是分不开的。那么nginx到底是怎么样的呢?这一节咱们先来初识一下nginx框架吧。html
nginx在启动后,在unix系统中会以daemon的方式在后台运行,后台进程包含一个master进程和多个worker进程。咱们也能够手动地关掉后台模式,让nginx在前台运行,而且经过配置让nginx取消master进程,从而能够使nginx以单进程方式运行。很显然,生产环境下咱们确定不会这么作,因此关闭后台模式,通常是用来调试用的,在后面的章节里面,咱们会详细地讲解如何调试nginx。因此,咱们能够看到,nginx是以多进程的方式来工做的,固然nginx也是支持多线程的方式的,只是咱们主流的方式仍是多进程的方式,也是nginx的默认方式。nginx采用多进程的方式有诸多好处,因此我就主要讲解nginx的多进程模式吧。python
刚才讲到,nginx在启动后,会有一个master进程和多个worker进程。master进程主要用来管理worker进程,包含:接收来自外界的信号,向各worker进程发送信号,监控worker进程的运行状态,当worker进程退出后(异常状况下),会自动从新启动新的worker进程。而基本的网络事件,则是放在worker进程中来处理了。多个worker进程之间是对等的,他们同等竞争来自客户端的请求,各进程互相之间是独立的。一个请求,只可能在一个worker进程中处理,一个worker进程,不可能处理其它进程的请求。worker进程的个数是能够设置的,通常咱们会设置与机器cpu核数一致,这里面的缘由与nginx的进程模型以及事件处理模型是分不开的。nginx的进程模型,能够由下图来表示:linux
在nginx启动后,若是咱们要操做nginx,要怎么作呢?从上文中咱们能够看到,master来管理worker进程,因此咱们只须要与master进程通讯就好了。master进程会接收来自外界发来的信号,再根据信号作不一样的事情。因此咱们要控制nginx,只须要经过kill向master进程发送信号就好了。好比kill -HUP pid,则是告诉nginx,从容地重启nginx,咱们通常用这个信号来重启nginx,或从新加载配置,由于是从容地重启,所以服务是不中断的。master进程在接收到HUP信号后是怎么作的呢?首先master进程在接到信号后,会先从新加载配置文件,而后再启动新的worker进程,并向全部老的worker进程发送信号,告诉他们能够光荣退休了。新的worker在启动后,就开始接收新的请求,而老的worker在收到来自master的信号后,就再也不接收新的请求,而且在当前进程中的全部未处理完的请求处理完成后,再退出。固然,直接给master进程发送信号,这是比较老的操做方式,nginx在0.8版本以后,引入了一系列命令行参数,来方便咱们管理。好比,./nginx -s reload,就是来重启nginx,./nginx -s stop,就是来中止nginx的运行。如何作到的呢?咱们仍是拿reload来讲,咱们看到,执行命令时,咱们是启动一个新的nginx进程,而新的nginx进程在解析到reload参数后,就知道咱们的目的是控制nginx来从新加载配置文件了,它会向master进程发送信号,而后接下来的动做,就和咱们直接向master进程发送信号同样了。android
如今,咱们知道了当咱们在操做nginx的时候,nginx内部作了些什么事情,那么,worker进程又是如何处理请求的呢?咱们前面有提到,worker进程之间是平等的,每一个进程,处理请求的机会也是同样的。当咱们提供80端口的http服务时,一个链接请求过来,每一个进程都有可能处理这个链接,怎么作到的呢?首先,每一个worker进程都是从master进程fork过来,在master进程里面,先创建好须要listen的socket(listenfd)以后,而后再fork出多个worker进程。全部worker进程的listenfd会在新链接到来时变得可读,为保证只有一个进程处理该链接,全部worker进程在注册listenfd读事件前抢accept_mutex,抢到互斥锁的那个进程注册listenfd读事件,在读事件里调用accept接受该链接。当一个worker进程在accept这个链接以后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开链接,这样一个完整的请求就是这样的了。咱们能够看到,一个请求,彻底由worker进程来处理,并且只在一个worker进程中处理。nginx
那么,nginx采用这种进程模型有什么好处呢?固然,好处确定会不少了。首先,对于每一个worker进程来讲,独立的进程,不须要加锁,因此省掉了锁带来的开销,同时在编程以及问题查找时,也会方便不少。其次,采用独立的进程,可让互相之间不会影响,一个进程退出后,其它进程还在工做,服务不会中断,master进程则很快启动新的worker进程。固然,worker进程的异常退出,确定是程序有bug了,异常退出,会致使当前worker上的全部请求失败,不过不会影响到全部请求,因此下降了风险。固然,好处还有不少,你们能够慢慢体会。程序员
上面讲了不少关于nginx的进程模型,接下来,咱们来看看nginx是如何处理事件的。web
有人可能要问了,nginx采用多worker的方式来处理请求,每一个worker里面只有一个主线程,那可以处理的并发数颇有限啊,多少个worker就能处理多少个并发,何来高并发呢?非也,这就是nginx的高明之处,nginx采用了异步非阻塞的方式来处理请求,也就是说,nginx是能够同时处理成千上万个请求的。想一想apache的经常使用工做方式(apache也有异步非阻塞版本,但因其与自带某些模块冲突,因此不经常使用),每一个请求会独占一个工做线程,当并发数上到几千时,就同时有几千的线程在处理请求了。这对操做系统来讲,是个不小的挑战,线程带来的内存占用很是大,线程的上下文切换带来的cpu开销很大,天然性能就上不去了,而这些开销彻底是没有意义的。算法
为何nginx能够采用异步非阻塞的方式来处理呢,或者异步非阻塞究竟是怎么回事呢?咱们先回到原点,看看一个请求的完整过程。首先,请求过来,要创建链接,而后再接收数据,接收数据后,再发送数据。具体到系统底层,就是读写事件,而当读写事件没有准备好时,必然不可操做,若是不用非阻塞的方式来调用,那就得阻塞调用了,事件没有准备好,那就只能等了,等事件准备好了,你再继续吧。阻塞调用会进入内核等待,cpu就会让出去给别人用了,对单线程的worker来讲,显然不合适,当网络事件越多时,你们都在等待呢,cpu空闲下来没人用,cpu利用率天然上不去了,更别谈高并发了。好吧,你说加进程数,这跟apache的线程模型有什么区别,注意,别增长无谓的上下文切换。因此,在nginx里面,最忌讳阻塞的系统调用了。不要阻塞,那就非阻塞喽。非阻塞就是,事件没有准备好,立刻返回EAGAIN,告诉你,事件还没准备好呢,你慌什么,过会再来吧。好吧,你过一会,再来检查一下事件,直到事件准备好了为止,在这期间,你就能够先去作其它事情,而后再来看看事件好了没。虽然不阻塞了,但你得不时地过来检查一下事件的状态,你能够作更多的事情了,但带来的开销也是不小的。因此,才会有了异步非阻塞的事件处理机制,具体到系统调用就是像select/poll/epoll/kqueue这样的系统调用。它们提供了一种机制,让你能够同时监控多个事件,调用他们是阻塞的,但能够设置超时时间,在超时时间以内,若是有事件准备好了,就返回。这种机制正好解决了咱们上面的两个问题,拿epoll为例(在后面的例子中,咱们多以epoll为例子,以表明这一类函数),当事件没准备好时,放到epoll里面,事件准备好了,咱们就去读写,当读写返回EAGAIN时,咱们将它再次加入到epoll里面。这样,只要有事件准备好了,咱们就去处理它,只有当全部事件都没准备好时,才在epoll里面等着。这样,咱们就能够并发处理大量的并发了,固然,这里的并发请求,是指未处理完的请求,线程只有一个,因此同时能处理的请求固然只有一个了,只是在请求间进行不断地切换而已,切换也是由于异步事件未准备好,而主动让出的。这里的切换是没有任何代价,你能够理解为循环处理多个准备好的事件,事实上就是这样的。与多线程相比,这种事件处理方式是有很大的优点的,不须要建立线程,每一个请求占用的内存也不多,没有上下文切换,事件处理很是的轻量级。并发数再多也不会致使无谓的资源浪费(上下文切换)。更多的并发数,只是会占用更多的内存而已。 我以前有对链接数进行过测试,在24G内存的机器上,处理的并发请求数达到过200万。如今的网络服务器基本都采用这种方式,这也是nginx性能高效的主要缘由。apache
咱们以前说过,推荐设置worker的个数为cpu的核数,在这里就很容易理解了,更多的worker数,只会致使进程来竞争cpu资源了,从而带来没必要要的上下文切换。并且,nginx为了更好的利用多核特性,提供了cpu亲缘性的绑定选项,咱们能够将某一个进程绑定在某一个核上,这样就不会由于进程的切换带来cache的失效。像这种小的优化在nginx中很是常见,同时也说明了nginx做者的苦心孤诣。好比,nginx在作4个字节的字符串比较时,会将4个字符转换成一个int型,再做比较,以减小cpu的指令数等等。
如今,知道了nginx为何会选择这样的进程模型与事件模型了。对于一个基本的web服务器来讲,事件一般有三种类型,网络事件、信号、定时器。从上面的讲解中知道,网络事件经过异步非阻塞能够很好的解决掉。如何处理信号与定时器?
首先,信号的处理。对nginx来讲,有一些特定的信号,表明着特定的意义。信号会中断掉程序当前的运行,在改变状态后,继续执行。若是是系统调用,则可能会致使系统调用的失败,须要重入。关于信号的处理,你们能够学习一些专业书籍,这里很少说。对于nginx来讲,若是nginx正在等待事件(epoll_wait时),若是程序收到信号,在信号处理函数处理完后,epoll_wait会返回错误,而后程序可再次进入epoll_wait调用。
另外,再来看看定时器。因为epoll_wait等函数在调用的时候是能够设置一个超时时间的,因此nginx借助这个超时时间来实现定时器。nginx里面的定时器事件是放在一颗维护定时器的红黑树里面,每次在进入epoll_wait前,先从该红黑树里面拿到全部定时器事件的最小时间,在计算出epoll_wait的超时时间后进入epoll_wait。因此,当没有事件产生,也没有中断信号时,epoll_wait会超时,也就是说,定时器事件到了。这时,nginx会检查全部的超时事件,将他们的状态设置为超时,而后再去处理网络事件。由此能够看出,当咱们写nginx代码时,在处理网络事件的回调函数时,一般作的第一个事情就是判断超时,而后再去处理网络事件。
咱们能够用一段伪代码来总结一下nginx的事件处理模型:
while (true) {
for t in run_tasks:
t.handler();
update_time(&now);
timeout = ETERNITY;
for t in wait_tasks: /* sorted already */
if (t.time <= now) {
t.timeout_handler();
} else {
timeout = t.time - now;
break;
}
nevents = poll_function(events, timeout);
for i in nevents:
task t;
if (events[i].type == READ) {
t.handler = read_handler;
} else { /* events[i].type == WRITE */
t.handler = write_handler;
}
run_tasks_add(t);
}
好,本节咱们讲了进程模型,事件模型,包括网络事件,信号,定时器事件。
在nginx中connection就是对tcp链接的封装,其中包括链接的socket,读事件,写事件。利用nginx封装的connection,咱们能够很方便的使用nginx来处理与链接相关的事情,好比,创建链接,发送与接受数据等。而nginx中的http请求的处理就是创建在connection之上的,因此nginx不只能够做为一个web服务器,也能够做为邮件服务器。固然,利用nginx提供的connection,咱们能够与任何后端服务打交道。
结合一个tcp链接的生命周期,咱们看看nginx是如何处理一个链接的。首先,nginx在启动时,会解析配置文件,获得须要监听的端口与ip地址,而后在nginx的master进程里面,先初始化好这个监控的socket(建立socket,设置addrreuse等选项,绑定到指定的ip地址端口,再listen),而后再fork出多个子进程出来,而后子进程会竞争accept新的链接。此时,客户端就能够向nginx发起链接了。当客户端与服务端经过三次握手创建好一个链接后,nginx的某一个子进程会accept成功,获得这个创建好的链接的socket,而后建立nginx对链接的封装,即ngx_connection_t结构体。接着,设置读写事件处理函数并添加读写事件来与客户端进行数据的交换。最后,nginx或客户端来主动关掉链接,到此,一个链接就寿终正寝了。
固然,nginx也是能够做为客户端来请求其它server的数据的(如upstream模块),此时,与其它server建立的链接,也封装在ngx_connection_t中。做为客户端,nginx先获取一个ngx_connection_t结构体,而后建立socket,并设置socket的属性( 好比非阻塞)。而后再经过添加读写事件,调用connect/read/write来调用链接,最后关掉链接,并释放ngx_connection_t。
在nginx中,每一个进程会有一个链接数的最大上限,这个上限与系统对fd的限制不同。在操做系统中,经过ulimit -n,咱们能够获得一个进程所可以打开的fd的最大数,即nofile,由于每一个socket链接会占用掉一个fd,因此这也会限制咱们进程的最大链接数,固然也会直接影响到咱们程序所能支持的最大并发数,当fd用完后,再建立socket时,就会失败。nginx经过设置worker_connectons来设置每一个进程支持的最大链接数。若是该值大于nofile,那么实际的最大链接数是nofile,nginx会有警告。nginx在实现时,是经过一个链接池来管理的,每一个worker进程都有一个独立的链接池,链接池的大小是worker_connections。这里的链接池里面保存的其实不是真实的链接,它只是一个worker_connections大小的一个ngx_connection_t结构的数组。而且,nginx会经过一个链表free_connections来保存全部的空闲ngx_connection_t,每次获取一个链接时,就从空闲链接链表中获取一个,用完后,再放回空闲链接链表里面。
在这里,不少人会误解worker_connections这个参数的意思,认为这个值就是nginx所能创建链接的最大值。其实否则,这个值是表示每一个worker进程所能创建链接的最大值,因此,一个nginx能创建的最大链接数,应该是worker_connections * worker_processes。固然,这里说的是最大链接数,对于HTTP请求本地资源来讲,可以支持的最大并发数量是worker_connections * worker_processes,而若是是HTTP做为反向代理来讲,最大并发数量应该是worker_connections * worker_processes/2。由于做为反向代理服务器,每一个并发会创建与客户端的链接和与后端服务的链接,会占用两个链接。
那么,咱们前面有说过一个客户端链接过来后,多个空闲的进程,会竞争这个链接,很容易看到,这种竞争会致使不公平,若是某个进程获得accept的机会比较多,它的空闲链接很快就用完了,若是不提早作一些控制,当accept到一个新的tcp链接后,由于没法获得空闲链接,并且没法将此链接转交给其它进程,最终会致使此tcp链接得不处处理,就停止掉了。很显然,这是不公平的,有的进程有空余链接,却没有处理机会,有的进程由于没有空余链接,却人为地丢弃链接。那么,如何解决这个问题呢?首先,nginx的处理得先打开accept_mutex选项,此时,只有得到了accept_mutex的进程才会去添加accept事件,也就是说,nginx会控制进程是否添加accept事件。nginx使用一个叫ngx_accept_disabled的变量来控制是否去竞争accept_mutex锁。在第一段代码中,计算ngx_accept_disabled的值,这个值是nginx单进程的全部链接总数的八分之一,减去剩下的空闲链接数量,获得的这个ngx_accept_disabled有一个规律,当剩余链接数小于总链接数的八分之一时,其值才大于0,并且剩余的链接数越小,这个值越大。再看第二段代码,当ngx_accept_disabled大于0时,不会去尝试获取accept_mutex锁,而且将ngx_accept_disabled减1,因而,每次执行到此处时,都会去减1,直到小于0。不去获取accept_mutex锁,就是等于让出获取链接的机会,很显然能够看出,当空余链接越少时,ngx_accept_disable越大,因而让出的机会就越多,这样其它进程获取锁的机会也就越大。不去accept,本身的链接就控制下来了,其它进程的链接池就会获得利用,这样,nginx就控制了多进程间链接的平衡了。
ngx_accept_disabled = ngx_cycle->connection_n / 8
- ngx_cycle->free_connection_n;
if (ngx_accept_disabled > 0) {
ngx_accept_disabled--;
} else {
if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
return;
}
if (ngx_accept_mutex_held) {
flags |= NGX_POST_EVENTS;
} else {
if (timer == NGX_TIMER_INFINITE
|| timer > ngx_accept_mutex_delay)
{
timer = ngx_accept_mutex_delay;
}
}
}
好了,链接就先介绍到这,本章的目的是介绍基本概念,知道在nginx中链接是个什么东西就好了,并且链接是属于比较高级的用法,在后面的模块开发高级篇会有专门的章节来说解链接与事件的实现及使用。
这节咱们讲request,在nginx中咱们指的是http请求,具体到nginx中的数据结构是ngx_http_request_t。ngx_http_request_t是对一个http请求的封装。 咱们知道,一个http请求,包含请求行、请求头、请求体、响应行、响应头、响应体。
http请求是典型的请求-响应类型的的网络协议,而http是文件协议,因此咱们在分析请求行与请求头,以及输出响应行与响应头,每每是一行一行的进行处理。若是咱们本身来写一个http服务器,一般在一个链接创建好后,客户端会发送请求过来。而后咱们读取一行数据,分析出请求行中包含的method、uri、http_version信息。而后再一行一行处理请求头,并根据请求method与请求头的信息来决定是否有请求体以及请求体的长度,而后再去读取请求体。获得请求后,咱们处理请求产生须要输出的数据,而后再生成响应行,响应头以及响应体。在将响应发送给客户端以后,一个完整的请求就处理完了。固然这是最简单的webserver的处理方式,其实nginx也是这样作的,只是有一些小小的区别,好比,当请求头读取完成后,就开始进行请求的处理了。nginx经过ngx_http_request_t来保存解析请求与输出响应相关的数据。
那接下来,简要讲讲nginx是如何处理一个完整的请求的。对于nginx来讲,一个请求是从ngx_http_init_request开始的,在这个函数中,会设置读事件为ngx_http_process_request_line,也就是说,接下来的网络事件,会由ngx_http_process_request_line来执行。从ngx_http_process_request_line的函数名,咱们能够看到,这就是来处理请求行的,正好与以前讲的,处理请求的第一件事就是处理请求行是一致的。经过ngx_http_read_request_header来读取请求数据。而后调用ngx_http_parse_request_line函数来解析请求行。nginx为提升效率,采用状态机来解析请求行,并且在进行method的比较时,没有直接使用字符串比较,而是将四个字符转换成一个整型,而后一次比较以减小cpu的指令数,这个前面有说过。不少人可能很清楚一个请求行包含请求的方法,uri,版本,殊不知道其实在请求行中,也是能够包含有host的。好比一个请求GET http://www.taobao.com/uri HTTP/1.0这样一个请求行也是合法的,并且host是www.taobao.com,这个时候,nginx会忽略请求头中的host域,而以请求行中的这个为准来查找虚拟主机。另外,对于对于http0.9版来讲,是不支持请求头的,因此这里也是要特别的处理。因此,在后面解析请求头时,协议版本都是1.0或1.1。整个请求行解析到的参数,会保存到ngx_http_request_t结构当中。
在解析完请求行后,nginx会设置读事件的handler为ngx_http_process_request_headers,而后后续的请求就在ngx_http_process_request_headers中进行读取与解析。ngx_http_process_request_headers函数用来读取请求头,跟请求行同样,仍是调用ngx_http_read_request_header来读取请求头,调用ngx_http_parse_header_line来解析一行请求头,解析到的请求头会保存到ngx_http_request_t的域headers_in中,headers_in是一个链表结构,保存全部的请求头。而HTTP中有些请求是须要特别处理的,这些请求头与请求处理函数存放在一个映射表里面,即ngx_http_headers_in,在初始化时,会生成一个hash表,当每解析到一个请求头后,就会先在这个hash表中查找,若是有找到,则调用相应的处理函数来处理这个请求头。好比:Host头的处理函数是ngx_http_process_host。
当nginx解析到两个回车换行符时,就表示请求头的结束,此时就会调用ngx_http_process_request来处理请求了。ngx_http_process_request会设置当前的链接的读写事件处理函数为ngx_http_request_handler,而后再调用ngx_http_handler来真正开始处理一个完整的http请求。这里可能比较奇怪,读写事件处理函数都是ngx_http_request_handler,其实在这个函数中,会根据当前事件是读事件仍是写事件,分别调用ngx_http_request_t中的read_event_handler或者是write_event_handler。因为此时,咱们的请求头已经读取完成了,以前有说过,nginx的作法是先不读取请求body,因此这里面咱们设置read_event_handler为ngx_http_block_reading,即不读取数据了。刚才说到,真正开始处理数据,是在ngx_http_handler这个函数里面,这个函数会设置write_event_handler为ngx_http_core_run_phases,并执行ngx_http_core_run_phases函数。ngx_http_core_run_phases这个函数将执行多阶段请求处理,nginx将一个http请求的处理分为多个阶段,那么这个函数就是执行这些阶段来产生数据。由于ngx_http_core_run_phases最后会产生数据,因此咱们就很容易理解,为何设置写事件的处理函数为ngx_http_core_run_phases了。在这里,我简要说明了一下函数的调用逻辑,咱们须要明白最终是调用ngx_http_core_run_phases来处理请求,产生的响应头会放在ngx_http_request_t的headers_out中,这一部份内容,我会放在请求处理流程里面去讲。nginx的各类阶段会对请求进行处理,最后会调用filter来过滤数据,对数据进行加工,如truncked传输、gzip压缩等。这里的filter包括header filter与body filter,即对响应头或响应体进行处理。filter是一个链表结构,分别有header filter与body filter,先执行header filter中的全部filter,而后再执行body filter中的全部filter。在header filter中的最后一个filter,即ngx_http_header_filter,这个filter将会遍历全部的响应头,最后须要输出的响应头在一个连续的内存,而后调用ngx_http_write_filter进行输出。ngx_http_write_filter是body filter中的最后一个,因此nginx首先的body信息,在通过一系列的body filter以后,最后也会调用ngx_http_write_filter来进行输出(有图来讲明)。
这里要注意的是,nginx会将整个请求头都放在一个buffer里面,这个buffer的大小经过配置项client_header_buffer_size来设置,若是用户的请求头太大,这个buffer装不下,那nginx就会从新分配一个新的更大的buffer来装请求头,这个大buffer能够经过large_client_header_buffers来设置,这个large_buffer这一组buffer,好比配置4 8k,就是表示有四个8k大小的buffer能够用。注意,为了保存请求行或请求头的完整性,一个完整的请求行或请求头,须要放在一个连续的内存里面,因此,一个完整的请求行或请求头,只会保存在一个buffer里面。这样,若是请求行大于一个buffer的大小,就会返回414错误,若是一个请求头大小大于一个buffer大小,就会返回400错误。在了解了这些参数的值,以及nginx实际的作法以后,在应用场景,咱们就须要根据实际的需求来调整这些参数,来优化咱们的程序了。
处理流程图:
以上这些,就是nginx中一个http请求的生命周期了。咱们再看看与请求相关的一些概念吧。
固然,在nginx中,对于http1.0与http1.1也是支持长链接的。什么是长链接呢?咱们知道,http请求是基于TCP协议之上的,那么,当客户端在发起请求前,须要先与服务端创建TCP链接,而每一次的TCP链接是须要三次握手来肯定的,若是客户端与服务端之间网络差一点,这三次交互消费的时间会比较多,并且三次交互也会带来网络流量。固然,当链接断开后,也会有四次的交互,固然对用户体验来讲就不重要了。而http请求是请求应答式的,若是咱们能知道每一个请求头与响应体的长度,那么咱们是能够在一个链接上面执行多个请求的,这就是所谓的长链接,但前提条件是咱们先得肯定请求头与响应体的长度。对于请求来讲,若是当前请求须要有body,如POST请求,那么nginx就须要客户端在请求头中指定content-length来代表body的大小,不然返回400错误。也就是说,请求体的长度是肯定的,那么响应体的长度呢?先来看看http协议中关于响应body长度的肯定:
从上面,咱们能够看到,除了http1.0不带content-length以及http1.1非chunked不带content-length外,body的长度是可知的。此时,当服务端在输出完body以后,会能够考虑使用长链接。可否使用长链接,也是有条件限制的。若是客户端的请求头中的connection为close,则表示客户端须要关掉长链接,若是为keep-alive,则客户端须要打开长链接,若是客户端的请求中没有connection这个头,那么根据协议,若是是http1.0,则默认为close,若是是http1.1,则默认为keep-alive。若是结果为keepalive,那么,nginx在输出完响应体后,会设置当前链接的keepalive属性,而后等待客户端下一次请求。固然,nginx不可能一直等待下去,若是客户端一直不发数据过来,岂不是一直占用这个链接?因此当nginx设置了keepalive等待下一次的请求时,同时也会设置一个最大等待时间,这个时间是经过选项keepalive_timeout来配置的,若是配置为0,则表示关掉keepalive,此时,http版本不管是1.1仍是1.0,客户端的connection无论是close仍是keepalive,都会强制为close。
若是服务端最后的决定是keepalive打开,那么在响应的http头里面,也会包含有connection头域,其值是”Keep-Alive”,不然就是”Close”。若是connection值为close,那么在nginx响应完数据后,会主动关掉链接。因此,对于请求量比较大的nginx来讲,关掉keepalive最后会产生比较多的time-wait状态的socket。通常来讲,当客户端的一次访问,须要屡次访问同一个server时,打开keepalive的优点很是大,好比图片服务器,一般一个网页会包含不少个图片。打开keepalive也会大量减小time-wait的数量。
在http1.1中,引入了一种新的特性,即pipeline。那么什么是pipeline呢?pipeline其实就是流水线做业,它能够看做为keepalive的一种升华,由于pipeline也是基于长链接的,目的就是利用一个链接作屡次请求。若是客户端要提交多个请求,对于keepalive来讲,那么第二个请求,必需要等到第一个请求的响应接收彻底后,才能发起,这和TCP的中止等待协议是同样的,获得两个响应的时间至少为2*RTT。而对pipeline来讲,客户端没必要等到第一个请求处理完后,就能够立刻发起第二个请求。获得两个响应的时间可能可以达到1*RTT。nginx是直接支持pipeline的,可是,nginx对pipeline中的多个请求的处理却不是并行的,依然是一个请求接一个请求的处理,只是在处理第一个请求的时候,客户端就能够发起第二个请求。这样,nginx利用pipeline减小了处理完一个请求后,等待第二个请求的请求头数据的时间。其实nginx的作法很简单,前面说到,nginx在读取数据时,会将读取的数据放到一个buffer里面,因此,若是nginx在处理完前一个请求后,若是发现buffer里面还有数据,就认为剩下的数据是下一个请求的开始,而后就接下来处理下一个请求,不然就设置keepalive。
lingering_close,字面意思就是延迟关闭,也就是说,当nginx要关闭链接时,并不是当即关闭链接,而是先关闭tcp链接的写,再等待一段时间后再关掉链接的读。为何要这样呢?咱们先来看看这样一个场景。nginx在接收客户端的请求时,可能因为客户端或服务端出错了,要当即响应错误信息给客户端,而nginx在响应错误信息后,大分部状况下是须要关闭当前链接。nginx执行完write()系统调用把错误信息发送给客户端,write()系统调用返回成功并不表示数据已经发送到客户端,有可能还在tcp链接的write buffer里。接着若是直接执行close()系统调用关闭tcp链接,内核会首先检查tcp的read buffer里有没有客户端发送过来的数据留在内核态没有被用户态进程读取,若是有则发送给客户端RST报文来关闭tcp链接丢弃write buffer里的数据,若是没有则等待write buffer里的数据发送完毕,而后再通过正常的4次分手报文断开链接。因此,当在某些场景下出现tcp write buffer里的数据在write()系统调用以后到close()系统调用执行以前没有发送完毕,且tcp read buffer里面还有数据没有读,close()系统调用会致使客户端收到RST报文且不会拿到服务端发送过来的错误信息数据。那客户端确定会想,这服务器好霸道,动不动就reset个人链接,连个错误信息都没有。
在上面这个场景中,咱们能够看到,关键点是服务端给客户端发送了RST包,致使本身发送的数据在客户端忽略掉了。因此,解决问题的重点是,让服务端别发RST包。再想一想,咱们发送RST是由于咱们关掉了链接,关掉链接是由于咱们不想再处理此链接了,也不会有任何数据产生了。对于全双工的TCP链接来讲,咱们只须要关掉写就好了,读能够继续进行,咱们只须要丢掉读到的任何数据就好了,这样的话,当咱们关掉链接后,客户端再发过来的数据,就不会再收到RST了。固然最终咱们仍是须要关掉这个读端的,因此咱们会设置一个超时时间,在这个时间事后,就关掉读,客户端再发送数据来就无论了,做为服务端我会认为,都这么长时间了,发给你的错误信息也应该读到了,再慢就不关我事了,要怪就怪你RP很差了。固然,正常的客户端,在读取到数据后,会关掉链接,此时服务端就会在超时时间内关掉读端。这些正是lingering_close所作的事情。协议栈提供 SO_LINGER 这个选项,它的一种配置状况就是来处理lingering_close的状况的,不过nginx是本身实现的lingering_close。lingering_close存在的意义就是来读取剩下的客户端发来的数据,因此nginx会有一个读超时时间,经过lingering_timeout选项来设置,若是在lingering_timeout时间内尚未收到数据,则直接关掉链接。nginx还支持设置一个总的读取时间,经过lingering_time来设置,这个时间也就是nginx在关闭写以后,保留socket的时间,客户端须要在这个时间内发送完全部的数据,不然nginx在这个时间事后,会直接关掉链接。固然,nginx是支持配置是否打开lingering_close选项的,经过lingering_close选项来配置。 那么,咱们在实际应用中,是否应该打开lingering_close呢?这个就没有固定的推荐值了,如Maxim Dounin所说,lingering_close的主要做用是保持更好的客户端兼容性,可是却须要消耗更多的额外资源(好比链接会一直占着)。
这节,咱们介绍了nginx中,链接与请求的基本概念,下节,咱们讲基本的数据结构。
nginx的做者为追求极致的高效,本身实现了不少颇具特点的nginx风格的数据结构以及公共函数。好比,nginx提供了带长度的字符串,根据编译器选项优化过的字符串拷贝函数ngx_copy等。因此,在咱们写nginx模块时,应该尽可能调用nginx提供的api,尽管有些api只是对glibc的宏定义。本节,咱们介绍string、list、buffer、chain等一系列最基本的数据结构及相关api的使用技巧以及注意事项。
在nginx源码目录的src/core下面的ngx_string.h|c里面,包含了字符串的封装以及字符串相关操做的api。nginx提供了一个带长度的字符串结构ngx_str_t,它的原型以下:
typedef struct {
size_t len;
u_char *data;
} ngx_str_t;
在结构体当中,data指向字符串数据的第一个字符,字符串的结束用长度来表示,而不是由’\0’来表示结束。因此,在写nginx代码时,处理字符串的方法跟咱们平时使用有很大的不同,但要时刻记住,字符串不以’\0’结束,尽可能使用nginx提供的字符串操做的api来操做字符串。 那么,nginx这样作有什么好处呢?首先,经过长度来表示字符串长度,减小计算字符串长度的次数。其次,nginx能够重复引用一段字符串内存,data能够指向任意内存,长度表示结束,而不用去copy一份本身的字符串(由于若是要以’\0’结束,而不能更改原字符串,因此势必要copy一段字符串)。咱们在ngx_http_request_t结构体的成员中,能够找到不少字符串引用一段内存的例子,好比request_line、uri、args等等,这些字符串的data部分,都是指向在接收数据时建立buffer所指向的内存中,uri,args就没有必要copy一份出来。这样的话,减小了不少没必要要的内存分配与拷贝。 正是基于此特性,在nginx中,必须谨慎的去修改一个字符串。在修改字符串时须要认真的去考虑:是否能够修改该字符串;字符串修改后,是否会对其它的引用形成影响。在后面介绍ngx_unescape_uri函数的时候,就会看到这一点。可是,使用nginx的字符串会产生一些问题,glibc提供的不少系统api函数大可能是经过’\0’来表示字符串的结束,因此咱们在调用系统api时,就不能直接传入str->data了。此时,一般的作法是建立一段str->len + 1大小的内存,而后copy字符串,最后一个字节置为’\0’。比较hack的作法是,将字符串最后一个字符的后一个字符backup一个,而后设置为’\0’,在作完调用后,再由backup改回来,但前提条件是,你得肯定这个字符是能够修改的,并且是有内存分配,不会越界,但通常不建议这么作。 接下来,看看nginx提供的操做字符串相关的api。
#define ngx_string(str) { sizeof(str) - 1, (u_char *) str }
ngx_string(str)是一个宏,它经过一个以’\0’结尾的普通字符串str构造一个nginx的字符串,鉴于其中采用sizeof操做符计算字符串长度,所以参数必须是一个常量字符串。
#define ngx_null_string { 0, NULL }
定义变量时,使用ngx_null_string初始化字符串为空字符串,符串的长度为0,data为NULL。
#define ngx_str_set(str, text) \
(str)->len = sizeof(text) - 1; (str)->data = (u_char *) text
ngx_str_set用于设置字符串str为text,因为使用sizeof计算长度,故text必须为常量字符串。
#define ngx_str_null(str) (str)->len = 0; (str)->data = NULL
ngx_str_null用于设置字符串str为空串,长度为0,data为NULL。
上面这四个函数,使用时必定要当心,ngx_string与ngx_null_string是“{,}”格式的,故只能用于赋值时初始化,如:
ngx_str_t str = ngx_string("hello world");
ngx_str_t str1 = ngx_null_string;
若是向下面这样使用,就会有问题,这里涉及到c语言中对结构体变量赋值操做的语法规则,在此不作介绍。
ngx_str_t str, str1; str = ngx_string("hello world"); // 编译出错 str1 = ngx_null_string; // 编译出错
这种状况,能够调用ngx_str_set与ngx_str_null这两个函数来作:
ngx_str_t str, str1;
ngx_str_set(&str, "hello world");
ngx_str_null(&str1);
按照C99标准,您也能够这么作:
ngx_str_t str, str1;
str = (ngx_str_t) ngx_string("hello world");
str1 = (ngx_str_t) ngx_null_string;
另外要注意的是,ngx_string与ngx_str_set在调用时,传进去的字符串必定是常量字符串,不然会获得意想不到的错误(由于ngx_str_set内部使用了sizeof(),若是传入的是u_char*,那么计算的是这个指针的长度,而不是字符串的长度)。如:
ngx_str_t str; u_char *a = "hello world"; ngx_str_set(&str, a); // 问题产生
此外,值得注意的是,因为ngx_str_set与ngx_str_null其实是两行语句,故在if/for/while等语句中单独使用须要用花括号括起来,例如:
ngx_str_t str; if (cond) ngx_str_set(&str, "true"); // 问题产生 else ngx_str_set(&str, "false"); // 问题产生
void ngx_strlow(u_char *dst, u_char *src, size_t n);
将src的前n个字符转换成小写存放在dst字符串当中,调用者须要保证dst指向的空间大于等于n,且指向的空间必须可写。操做不会对原字符串产生变更。如要更改原字符串,能够:
ngx_strlow(str->data, str->data, str->len);
ngx_strncmp(s1, s2, n)
区分大小写的字符串比较,只比较前n个字符。
ngx_strcmp(s1, s2)
区分大小写的不带长度的字符串比较。
ngx_int_t ngx_strcasecmp(u_char *s1, u_char *s2);
不区分大小写的不带长度的字符串比较。
ngx_int_t ngx_strncasecmp(u_char *s1, u_char *s2, size_t n);
不区分大小写的带长度的字符串比较,只比较前n个字符。
u_char * ngx_cdecl ngx_sprintf(u_char *buf, const char *fmt, ...);
u_char * ngx_cdecl ngx_snprintf(u_char *buf, size_t max, const char *fmt, ...);
u_char * ngx_cdecl ngx_slprintf(u_char *buf, u_char *last, const char *fmt, ...);
上面这三个函数用于字符串格式化,ngx_snprintf的第二个参数max指明buf的空间大小,ngx_slprintf则经过last来指明buf空间的大小。推荐使用第二个或第三个函数来格式化字符串,ngx_sprintf函数仍是比较危险的,容易产生缓冲区溢出漏洞。在这一系列函数中,nginx在兼容glibc中格式化字符串的形式以外,还添加了一些方便格式化nginx类型的一些转义字符,好比%V用于格式化ngx_str_t结构。在nginx源文件的ngx_string.c中有说明:
/*
* supported formats:
* %[0][width][x][X]O off_t
* %[0][width]T time_t
* %[0][width][u][x|X]z ssize_t/size_t
* %[0][width][u][x|X]d int/u_int
* %[0][width][u][x|X]l long
* %[0][width|m][u][x|X]i ngx_int_t/ngx_uint_t
* %[0][width][u][x|X]D int32_t/uint32_t
* %[0][width][u][x|X]L int64_t/uint64_t
* %[0][width|m][u][x|X]A ngx_atomic_int_t/ngx_atomic_uint_t
* %[0][width][.width]f double, max valid number fits to %18.15f
* %P ngx_pid_t
* %M ngx_msec_t
* %r rlim_t
* %p void *
* %V ngx_str_t *
* %v ngx_variable_value_t *
* %s null-terminated string
* %*s length and string
* %Z '\0'
* %N '\n'
* %c char
* %% %
*
* reserved:
* %t ptrdiff_t
* %S null-terminated wchar string
* %C wchar
*/
这里特别要提醒的是,咱们最经常使用于格式化ngx_str_t结构,其对应的转义符是%V,传给函数的必定要是指针类型,不然程序就会coredump掉。这也是咱们最容易犯的错。好比:
ngx_str_t str = ngx_string("hello world"); char buffer[1024]; ngx_snprintf(buffer, 1024, "%V", &str); // 注意,str取地址
void ngx_encode_base64(ngx_str_t *dst, ngx_str_t *src);
ngx_int_t ngx_decode_base64(ngx_str_t *dst, ngx_str_t *src);
这两个函数用于对str进行base64编码与解码,调用前,须要保证dst中有足够的空间来存放结果,若是不知道具体大小,可先调用ngx_base64_encoded_length与ngx_base64_decoded_length来预估最大占用空间。
uintptr_t ngx_escape_uri(u_char *dst, u_char *src, size_t size,
ngx_uint_t type);
对src进行编码,根据type来按不一样的方式进行编码,若是dst为NULL,则返回须要转义的字符的数量,由此可获得须要的空间大小。type的类型能够是:
#define NGX_ESCAPE_URI 0
#define NGX_ESCAPE_ARGS 1
#define NGX_ESCAPE_HTML 2
#define NGX_ESCAPE_REFRESH 3
#define NGX_ESCAPE_MEMCACHED 4
#define NGX_ESCAPE_MAIL_AUTH 5
void ngx_unescape_uri(u_char **dst, u_char **src, size_t size, ngx_uint_t type);
对src进行反编码,type能够是0、NGX_UNESCAPE_URI、NGX_UNESCAPE_REDIRECT这三个值。若是是0,则表示src中的全部字符都要进行转码。若是是NGX_UNESCAPE_URI与NGX_UNESCAPE_REDIRECT,则遇到’?’后就结束了,后面的字符就无论了。而NGX_UNESCAPE_URI与NGX_UNESCAPE_REDIRECT之间的区别是NGX_UNESCAPE_URI对于遇到的须要转码的字符,都会转码,而NGX_UNESCAPE_REDIRECT则只会对非可见字符进行转码。
uintptr_t ngx_escape_html(u_char *dst, u_char *src, size_t size);
对html标签进行编码。
固然,我这里只介绍了一些经常使用的api的使用,你们能够先熟悉一下,在实际使用过程当中,遇到不明白的,最快最直接的方法就是去看源码,看api的实现或看nginx自身调用api的地方是怎么作的,代码就是最好的文档。
ngx_pool_t是一个很是重要的数据结构,在不少重要的场合都有使用,不少重要的数据结构也都在使用它。那么它到底是一个什么东西呢?简单的说,它提供了一种机制,帮助管理一系列的资源(如内存,文件等),使得对这些资源的使用和释放统一进行,免除了使用过程当中考虑到对各类各样资源的何时释放,是否遗漏了释放的担忧。
例如对于内存的管理,若是咱们须要使用内存,那么老是从一个ngx_pool_t的对象中获取内存,在最终的某个时刻,咱们销毁这个ngx_pool_t对象,全部这些内存都被释放了。这样咱们就没必要要对对这些内存进行malloc和free的操做,不用担忧是否某块被malloc出来的内存没有被释放。由于当ngx_pool_t对象被销毁的时候,全部从这个对象中分配出来的内存都会被统一释放掉。
再好比咱们要使用一系列的文件,可是咱们打开之后,最终须要都关闭,那么咱们就把这些文件统一登记到一个ngx_pool_t对象中,当这个ngx_pool_t对象被销毁的时候,全部这些文件都将会被关闭。
从上面举的两个例子中咱们能够看出,使用ngx_pool_t这个数据结构的时候,全部的资源的释放都在这个对象被销毁的时刻,统一进行了释放,那么就会带来一个问题,就是这些资源的生存周期(或者说被占用的时间)是跟ngx_pool_t的生存周期基本一致(ngx_pool_t也提供了少许操做能够提早释放资源)。从最高效的角度来讲,这并非最好的。好比,咱们须要依次使用A,B,C三个资源,且使用完B的时候,A就不会再被使用了,使用C的时候A和B都不会被使用到。若是不使用ngx_pool_t来管理这三个资源,那咱们可能从系统里面申请A,使用A,而后在释放A。接着申请B,使用B,再释放B。最后申请C,使用C,而后释放C。可是当咱们使用一个ngx_pool_t对象来管理这三个资源的时候,A,B和C的释放是在最后一块儿发生的,也就是在使用完C之后。诚然,这在客观上增长了程序在一段时间的资源使用量。可是这也减轻了程序员分别管理三个资源的生命周期的工做。这也就是有所得,必有所失的道理。其实是一个取舍的问题,要看在具体的状况下,你更在意的是哪一个。
能够看一下在nginx里面一个典型的使用ngx_pool_t的场景,对于nginx处理的每一个http request, nginx会生成一个ngx_pool_t对象与这个http request关联,全部处理过程当中须要申请的资源都从这个ngx_pool_t对象中获取,当这个http request处理完成之后,全部在处理过程当中申请的资源,都将随着这个关联的ngx_pool_t对象的销毁而释放。
ngx_pool_t相关结构及操做被定义在文件src/core/ngx_palloc.h|c中。
typedef struct ngx_pool_s ngx_pool_t;
struct ngx_pool_s {
ngx_pool_data_t d;
size_t max;
ngx_pool_t *current;
ngx_chain_t *chain;
ngx_pool_large_t *large;
ngx_pool_cleanup_t *cleanup;
ngx_log_t *log;
};
从ngx_pool_t的通常使用者的角度来讲,可不用关注ngx_pool_t结构中各字段做用。因此这里也不会进行详细的解释,固然在说明某些操做函数的使用的时候,若有必要,会进行说明。
下面咱们来分别解释下ngx_pool_t的相关操做。
ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log);
建立一个初始节点大小为size的pool,log为后续在该pool上进行操做时输出日志的对象。 须要说明的是size的选择,size的大小必须小于等于NGX_MAX_ALLOC_FROM_POOL,且必须大于sizeof(ngx_pool_t)。
选择大于NGX_MAX_ALLOC_FROM_POOL的值会形成浪费,由于大于该限制的空间不会被用到(只是说在第一个由ngx_pool_t对象管理的内存块上的内存,后续的分配若是第一个内存块上的空闲部分已用完,会再分配的)。
选择小于sizeof(ngx_pool_t)的值会形成程序崩溃。因为初始大小的内存块中要用一部分来存储ngx_pool_t这个信息自己。
当一个ngx_pool_t对象被建立之后,该对象的max字段被赋值为size-sizeof(ngx_pool_t)和NGX_MAX_ALLOC_FROM_POOL这二者中比较小的。后续的从这个pool中分配的内存块,在第一块内存使用完成之后,若是要继续分配的话,就须要继续从操做系统申请内存。当内存的大小小于等于max字段的时候,则分配新的内存块,连接在d这个字段(其实是d.next字段)管理的一条链表上。当要分配的内存块是比max大的,那么从系统中申请的内存是被挂接在large字段管理的一条链表上。咱们暂且把这个称之为大块内存链和小块内存链。
void *ngx_palloc(ngx_pool_t *pool, size_t size);
从这个pool中分配一块为size大小的内存。注意,此函数分配的内存的起始地址按照NGX_ALIGNMENT进行了对齐。对齐操做会提升系统处理的速度,但会形成少许内存的浪费。
void *ngx_pnalloc(ngx_pool_t *pool, size_t size);
从这个pool中分配一块为size大小的内存。可是此函数分配的内存并无像上面的函数那样进行过对齐。
void *ngx_pcalloc(ngx_pool_t *pool, size_t size);
该函数也是分配size大小的内存,而且对分配的内存块进行了清零。内部其实是转调用ngx_palloc实现的。
void *ngx_pmemalign(ngx_pool_t *pool, size_t size, size_t alignment);
按照指定对齐大小alignment来申请一块大小为size的内存。此处获取的内存无论大小都将被置于大内存块链中管理。
ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p);
对于被置于大块内存链,也就是被large字段管理的一列内存中的某块进行释放。该函数的实现是顺序遍历large管理的大块内存链表。因此效率比较低下。若是在这个链表中找到了这块内存,则释放,并返回NGX_OK。不然返回NGX_DECLINED。
因为这个操做效率比较低下,除非必要,也就是说这块内存很是大,确应及时释放,不然通常不须要调用。反正内存在这个pool被销毁的时候,总归会都释放掉的嘛!
ngx_pool_cleanup_t *ngx_pool_cleanup_add(ngx_pool_t *p, size_t size);
ngx_pool_t中的cleanup字段管理着一个特殊的链表,该链表的每一项都记录着一个特殊的须要释放的资源。对于这个链表中每一个节点所包含的资源如何去释放,是自说明的。这也就提供了很是大的灵活性。意味着,ngx_pool_t不只仅能够管理内存,经过这个机制,也能够管理任何须要释放的资源,例如,关闭文件,或者删除文件等等。下面咱们看一下这个链表每一个节点的类型:
typedef struct ngx_pool_cleanup_s ngx_pool_cleanup_t;
typedef void (*ngx_pool_cleanup_pt)(void *data);
struct ngx_pool_cleanup_s {
ngx_pool_cleanup_pt handler;
void *data;
ngx_pool_cleanup_t *next;
};
data: | 指明了该节点所对应的资源。 |
---|---|
handler: | 是一个函数指针,指向一个能够释放data所对应资源的函数。该函数只有一个参数,就是data。 |
next: | 指向该链表中下一个元素。 |
看到这里,ngx_pool_cleanup_add这个函数的用法,我相信你们都应该有一些明白了。可是这个参数size是起什么做用的呢?这个 size就是要存储这个data字段所指向的资源的大小,该函数会为data分配size大小的空间。
好比咱们须要最后删除一个文件。那咱们在调用这个函数的时候,把size指定为存储文件名的字符串的大小,而后调用这个函数给cleanup链表中增长一项。该函数会返回新添加的这个节点。咱们而后把这个节点中的data字段拷贝为文件名。把hander字段赋值为一个删除文件的函数(固然该函数的原型要按照void (*ngx_pool_cleanup_pt)(void *data))。
void ngx_destroy_pool(ngx_pool_t *pool);
该函数就是释放pool中持有的全部内存,以及依次调用cleanup字段所管理的链表中每一个元素的handler字段所指向的函数,来释放掉全部该pool管理的资源。而且把pool指向的ngx_pool_t也释放掉了,彻底不可用了。
void ngx_reset_pool(ngx_pool_t *pool);
该函数释放pool中全部大块内存链表上的内存,小块内存链上的内存块都修改成可用。可是不会去处理cleanup链表上的项目。
ngx_array_t是nginx内部使用的数组结构。nginx的数组结构在存储上与你们认知的C语言内置的数组有类似性,好比实际上存储数据的区域也是一大块连续的内存。可是数组除了存储数据的内存之外还包含一些元信息来描述相关的一些信息。下面咱们从数组的定义上来详细的了解一下。ngx_array_t的定义位于src/core/ngx_array.c|h里面。
typedef struct ngx_array_s ngx_array_t;
struct ngx_array_s {
void *elts;
ngx_uint_t nelts;
size_t size;
ngx_uint_t nalloc;
ngx_pool_t *pool;
};
elts: | 指向实际的数据存储区域。 |
---|---|
nelts: | 数组实际元素个数。 |
size: | 数组单个元素的大小,单位是字节。 |
nalloc: | 数组的容量。表示该数组在不引起扩容的前提下,能够最多存储的元素的个数。当nelts增加到达nalloc 时,若是再往此数组中存储元素,则会引起数组的扩容。数组的容量将会扩展到原有容量的2倍大小。其实是分配新的一块内存,新的一块内存的大小是原有内存大小的2倍。原有的数据会被拷贝到新的一块内存中。 |
pool: | 该数组用来分配内存的内存池。 |
下面介绍ngx_array_t相关操做函数。
ngx_array_t *ngx_array_create(ngx_pool_t *p, ngx_uint_t n, size_t size);
建立一个新的数组对象,并返回这个对象。
p: | 数组分配内存使用的内存池; |
---|---|
n: | 数组的初始容量大小,即在不扩容的状况下最多能够容纳的元素个数。 |
size: | 单个元素的大小,单位是字节。 |
void ngx_array_destroy(ngx_array_t *a);
销毁该数组对象,并释放其分配的内存回内存池。
void *ngx_array_push(ngx_array_t *a);
在数组a上新追加一个元素,并返回指向新元素的指针。须要把返回的指针使用类型转换,转换为具体的类型,而后再给新元素自己或者是各字段(若是数组的元素是复杂类型)赋值。
void *ngx_array_push_n(ngx_array_t *a, ngx_uint_t n);
在数组a上追加n个元素,并返回指向这些追加元素的首个元素的位置的指针。
static ngx_inline ngx_int_t ngx_array_init(ngx_array_t *array, ngx_pool_t *pool, ngx_uint_t n, size_t size);
若是一个数组对象是被分配在堆上的,那么当调用ngx_array_destroy销毁之后,若是想再次使用,就能够调用此函数。
若是一个数组对象是被分配在栈上的,那么就须要调用此函数,进行初始化的工做之后,才能够使用。
注意事项: 因为使用ngx_palloc分配内存,数组在扩容时,旧的内存不会被释放,会形成内存的浪费。所以,最好能提早规划好数组的容量,在建立或者初始化的时候一次搞定,避免屡次扩容,形成内存浪费。
ngx_hash_t是nginx本身的hash表的实现。定义和实现位于src/core/ngx_hash.h|c中。ngx_hash_t的实现也与数据结构教科书上所描述的hash表的实现是大同小异。对于经常使用的解决冲突的方法有线性探测,二次探测和开链法等。ngx_hash_t使用的是最经常使用的一种,也就是开链法,这也是STL中的hash表使用的方法。
可是ngx_hash_t的实现又有其几个显著的特色:
从上面的描述,咱们能够看出来,这个值越大,越形成内存的浪费。就两步,首先是初始化,而后就能够在里面进行查找了。下面咱们详细来看一下。
ngx_hash_t的初始化。
ngx_int_t ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names,
ngx_uint_t nelts);
首先咱们来看一下初始化函数。该函数的第一个参数hinit是初始化的一些参数的一个集合。 names是初始化一个ngx_hash_t所须要的全部key的一个数组。而nelts就是key的个数。下面先看一下ngx_hash_init_t类型,该类型提供了初始化一个hash表所须要的一些基本信息。
typedef struct {
ngx_hash_t *hash;
ngx_hash_key_pt key;
ngx_uint_t max_size;
ngx_uint_t bucket_size;
char *name;
ngx_pool_t *pool;
ngx_pool_t *temp_pool;
} ngx_hash_init_t;
hash: | 该字段若是为NULL,那么调用完初始化函数后,该字段指向新建立出来的hash表。若是该字段不为NULL,那么在初始的时候,全部的数据被插入了这个字段所指的hash表中。 |
---|---|
key: | 指向从字符串生成hash值的hash函数。nginx的源代码中提供了默认的实现函数ngx_hash_key_lc。 |
max_size: | hash表中的桶的个数。该字段越大,元素存储时冲突的可能性越小,每一个桶中存储的元素会更少,则查询起来的速度更快。固然,这个值越大,越形成内存的浪费也越大,(实际上也浪费不了多少)。 |
bucket_size: | 每一个桶的最大限制大小,单位是字节。若是在初始化一个hash表的时候,发现某个桶里面没法存的下全部属于该桶的元素,则hash表初始化失败。 |
name: | 该hash表的名字。 |
pool: | 该hash表分配内存使用的pool。 |
temp_pool: | 该hash表使用的临时pool,在初始化完成之后,该pool能够被释放和销毁掉。 |
下面来看一下存储hash表key的数组的结构。
typedef struct {
ngx_str_t key;
ngx_uint_t key_hash;
void *value;
} ngx_hash_key_t;
key和value的含义显而易见,就不用解释了。key_hash是对key使用hash函数计算出来的值。 对这两个结构分析完成之后,我想你们应该都已经明白这个函数应该是如何使用了吧。该函数成功初始化一个hash表之后,返回NGX_OK,不然返回NGX_ERROR。
void *ngx_hash_find(ngx_hash_t *hash, ngx_uint_t key, u_char *name, size_t len);
在hash里面查找key对应的value。实际上这里的key是对真正的key(也就是name)计算出的hash值。len是name的长度。
若是查找成功,则返回指向value的指针,不然返回NULL。
nginx为了处理带有通配符的域名的匹配问题,实现了ngx_hash_wildcard_t这样的hash表。他能够支持两种类型的带有通配符的域名。一种是通配符在前的,例如:“*.abc.com”,也能够省略掉星号,直接写成”.abc.com”。这样的key,能够匹配www.abc.com,qqq.www.abc.com之类的。另一种是通配符在末尾的,例如:“mail.xxx.*”,请特别注意通配符在末尾的不像位于开始的通配符能够被省略掉。这样的通配符,能够匹配mail.xxx.com、mail.xxx.com.cn、mail.xxx.net之类的域名。
有一点必须说明,就是一个ngx_hash_wildcard_t类型的hash表只能包含通配符在前的key或者是通配符在后的key。不能同时包含两种类型的通配符的key。ngx_hash_wildcard_t类型变量的构建是经过函数ngx_hash_wildcard_init完成的,而查询是经过函数ngx_hash_find_wc_head或者ngx_hash_find_wc_tail来作的。ngx_hash_find_wc_head是查询包含通配符在前的key的hash表的,而ngx_hash_find_wc_tail是查询包含通配符在后的key的hash表的。
下面详细说明这几个函数的用法。
ngx_int_t ngx_hash_wildcard_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names,
ngx_uint_t nelts);
该函数迎来构建一个能够包含通配符key的hash表。
hinit: | 构造一个通配符hash表的一些参数的一个集合。关于该参数对应的类型的说明,请参见ngx_hash_t类型中ngx_hash_init函数的说明。 |
---|---|
names: | 构造此hash表的全部的通配符key的数组。特别要注意的是这里的key已经都是被预处理过的。例如:“*.abc.com”或者“.abc.com”被预处理完成之后,变成了“com.abc.”。而“mail.xxx.*”则被预处理为“mail.xxx.”。为何会被处理这样?这里不得不简单地描述一下通配符hash表的实现原理。当构造此类型的hash表的时候,其实是构造了一个hash表的一个“链表”,是经过hash表中的key“连接”起来的。好比:对于“*.abc.com”将会构造出2个hash表,第一个hash表中有一个key为com的表项,该表项的value包含有指向第二个hash表的指针,而第二个hash表中有一个表项abc,该表项的value包含有指向*.abc.com对应的value的指针。那么查询的时候,好比查询www.abc.com的时候,先查com,经过查com能够找到第二级的hash表,在第二级hash表中,再查找abc,依次类推,直到在某一级的hash表中查到的表项对应的value对应一个真正的值而非一个指向下一级hash表的指针的时候,查询过程结束。这里有一点须要特别注意的,就是names数组中元素的value值低两位bit必须为0(有特殊用途)。若是不知足这个条件,这个hash表查询不出正确结果。 |
nelts: | names数组元素的个数。 |
该函数执行成功返回NGX_OK,不然NGX_ERROR。
void *ngx_hash_find_wc_head(ngx_hash_wildcard_t *hwc, u_char *name, size_t len);
该函数查询包含通配符在前的key的hash表的。
hwc: | hash表对象的指针。 |
---|---|
name: | 须要查询的域名,例如: www.abc.com。 |
len: | name的长度。 |
该函数返回匹配的通配符对应value。若是没有查到,返回NULL。
void *ngx_hash_find_wc_tail(ngx_hash_wildcard_t *hwc, u_char *name, size_t len);
该函数查询包含通配符在末尾的key的hash表的。 参数及返回值请参加上个函数的说明。
组合类型hash表,该hash表的定义以下:
typedef struct {
ngx_hash_t hash;
ngx_hash_wildcard_t *wc_head;
ngx_hash_wildcard_t *wc_tail;
} ngx_hash_combined_t;
从其定义显见,该类型实际上包含了三个hash表,一个普通hash表,一个包含前向通配符的hash表和一个包含后向通配符的hash表。
nginx提供该类型的做用,在于提供一个方便的容器包含三个类型的hash表,当有包含通配符的和不包含通配符的一组key构建hash表之后,以一种方便的方式来查询,你不须要再考虑一个key究竟是应该到哪一个类型的hash表里去查了。
构造这样一组合hash表的时候,首先定义一个该类型的变量,再分别构造其包含的三个子hash表便可。
对于该类型hash表的查询,nginx提供了一个方便的函数ngx_hash_find_combined。
void *ngx_hash_find_combined(ngx_hash_combined_t *hash, ngx_uint_t key,
u_char *name, size_t len);
该函数在此组合hash表中,依次查询其三个子hash表,看是否匹配,一旦找到,当即返回查找结果,也就是说若是有多个可能匹配,则只返回第一个匹配的结果。
hash: | 此组合hash表对象。 |
---|---|
key: | 根据name计算出的hash值。 |
name: | key的具体内容。 |
len: | name的长度。 |
返回查询的结果,未查到则返回NULL。
你们看到在构建一个ngx_hash_wildcard_t的时候,须要对通配符的哪些key进行预处理。这个处理起来比较麻烦。而当有一组key,这些里面既有无通配符的key,也有包含通配符的key的时候。咱们就须要构建三个hash表,一个包含普通的key的hash表,一个包含前向通配符的hash表,一个包含后向通配符的hash表(或者也能够把这三个hash表组合成一个ngx_hash_combined_t)。在这种状况下,为了让你们方便的构造这些hash表,nginx提供给了此辅助类型。
该类型以及相关的操做函数也定义在src/core/ngx_hash.h|c里。咱们先来看一下该类型的定义。
typedef struct {
ngx_uint_t hsize;
ngx_pool_t *pool;
ngx_pool_t *temp_pool;
ngx_array_t keys;
ngx_array_t *keys_hash;
ngx_array_t dns_wc_head;
ngx_array_t *dns_wc_head_hash;
ngx_array_t dns_wc_tail;
ngx_array_t *dns_wc_tail_hash;
} ngx_hash_keys_arrays_t;
hsize: | 将要构建的hash表的桶的个数。对于使用这个结构中包含的信息构建的三种类型的hash表都会使用此参数。 |
---|---|
pool: | 构建这些hash表使用的pool。 |
temp_pool: | 在构建这个类型以及最终的三个hash表过程当中可能用到临时pool。该temp_pool能够在构建完成之后,被销毁掉。这里只是存放临时的一些内存消耗。 |
keys: | 存放全部非通配符key的数组。 |
keys_hash: | 这是个二维数组,第一个维度表明的是bucket的编号,那么keys_hash[i]中存放的是全部的key算出来的hash值对hsize取模之后的值为i的key。假设有3个key,分别是key1,key2和key3假设hash值算出来之后对hsize取模的值都是i,那么这三个key的值就顺序存放在keys_hash[i][0],keys_hash[i][1], keys_hash[i][2]。该值在调用的过程当中用来保存和检测是否有冲突的key值,也就是是否有重复。 |
dns_wc_head: | 放前向通配符key被处理完成之后的值。好比:“*.abc.com” 被处理完成之后,变成 “com.abc.” 被存放在此数组中。 |
dns_wc_tail: | 存放后向通配符key被处理完成之后的值。好比:“mail.xxx.*” 被处理完成之后,变成 “mail.xxx.” 被存放在此数组中。 |
dns_wc_head_hash: | |
该值在调用的过程当中用来保存和检测是否有冲突的前向通配符的key值,也就是是否有重复。 | |
dns_wc_tail_hash: | |
该值在调用的过程当中用来保存和检测是否有冲突的后向通配符的key值,也就是是否有重复。 |
在定义一个这个类型的变量,并对字段pool和temp_pool赋值之后,就能够调用函数ngx_hash_add_key把全部的key加入到这个结构中了,该函数会自动实现普通key,带前向通配符的key和带后向通配符的key的分类和检查,并将这个些值存放到对应的字段中去, 而后就能够经过检查这个结构体中的keys、dns_wc_head、dns_wc_tail三个数组是否为空,来决定是否构建普通hash表,前向通配符hash表和后向通配符hash表了(在构建这三个类型的hash表的时候,能够分别使用keys、dns_wc_head、dns_wc_tail三个数组)。
构建出这三个hash表之后,能够组合在一个ngx_hash_combined_t对象中,使用ngx_hash_find_combined进行查找。或者是仍然保持三个独立的变量对应这三个hash表,本身决定什么时候以及在哪一个hash表中进行查询。
ngx_int_t ngx_hash_keys_array_init(ngx_hash_keys_arrays_t *ha, ngx_uint_t type);
初始化这个结构,主要是对这个结构中的ngx_array_t类型的字段进行初始化,成功返回NGX_OK。
ha: | 该结构的对象指针。 |
---|---|
type: | 该字段有2个值可选择,即NGX_HASH_SMALL和NGX_HASH_LARGE。用来指明将要创建的hash表的类型,若是是NGX_HASH_SMALL,则有比较小的桶的个数和数组元素大小。NGX_HASH_LARGE则相反。 |
ngx_int_t ngx_hash_add_key(ngx_hash_keys_arrays_t *ha, ngx_str_t *key,
void *value, ngx_uint_t flags);
通常是循环调用这个函数,把一组键值对加入到这个结构体中。返回NGX_OK是加入成功。返回NGX_BUSY意味着key值重复。
ha: | 该结构的对象指针。 |
---|---|
key: | 参数名自解释了。 |
value: | 参数名自解释了。 |
flags: | 有两个标志位能够设置,NGX_HASH_WILDCARD_KEY和NGX_HASH_READONLY_KEY。同时要设置的使用逻辑与操做符就能够了。NGX_HASH_READONLY_KEY被设置的时候,在计算hash值的时候,key的值不会被转成小写字符,不然会。NGX_HASH_WILDCARD_KEY被设置的时候,说明key里面可能含有通配符,会进行相应的处理。若是两个标志位都不设置,传0。 |
有关于这个数据结构的使用,能够参考src/http/ngx_http.c中的ngx_http_server_names函数。
nginx的filter模块在处理从别的filter模块或者是handler模块传递过来的数据(实际上就是须要发送给客户端的http response)。这个传递过来的数据是以一个链表的形式(ngx_chain_t)。并且数据可能被分屡次传递过来。也就是屡次调用filter的处理函数,以不一样的ngx_chain_t。
该结构被定义在src/core/ngx_buf.h|c。下面咱们来看一下ngx_chain_t的定义。
typedef struct ngx_chain_s ngx_chain_t;
struct ngx_chain_s {
ngx_buf_t *buf;
ngx_chain_t *next;
};
就2个字段,next指向这个链表的下个节点。buf指向实际的数据。因此在这个链表上追加节点也是很是容易,只要把末尾元素的next指针指向新的节点,把新节点的next赋值为NULL便可。
ngx_chain_t *ngx_alloc_chain_link(ngx_pool_t *pool);
该函数建立一个ngx_chain_t的对象,并返回指向对象的指针,失败返回NULL。
#define ngx_free_chain(pool, cl) \
cl->next = pool->chain; \
pool->chain = cl
该宏释放一个ngx_chain_t类型的对象。若是要释放整个chain,则迭代此链表,对每一个节点使用此宏便可。
注意: 对ngx_chaint_t类型的释放,并非真的释放了内存,而仅仅是把这个对象挂在了这个pool对象的一个叫作chain的字段对应的chain上,以供下次从这个pool上分配ngx_chain_t类型对象的时候,快速的从这个pool->chain上取下链首元素就返回了,固然,若是这个链是空的,才会真的在这个pool上使用ngx_palloc函数进行分配。
这个ngx_buf_t就是这个ngx_chain_t链表的每一个节点的实际数据。该结构其实是一种抽象的数据结构,它表明某种具体的数据。这个数据多是指向内存中的某个缓冲区,也可能指向一个文件的某一部分,也多是一些纯元数据(元数据的做用在于指示这个链表的读取者对读取的数据进行不一样的处理)。
该数据结构位于src/core/ngx_buf.h|c文件中。咱们来看一下它的定义。
struct ngx_buf_s {
u_char *pos;
u_char *last;
off_t file_pos;
off_t file_last;
u_char *start; /* start of buffer */
u_char *end; /* end of buffer */
ngx_buf_tag_t tag;
ngx_file_t *file;
ngx_buf_t *shadow;
/* the buf's content could be changed */
unsigned temporary:1;
/*
* the buf's content is in a memory cache or in a read only memory
* and must not be changed
*/
unsigned memory:1;
/* the buf's content is mmap()ed and must not be changed */
unsigned mmap:1;
unsigned recycled:1;
unsigned in_file:1;
unsigned flush:1;
unsigned sync:1;
unsigned last_buf:1;
unsigned last_in_chain:1;
unsigned last_shadow:1;
unsigned temp_file:1;
/* STUB */ int num;
};
pos: | 当buf所指向的数据在内存里的时候,pos指向的是这段数据开始的位置。 |
---|---|
last: | 当buf所指向的数据在内存里的时候,last指向的是这段数据结束的位置。 |
file_pos: | 当buf所指向的数据是在文件里的时候,file_pos指向的是这段数据的开始位置在文件中的偏移量。 |
file_last: | 当buf所指向的数据是在文件里的时候,file_last指向的是这段数据的结束位置在文件中的偏移量。 |
start: | 当buf所指向的数据在内存里的时候,这一整块内存包含的内容可能被包含在多个buf中(好比在某段数据中间插入了其余的数据,这一块数据就须要被拆分开)。那么这些buf中的start和end都指向这一块内存的开始地址和结束地址。而pos和last指向本buf所实际包含的数据的开始和结尾。 |
end: | 解释参见start。 |
tag: | 其实是一个void*类型的指针,使用者能够关联任意的对象上去,只要对使用者有意义。 |
file: | 当buf所包含的内容在文件中时,file字段指向对应的文件对象。 |
shadow: | 当这个buf完整copy了另一个buf的全部字段的时候,那么这两个buf指向的其实是同一块内存,或者是同一个文件的同一部分,此时这两个buf的shadow字段都是指向对方的。那么对于这样的两个buf,在释放的时候,就须要使用者特别当心,具体是由哪里释放,要提早考虑好,若是形成资源的屡次释放,可能会形成程序崩溃! |
temporary: | 为1时表示该buf所包含的内容是在一个用户建立的内存块中,而且能够被在filter处理的过程当中进行变动,而不会形成问题。 |
memory: | 为1时表示该buf所包含的内容是在内存中,可是这些内容却不能被进行处理的filter进行变动。 |
mmap: | 为1时表示该buf所包含的内容是在内存中, 是经过mmap使用内存映射从文件中映射到内存中的,这些内容却不能被进行处理的filter进行变动。 |
recycled: | 能够回收的。也就是这个buf是能够被释放的。这个字段一般是配合shadow字段一块儿使用的,对于使用ngx_create_temp_buf 函数建立的buf,而且是另一个buf的shadow,那么能够使用这个字段来标示这个buf是能够被释放的。 |
in_file: | 为1时表示该buf所包含的内容是在文件中。 |
flush: | 遇到有flush字段被设置为1的的buf的chain,则该chain的数据即使不是最后结束的数据(last_buf被设置,标志全部要输出的内容都完了),也会进行输出,不会受postpone_output配置的限制,可是会受到发送速率等其余条件的限制。 |
sync: | |
last_buf: | 数据被以多个chain传递给了过滤器,此字段为1代表这是最后一个buf。 |
last_in_chain: | 在当前的chain里面,此buf是最后一个。特别要注意的是last_in_chain的buf不必定是last_buf,可是last_buf的buf必定是last_in_chain的。这是由于数据会被以多个chain传递给某个filter模块。 |
last_shadow: | 在建立一个buf的shadow的时候,一般将新建立的一个buf的last_shadow置为1。 |
temp_file: | 因为受到内存使用的限制,有时候一些buf的内容须要被写到磁盘上的临时文件中去,那么这时,就设置此标志 。 |
对于此对象的建立,能够直接在某个ngx_pool_t上分配,而后根据须要,给对应的字段赋值。也能够使用定义好的2个宏:
#define ngx_alloc_buf(pool) ngx_palloc(pool, sizeof(ngx_buf_t))
#define ngx_calloc_buf(pool) ngx_pcalloc(pool, sizeof(ngx_buf_t))
这两个宏使用相似函数,也是不说自明的。
对于建立temporary字段为1的buf(就是其内容能够被后续的filter模块进行修改),能够直接使用函数ngx_create_temp_buf进行建立。
ngx_buf_t *ngx_create_temp_buf(ngx_pool_t *pool, size_t size);
该函数建立一个ngx_but_t类型的对象,并返回指向这个对象的指针,建立失败返回NULL。
对于建立的这个对象,它的start和end指向新分配内存开始和结束的地方。pos和last都指向这块新分配内存的开始处,这样,后续的操做能够在这块新分配的内存上存入数据。
pool: | 分配该buf和buf使用的内存所使用的pool。 |
---|---|
size: | 该buf使用的内存的大小。 |
为了配合对ngx_buf_t的使用,nginx定义了如下的宏方便操做。
#define ngx_buf_in_memory(b) (b->temporary || b->memory || b->mmap)
返回这个buf里面的内容是否在内存里。
#define ngx_buf_in_memory_only(b) (ngx_buf_in_memory(b) && !b->in_file)
返回这个buf里面的内容是否仅仅在内存里,而且没有在文件里。
#define ngx_buf_special(b) \
((b->flush || b->last_buf || b->sync) \
&& !ngx_buf_in_memory(b) && !b->in_file)
返回该buf是不是一个特殊的buf,只含有特殊的标志和没有包含真正的数据。
#define ngx_buf_sync_only(b) \
(b->sync \
&& !ngx_buf_in_memory(b) && !b->in_file && !b->flush && !b->last_buf)
返回该buf是不是一个只包含sync标志而不包含真正数据的特殊buf。
#define ngx_buf_size(b) \
(ngx_buf_in_memory(b) ? (off_t) (b->last - b->pos): \
(b->file_last - b->file_pos))
返回该buf所含数据的大小,无论这个数据是在文件里仍是在内存里。
ngx_list_t顾名思义,看起来好像是一个list的数据结构。这样的说法,算对也不算对。由于它符合list类型数据结构的一些特色,好比能够添加元素,实现自增加,不会像数组类型的数据结构,受到初始设定的数组容量的限制,而且它跟咱们常见的list型数据结构也是同样的,内部实现使用了一个链表。
那么它跟咱们常见的链表实现的list有什么不一样呢?不一样点就在于它的节点,它的节点不像咱们常见的list的节点,只能存放一个元素,ngx_list_t的节点其实是一个固定大小的数组。
在初始化的时候,咱们须要设定元素须要占用的空间大小,每一个节点数组的容量大小。在添加元素到这个list里面的时候,会在最尾部的节点里的数组上添加元素,若是这个节点的数组存满了,就再增长一个新的节点到这个list里面去。
好了,看到这里,你们应该基本上明白这个list结构了吧?还不明白也没有关系,下面咱们来具体看一下它的定义,这些定义和相关的操做函数定义在src/core/ngx_list.h|c文件中。
typedef struct {
ngx_list_part_t *last;
ngx_list_part_t part;
size_t size;
ngx_uint_t nalloc;
ngx_pool_t *pool;
} ngx_list_t;
last: | 指向该链表的最后一个节点。 |
---|---|
part: | 该链表的首个存放具体元素的节点。 |
size: | 链表中存放的具体元素所需内存大小。 |
nalloc: | 每一个节点所含的固定大小的数组的容量。 |
pool: | 该list使用的分配内存的pool。 |
好,咱们在看一下每一个节点的定义。
typedef struct ngx_list_part_s ngx_list_part_t;
struct ngx_list_part_s {
void *elts;
ngx_uint_t nelts;
ngx_list_part_t *next;
};
elts: | 节点中存放具体元素的内存的开始地址。 |
---|---|
nelts: | 节点中已有元素个数。这个值是不能大于链表头节点ngx_list_t类型中的nalloc字段的。 |
next: | 指向下一个节点。 |
咱们来看一下提供的一个操做的函数。
ngx_list_t *ngx_list_create(ngx_pool_t *pool, ngx_uint_t n, size_t size);
该函数建立一个ngx_list_t类型的对象,并对该list的第一个节点分配存放元素的内存空间。
pool: | 分配内存使用的pool。 |
---|---|
n: | 每一个节点固定长度的数组的长度。 |
size: | 存放的具体元素的个数。 |
返回值: | 成功返回指向建立的ngx_list_t对象的指针,失败返回NULL。 |
void *ngx_list_push(ngx_list_t *list);
该函数在给定的list的尾部追加一个元素,并返回指向新元素存放空间的指针。若是追加失败,则返回NULL。
static ngx_inline ngx_int_t
ngx_list_init(ngx_list_t *list, ngx_pool_t *pool, ngx_uint_t n, size_t size);
该函数是用于ngx_list_t类型的对象已经存在,可是其第一个节点存放元素的内存空间还未分配的状况下,能够调用此函数来给这个list的首节点来分配存放元素的内存空间。
那么何时会出现已经有了ngx_list_t类型的对象,而其首节点存放元素的内存还没有分配的状况呢?那就是这个ngx_list_t类型的变量并非经过调用ngx_list_create函数建立的。例如:若是某个结构体的一个成员变量是ngx_list_t类型的,那么当这个结构体类型的对象被建立出来的时候,这个成员变量也被建立出来了,可是它的首节点的存放元素的内存并未被分配。
总之,若是这个ngx_list_t类型的变量,若是不是你经过调用函数ngx_list_create建立的,那么就必须调用此函数去初始化,不然,你往这个list里追加元素就可能引起不可预知的行为,亦或程序会崩溃!
ngx_queue_t是nginx中的双向链表,在nginx源码目录src/core下面的ngx_queue.h|c里面。它的原型以下:
typedef struct ngx_queue_s ngx_queue_t;
struct ngx_queue_s {
ngx_queue_t *prev;
ngx_queue_t *next;
};
不一样于教科书中将链表节点的数据成员声明在链表节点的结构体中,ngx_queue_t只是声明了前向和后向指针。在使用的时候,咱们首先须要定义一个哨兵节点(对于后续具体存放数据的节点,咱们称之为数据节点),好比:
ngx_queue_t free;
接下来须要进行初始化,经过宏ngx_queue_init()来实现:
ngx_queue_init(&free);
ngx_queue_init()的宏定义以下:
#define ngx_queue_init(q) \
(q)->prev = q; \
(q)->next = q;
可见初始的时候哨兵节点的 prev 和 next 都指向本身,所以实际上是一个空链表。ngx_queue_empty()能够用来判断一个链表是否为空,其实现也很简单,就是:
#define ngx_queue_empty(h) \
(h == (h)->prev)
那么如何声明一个具备数据元素的链表节点呢?只要在相应的结构体中加上一个 ngx_queue_t 的成员就好了。好比ngx_http_upstream_keepalive_module中的ngx_http_upstream_keepalive_cache_t:
typedef struct {
ngx_http_upstream_keepalive_srv_conf_t *conf;
ngx_queue_t queue;
ngx_connection_t *connection;
socklen_t socklen;
u_char sockaddr[NGX_SOCKADDRLEN];
} ngx_http_upstream_keepalive_cache_t;
对于每个这样的数据节点,能够经过ngx_queue_insert_head()来添加到链表中,第一个参数是哨兵节点,第二个参数是数据节点,好比:
ngx_http_upstream_keepalive_cache_t cache;
ngx_queue_insert_head(&free, &cache.queue);
相应的几个宏定义以下:
#define ngx_queue_insert_head(h, x) \
(x)->next = (h)->next; \
(x)->next->prev = x; \
(x)->prev = h; \
(h)->next = x
#define ngx_queue_insert_after ngx_queue_insert_head #define ngx_queue_insert_tail(h, x) \ (x)->prev = (h)->prev; \ (x)->prev->next = x; \ (x)->next = h; \ (h)->prev = x
ngx_queue_insert_head()和ngx_queue_insert_after()都是往头部添加节点,ngx_queue_insert_tail()是往尾部添加节点。从代码能够看出哨兵节点的 prev 指向链表的尾数据节点,next 指向链表的头数据节点。另外ngx_queue_head()和ngx_queue_last()这两个宏分别能够获得头节点和尾节点。
那假如如今有一个ngx_queue_t *q 指向的是链表中的数据节点的queue成员,如何获得ngx_http_upstream_keepalive_cache_t的数据呢? nginx提供了ngx_queue_data()宏来获得ngx_http_upstream_keepalive_cache_t的指针,例如:
ngx_http_upstream_keepalive_cache_t *cache = ngx_queue_data(q, ngx_http_upstream_keepalive_cache_t, queue);
也许您已经能够猜到ngx_queue_data是经过地址相减来获得的:
#define ngx_queue_data(q, type, link) \
(type *) ((u_char *) q - offsetof(type, link))
另外nginx也提供了ngx_queue_remove()宏来从链表中删除一个数据节点,以及ngx_queue_add()用来将一个链表添加到另外一个链表。
nginx的配置系统由一个主配置文件和其余一些辅助的配置文件构成。这些配置文件均是纯文本文件,所有位于nginx安装目录下的conf目录下。
配置文件中以#开始的行,或者是前面有若干空格或者TAB,而后再跟#的行,都被认为是注释,也就是只对编辑查看文件的用户有意义,程序在读取这些注释行的时候,其实际的内容是被忽略的。
因为除主配置文件nginx.conf之外的文件都是在某些状况下才使用的,而只有主配置文件是在任何状况下都被使用的。因此在这里咱们就以主配置文件为例,来解释nginx的配置系统。
在nginx.conf中,包含若干配置项。每一个配置项由配置指令和指令参数2个部分构成。指令参数也就是配置指令对应的配置值。
指令的参数使用一个或者多个空格或者TAB字符与指令分开。指令的参数有一个或者多个TOKEN串组成。TOKEN串之间由空格或者TAB键分隔。
TOKEN串分为简单字符串或者是复合配置块。复合配置块便是由大括号括起来的一堆内容。一个复合配置块中可能包含若干其余的配置指令。
若是一个配置指令的参数所有由简单字符串构成,也就是不包含复合配置块,那么咱们就说这个配置指令是一个简单配置项,不然称之为复杂配置项。例以下面这个是一个简单配置项:
error_page 500 502 503 504 /50x.html;
对于简单配置,配置项的结尾使用分号结束。对于复杂配置项,包含多个TOKEN串的,通常都是简单TOKEN串放在前面,复合配置块通常位于最后,并且其结尾,并不须要再添加分号。例以下面这个复杂配置项:
location / {
root /home/jizhao/nginx-book/build/html;
index index.html index.htm;
}
nginx.conf中的配置信息,根据其逻辑上的意义,对它们进行了分类,也就是分红了多个做用域,或者称之为配置指令上下文。不一样的做用域含有一个或者多个配置项。
当前nginx支持的几个指令上下文:
main: | nginx在运行时与具体业务功能(好比http服务或者email服务代理)无关的一些参数,好比工做进程数,运行的身份等。 |
---|---|
http: | 与提供http服务相关的一些配置参数。例如:是否使用keepalive啊,是否使用gzip进行压缩等。 |
server: | http服务上支持若干虚拟主机。每一个虚拟主机一个对应的server配置项,配置项里面包含该虚拟主机相关的配置。在提供mail服务的代理时,也能够创建若干server.每一个server经过监听的地址来区分。 |
location: | http服务中,某些特定的URL对应的一系列配置项。 |
mail: | 实现email相关的SMTP/IMAP/POP3代理时,共享的一些配置项(由于可能实现多个代理,工做在多个监听地址上)。 |
指令上下文,可能有包含的状况出现。例如:一般http上下文和mail上下文必定是出如今main上下文里的。在一个上下文里,可能包含另一种类型的上下文屡次。例如:若是http服务,支持了多个虚拟主机,那么在http上下文里,就会出现多个server上下文。
咱们来看一个示例配置:
user nobody;
worker_processes 1;
error_log logs/error.log info;
events {
worker_connections 1024;
}
http {
server {
listen 80;
server_name www.linuxidc.com;
access_log logs/linuxidc.access.log main;
location / {
index index.html;
root /var/www/linuxidc.com/htdocs;
}
}
server {
listen 80;
server_name www.Androidj.com;
access_log logs/androidj.access.log main;
location / {
index index.html;
root /var/www/androidj.com/htdocs;
}
}
}
mail {
auth_http 127.0.0.1:80/auth.php;
pop3_capabilities "TOP" "USER";
imap_capabilities "IMAP4rev1" "UIDPLUS";
server {
listen 110;
protocol pop3;
proxy on;
}
server {
listen 25;
protocol smtp;
proxy on;
smtp_auth login plain;
xclient off;
}
}
在这个配置中,上面提到个五种配置指令上下文都存在。
存在于main上下文中的配置指令以下:
存在于http上下文中的指令以下:
存在于mail上下文中的指令以下:
存在于server上下文中的配置指令以下:
存在于location上下文中的指令以下:
固然,这里只是一些示例。具体有哪些配置指令,以及这些配置指令能够出如今什么样的上下文中,须要参考nginx的使用文档。
nginx的内部结构是由核心部分和一系列的功能模块所组成。这样划分是为了使得每一个模块的功能相对简单,便于开发,同时也便于对系统进行功能扩展。为了便于描述,下文中咱们将使用nginx core来称呼nginx的核心功能部分。
nginx提供了web服务器的基础功能,同时提供了web服务反向代理,email服务反向代理功能。nginx core实现了底层的通信协议,为其余模块和nginx进程构建了基本的运行时环境,而且构建了其余各模块的协做基础。除此以外,或者说大部分与协议相关的,或者应用相关的功能都是在这些模块中所实现的。
nginx将各功能模块组织成一条链,当有请求到达的时候,请求依次通过这条链上的部分或者所有模块,进行处理。每一个模块实现特定的功能。例如,实现对请求解压缩的模块,实现SSI的模块,实现与上游服务器进行通信的模块,实现与FastCGI服务进行通信的模块。
有两个模块比较特殊,他们居于nginx core和各功能模块的中间。这两个模块就是http模块和mail模块。这2个模块在nginx core之上实现了另一层抽象,处理与HTTP协议和email相关协议(SMTP/POP3/IMAP)有关的事件,而且确保这些事件能被以正确的顺序调用其余的一些功能模块。
目前HTTP协议是被实如今http模块中的,可是有可能未来被剥离到一个单独的模块中,以扩展nginx支持SPDY协议。
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: | 负载均衡模块,实现特定的算法,在众多的后端服务器中,选择一个服务器出来做为某个请求的转发服务器。 |
nginx使用一个多进程模型来对外提供服务,其中一个master进程,多个worker进程。master进程负责管理nginx自己和其余worker进程。
全部实际上的业务处理逻辑都在worker进程。worker进程中有一个函数,执行无限循环,不断处理收到的来自客户端的请求,并进行处理,直到整个nginx服务被中止。
worker进程中,ngx_worker_process_cycle()函数就是这个无限循环的处理函数。在这个函数中,一个请求的简单处理流程以下:
为了让你们更好的了解nginx中请求处理过程,咱们以HTTP Request为例,来作一下详细地说明。
从nginx的内部来看,一个HTTP Request的处理过程涉及到如下几个阶段。
在这里,咱们须要了解一下phase handler这个概念。phase字面的意思,就是阶段。因此phase handlers也就好理解了,就是包含若干个处理阶段的一些handler。
在每个阶段,包含有若干个handler,再处理到某个阶段的时候,依次调用该阶段的handler对HTTP Request进行处理。
一般状况下,一个phase handler对这个request进行处理,并产生一些输出。一般phase handler是与定义在配置文件中的某个location相关联的。
一个phase handler一般执行如下几项任务:
当nginx读取到一个HTTP Request的header的时候,nginx首先查找与这个请求关联的虚拟主机的配置。若是找到了这个虚拟主机的配置,那么一般状况下,这个HTTP Request将会通过如下几个阶段的处理(phase handlers):
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: | |
日志模块处理阶段 |
在内容产生阶段,为了给一个request产生正确的响应,nginx必须把这个request交给一个合适的content handler去处理。若是这个request对应的location在配置文件中被明确指定了一个content handler,那么nginx就能够经过对location的匹配,直接找到这个对应的handler,并把这个request交给这个content handler去处理。这样的配置指令包括像,perl,flv,proxy_pass,mp4等。
若是一个request对应的location并无直接有配置的content handler,那么nginx依次尝试:
内容产生阶段完成之后,生成的输出会被传递到filter模块去进行处理。filter模块也是与location相关的。全部的fiter模块都被组织成一条链。输出会依次穿越全部的filter,直到有一个filter模块的返回值代表已经处理完成。
这里列举几个常见的filter模块,例如:
在全部的filter中,有几个filter模块须要关注一下。按照调用的顺序依次说明以下:
write: | 写输出到客户端,其实是写到链接对应的socket上。 |
---|---|
postpone: | 这个filter是负责subrequest的,也就是子请求的。 |
copy: | 将一些须要复制的buf(文件或者内存)从新复制一份而后交给剩余的body filter处理。 |