11.3 第一次可读事件的处理

当TCP连接上第一次出现可读事件时,将会调用ngx_http_init_request方法初始化这个HTTP请求,如下所示。


static void ngx_http_init_request(ngx_event_t*rev)


实际上,HTTP框架并不会在连接建立成功后就开始初始化请求(参见11.2节),而是在这个连接对应的套接字缓冲区上确实接收到了用户发来的请求内容时才进行,这种设计体现了Nginx出于高性能的考虑,这样减少了无谓的内存消耗,降低了一个请求占用内存资源的时间。因此,当有些客户端建立起TCP连接后一直没有发送内容时,Nginx是不会为它分配内存的。

从11.2节中可以看出,在有些情况下,当TCP连接建立成功时同时也出现了可读事件(例如,在套接字设置了deferred选项时,内核仅在套接字上确实收到请求时才会通知epoll调度事件的回调方法),这时ngx_http_init_request方法是在图11-1的第2步中执行的。当然,在大部分情况下,ngx_http_init_request方法和ngx_http_init_connection方法都是由两个事件(TCP连接建立成功事件和连接上的可读事件)触发调用的。图11-2中展示了在ngx_http_init_request方法中究竟做了哪些工作。

11.3 第一次可读事件的处理 - 图1

图 11-2 第一次接收到可读事件后的行为

从图11-2中可以看出,第一次读事件的回调方法ngx_http_init_request主要做了3件事:对请求构造ngx_http_request_t结构体并初始化部分参数、修改读事件的回调方法为ngx_http_process_request_line并调用该方法开始接收并解析HTTP请求行。下面详细分析图11-2中的11个步骤:

1)首先回顾一下图11-1的第3步,那里曾经将读事件也添加到了定时器中,超时时间就是配置文件中的client_header_timeout,因此,首先要检查读事件是否已经超时,也就是检查ngx_event_t事件的timeout成员是否为1。如果timeout为1,则表示接收请求已经超时,则不应该继续处理该请求,于是调用ngx_http_close_request方法关闭请求,同时由ngx_http_init_request方法中返回。ngx_http_close_request方法的详细介绍可参见11.10.3节。

2)在第3章介绍HTTP模块的开发时曾提到,每个请求都会有一个ngx_http_request_t结构体,所有的HTTP模块都以此作为核心结构体来处理请求。这个ngx_http_request_t结构体就是在此步骤中创建的,同时还将这个关键结构体的地址存放到表示TCP连接的ngx_connection_t结构体中的data成员上。这一步中还会把表示这个ngx_connection_t结构体被使用次数的requests成员加1。11.3.1节将会详细介绍ngx_http_request_t结构体。

3)从10.3节可以看出,配置文件的每个server{}块中都可以针对不同的本机IP地址监听同一个端口,事实上每一个监听对象ngx_listening_t都会对应着监听这个端口的所有监听地址。回顾一下8.3.1节,ngx_listening_t结构体中有一个servers指针,在HTTP框架中,它指向监听这一端口的所有监听地址,而每个监听地址也包含了其所属server块的配置项,如下所示。


typedef struct{

ngx_http_core_srv_conf_t*default_server;

ngx_http_virtual_names_t*virtual_names;

}ngx_http_addr_conf_t;


default_server也就是这个监听地址对应的server块配置信息,在第10章中我们曾经介绍过ngx_http_core_srv_conf_t结构体是如何管理配置信息的。这一步中将会遍历ngx_listening_t结构体的servers指向的数组,找到合适的监听地址,然后找到默认的server虚拟主机对应的ngx_http_core_srv_conf_t配置结构体。

4)在第2步建立好的ngx_http_request_t结构体中,main_conf、srv_conf、loc_conf这3个成员表示这个请求对应的main、srv、loc级别的配置项,这时会通过刚刚获取到的默认的ngx_http_core_srv_conf_t结构体设置(10.2节中介绍过,ngx_http_core_srv_conf_t结构体具有一个ngx_http_conf_ctx_t类型的成员ctx,从这里可以获取到3个级别的配置项指针数组)。

5)第一次读事件的回调方法是ngx_http_init_request,它仅用于初始化请求,之后的读事件意味着接收到请求内容,显而易见,它的回调方法是需要改变一下的,即在这一步中将把这个读事件的回调方法设为ngx_http_process_request_line,这个方法将会负责接收并解析出完整的HTTP请求行。

6)读事件被触发,其实就意味着对应的套接字缓冲区上已经接收到用户的请求了,这时需要在用户态的进程空间分配内存,用来把内核缓冲区上的TCP流复制到用户态的内存中,并使用状态机来解析它是否是合法的、完整的HTTP请求。这一步将在ngx_connection_t的内存池中分配一块内存(读者可以思考为何没有在ngx_http_request_t结构体的内存池中分配接收请求的缓存),内存块的大小与nginx.conf文件中的client_header_buffer_size配置项参数一致,ngx_connection_t结构体的buffer指针以及ngx_http_request_t结构体的header_in指针共同指向这块内存缓冲区。这个header_in缓冲区(除了在11.8.1节外)将负责接收用户发送来的请求内容。当这个TCP连接复用于其他HTTP请求时,这个buffer指针指向的内存仍然是可用的,新的HTTP请求初始化执行到这一步时,就不用再次由ngx_connection_t的内存池分配内存了。

