11.8 处理HTTP包体

本节开始介绍HTTP框架为HTTP模块提供的工具方法。在HTTP中,一个请求通常由必选的HTTP请求行、请求头部,以及可选的包体组成,因此,在接收完HTTP头部后,就可以开始调用各HTTP模块处理请求了(见11.6节),然后由HTTP模块决定如何处理包体。

HTTP框架提供了两种方式处理HTTP包体,当然,这两种方式保持了完全无阻塞的事件驱动机制,非常高效。第一种方式就是把请求中的包体接收到内存或者文件中,当然,由于包体的长度是可变的,同时内存又是有限的,因此,一般都是将包体存放到文件中(本节不会详细讨论包体的存储策略)。第二种方式是选择丢弃包体,注意,丢弃不等于可以不接收包体,这样做可能会导致客户端出现发送请求超时的错误,所以,这个丢弃只是对于HTTP模块而言的,HTTP框架还是需要“尽职尽责”地接收包体,在接收后直接丢弃。

本节将会遇到一个问题,这个问题需要用请求ngx_http_request_t结构体中的count引用计数解决。举个例子,HTTP模块在处理请求时,接收包体的同时可能还需要处理其他业务,如使用upstream机制与另一台服务器通信,这样两个动作都不是一次调度可以完成的,它们各自都可能需要多次调度才能完成,那么在其中一个动作出现错误导致请求失败时,如果销毁请求可能会导致另一个动作出现严重错误,怎么办?这时就需要用到引用计数了。

在HTTP模块中每进行一类新的操作,包括为一个请求添加新的事件,或者把一些已经由定时器、epoll中移除的事件重新加入其中,都需要把这个请求的引用计数加1。这是因为需要让HTTP框架知道,HTTP模块对于该请求有独立的异步处理机制,将由该HTTP模块决定这个操作什么时候结束,防止在这个操作还未结束时HTTP框架却把这个请求销毁了(如其他HTTP模块通过调用ngx_http_finalize_request方法要求HTTP框架结束请求),导致请求出现不可知的严重错误。这就要求每个操作在“认为”自身的动作结束时,都得最终调用到ngx_http_close_request方法,该方法会自动检查引用计数,当引用计数为0时才真正地销毁请求。实际上,很多结束请求的方法最后一定会调用到ngx_http_close_request方法(参见11.10.3节)。

由于HTTP包体是可变长度的,接收包体可能导致HTTP框架将TCP连接上的读事件再次添加到epoll和定时器中,表示希望事件驱动机制发现TCP连接上接收到全部或者部分HTTP包体时,回调相应的方法读取套接字缓冲区上的TCP流,这时必须把请求的引用计数加1,这在图11-13的第1步中就可以看到。类似的,在第5章介绍的subrequest子请求的使用方法中,派生子请求也是独立的动作,它会向epoll和定时器中添加新的事件,引用计数也会加1,而upstream试图连接新的服务器,它同样也需要把当前请求的引用计数加1。当这类操作结束时,如HTTP包体全部接收完毕时,务必调用或者间接地调用ngx_http_close_request方法,把引用计数减1,这才能使引用计数机制正常工作。

注意 引用计数一般都作用于这个请求的原始请求上,因此,在结束请求时统一检查原始请求的引用计数就可以了。当然,目前的HTTP框架也要求我们必须这样做,因为ngx_http_close_request方法只是把原始请求上的引用计数减1。对应到代码就是操作r->main->count成员,其中r是请求对应的ngx_http_request_t结构体。

下面来看看HTTP框架提供的方法是如何使用的,接收包体的方法其实在3.6.4节中已经讲过,再来回顾一下。


ngx_int_t ngx_http_read_client_request_body(ngx_http_request_t*r,ngx_http_client_body_handler_pt post_handler);


调用了ngx_http_read_client_request_body方法就相当于启动了接收包体这一动作,在这个动作完成后,就会回调HTTP模块定义的post_handler方法。post_handler是一个函数指针,如下所示。


typedef void(ngx_http_client_body_handler_pt)(ngx_http_request_tr);


而决定丢弃包体时,HTTP框架提供的方法是ngx_http_discard_request_body,如下所示。


ngx_int_t ngx_http_discard_request_body(ngx_http_request_t*r)


当然,它是不需要再让HTTP模块定义类似post_handler的回调方法的,当丢弃包体后,HTTP框架会自动调用ngx_http_finalize_request方法把引用计数减1,详见11.8.2节。

在11.8.1节中将会讨论HTTP框架是怎样实现ngx_http_read_client_request_body方法的,而在11.8.2节中则会讨论ngx_http_discard_request_body方法的实现,由于这两个方法都需要被事件框架多次调度,学习它们的设计方法可以帮助我们开发高效的Nginx模块。

11.8.1 接收包体

在讨论ngx_http_read_client_request_body方法的实现方式前,先来看一下用于保存HTTP包体的结构体ngx_http_request_body_t,如下所示。


