今年我们组计划写一本 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 方法:
要访问的资源由统一资源地位符 URI(Uniform Resource Identifier) 确定,它的一个比较通用的组成格式(rfc2396)如下:
- Method = "OPTIONS" | "GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "TRACE" | "CONNECT" | extension - method extension - method = token
一般来说根据请求方法(Method)的不同,请求 URI 的格式会有所不同,通常只需写出 path 和 query 部分。
- <scheme>
- ://
- <authority>
- <path>
- ?
- <query>
http 版本 (version) 定义如下,现在用的一般为 1.0 和 1.1 版本:
请求行的下一行则是请求头,rfc2616 中定义了 3 种不同类型的请求头,分别为 general-header,request-header 和 entity-header,每种类型 rfc 中都定义了一些通用的头,其中 entity-header 类型可以包含自定义的头。
- HTTP / <major > . < minor >
- 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;
- }
ngx_http_init_request 函数主要工作即是初始化请求,由于它是一个事件处理函数,它只有唯一一个 ngx_event_t * 类型的参数,ngx_event_t 结构在 nginx 中表示一个事件,事件处理的上下文类似于一个中断处理的上下文,为了在这个上下文得到相关的信息,nginx 中一般会将连接结构的引用保存在事件结构的 data 字段,请求结构的引用则保存在连接结构的 data 字段,这样在事件处理函数中可以方便的得到对应的连接结构和请求结构。进入函数内部看一下,首先判断该事件是否是超时事件,如果是的话直接关闭连接并返回;反之则是指之前 accept 的连接上有请求过来需要处理,ngx_http_init_request 函数首先在连接的内存池中为该请求分配一个 ngx_http_request_t 结构,这个结构将用来保存该请求所有的信息。分配完之后,这个结构的引用会被包存在连接的 hc 成员的 request 字段,以便于在长连接或 pipelined 请求中复用该请求结构。在这个函数中,nginx 根据该请求的接收端口和地址找到一个默认虚拟服务器配置(listen 指令的 default_server 属性用来标识一个默认虚拟服务器,否则监听在相同端口和地址的多个虚拟服务器,其中第一个定义的则为默认),因为在 nginx 配置文件中可以设置多个监听在不同端口和地址的虚拟服务器(每个 server 块对应一个虚拟服务器),另外还根据域名(server_name 指令可以配置该虚拟服务器对应的域名)来区分监听在相同端口和地址的虚拟服务器,每个虚拟服务器可以拥有不同的配置内容,而这些配置内容决定了 nginx 在接收到一个请求之后如何处理该请求。找到之后,相应的配置被保存在该请求对应的 ngx_http_request_t 结构中。注意这里根据端口和地址找到的默认配置只是临时使用一下,最终 nginx 会根据域名找到真正的虚拟服务器配置,随后的初始化工作还包括:
将连接的读事件的处理函数设置为 ngx_http_process_request_line 函数,这个函数用来解析请求行,将请求的 read_event_handler 设置为 ngx_http_block_reading 函数,这个函数实际上什么都不做(当然在事件模型设置为水平触发时,唯一做的事情就是将事件从事件模型监听列表中删除,防止该事件一直被触发),后面会说到这里为什么会将 read_event_handler 设置为此函数;
为这个请求分配一个缓冲区用来保存它的请求头,地址保存在 header_in 字段,默认大小为 1024 个字节,可以使用 client_header_buffer_size 指令修改,这里需要注意一下,nginx 用来保存请求头的缓冲区是在该请求所在连接的内存池中分配,而且会将地址保存一份在连接的 buffer 字段中,这样做的目的也是为了给该连接的下一次请求重用这个缓冲区,另外如果客户端发过来的请求头大于 1024 个字节,nginx 会重新分配更大的缓存区,默认用于大请求的头的缓冲区最大为 8K,最多 4 个,这 2 个值可以用 large_client_header_buffers 指令设置,后面还会说到请求行和一个请求头都不能超过一个最大缓冲区的大小;
同样的 nginx 会为这个请求分配一个内存池,后续所有与该请求相关的内存分配一般都会使用该内存池,默认大小为 4096 个字节,可以使用 request_pool_size 指令修改;
为这个请求分配响应头链表,初始大小为 20;
创建所有模块的上下文 ctx 指针数组,变量数据;
将该请求的 main 字段设置为它本身,表示这是一个主请求,nginx 中对应的还有子请求概念,后面的章节会做详细的介绍;
将该请求的 count 字段设置为 1,count 字段表示请求的引用计数;
将当前时间保持在 start_sec 和 start_msec 字段,这个时间是该请求的起始时刻,将被用来计算一个请求的处理时间(request time),nginx 使用的这个起始点和 apache 略有差别,nginx 中请求的起始点是接收到客户端的第一个数据包开始,而 apache 则是接收到客户端的整个 request line 后开始算起;
初始化请求的其他字段,比如将 uri_changes 设置为 11,表示最多可以将该请求的 uri 改写 10 次,subrequests 被设置为 201,表示一个请求最多可以发起 200 个子请求;
做完所有这些初始化工作之后,ngx_http_init_request 函数会调用读事件的处理函数来真正的解析客户端发过来的数据,也就是会进入 ngx_http_process_request_line 函数中处理。
ngx_http_process_request_line 函数的主要作用即是解析请求行,同样由于涉及到网络 IO 操作,即使是很短的一行请求行可能也不能被一次读完,所以在之前的 ngx_http_init_request 函数中,ngx_http_process_request_line 函数被设置为读事件的处理函数,它也只拥有一个唯一的 ngx_event_t * 类型参数,并且在函数的开头,同样需要判断是否是超时事件,如果是的话,则关闭这个请求和连接;否则开始正常的解析流程。先调用 ngx_http_read_request_header 函数读取数据。
由于可能多次进入 ngx_http_process_request_line 函数,ngx_http_read_request_header 函数首先检查请求的 header_in 指向的缓冲区内是否有数据,有的话直接返回;否则从连接读取数据并保存在请求的 header_in 指向的缓存区,而且只要缓冲区有空间的话,会一次尽可能多的读数据,读到多少返回多少;如果客户端暂时没有发任何数据过来,并返回 NGX_AGAIN,返回之前会做 2 件事情:1,设置一个定时器,时长默认为 60s,可以通过指令 client_header_timeout 设置,如果定时事件到达之前没有任何可读事件,nginx 将会关闭此请求;2,调用 ngx_handle_read_event 函数处理一下读事件 - 如果该连接尚未在事件处理模型上挂载读事件,则将其挂载上;如果客户端提前关闭了连接或者读取数据发生了其他错误,则给客户端返回一个 400 错误(当然这里并不保证客户端能够接收到响应数据,因为客户端可能都已经关闭了连接),最后函数返回 NGX_ERROR;
如果 ngx_http_read_request_header 函数正常的读取到了数据,ngx_http_process_request_line 函数将调用 ngx_http_parse_request_line 函数来解析,这个函数根据 http 协议规范中对请求行的定义实现了一个有限状态机,经过这个状态机,nginx 会记录请求行中的请求方法(Method),请求 uri 以及 http 协议版本在缓冲区中的起始位置,在解析过程中还会记录一些其他有用的信息,以便后面的处理过程中使用。如果解析请求行的过程中没有产生任何问题,该函数会返回 NGX_OK;如果请求行不满足协议规范,该函数会立即终止解析过程,并返回相应错误号;如果缓冲区数据不够,该函数返回 NGX_AGAIN。在整个解析 http 请求的状态机中始终遵循着两条重要的原则:减少内存拷贝和回溯。内存拷贝是一个相对比较昂贵的操作,大量的内存拷贝会带来较低的运行时效率。nginx 在需要做内存拷贝的地方尽量只拷贝内存的起始和结束地址而不是内存本身,这样做的话仅仅只需要两个赋值操作而已,大大降低了开销,当然这样带来的影响是后续的操作不能修改内存本身,如果修改的话,会影响到所有引用到该内存区间的地方,所以必须很小心的管理,必要的时候需要拷贝一份。这里不得不提到 nginx 中最能体现这一思想的数据结构,ngx_buf_t,它用来表示 nginx 中的缓存,在很多情况下,只需要将一块内存的起始地址和结束地址分别保存在它的 pos 和 last 成员中,再将它的 memory 标志置 1,即可表示一块不能修改的内存区间,在另外的需要一块能够修改的缓存的情形中,则必须分配一块所需大小的内存并保存其起始地址,再将 ngx_bug_t 的 temprary 标志置 1,表示这是一块能够被修改的内存区域。
再回到 ngx_http_process_request_line 函数中,如果 ngx_http_parse_request_line 函数返回了错误,则直接给客户端返回 400 错误;
如果返回 NGX_AGAIN,则需要判断一下是否是由于缓冲区空间不够,还是已读数据不够。如果是缓冲区大小不够了,nginx 会调用 ngx_http_alloc_large_header_buffer 函数来分配另一块大缓冲区,如果大缓冲区还不够装下整个请求行,nginx 则会返回 414 错误给客户端,否则分配了更大的缓冲区并拷贝之前的数据之后,继续调用 ngx_http_read_request_header 函数读取数据来进入请求行自动机处理,直到请求行解析结束;
如果返回了 NGX_OK,则表示请求行被正确的解析出来了,这时先记录好请求行的起始地址以及长度,并将请求 uri 的 path 和参数部分保存在请求结构的 uri 字段,请求方法起始位置和长度保存在 method_name 字段,http 版本起始位置和长度记录在 http_protocol 字段。还要从 uri 中解析出参数以及请求资源的拓展名,分别保存在 args 和 exten 字段。接下来将要解析请求头,我将在下一篇文章中接着介绍。
来源: http://lib.csdn.net/article/liveplay/44515