|
今年我们组计划写一本nginx模块开发以及原理解析方面的书,整本书是以open book的形式在网上会定时的更新,网址为http://tengine.taobao.org/book/index.html。本书分析的nginx源码版本为1.2.0,环境为linux,事件处理模型为epoll,大部分分析流程都基于以上假设。我会负责其中一些章节的编写,所以打算在这里写一系列我负责章节内容相关的文章(主要包括nginx各phase模块的开发,nginx请求的处理流程等)。本篇文章主要会介绍nginx中请求的接收流程,包括请求头的解析和请求体的读取流程。 首先介绍一下rfc2616中定义的http请求基本格式:
Request = Request-Line
*(( general-header
| request-header
| entity-header ) CRLF)
CRLF
[ message-body ]
第一行是请求行(request line),用来说明请求方法,要访问的资源以及所使用的HTTP版本:
Request-Line = Method SP Request-URI SP HTTP-Version CRLF
请求方法(Method)的定义如下,其中最常用的是GET,POST方法:
Method = "OPTIONS" | "GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "TRACE" | "CONNECT" | extension-method extension-method = token
要访问的资源由统一资源地位符URI(Uniform Resource Identifier)确定,它的一个比较通用的组成格式(rfc2396)如下:
<scheme>://<authority><path>?<query> 一般来说根据请求方法(Method)的不同,请求URI的格式会有所不同,通常只需写出path和query部分。 http版本(version)定义如下,现在用的一般为1.0和1.1版本:
HTTP/<major>.<minor>
请求行的下一行则是请求头,rfc2616中定义了3种不同类型的请求头,分别为general-header,request-header和entity-header,每种类型rfc中都定义了一些通用的头,其中entity-header类型可以包含自定义的头。 现在开始介绍nginx中请求头的解析,nginx的请求处理流程中,会涉及到2个非常重要的数据结构,ngx_connection_t和ngx_http_request_t,分别用来表示连接和请求,这2个数据结构在本书的前篇中已经做了比较详细的介绍,没有印象的读者可以翻回去复习一下,整个请求处理流程从头到尾,对应着这2个数据结构的分配,初始化,使用,重用和销毁。 nginx在初始化阶段,具体是在init process阶段的ngx_event_process_init函数中会为每一个监听套接字分配一个连接结构(ngx_connection_t),并将该连接结构的读事件成员(read)的事件处理函数设置为ngx_event_accept,并且如果没有使用accept互斥锁的话,在这个函数中会将该读事件挂载到nginx的事件处理模型上(poll或者epoll等),反之则会等到init process阶段结束,在工作进程的事件处理循环中,某个进程抢到了accept锁才能挂载该读事件。
static ngx_int_t
ngx_event_process_init(ngx_cycle_t *cycle)
{
...
/* 初始化用来管理所有定时器的红黑树 */
if (ngx_event_timer_init(cycle->log) == NGX_ERROR) {
return NGX_ERROR;
}
/* 初始化事件模型 */
for (m = 0; ngx_modules[m]; m++) {
if (ngx_modules[m]->type != NGX_EVENT_MODULE) {
continue;
}
if (ngx_modules[m]->ctx_index != ecf->use) {
continue;
}
module = ngx_modules[m]->ctx;
if (module->actions.init(cycle, ngx_timer_resolution) != NGX_OK) {
/* fatal */
exit(2);
}
break;
}
...
/* for each listening socket */
/* 为每个监听套接字分配一个连接结构 */
ls = cycle->listening.elts;
for (i = 0; i < cycle->listening.nelts; i++) {
c = ngx_get_connection(ls[i].fd, cycle->log);
if (c == NULL) {
return NGX_ERROR;
}
c->log = &ls[i].log;
c->listening = &ls[i];
ls[i].connection = c;
rev = c->read;
rev->log = c->log;
/* 标识此读事件为新请求连接事件 */
rev->accept = 1;
...
#if (NGX_WIN32)
/* windows环境下不做分析,但原理类似 */
#else
/* 将读事件结构的处理函数设置为ngx_event_accept */
rev->handler = ngx_event_accept;
/* 如果使用accept锁的话,要在后面抢到锁才能将监听句柄挂载上事件处理模型上 */
if (ngx_use_accept_mutex) {
continue;
}
/* 否则,将该监听句柄直接挂载上事件处理模型 */
if (ngx_event_flags & NGX_USE_RTSIG_EVENT) {
if (ngx_add_conn(c) == NGX_ERROR) {
return NGX_ERROR;
}
} else {
if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR) {
return NGX_ERROR;
}
}
#endif
}
return NGX_OK;
}
当一个工作进程在某个时刻将监听事件挂载上事件处理模型之后,nginx就可以正式的接收并处理客户端过来的请求了。这时如果有一个用户在浏览器的地址栏内输入一个域名,并且域名解析服务器将该域名解析到一台由nginx监听的服务器上,nginx的事件处理模型接收到这个读事件之后,会速度交由之前注册好的事件处理函数ngx_event_accept来处理。 在ngx_event_accept函数中,nginx调用accept函数,从已连接队列得到一个连接以及对应的套接字,接着分配一个连接结构(ngx_connection_t),并将新得到的套接字保存在该连接结构中,这里还会做一些基本的连接初始化工作: 首先给该连接分配一个内存池,初始大小默认为256字节,可通过connection_pool_size指令设置; 分配日志结构,并保存在其中,以便后续的日志系统使用; 初始化连接相应的io收发函数,具体的io收发函数和使用的事件模型及操作系统相关; 分配一个套接口地址(sockaddr),并将accept得到的对端地址拷贝在其中,保存在sockaddr字段; 将本地套接口地址保存在local_sockaddr字段,因为这个值是从监听结构ngx_listening_t中可得,而监听结构中保存的只是配置文件中设置的监听地址,但是配置的监听地址可能是通配符*,即监听在所有的地址上,所以连接中保存的这个值最终可能还会变动,会被确定为真正的接收地址; 将连接的写事件设置为已就绪,即设置ready为1,nginx默认连接第一次为可写; 如果监听套接字设置了TCP_DEFER_ACCEPT属性,则表示该连接上已经有数据包过来,于是设置读事件为就绪; 将sockaddr字段保存的对端地址格式化为可读字符串,并保存在addr_text字段; 最后调用ngx_http_init_connection函数初始化该连接结构的其他部分。 ngx_http_init_connection函数最重要的工作是初始化读写事件的处理函数:将该连接结构的写事件的处理函数设置为ngx_http_empty_handler,这个事件处理函数不会做任何操作,实际上nginx默认连接第一次可写,不会挂载写事件,如果有数据需要发送,nginx会直接写到这个连接,只有在发生一次写不完的情况下,才会挂载写事件到事件模型上,并设置真正的写事件处理函数,这里后面的章节还会做详细介绍;读事件的处理函数设置为ngx_http_init_request,此时如果该连接上已经有数据过来(设置了deferred accept),则会直接调用ngx_http_init_request函数来处理该请求,反之则设置一个定时器并在事件处理模型上挂载一个读事件,等待数据到来或者超时。当然这里不管是已经有数据到来,或者需要等待数据到来,又或者等待超时,最终都会进入读事件的处理函数-ngx_http_init_request。
(责任编辑:IT) |