7)ngx_http_request_t结构体同样有一个内存池,HTTP模块更应该在ngx_http_request_t结构体的pool内存池上申请新的内存,这样请求结束时(连接可能会被复用)该内存池中分配的内存都会及时回收。这一步中将会创建这个内存池,内存池的初始大小由nginx.conf文件中的request_pool_size配置项参数决定。这个内存池只会在11.10.2节中介绍的ngx_http_free_request方法中销毁。

8)初始化ngx_http_request_t结构体中的部分容器,如headers_out结构体中的ngx_list_t类型的headers链表、variables数组等。

9)在4.5节曾经讲过,每个HTTP模块都可以针对一个请求设置上下文结构体,并通过ngx_http_set_ctx和ngx_http_get_module_ctx宏来设置和获取上下文。那么,这些HTTP模块针对请求设置的上下文结构体指针,实际上是保存到ngx_http_request_t结构体的ctx指针数组中的。在这一步骤中,会分配一个具有ngx_http_max_module(HTTP模块的总数)个成员的指针数组,也就是说,为每个HTTP模块都提供一个位置存放上下文结构体的指针。

10)ngx_http_request_t结构体中有两个成员表示这个请求的开始处理时间:start_sec成员和start_msec成员。这一步中将会初始化这两个成员。在11.9.2节中将会看到这两个成员的用法,它们会为限速功能服务。

11)调用ngx_http_process_request_line方法开始接收、解析HTTP请求行。

以上步骤构成了ngx_http_init_request方法的主要内容,其中构造的ngx_http_request_t结构体在接下来的小节中会详细介绍。

从第3章开始,我们已经多次见过ngx_http_request_t结构体了,但大多是站在HTTP模块的角度来思考如何使用Nginx已经为我们构造好的ngx_http_request_t结构体。本节再次介绍ngx_http_request_t结构体,则是站在HTTP框架的角度来思考如何完成HTTP框架的基本功能。下面首先说明它与HTTP框架密切相关的成员。


typedef struct ngx_http_request_s ngx_http_request_t;