typedef struct{

//存放HTTP包体的临时文件

ngx_temp_file_t*temp_file;

/接收HTTP包体的缓冲区链表。当包体需要全部存放在内存中时,如果一块ngx_buf_t缓冲区无法存放完,这时就需要使用ngx_chain_t链表来存放/

ngx_chain_t*bufs;

//直接接收HTTP包体的缓存

ngx_buf_t*buf;

/根据content-length头部和已接收到的包体长度,计算出的还需要接收的包体长度/

off_t rest;

//该缓冲区链表存放着将要写入文件的包体

ngx_chain_t*to_write;

/HTTP包体接收完毕后执行的回调方法,也就是ngx_http_read_client_request_body方法传递的第2个参数/

ngx_http_client_body_handler_pt post_handler;

}ngx_http_request_body_t;


这个ngx_http_request_body_t结构体就存放在保存着请求的ngx_http_request_t结构体的request_body成员中,接收HTTP包体就是围绕着这个数据结构进行的。

上文说过,在接收较大的包体时,无法在一次调度中完成。通俗地讲,就是接收包体不是调用一次ngx_http_read_client_request_body方法就能完成的。但是HTTP框架希望对于它的用户,也就是HTTP模块而言,接收包体时只需要调用一次ngx_http_read_client_request_body方法就好,这时就需要有另一个方法在ngx_http_read_client_request_body没接收到完整的包体时,如果连接上再次接收到包体就被调用,这个方法就是ngx_http_read_client_request_body_handler。

ngx_http_read_client_request_body_handler方法对于HTTP模块是不可见的,它在“幕后”工作。当继续接收发自客户端的包体时,将由它来处理。可见,它与ngx_http_read_client_request_body方法有很多共通之处,它们都会去试图读取连接套接字上的缓冲区,把它们共性的部分提取出来构成ngx_http_do_read_client_request_body方法,它负责具体的读取包体工作。本节的内容就在于说明这3个方法的流程。

图11-13为ngx_http_read_client_request_body方法的流程图,在该图中同时可以看到ngx_http_request_t结构体中的request_body成员是如何分配和使用的。

图11-13把ngx_http_read_client_request_body方法的主要流程概括为7个步骤,下面详细说明一下。

11.8 处理HTTP包体 - 图1

图 11-13 ngx_http_read_client_request_body方法的流程图

1)首先把该请求对应的原始请求的引用计数加1。这同时是在要求每一个HTTP模块在传入的post_handler方法被回调时,务必调用类似ngx_http_finalize_request的方法去结束请求,否则引用计数会始终无法清零,从而导致请求无法释放。

检查请求ngx_http_request_t结构体中的request_body成员,如果它已经被分配过了,证明已经读取过HTTP包体了,不需要再次读取一遍,这时跳到第2步执行;再检查请求ngx_http_request_t结构体中的discard_body标志位,如果discard_body为1,则证明曾经执行过丢弃包体的方法,现在包体正在被丢弃中,仍然跳到第2步执行。只有这两个条件都不满足,才说明真正需要接收HTTP包体,这时跳到第3步执行。

2)这一步将直接执行各HTTP模块提供的post_handler回调方法,接着,ngx_http_read_client_request_body方法返回NGX_OK。

3)分配请求的ngx_http_request_t结构体中的request_body成员(之前request_body是NULL空指针),准备接收包体。

4)检查请求的content-length头部,如果指定了包体长度的content-length字段小于或等于0,当然不用继续接收包体,跳到第2步执行;如果content-length大于0,则意味着继续执行,但HTTP模块定义的post_handler方法不会知道在哪一次事件的触发中会被回调,所以先把它设置到request_body结构体的post_handler成员中。

5)注意,在11.5节描述的接收HTTP头部的流程中,是有可能接收到HTTP包体的。首先我们需要检查在header_in缓冲区中已经接收到的包体长度,确定其是否大于或者等于content-length头部指定的长度,如果大于或等于则说明已经接收到完整的包体,这时跳到第2步执行。

当上述条件不满足时,再检查header_in缓冲区里的剩余空闲空间是否可以存放下全部的包体(content-length头部指定),如果可以,就不用分配新的包体缓冲区浪费内存了,直接跳到第6步执行。

当以上两个条件都不满足时,说明确实需要分配用于接收包体的缓冲区了。缓冲区长度由nginx.conf文件中的client_body_buffer_size配置项指定,缓冲区就在ngx_http_request_body_t结构体的buf成员中存放着,同时,bufs和to_write这两个缓冲区链表首部也指向该buf。

6)设置请求ngx_http_request_t结构体的read_event_handler成员为上面介绍过的ngx_http_read_client_request_body_handler方法,它意味着如果epoll再次检测到可读事件或者读事件的定时器超时,HTTP框架将调用ngx_http_read_client_request_body_handler方法处理,该方法所做的工作参见图11-15。

