11.8.2 放弃接收包体
对于HTTP模块而言,放弃接收包体就是简单地不处理包体了,可是对于HTTP框架而言,并不是不接收包体就可以的。因为对于客户端而言,通常会调用一些阻塞的发送方法来发送包体,如果HTTP框架一直不接收包体,会导致实现上不够健壮的客户端认为服务器超时无响应,因而简单地关闭连接,可这时Nginx模块可能还在处理这个连接。因此,HTTP模块中的放弃接收包体,对HTTP框架而言就是接收包体,但是接收后不做保存,直接丢弃。
HTTP框架提供了一个方法——ngx_http_discard_request_body用于丢弃包体,使用上也非常简单,直接调用这个方法就可以了,不像11.8.1节中接收包体一样还需要一个回调方法。下面先来看看ngx_http_discard_request_body方法的定义。
ngx_int_t ngx_http_discard_request_body(ngx_http_request_t*r)
可以看到,它是没有post_handler回调方法的,那么接收完全部的包体后怎么办呢?很简单,在图11-18的第3步就是接收到全部包体后的动作,其代码如下所示。
ngx_http_finalize_request(r,NGX_DONE);
这里实际上相当于把原始请求的引用计数减1了,当然,如果引用计数为0(如HTTP模块已经调用过结束请求的方法),还是会真正结束请求的。
放弃接收包体和接收包体的实现方式是极其相似的,它也使用了3个方法实现,HTTP模块调用的ngx_http_discard_request_body方法用于第一次启动丢弃包体动作,而ngx_http_discarded_request_body_handler是作为请求的read_event_handler方法的,在有新的可读事件时会调用它处理包体。ngx_http_read_discarded_request_body方法则是根据上述两个方法通用部分提取出的公共方法,用来读取包体且不做任何处理。
下面看看ngx_http_discard_request_body方法做了些什么,如图11-16所示。
图 11-16 ngx_http_discard_request_body方法的流程图
下面解释一下图11-16中所列的7个步骤。
1)首先检查当前请求是一个子请求还是原始请求。为什么要检查这个呢?因为对于子请求而言,它不是来自客户端的请求,所以不存在处理HTTP请求包体的概念。如果当前请求是原始请求,则跳到第2步中继续执行;如果它是子请求,则直接返回NGX_OK表示丢弃包体成功。
2)检查请求连接上的读事件是否在定时器中,这是因为丢弃包体不用考虑超时问题(linger_timer例外,本章不考虑此情况)。如果读事件的timer_set标志位为1,则从定时器中移除此事件。还要检查content-length头部,如果它的值小于或等于0,同样意味着可以直接返回NGX_OK,表示成功丢弃了全部包体。或者检查ngx_http_request_t结构体的request_body成员,如果它已经被赋值过且不再为NULL空指针,则说明已经接收过包体了,这时也需要返回NGX_OK表示成功。
3)就像11.8.1节中介绍的那样,在接收HTTP头部时,还是要检查是否凑巧已经接收到完整的包体(如果包体很小,那么这是非常可能发生的事),如果已经接收到完整的包体,则跳到第1步直接返回NGX_OK,表示丢弃包体成功,否则,说明需要多次的调度才能完成丢弃包体这一动作,此时把请求的read_event_handler成员设置为ngx_http_discarded_request_body_handler方法。
4)调用ngx_handle_read_event方法把读事件添加到epoll中。
5)调用ngx_http_read_discarded_request_body方法接收包体,检测它的返回值。如果返回NGX_OK,则跳到第7步,否则跳到第6步。
6)返回非NGX_OK表示Nginx的事件框架触发事件需要多次调度才能完成丢弃包体这一动作,于是先把引用计数加1,防止这边还在丢弃包体,而其他事件却已让请求意外销毁,引发严重错误。同时把ngx_http_request_t结构体的discard_body标志位置为1,表示正在丢弃包体,并返回NGX_OK,当然,这时的NGX_OK绝不表示已经成功地接收完包体,只是说明ngx_http_discard_request_body执行完毕而已。
7)返回NGX_OK表示已经接收到完整的包体了,这时将请求的lingering_close延时关闭标志位设为0,表示不需要为了包体的接收而延时关闭了,同时返回NGX_OK表示丢弃包体成功。
从以上步骤可以看出,当ngx_http_discard_request_body方法返回NGX_OK时,是可能表达很多意思的。HTTP框架的目的是希望各个HTTP模块不要去关心丢弃包体的执行情况,这些工作完全由HTTP框架完成。
下面再看看在第5步调用的ngx_http_read_discarded_request_body方法的执行流程,如图11-17所示。
图 11-17 ngx_http_read_discarded_request_body方法的流程图
可以看到,虽然ngx_http_read_discarded_request_body方法与ngx_http_do_read_client_request_body方法很类似,但前者比后者简单多了,毕竟不需要保存接收到的包体。下面简单分析一下图11-17中的5个步骤。
1)丢弃包体时请求的request_body成员实际上是NULL空指针,那么用什么变量来表示已经丢弃的包体有多大呢?实际上这时使用了请求ngx_http_request_t结构体headers_in成员里的content_length_n,最初它等于content-length头部,而每丢弃一部分包体,就会在content_length_n变量中减去相应的大小。因此,content_length_n表示还需要丢弃的包体长度,这里首先检查请求的content_length_n成员,如果它已经等于0,则表示已经接收到完整的包体,这时要把read_event_handler重置为ngx_http_block_reading方法,表示如果再有可读事件被触发时,不做任何处理。同时返回NGX_OK,告诉上层的方法已经丢弃了所有包体。
2)如果连接套接字的缓冲区上没有可读内容,则直接返回NGX_AGAIN,告诉上层方法需要等待读事件的触发,等待Nginx框架的再次调度。
3)调用recv方法读取包体。根据返回值确定,如果套接字缓冲区中没有读取到内容,而需要继续读取则跳到第2步;如果客户端主动关闭了连接,则跳到第4步;如果读取到了内容,则跳到第5步。
4)既然客户端主动关闭了连接,直接返回NGX_OK告诉上层方法结束丢弃包体动作即可。
5)接收到包体后,要更新请求的content_length_n成员(参见第1步中的描述),同时再跳回到第1步准备再次接收包体。
最后再看看请求的ngx_handle_read_event指定的ngx_http_discarded_request_body_handler方法,在新的可读事件被触发时,HTTP框架将会调用它来处理事件,图11-18给出了该方法的流程。
图 11-18 ngx_http_discarded_request_body_handler方法的流程图
实际上,ngx_http_discarded_request_body_handler方法还涉及lingering_time的处理,为了减少非主干内容的篇幅,本章将不涉及此内容,因此图11-18中也没有给出。下面分析一下图11-18中的4个步骤:
1)首先检查TCP连接上的读事件的timedout标志位,为1时表示已经超时,这时调用ngx_http_finalize_request方法结束请求,传递的参数是NGX_ERROR,流程结束。
2)调用ngx_http_read_discarded_request_body方法接收包体,检测其返回值。如果返回NGX_OK,则跳到第3步执行,否则跳到第4步。
3)此时表示已经成功地丢弃完所有的包体,这一步骤将请求的正在丢弃包体discard_body标志位置为0,将延迟关闭标志位lingering_close也置为0,再调用ngx_http_finalize_request方法结束请求注意,它的第2个参数是NGX_DONE,11.10.6节将会介绍NGX_DONE参数引发的动作。然后流程结束。
4)仍然需要调用ngx_handle_read_event方法把读事件添加到epoll中,期待新的可读事件到来。
以上介绍了丢弃包体的全部流程,可以看到,这个简单的动作其实也需要很多步骤才能完成,但它非常高效,没有任何阻塞进程,也没有让进程休眠的操作。同时,对于HTTP模块而言,它使用起来也比较简单,值得读者学习。