struct ngx_http_request_s{

//这个请求对应的客户端连接

ngx_connection_t*connection;

//指向存放所有HTTP模块的上下文结构体的指针数组

void**ctx;

//指向请求对应的存放main级别配置结构体的指针数组

void**main_conf;

//指向请求对应的存放srv级别配置结构体的指针数组

void**srv_conf;

//指向请求对应的存放loc级别配置结构体的指针数组

void**loc_conf;

/在接收完HTTP头部,第一次在业务上处理HTTP请求时,HTTP框架提供的处理方法是ngx_http_process_request。但如果该方法无法一次处理完该请求的全部业务,在归还控制权到epoll事件模块后,该请求再次被回调时,将通过ngx_http_request_handler方法来处理,而这个方法中对于可读事件的处理就是调用read_event_handler处理请求。也就是说,HTTP模块希望在底层处理请求的读事件时,重新实现read_event_handler方法/

ngx_http_event_handler_pt read_event_handler;

/与read_event_handler回调方法类似,如果ngx_http_request_handler方法判断当前事件是可写事件,则调用write_event_handler处理请求。ngx_http_request_handler的流程可参见图11-7/

ngx_http_event_handler_pt write_event_handler;

//upstream机制用到的结构体,在第12章中会详细说明

ngx_http_upstream_t*upstream;

/表示这个请求的内存池,在ngx_http_free_request方法中销毁。它与ngx_connection_t中的内存池意义不同,当请求释放时,TCP连接可能并没有关闭,这时请求的内存池会销毁,但ngx_connection_t的内存池并不会销毁/

ngx_pool_t*pool;

//用于接收HTTP请求内容的缓冲区,主要用于接收HTTP头部

ngx_buf_t*header_in;

/ngx_http_process_request_headers方法在接收、解析完HTTP请求的头部后,会把解析完的每一个HTTP头部加入到headers_in的headers链表中,同时会构造headers_in中的其他成员/

ngx_http_headers_in_t headers_in;

/HTTP模块会把想要发送的HTTP响应信息放到headers_out中,期望HTTP框架将headers_out中的成员序列化为HTTP响应包发送给用户/

ngx_http_headers_out_t headers_out;

//接收HTTP请求中包体的数据结构,详见11.8节

ngx_http_request_body_t*request_body;

//延迟关闭连接的时间

time_t lingering_time;

/当前请求初始化时的时间。start_sec是格林威治时间1970年1月1日凌晨0点0分0秒到当前时间的秒数。如果这个请求是子请求,则该时间是子请求的生成时间;如果这个请求是用户发来的请求,则是在建立起TCP连接后,第一次接收到可读事件时的时间/

time_t start_sec;

//与start_sec配合使用,表示相对于start_set秒的毫秒偏移量

ngx_msec_t start_msec;

/以下9个成员都是ngx_http_process_request_line方法在接收、解析HTTP请求行时解析出的信息,其意义在第3章已经详细描述过,这里不再介绍/

ngx_uint_t method;

ngx_uint_t http_version;

ngx_str_t request_line;

ngx_str_t uri;

ngx_str_t args;

ngx_str_t exten;

ngx_str_t unparsed_uri;

ngx_str_t method_name;

ngx_str_t http_protocol;

/表示需要发送给客户端的HTTP响应。out中保存着由headers_out中序列化后的表示HTTP头部的TCP流。在调用ngx_http_output_filter方法后,out中还会保存待发送的HTTP包体,它是实现异步发送HTTP响应的关键,参见11.9节/

ngx_chain_t*out;

/当前请求既可能是用户发来的请求,也可能是派生出的子请求,而main则标识一系列相关的派生子请求的原始请求,我们一般可通过main和当前请求的地址是否相等来判断当前请求是否为用户发来的原始请求/

ngx_http_request_t*main;

//当前请求的父请求。注意,父请求未必是原始请求

ngx_http_request_t*parent;

/与subrequest子请求相关的功能。在11.10.6节中会看到它们在HTTP框架中的部分使用方式/

ngx_http_postponed_request_t*postponed;

ngx_http_post_subrequest_t*post_subrequest;

/所有的子请求都是通过posted_requests这个单链表来链接起来的,执行post子请求时调用的ngx_http_run_posted_requests方法就是通过遍历该单链表来执行子请求的/

ngx_http_posted_request_t*posted_requests;

/全局的ngx_http_phase_engine_t结构体中定义了一个ngx_http_phase_handler_t回调方法组成的数组,而phase_handler成员则与该数组配合使用,表示请求下次应当执行以phase_handler作为序号指定的数组中的回调方法。HTTP框架正是以这种方式把各个HTTP模块集成起来处理请求的/

ngx_int_t phase_handler;

/表示NGX_HTTP_CONTENT_PHASE阶段提供给HTTP模块处理请求的一种方式,content_handler指向HTTP模块实现的请求处理方法,详见11.6.4节/

ngx_http_handler_pt content_handler;

/在NGX_HTTP_ACCESS_PHASE阶段需要判断请求是否具有访问权限时,通过access_code来传递HTTP模块的handler回调方法的返回值,如果access_code为0,则表示请求具备访问权限,反之则说明请求不具备访问权限/

ngx_uint_t access_code;

//HTTP请求的全部长度,包括HTTP包体

off_t request_length;

/在这个请求中如果打开了某些资源,并需要在请求结束时释放,那么都需要在把定义的释放资源方法添加到cleanup成员中,详见11.10.2节/

ngx_http_cleanup_t*cleanup;

/表示当前请求的引用次数。例如,在使用subrequest功能时,依附在这个请求上的子请求数目会返回到count上,每增加一个子请求,count数就要加1。其中任何一个子请求派生出新的子请求时,对应的原始请求(main指针指向的请求)的count值都要加1。又如,当我们接收HTTP包体时,由于这也是一个异步调用,所以count上也需要加1,这样在结束请求时(11.10节中介绍),就不会在count引用计数未清零时销毁请求。可以参见11.10.3节的ngx_http_close_request方法/

unsigned count:8;

//阻塞标志位,目前仅由aio使用,本章不涉及

unsigned blocked:8;

//标志位,为1时表示当前请求正在使用异步文件IO

unsigned aio:1;

//标志位,为1时表示URL发生过rewrite重写

unsigned uri_changed:1;

/表示使用rewrite重写URL的次数。因为目前最多可以更改10次,所以uri_changes初始化为11,而每重写URL一次就把uri_changes减1,一旦uri_changes等于0,则向用户返回失败/

unsigned uri_changes:4;

/标志位,为1时表示当前请求是keepalive请求/

unsigned keepalive:1;

/延迟关闭标志位,为1时表示需要延迟关闭。例如,在接收完HTTP头部时如果发现包体存在,该标志位会设为1,而放弃接收包体时则会设为0,参见11.8节/

unsigned lingering_close:1;

//标志位,为1时表示正在丢弃HTTP请求中的包体

unsigned discard_body:1;

/标志位,为1时表示请求的当前状态是在做内部跳转。具体用法可参见图11-5中的第4步和第5步/

unsigned internal:1;

/标志位,为1时表示发送给客户端的HTTP响应头部已经发送。在调用ngx_http_send_header方法(参见11.9.1节)后,若已经成功地启动响应头部发送流程,该标志位就会置为1,用来防止反复地发送头部/

unsigned header_sent:1;

//表示缓冲中是否有待发送内容的标志位

unsigned buffered:4;

//状态机解析HTTP时使用state来表示当前的解析状态

ngx_uint_t state;

……

};


以上介绍的ngx_http_request_t结构体成员,大多都会出现在本章后续章节中,读者在看到相应的变量时可及时回到本节查询其意义。