7)调用ngx_http_do_read_client_request_body方法接收包体。该方法的意义在于把客户端与Nginx之间TCP连接上套接字缓冲区中的当前字符流全部读出来,并判断是否需要写入文件,以及是否接收到全部的包体,同时在接收到完整的包体后激活post_handler回调方法,如图11-14所示。

11.8 处理HTTP包体 - 图2

图 11-14 ngx_http_do_read_client_request_body方法的流程图

图11-14中列出的ngx_http_do_read_client_request_body方法流程稍显复杂,下面详细解释一下这11个步骤。

1)首先检查请求的request_body成员中的buf缓冲区,如果缓冲区还有空闲的空间,则跳到第3步读取内核中套接字缓冲区里的TCP字符流;如果缓冲区已经写满,则调用ngx_http_write_request_body方法把缓冲区中的字符流写入文件。

2)通过第1步把request_body缓冲区中的内容写入文件后,缓冲区就可以重复使用了,只需要把缓冲区ngx_buf_t结构体的last指针指向start指针,缓冲区即可复用。

3)调用封装了recv的方法从套接字缓冲区中读取包体到缓冲区中。如果recv方法返回错误,或者客户端主动关闭了连接,则跳到第4步执行;如果读取到内容,则跳到第5步执行。

4)设置ngx_http_request_t结构体的error标志位为1,同时返回NGX_HTTP_BAD_REQUEST错误码。

5)根据接收到的TCP流长度,修改缓冲区参数。例如,把缓冲区ngx_buf_t结构体的last指针加上接收到的长度,同时更新request_body结构体中表示待接收的剩余包体长度的rest成员、更新ngx_http_request_t结构体中表示已接收请求长度的request_length成员。

根据rest成员检查是否接收到完整的包体,如果接收到了完整的包体,则跳到第8步继续执行;否则查看套接字缓冲区上是否仍然有可读的字符流,如果有则跳到第1步继续接收包体,如果没有则跳到第6步。

6)如果当前已经没有可读的字符流,同时还没有接收到完整的包体,则说明需要把读事件添加到事件模块,等待可读事件发生时,事件框架可以再次调度到这个方法接收包体。这一步是调用ngx_add_timer方法将读事件添加到定时器中,超时时间以nginx.conf文件中的client_body_timeout配置项参数为准。

7)调用ngx_handle_read_event方法将读事件添加到epoll等事件收集器中,同时ngx_http_do_read_client_request_body方法结束,返回NGX_AGAIN。

8)到这一步,表明已经接收到完整的包体,需要做一些收尾工作了。首先不需要检查是否接收HTTP包体超时了,要把读事件从定时器中取出,防止不必要的定时器触发。这一步会检查读事件的timer_set标志位,如果为1,则调用ngx_del_timer方法把读事件从定时器中移除。

9)如果缓冲区中还有未写入文件的内容,调用ngx_http_write_request_body方法把最后的包体内容也写入文件。

10)在图11-13的第5步中曾经把请求的read_event_handler成员设置为ngx_http_read_client_request_body_handler方法,现在既然已经接收到完整的包体了,就会把read_event_handler设为ngx_http_block_reading方法,表示连接上再有读事件将不做任何处理。

11)执行HTTP模块提供的post_handler回调方法后,ngx_http_do_read_client_request_body方法结束,返回NGX_OK。

图11-13中的第6步把请求的read_event_handler成员设置为ngx_http_read_client_request_body_handler方法,从11.6节的图11-7可以看出,这个请求连接上的读事件触发时的回调方法ngx_http_request_handler会调用read_event_handler方法,下面根据图11-15来看看这时ngx_http_read_client_request_body_handler方法做了些什么。

11.8 处理HTTP包体 - 图3

图 11-15 ngx_http_read_client_request_body_handler方法的流程图

简单解释一下图11-15中的3个步骤。

1)首先检查连接上读事件的timeout标志位,如果为1,则表示接收HTTP包体超时,这时把连接ngx_connection_t结构体上的timeout标志位也置为1,同时调用ngx_http_finalize_request方法结束请求,并发送408超时错误码。如果没有超时,则跳到第2步执行。

2)调用图11-14中介绍的ngx_http_do_read_client_request_body方法接收包体,检测这个方法的返回值,如果它大于300,那么一定表示希望返回错误码。例如,图11-14的第4步就返回了400错误码,这时跳到第3步执行;否则ngx_http_read_client_request_body_handler方法结束,直接返回NGX_OK。

3)调用ngx_http_finalize_request方法结束请求,第2个参数传递的是ngx_http_do_read_client_request_body方法的返回值,详见11.10.6节。

以上3个方法完整地描述了HTTP框架接收包体的流程,以及最后如何执行HTTP模块实现的post_handler方法。读者可以参照它再看看第3章中开发HTTP模块时是如何接收包体的,相信经过本章的分析,读者会对这一机制有新的认识。