- 论坛徽章:
- 0
|
第七章 请求处理
创建监听套接口
前面章节曾陆陆续续的提到过nginx对客户端请求的处理,但不甚连贯,所以本章就尝试把这个请求处理响应过程完整的描述一遍。下面先来看后http请求处理的前置准备工作,也就是监听套接口的创建以及组织等。
创建哪些监听套接口当然是由用户来指定的,nginx提供的配置指令为listen(仅关注http模块:http://wiki.nginx.org/HttpCoreModule#listen),该指令功能非常的丰富,不过在大部分情况下,我们都用得比较简单,一般是指定监听ip和端口号(因为http协议是基于tcp,所以这里自然也就是tcp端口),比如:listen 192.168.1.1:80;,这表示nginx仅监听目的ip是192.168.1.1且端口是80的http请求;如果主机上还有一个192.168.1.2的ip地址,那么客户端对该地址的80端口访问将被拒绝,要让该地址也正常访问需同样把该ip加入:listen 192.168.1.2:80;,如果有更多ip,这样逐个加入比较麻烦,因而另一种更偷懒的配置方法是只指定端口号:listen 80;,那么此时任意目的ip都可以访问到。不过,这两种不同的配置方式会影响到nginx创建监听套接口的数目,前一种方式nginx会对应的创建多个监听套接口,而后一种方式,由于listen 80;包含了所有的目标ip,所以创建一个监听套接口就足以,即便是配置文件里还有listen 192.168.1.1:80;这样的配置。看看实例,感性的认识一下:
30: Filename : nginx.conf
15: server {
16: listen 80;
17: ...
34: server {
35: listen 192.168.1.1:80;
36:
上面配置中有两个server,第一个配置listen的目标ip为任意(只要是本主机有的),第二个配置listen的目标ip为192.168.1.1,但是nginx在创建监听套接口时却只创建了一个:
[root@localhost html]# netstat -ntap | grep nginx
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 13040/nginx
如果将第16行配置改为listen 192.168.1.2:80;,那么此时的nginx将创建两个监听套接口:
[root@localhost nginx]# netstat -ntpa | grep nginx
tcp 0 0 192.168.1.1:80 0.0.0.0:* LISTEN 13145/nginx
tcp 0 0 192.168.1.2:80 0.0.0.0:* LISTEN 13145/nginx
再来看nginx代码的具体实现,配置指令listen的使用上下文为server,其对应的处理函数为ngx_http_core_listen(),该函数本身的功能比较单一,主要是解析listen指令并将对应的结果存到变量lsopt(可能有人注意到这是一个局部变量,不过没关系,在后面的函数调用里通过结构体赋值的方式,将它的值全部复制给另外一个变量addr->opt)内,最后调用函数ngx_http_add_listen(),这才是此处关注的核心函数,它将所有的listen配置以[port,addr]的形式组织在http核心配置ngx_http_core_main_conf_t下的ports数组字段内。另外,如果有一个server内没有配置监听端口,那么nginx会自动创建一个变量lsopt并给出一些默认值,然后调用函数ngx_http_add_listen()将其组织到ports数组字段内,这在server块配置的回调函数ngx_http_core_server()最后可以看到:
2700: Filename : ngx_http_core_module.c
2701: static char *
2702: ngx_http_core_server(ngx_conf_t *cf, ngx_command_t *cmd, void *dummy)
2703: {
2704: …
2790: if (rv == NGX_CONF_OK && !cscf->listen) {
2791: ngx_memzero(&lsopt, sizeof(ngx_http_listen_opt_t));
2792: …
2795: sin->sin_family = AF_INET;
2796: …
2799: sin->sin_port = htons((getuid() == 0) ? 80 : 8000);
2800: #endif
2801: sin->sin_addr.s_addr = INADDR_ANY;
2802: …
2816: if (ngx_http_add_listen(cf, cscf, &lsopt) != NGX_OK) {
2817: …
默认的设置是ipv4协议族、80或8000端口、任意目的ip,所以不管怎么样,一个server配置至少有一个监听套接口。回过头来看ngx_http_add_listen()函数以及相关逻辑,具体代码并没有什么难以理解的地方,我们直接看一个实例以及对应的图示,这样更直观且能把握全局。仍接着前面的实例,再加一个server配置:
00: Filename : nginx.conf
15: server {
16: listen 80;
17: server_name www.other_all.com;
18: ...
34: server {
35: listen 192.168.1.1:80;
36: server_name www.web_test1.com;
37: …
53: server {
54: listen 192.168.1.2:8000;
55: server_name www.web_test2.com;
56: …
当nginx的http配置块全部解析完后,所有的监听套接口信息(包括用户主动listen配置或nginx默认添加)都已被收集起来,先按port端口分类形成数组存储在cmcf->ports内,然后再在每一个port内按ip地址分类形成数组存储在port->addrs内,也就是一个[port, addr]的二维划分,如上图所示。附带说一下,其实一个[port, addr]可以对应有多个server配置块,但这里的实例中server配置块只有一个,所以也就是默认配置块default_server;对应server配置块的多少并不会影响到监听套接口的创建逻辑,因为创建监听套接口依赖的是[port, addr]本身,而非它对应的server配置块。回到刚才的思路上,在http配置指令的回调函数ngx_http_block()最后,也就是http配置块全部解析完后,将调用ngx_http_optimize_servers()函数‘创建’对应的监听套接口,之所以打上引号是因为这里还只是名义上的创建,也就只是创建了每个监听套接口所对应的结构体变量ngx_listening_s,并以数组的形式组织在全局变量cycle->listening内,具体的函数调用关系如下:
ngx_http_optimize_servers() -> ngx_http_init_listening() -> ngx_http_add_listening() -> ngx_create_listening()
先关注两点:第一,如果某端口上有任意目的ip的listen配置,那么在该端口上只会创建一个结构体变量ngx_listening_s,不管是否还有其他ip在该端口上的listen配置,进而在后面创建监听套接口描述符时也只创建一个,这在前面的演示实例里验证过这种情况,相关逻辑代码实现在函数ngx_http_init_listening()内,比如局部变量bind_wildcard,也包括前面函数中对ip地址排序的准备工作等。第二,ngx_listening_s结构体变量ls的回调字段handler被设置为ngx_http_init_connection,注意到这点可以帮助我们在看后面的逻辑时,代码回调该函数时,就能清楚它实际执行的是哪个函数。
另外要关注的是监听套接口与server配置块的关联,这是必须的,因为当监听套接口上一个客户端请求到达时,nginx必须知道它对应的server配置才能做进一步处理,这部分逻辑主要在函数ngx_http_add_addrs()内(以ipv4为例),调用关系如下:
ngx_http_optimize_servers() -> ngx_http_init_listening() -> ngx_http_add_addrs()
到此,在上面所举示例里,所有等待创建的监听套接口以及相关数据组织结构如下所示("www.web_test1.com"排到了"www.other_all.com"的前面,是因为ngx_sort()排序的缘故):
在所有配置解析完并且做了一些其它初始化工作后,就开始真正的监听套接口描述符创建以及特性设置,也就是调用诸如socket()、setsockopt()、bind()、listen()等这样的系统函数,相关逻辑实现在函数ngx_open_listening_sockets()和ngx_configure_listening_sockets()内,而这两个函数在nginx的初始化函数ngx_init_cycle()内靠结尾处被调用。从函数ngx_open_listening_sockets()内代码实现,可以看到就是遍历cycle->listening数组内每一个ls元素进行逐个创建,而ngx_configure_listening_sockets()内的描述符特性设置也是如此:
267: Filename : ngx_connection.c
268: ngx_int_t
269: ngx_open_listening_sockets(ngx_cycle_t *cycle)
270: {
271: …
292: ls = cycle->listening.elts;
293: for (i = 0; i < cycle->listening.nelts; i++) {
294: …
312: s = ngx_socket(ls.sockaddr->sa_family, ls.type, 0);
313: …
在这两个函数执行完之后,cycle->listening数组的每一个ls元素,其fd就不再是-1,而是一个可用的监听套接口描述符,并且该描述符根据用户设置赋予了不同的特性,比如收包缓存区大小,发包缓存区大小等。
关于监听套接口,nginx主进程的工作就做完了,接下来主进程通过fork()创建子进程,也就是工作进程,它们将全部继承这些已初始化好的监听套接口。在每个工作进程的事件初始化函数ngx_event_process_init()内,对每一个监听套接口创建对应的connection连接对象(为什么不直接用一个event事件对象呢?主要是考虑到可以传递更多信息到函数ngx_event_accept()内,并且这个连接对象虽然并没有对应的客户端,但可以与accept()创建的连接套接口统一起来,因为连接套接口对应的是connection连接对象),并利用该connection的read事件对象(因为在监听套接口上触发的肯定是读事件):
582: Filename : ngx_event.c
583: static ngx_int_t
584: ngx_event_process_init(ngx_cycle_t *cycle)
585: {
586: …
745: ls = cycle->listening.elts;
746: for (i = 0; i < cycle->listening.nelts; i++) {
747:
748: c = ngx_get_connection(ls.fd, cycle->log);
749: …
759: rev = c->read;
760: …
762: rev->accept = 1;
763: …
826: rev->handler = ngx_event_accept;
read事件对象的回调处理函数为ngx_event_accept(),请记住它。一切都准备就绪,接下来就是对rev事件对象进行监控,即将监听套接口所对应的事件对象加入到nginx的事件处理模型里,那什么时候加入呢?如果没有启动accept_mutex,那么在函数ngx_event_process_init()末尾就会通过ngx_add_event()将它加入到事件监控机制内:
837: Filename : ngx_event.c
838: if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR) {
否则就需在核心执行函数ngx_process_events_and_timers()内进行竞争,抢占到accept_mutex锁后才会把它加入:
353: Filename : ngx_event_accept.c
354: if (ngx_add_event(c->read, NGX_READ_EVENT,0)==NGX_ERROR){
在事件机制一章的负载均衡一节有对这两方面的详细描述,而在这里我们需特别注意的是,这些事件对象是以水平触发的方式加入到事件监控机制内的,这意味着一个进程一次没有处理完的客户端连接请求可以再次被捕获。
举个例子,我们知道在默认情况下,nginx一次只accept()一个请求(即multi_accept为off),如果某个监听套接口A上同时来了两个客户端请求连接,触发可读事件被工作进程捕获,工作进程accept()处理了一个请求后,重新阻塞在事件机制监控处(比如epoll_wait()),但事实上,监听套接口A上还有一个客户端请求连接没有被处理,如果监听套接口A不是以水平触发而是以边缘触发加入到事件监控机制,此时监听套接口A虽然可读却无法触发可读事件而让epoll_wait()返回,除非监听套接口A上又来了新的请求重新触发可读事件,但这无疑会导致连接请求得不到及时处理并逐渐累积,到最后该监听套接口A彻底失效。理解这个例子需要对epoll事件模型的水平触发LT与边缘触发ET两种模式特性有一定的了解,但这里我也不多详叙,不清楚的请翻阅man手册或Google。
另外,可以看到nginx主进程在创建完工作进程后并没有关闭这些监听套接口,但主进程却又并没有进行accept()客户端请求连接,那么是否会导致一些客户端请求失败呢?答案当然是否定的,虽然主进程也拥有那些监听套接口,并且它也的确能收到客户端的请求,但是主进程并没有监控这些监听套接口上的事件,没有去读取客户端的请求数据。既然主进程没有去读监听套接口上的数据,那么数据就阻塞在那里,等待任意一个工作进程捕获到对应的可读事件后,进而就可以去处理并响应客户端请求。至于主进程为什么要保留(不关闭)那些监听套接口,是因为再后续再创建新工作进程(比如某工作进程异常退出,主进程收到SIGCHLD信号)时,还要把这些监听套接口传承过去。
创建连接套接口
当有客户端发起请求连接,监控监听套接口的事件管理机制就会捕获到可读事件,工作进程便执行对应的回调函数ngx_event_accept(),从而开始连接套接口的创建工作。
函数ngx_event_accept()的整体逻辑都比较简单,但是有两个需要解析的处理。首先是每次处理调用accept()的次数,默认情况下也就是调用accept()一次,即工作进程每次捕获到监听套接口上的可读事件后,只接受一个服务请求,如果同时收到多个客户端请求,那么除第一个以外的请求需等到再一次触发事件才能被accept()接受。但是,如果用户配置有multi_accept on;,那么工作进程每次捕获到监听套接口上的可读事件后,将反复accept(),一次接受所有的客户端服务请求。这样看起来,似乎“一次接受所有的客户端服务请求”更高效,可为什么它却不是默认配置呢?
举个例子就懂了,假设两个工作进程A和B相互争用监听套接口并对其上的客户端服务请求进行处理,假定两者争用成功的概率都为50%,但是进程A的运气有点差,亦或者说运气有点好,反正不管怎么说,每次进程A对监听套接口争用成功时,总是同时有很多个客户端请求到达,而进程B却每次只有少数的几个请求,这样几个循环下来,进程A就非常繁忙了,工作进程之间的负载没有得到均衡,所以默认情况下,工作进程一次只accept()一个客户端服务请求。相关的逻辑代码如下所示(系统调用accept4()与accept()差别不大,请查man手册,下面代码以使用accept()为例):
17: Filename : ngx_event_accept.c
18: void
19: ngx_event_accept(ngx_event_t *ev)
20: {
21: …
40: ev->available = ecf->multi_accept;
41: …
50: do {
51: …
61: s = accept(lc->fd, (struct sockaddr *) sa, &socklen);
62: …
64: if (s == -1) {
65: …
70: return;
71: …
290: } while (ev->available);
当配置文件中有multi_accept on;时,对应的解析值ecf->multi_accept为1,从而ev->available值为1,所以这个do{}while是一个死循环,直到accept()接受不到客户端请求时,即返回值s等于-1时,循环才得以退出。在未配置multi_accept或multi_accept为off的情况下,ev->available值为0,此时循环主体自然也就只执行一次。
函数accept()调用成功接受客户端请求后,就通过函数ngx_get_connection()申请对应的连接对象,做一些初始赋值等,简单明了而无需多说,但有一个需要解析的处理是deferred_accept,相关代码如下:
204: Filename : ngx_event_accept.c
205: if (ev->deferred_accept) {
206: rev->ready = 1;
207: …
284: ls->handler(c);
ev->deferred_accept值的最初影响设置是listen配置的附属项目里,前面曾讲过listen配置项非常的复杂,有大量的附属项目提供用户来指定这个监听套接口的相关属性,而deferred就是其中的一个,带有该附属项目的对应监听套接口描述符会被设置TCP_DEFER_ACCEPT特性,并且对应到这里的ev->deferred_accept值为1(前后是怎样的转换与逐步赋值略过不讲,翻下代码很容易理解)。TCP_DEFER_ACCEPT特性意味当工作进程accept()这个监听套接口上的客户端请求时,请求的数据内容已经到达了,所以这里第206行将rev->ready设置为1,表示数据准备就绪。最后执行的ls->handler回调也就是函数ngx_http_init_connection(),这是在很早之前赋值(还记得么?上一节提到过)上的。
181: Filename : ngx_http_request.c
182: void
183: ngx_http_init_connection(ngx_connection_t *c)
184: {
185: …
206: rev->handler = ngx_http_init_request;
207: …
213: if (rev->ready) {
214: /* the deferred accept(), rtsig, aio, iocp */
215:
216: if (ngx_use_accept_mutex) {
217: ngx_post_event(rev, &ngx_posted_events);
218: return;
219: }
220:
221: ngx_http_init_request(rev);
222: return;
223: }
224:
225: ngx_add_timer(rev, c->listening->post_accept_timeout);
226:
227: if (ngx_handle_read_event(rev, 0) != NGX_OK) {
228: …
函数ngx_http_init_connection()很简单,但注意到rev->ready,如果它为0,则将事件对象rev加入到超时管理机制和事件监控机制,等超时或请求数据到达。如果rev->ready为1,也就是监听套接口描述符使用刚才讲到的TCP_DEFER_ACCEPT特性,accept()接受服务请求后,请求数据已经准备好了,当然是开始着手处理。第216行的if判断为真则意味着有加锁,所以先把该事件对象加到ngx_posted_events链表,返回解锁后再进行处理,否则在第221行就开始处理,这部分逻辑结合事件机制一章的描述应该容易理解,不过需注意在rev->ready为1的处理情况下,到此时为止,我们新建连接对象都还没有被加入到事件监控机制里,因为当前我们是知道有数据可读,如果运气好,需要的所有请求数据都已经全部到达了,读取数据处理请求然后响应即可,就没有必要把连接对象加到事件监控机制里,却什么作用都没起到又把它从事件监控机制里删除;只有当进行数据读取时,发现所需要的请求数据没有全部到达,此时才将连接对象加到事件监控机制里,等待进一步数据到达时以便获得事件通知,所以在后面的ngx_http_read_request_header()类似函数内能看到ngx_handle_read_event()这样的函数调用:
1139: Filename : ngx_http_request.c
1140: static ssize_t
1141: ngx_http_read_request_header(ngx_http_request_t *r)
1142: {
1143: …
1164: if (n == NGX_AGAIN) {
1165: …
1167: ngx_add_timer(rev, cscf->client_header_timeout);
1168: …
1170: if (ngx_handle_read_event(rev, 0) != NGX_OK) {
如上所示,在读到NGX_AGAIN时,也就是需要的数据没有全部到达,于是将事件对象rev加入到超时管理机制和事件监控机制,以等待后续数据可读事件或超时。
HTTP请求处理
函数ngx_http_init_request(),正式开始对一个客户端服务请求进行处理与响应工作。该函数的主要工作仍然只是做处理准备:建立http连接对象ngx_http_connection_t、http请求对象ngx_http_request_t、找到对应的server配置default_server、大量的初始化赋值操作,最后执行回调函数ngx_http_process_request_line(),进入到http请求头的处理中:
236: Filename : ngx_http_request.c
237: static void
238: ngx_http_init_request(ngx_event_t *rev)
239: {
240: …
276: hc = ngx_pcalloc(c->pool, sizeof(ngx_http_connection_t));
277: …
295: r = ngx_pcalloc(c->pool, sizeof(ngx_http_request_t));
296: …
380: addr = port->addrs;
381: addr_conf = &addr[0].conf;
382: …
388: /* the default server configuration for the address:port */
389: cscf = addr_conf->default_server;
390: …
395: rev->handler = ngx_http_process_request_line;
396: …
519: rev->handler(rev);
520: }
查找该请求该被端口上的哪个ip地址处理是通过对比目的ip地址来进行的(前面讲过,如果某端口上设置有任意ip监听,比如*:80,那么即便还有其他指定ip的监听,比如192.168.1.1:80,也只会创建一个监听套接口,所以对于该套接口上接收到的连接请求首先要进行目的ip匹配),这部分代码很容易理解,就是一个for循环遍历查找,要注意的是由于ipv4的地址只有32位可以直接比较,但ipv6的地址有128位,所以需采用memcmp()比较,上面没有显示这部分代码,给出的第380、381行代码是监听套接口上只有一个目的ip的情况,此时直接使用它,并且第389行取用该地址上的默认server配置。如果客户端请求对应的server不是这个默认的会怎么样?不用当心这种情况,因为在后面还会有处理,比如ngx_http_find_virtual_server()函数,具体请参见下一章内容。
在继续下面的内容讲解前,有必要先介绍一下HTTP协议,当然,关于HTTP协议方面的内容,如果展开来说一时半会说不完,也有专门的书籍,比如《O'Reilly - HTTP Pocket Reference》、《O'Reilly - HTTP The Definitive Guide》、《Sams - HTTP Developers Handbook》等,所以这里仅以RFC 2616(对应HTTP 1.1:http://www.ietf.org/rfc/rfc2616.txt)为依据简单介绍一下HTTP请求响应数据的格式。
根据RFC 2616内容可知,HTTP请求消息(也包括响应消息。消息,即message,可简单认为就是上面提到的请求响应数据的学术名称,我说数据是笼统说法,请勿拘泥这些名词概念,毕竟我不是在写学术论文,)是利用RFC 822(http://www.ietf.org/rfc/rfc822.txt)定义的常用消息格式来传输实体(消息的负载,即真正有价值的数据)。这种常用消息格式就是由开始行(start-line),零个或多个头域(经常被称作“头”)、一个指示头域结束的空行(一个仅包含CRLF的“空”行)以及一个可有可无的消息主体(message-body)。当然,RFC 2616文档里对HTTP请求响应消息格式描叙得更具体一点,其中请求消息格式的BNF(巴科斯诺尔范式)表示如下:
Request = Request-Line ; Section 5.1
*(( general-header ; Section 4.5
| request-header ; Section 5.3
| entity-header ) CRLF) ; Section 7.1
CRLF
[ message-body ] ; Section 4.3
Request-Line = Method SP Request-URI SP HTTP-Version CRLF
Method = "OPTIONS" ; Section 9.2
| "GET" ; Section 9.3
| "HEAD" ; Section 9.4
| "POST" ; Section 9.5
| "PUT" ; Section 9.6
| "DELETE" ; Section 9.7
| "TRACE" ; Section 9.8
| "CONNECT" ; Section 9.9
| extension-method
extension-method = token
Request-URI = "*" | absoluteURI | abs_path | authority
可以看到,工作进程收到的客户端请求头部数据以Request-Line开始(GET / HTTP/1.0\r\n),接着是不定数的请求头部(User-Agent: Wget/1.12 (linux-gnu)\r\nAccept: */*\r\nHost: www.web_test2.com\r\nConnection: Keep-Alive\r\n),最后以一个空行结束(\r\n)。
而函数ngx_http_process_request_line()处理的数据就是客户端发送过来的http请求头中的Request-Line,这个过程可分为三步:读取Request-Line数据、解析Request-Line、存储解析结果并设置相关值。当然,这个过程实际执行时可能重复多次(比如Request-Line数据分多次到达监听套接口),所以函数内的实现是一个for ( ;; )循环。下面逐一简单来看下各个步骤:
第一步,读取Request-Line数据。通过函数ngx_http_read_request_header()将数据读到缓存区r->header_in内。比如执行wget www.web_test2.com请求时,调试nginx对应工作进程打印的数据如下:
(gdb) p r->header_in->pos
$8 = (u_char *) 0x98bdef8 "GET / HTTP/1.0\r\nUser-Agent: Wget/1.12 (linux-gnu)\r\nAccept: */*\r\nHost: www.web_test2.com\r\nConnection: Keep-Alive\r\n\r\n"
一次就把整个请求头部数据读到了,当然也就包括完整的Request-Line数据(即:GET / HTTP/1.0\r\n)。另外说一句,可以看到命令wget默认是以HTTP 1.0协议发送请求,不过不影响(下面仍以它的数据为例),nginx也支持HTTP 1.0协议,也可以用curl命令curl www.web_test2.com进行请求:
(gdb) p r->header_in->pos
$9 = (u_char *) 0x98bdef8 "GET / HTTP/1.1\r\nUser-Agent: curl/7.19.7 (i686-pc-linux-gnu) libcurl/7.19.7 NSS/3.12.7.0 zlib/1.2.3 libidn/1.18 libssh2/1.2.2\r\nHost: www.web_test2.com\r\nAccept: */*\r\n\r\n"
刚才提到,由于客户端请求头部数据可能分多次到达,所以缓存区r->header_in内可能还有一些上一次没解析完的头部数据,所以会存在数据的移动等操作,不过也都比较简单,仅提一下而略过不讲。
第二步,解析Request-Line。对读取到的Request-Line数据进行解析的工作实现在函数ngx_http_parse_request_line()内。由于Request-Line数据有严格的BNF对应,所以其解析过程虽然繁琐,但并无不好理解的地方。
第三步,存储解析结果并设置相关值。在Request-Line的解析过程中会有一些赋值操作,但更多的是在成功解析后,ngx_http_request_t对象r内的相关字段值都将被设置,比如uri(/)、method_name(GET)、http_protocol(HTTP/1.0)等。
Request-Line解析成功,即函数ngx_http_parse_request_line()返回NGX_OK,意味着这初步算是一个合法的http请求,接下来就开始解析其它请求头(general-header、request-header、entity-header):
706: Filename : ngx_http_request.c
707: static void
708: ngx_http_process_request_line(ngx_event_t *rev)
709: {
710: …
732: for ( ;; ) {
733: …
735: n = ngx_http_read_request_header(r);
736: …
742: rc = ngx_http_parse_request_line(r, r->header_in);
743:
744: if (rc == NGX_OK) {
745: …
896: if (ngx_list_init(&r->headers_in.headers, r->pool, 20,
897: sizeof(ngx_table_elt_t))
898: …
915: rev->handler = ngx_http_process_request_headers;
916: ngx_http_process_request_headers(rev);
函数ngx_http_process_request_headers()对每一个请求头的处理步骤与函数ngx_http_process_request_line()处理Request-Line的情况类似,也是分为三步:读取数据(对应函数ngx_http_read_request_header(),如果数据已经从监听套接口描述符读到缓存区了,那么无需再读)、解析数据(对应函数ngx_http_parse_header_line())、存储解析结果。
在第二步骤中,函数ngx_http_parse_header_line()解析的每一个请求头都会放到r->headers_in.headers内,看看gdb断点后捕获到的实例数据:
(gdb) p r->headers_in.headers.part
$34 = {elts = 0x98be5d4, nelts = 4, next = 0x0}
(gdb) p *(ngx_table_elt_t *)r->headers_in.headers.part.elts
$35 = {hash = 486342275, key = {len = 10, data = 0x98bdf08 "User-Agent"}, value = {len = 21, data = 0x98bdf14 "Wget/1.12 (linux-gnu)"}, ...}
(gdb) p *(ngx_table_elt_t *)(r->headers_in.headers.part.elts + sizeof(ngx_table_elt_t) * 1)
$36 = {hash = 2871506184, key = {len = 6, data = 0x98bdf2b "Accept"}, value = {len = 3, data = 0x98bdf33 "*/*"}, ...}
(gdb) p *(ngx_table_elt_t *)(r->headers_in.headers.part.elts + sizeof(ngx_table_elt_t) * 2)
$37 = {hash = 3208616, key = {len = 4, data = 0x98bdf38 "Host"}, value = {len = 17, data = 0x98bdf3e "www.web_test2.com"}, ...}
(gdb) p *(ngx_table_elt_t *)(r->headers_in.headers.part.elts + sizeof(ngx_table_elt_t) * 3)
$38 = {hash = 3519315678, key = {len = 10, data = 0x98bdf51 "Connection"}, value = {len = 10, data = 0x98bdf5d "Keep-Alive"}, ...}
如果请求头有对应的回调处理函数还会被做进一步处理,所有可以被nginx识别并处理的请求头定义在数组ngx_http_headers_in内,比如:
80: Filename : ngx_http_request.c
81: ngx_http_header_t ngx_http_headers_in[] = {
82: { ngx_string("Host"), offsetof(ngx_http_headers_in_t, host),
83: ngx_http_process_host },
84:
85: { ngx_string("Connection"), offsetof(ngx_http_headers_in_t, connection),
86: ngx_http_process_connection },
87: …
而在函数ngx_http_process_request_headers()内的具体实现如下:
955: Filename : ngx_http_request.c
956: static void
957: ngx_http_process_request_headers(ngx_event_t *rev)
958: {
959: …
1038: rc = ngx_http_parse_header_line(r, r->header_in,
1039: cscf->underscores_in_headers);
1040:
1041: if (rc == NGX_OK) {
1042: ..
1056: h = ngx_list_push(&r->headers_in.headers);
1057: …
1085: hh = ngx_hash_find(&cmcf->headers_in_hash, h->hash,
1086: h->lowcase_key, h->key.len);
1087: …
1088: if (hh && hh->handler(r, h, hh->offset) != NGX_OK) {
1089: …
第1041行为真则表示一个请求头被成功解析,在1056行先把它加入到r->headers_in.headers内,然后在cmcf->headers_in_hash(该变量对应ngx_http_headers_in变量)内查找该请求头能否被nginx处理,比如"Host"请求头就能够被nginx处理,从而调用其对应的处理函数ngx_http_process_host()。
当函数ngx_http_parse_header_line()返回NGX_HTTP_PARSE_HEADER_DONE时,表示所有的请求头都已经处理完成(最后一个被处理的请求头为entity-header),客户端的具体请求已经基本被理解(可能还有请求体,比如POST时),nginx开始进入到内部处理,即开始执行各种模块Handler,不过在此之前,通过调用ngx_http_process_request_header()函数先做了一个简单的检查:
1099: Filename : ngx_http_request.c
1100: if (rc == NGX_HTTP_PARSE_HEADER_DONE) {
1101: …
1110: rc = ngx_http_process_request_header(r);
1111:
1112: if (rc != NGX_OK) {
1113: return;
1114: …
1116: ngx_http_process_request(r);
函数ngx_http_process_request_header()的检查比较简单,比如如果客户端使用HTTP 1.1协议发送请求却没有带上"Host"请求头则直接返回错误(HTTP 1.1协议明确要求必须有"Host"请求头);客户端发送TRACE请求则也返回错误(TRACE请求用于调试跟踪,nginx不支持);等。
调用函数ngx_http_process_request()也就是开始执行各种模块Handler,也就是那个前面章节曾提到过的“状态机”:
ngx_http_process_request() -> ngx_http_handler() -> ngx_http_core_run_phases()
874: Filename : ngx_http_core_module.c
875: while (ph[r->phase_handler].checker) {
876:
877: rc = ph[r->phase_handler].checker(r, &ph[r->phase_handler]);
878:
879: if (rc == NGX_OK) {
880: return;
881: }
882: }
对一个客户端请求的处理,终于衔接到nginx的Handler模块来了,各个Handler模块的处理在前面章节已经描述过,所以这里不再多讲。对于一个访问静态页面的GET类型请求,最终会被ngx_http_static_module模块的ngx_http_static_handler()函数捕获,该函数组织待响应的数据,然后调用ngx_http_output_filter()经过nginx过滤链后将数据发送到客户端,此时一个请求的处理与响应也就完成,所以当回到ngx_http_process_request()函数的最末,调用到函数ngx_http_run_posted_requests()内时,因为c->destroyed为真而直接退出。
请求处理响应完后,调用函数ngx_http_finalize_request()进行清理工作,由于nginx对内存的使用采用内存池的方式,所以回收起来非常的简单,也不会出现内存泄露,还有一些其它清理工作,比如从事件超时红黑树里移除等也都比较简单,无需多说。
HTTP数据响应
http响应消息也分为head头部和body主体,和请求消息一致,也是头部信息先发送,然后才是主体信息。本节仍以简单的GET请求静态页面为例,来看看nginx如何对客户端做出数据响应。
前面提到,简单的GET请求静态页面会最终被ngx_http_static_module模块实际处理,执行的函数为ngx_http_static_handler(),该函数首先要做的当然是找到请求静态页面所对应的磁盘文件,这通过组合当前location配置的根目录与GET请求里的绝对URI即可得到该磁盘文件的绝对路径。
接着通过绝对路径打开该磁盘文件,并且通过文件属性来设置相关响应头,比如通过文件大小来设置Content-Length响应头(这里还只是设置对应的字段值,并非创建实际的响应头,下同),告诉客户端接收数据的长度;通过文件修改时间来设置Last-Modified响应头,那么客户端下次再请求该静态文件时可带上该时间戳,那时nginx就有可能直接返回304状态码,让客户端直接使用本地缓存,从而提高性能;等等。发送响应体需要一些内存资源,这会在发送响应头以前分配好,因为如果内存申请失败可提前异常返回,避免可能出现响应头已经发送出去后却发现发送响应体所需要的内存资源却没法成功申请的情况。当然,发送响应头还需要经过nginx的过滤链,这是通过函数:
ngx_http_send_header() -> ngx_http_top_header_filter()
逐步顺链调用下去,过滤链上的回调函数可能会对响应头数据进行检测、截获、新增、修改和删除等操作,不管怎样,一般情况下,执行流程会走到过滤链最末端的两个函数内:
ngx_http_header_filter() -> ngx_http_write_filter()
其中函数ngx_http_header_filter()完成响应头字符串数据的组织工作。该函数申请一个buf缓存块,然后根据最初设置以及经过过滤链的修改后的相关响应头字段值,组织响应头数据以字符串的形式存储在该缓存块内,下面是在该函数接近末尾的地方,用gdb捕获到的数据:
(gdb) p b->pos
$39 = (u_char *) 0x98be920 "HTTP/1.1 200 OK\r\nServer: nginx/1.2.0\r\nDate: Sun, 27 May 2012 13:58:31 GMT\r\nContent-Type: text/html\r\nContent-Length: 219\r\nLast-Modified: Fri, 25 May 2012 15:20:11 GMT\r\nConnection:keep-alive\r\nAccept-Ranges: bytes\r\n\r\n"
该缓存块被接入到发送链变量out(注意这是一个局部变量)内,之后进入到函数ngx_http_write_filter()进行“写出”操作,打上引号是因为此处只有在满足某些条件的情况下才会执行实际的数据写出:
46: Filename : ngx_http_write_filter_module.c
47: ngx_int_t
48: ngx_http_write_filter(ngx_http_request_t *r, ngx_chain_t *in)
49: {
50: …
172: /*
173: * avoid the output if there are no last buf, no flush point,
174: * there are the incoming bufs and the size of all bufs
175: * is smaller than "postpone_output" directive
176: */
177:
178: if (!last && !flush && in && size < (off_t) clcf->postpone_output) {
179: return NGX_OK;
180: }
可以看到如果没有带最后一个缓存块(last)并且没有要求强制写出(flush)并且当前有新加缓存块(in为真)并且当前缓存块总数据大小小于设定值(clcf->postpone_output),此时可直接返回NGX_OK,这意味着会有数据马上跟来(所以该if语句为什么会有对in是否为真的判断就是因为这个原因,如果当前都没有新加入数据,那么也不要期待下一步会马上有数据加入,因此基于这种思路,从时延上考虑,就需要立即写出,从而这整个if判断为假),所以此次可以不写。之所以这么做,当然还是从性能上考虑,在其它章节我们可以看到不管是用哪种读/写方式,总还是要进行用户空间与内核空间的切换,性能损耗比较大,所以读/写操作能省一次就一次。
在我们的示例里,或者说是客户端访问服务器静态页面的这种情况下,那么此时一般就是从低179行退出返回了,但是在该函数前面的逻辑里,我们的待发送缓存块(即包含响应头数据的字符串数据)被连接到r->out链内了,这样做是必须的,毕竟传入进来的out发送链是个局部变量。此时的情况如下:
函数依次返回后到函数ngx_http_static_handler()内继续执行,看一下相关的完整代码:
47: Filename : ngx_http_static_module.c
48: static ngx_int_t
49: ngx_http_static_handler(ngx_http_request_t *r)
50: {
51: …
235: b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
236: …
240: b->file = ngx_pcalloc(r->pool, sizeof(ngx_file_t));
241: …
245: rc = ngx_http_send_header(r);
246: …
255: b->last_buf = (r == r->main) ? 1: 0;
256: …
258: b->file->fd = of.fd;
259: b->file->name = path;
260: …
266: return ngx_http_output_filter(r, &out);
267: }
代码第235、240、245行在前面已经描述过了,第255行的last_buf会被置1,即由于当前请求就是主请求,第258、259行在后面实现将静态文件写出到客户端时会要用到,第266行开始进入到body过滤链,最后也进入到函数ngx_http_write_filter()内,同样,该函数的前面逻辑把这个新缓存块也加入到r->out链内,但是在判断是否要实际写出的if判断时,由于last标记为真,所以此时的确需要做数据写出操作。此时的r->out链情况如下所示:
在进行实际的数据写出操作时,会有一些其它与本节无关的细节,比如限速、一次没完全写完等需设置定时器再写,撇开这些而关注我们的重点函数:
241: Filename : ngx_http_write_filter_module.c
242: chain = c->send_chain(c, r->out, limit);
回调指针send_chain根据系统环境的不同而指向不同的函数,关于这点在其它章节会讲到,在我这里指向的是ngx_linux_sendfile_chain()函数,该函数遍历r->out链上的每一个缓存块,根据缓存块里的数据类型调用不同的系统接口函数将数据写出到客户端。比如这里,对于第一个缓存块,其内数据是存放在内存中的字符串数据(响应头),所以调用系统接口函数writev()将其写出;对于第二个缓存块,其相关联数据是磁盘上文件系统内某个文件(该文件已被打开,对应文件描述符存放在buf->file->fd内)的内容,对于这些数据的写出采用的是系统调用sendfile()。相关代码为:
36: Filename : ngx_linux_sendfile_chain.c
37: ngx_chain_t *
38: ngx_linux_sendfile_chain(ngx_connection_t *c, ngx_chain_t *in, off_t limit)
39: {
40: …
78: for ( ;; ) {
79: …
92: for (cl = in; cl && send < limit; cl = cl->next) {
93: …
248: if (file) {
249: …
264: rc = sendfile(c->fd, file->file->fd, &offset, file_size);
265: …
293: } else {
294: rc = writev(c->fd, header.elts, header.nelts);
客户端需要的数据都发送出去了,那么剩下的工作也就是进行连接关闭和一些连接相关资源的清理,当然,如果需要与客户端进行keepalive,那么会执行函数ngx_http_set_keepalive()保留一些可重用的资源,这样在客户端新的请求到达时,处理能更快速。不过,对于一个客户端请求的处理与响应,到此就已经算是完满结束了。
|
|