详解Nginx的配置函数对于请求体的读取

nginx核心本身不会主动读取请求体,这个工作是交给请求处理阶段的模块来做,但是nginx核心提供了ngx_http_read_client_request_body()接口来读取请求体,另外还提供了一个丢弃请求体的接口-ngx_http_discard_request_body(),在请求执行的各个阶段中,任何一个阶段的模块如果对请求体感兴趣或者希望丢掉客户端发过来的请求体,可以分别调用这两个接口来完成。这两个接口是nginx核心提供的处理请求体的标准接口,如果希望配置文件中一些请求体相关的指令(比如client_body_in_file_only,client_body_buffer_size等)能够预期工作,以及能够正常使用nginx内置的一些和请求体相关的变量(比如$request_body和$request_body_file),一般来说所有模块都必须调用这些接口来完成相应操作,如果需要自定义接口来处理请求体,也应尽量兼容nginx默认的行为。

1,读取请求体

请求体的读取一般发生在nginx的content handler中,一些nginx内置的模块,比如proxy模块,fastcgi模块,uwsgi模块等,这些模块的行为必须将客户端过来的请求体(如果有的话)以相应协议完整的转发到后端服务进程,所有的这些模块都是调用了ngx_http_read_client_request_body()接口来完成请求体读取。值得注意的是这些模块会把客户端的请求体完整的读取后才开始往后端转发数据。

由于内存的限制,ngx_http_read_client_request_body()接口读取的请求体会部分或者全部写入一个临时文件中,根据请求体的大小以及相关的指令配置,请求体可能完整放置在一块连续内存中,也可能分别放置在两块不同内存中,还可能全部存在一个临时文件中,最后还可能一部分在内存,剩余部分在临时文件中。下面先介绍一下和这些不同存储行为相关的指令:

client_body_buffer_size:设置缓存请求体的buffer大小,默认为系统页大小的2倍,当请求体的大小超过此大小时,nginx会把请求体写入到临时文件中。可以根据业务需求设置合适的大小,尽量避免磁盘io操作;

client_body_in_single_buffer:指示是否将请求体完整的存储在一块连续的内存中,默认为off,如果此指令被设置为on,则nginx会保证请求体在不大于client_body_buffer_size设置的值时,被存放在一块连续的内存中,但超过大小时会被整个写入一个临时文件;

client_body_in_file_only:设置是否总是将请求体保存在临时文件中,默认为off,当此指定被设置为on时,即使客户端显示指示了请求体长度为0时,nginx还是会为请求创建一个临时文件。

接着介绍ngx_http_read_client_request_body()接口的实现,它的定义如下:

ngx_int_t
ngx_http_read_client_request_body(ngx_http_request_t *r,
 ngx_http_client_body_handler_pt post_handler) 

该接口有2个参数,第1个为指向请求结构的指针,第2个为一个函数指针,当请求体读完时,它会被调用。之前也说到根据nginx现有行为,模块逻辑会在请求体读完后执行,这个回调函数一般就是模块的逻辑处理函数。ngx_http_read_client_request_body()函数首先将参数r对应的主请求的引用加1,这样做的目的和该接口被调用的上下文有关,一般而言,模块是在content handler中调用此接口,一个典型的调用如下:

static ngx_int_t
ngx_http_proxy_handler(ngx_http_request_t *r)
{
 ...
 rc = ngx_http_read_client_request_body(r, ngx_http_upstream_init); 

 if (rc >= NGX_HTTP_SPECIAL_RESPONSE) {
 return rc;
 } 

 return NGX_DONE;
}

上面的代码是在porxy模块的content handler,ngx_http_proxy_handler()中调用了ngx_http_read_client_request_body()函数,其中ngx_http_upstream_init()被作为回调函数传入进接口中,另外nginx中模块的content handler调用的上下文如下:

ngx_int_t
ngx_http_core_content_phase(ngx_http_request_t *r,
 ngx_http_phase_handler_t *ph)
{
 ...
 if (r->content_handler) {
 r->write_event_handler = ngx_http_request_empty_handler;
 ngx_http_finalize_request(r, r->content_handler(r));
 return NGX_OK;
 }
 ...
} 

上面的代码中,content handler调用之后,它的返回值作为参数调用了ngx_http_finalize_request()函数,在请求体没有被接收完全时,ngx_http_read_client_request_body()函数返回值为NGX_AGAIN,此时content handler,比如ngx_http_proxy_handler()会返回NGX_DONE,而NGX_DONE作为参数传给ngx_http_finalize_request()函数会导致主请求的引用计数减1,所以正好抵消了ngx_http_read_client_request_body()函数开头对主请求计数的加1。

接下来回到ngx_http_read_client_request_body()函数,它会检查该请求的请求体是否已经被读取或者被丢弃了,如果是的话,则直接调用回调函数并返回NGX_OK,这里实际上是为子请求检查,子请求是nginx中的一个概念,nginx中可以在当前请求中发起另外一个或多个全新的子请求来访问其他的location,关于子请求的具体介绍会在后面的章节作详细分析,一般而言子请求不需要自己去读取请求体。

函数接着调用ngx_http_test_expect()检查客户端是否发送了Expect: 100-continue头,是的话则给客户端回复"HTTP/1.1 100 Continue",根据http 1.1协议,客户端可以发送一个Expect头来向服务器表明期望发送请求体,服务器如果允许客户端发送请求体,则会回复"HTTP/1.1 100 Continue",客户端收到时,才会开始发送请求体。

接着继续为接收请求体做准备工作,分配一个ngx_http_request_body_t结构,并保存在r->request_body,这个结构用来保存请求体读取过程用到的缓存引用,临时文件引用,剩余请求体大小等信息,它的定义如下。

<span style="font-family:SimSun;font-size:18px;">typedef struct {
 ngx_temp_file_t   *temp_file;
 ngx_chain_t   *bufs;
 ngx_buf_t   *buf;
 off_t    rest;
 ngx_chain_t   *to_write;
 ngx_http_client_body_handler_pt post_handler;
} ngx_http_request_body_t;</span>
  • temp_file: 指向储存请求体的临时文件的指针;
  • bufs: 指向保存请求体的链表头;
  • buf: 指向当前用于保存请求体的内存缓存;
  • rest: 当前剩余的请求体大小;
  • post_handler:保存传给ngx_http_read_client_request_body()函数的回调函数。

做好准备工作之后,函数开始检查请求是否带有content_length头,如果没有该头或者客户端发送了一个值为0的content_length头,表明没有请求体,这时直接调用回调函数并返回NGX_OK即可。当然如果client_body_in_file_only指令被设置为on,且content_length为0时,该函数在调用回调函数之前,会创建一个空的临时文件。

进入到函数下半部分,表明客户端请求确实表明了要发送请求体,该函数会先检查是否在读取请求头时预读了请求体,这里的检查是通过判断保存请求头的缓存(r->header_in)中是否还有未处理的数据。如果有预读数据,则分配一个ngx_buf_t结构,并将r->header_in中的预读数据保存在其中,并且如果r->header_in中还有剩余空间,并且能够容下剩余未读取的请求体,这些空间将被继续使用,而不用分配新的缓存,当然甚至如果请求体已经被整个预读了,则不需要继续处理了,此时调用回调函数后返回。

如果没有预读数据或者预读不完整,该函数会分配一块新的内存(除非r->header_in还有足够的剩余空间),另外如果request_body_in_single_buf指令被设置为no,则预读的数据会被拷贝进新开辟的内存块中,真正读取请求体的操作是在ngx_http_do_read_client_request_body()函数,该函数循环的读取请求体并保存在缓存中,如果缓存被写满了,其中的数据会被清空并写回到临时文件中。当然这里有可能不能一次将数据读到,该函数会挂载读事件并设置读事件handler为ngx_http_read_client_request_body_handler,另外nginx核心对两次请求体的读事件之间也做了超时设置,client_body_timeout指令可以设置这个超时时间,默认为60s,如果下次读事件超时了,nginx会返回408给客户端。

最终读完请求体后,ngx_http_do_read_client_request_body()会根据配置,将请求体调整到预期的位置(内存或者文件),所有情况下请求体都可以从r->request_body的bufs链表得到,该链表最多可能有2个节点,每个节点为一个buffer,但是这个buffer的内容可能是保存在内存中,也可能是保存在磁盘文件中。另外$request_body变量只在当请求体已经被读取并且是全部保存在内存中,才能取得相应的数据。

2,丢弃请求体

一个模块想要主动的丢弃客户端发过的请求体,可以调用nginx核心提供的ngx_http_discard_request_body()接口,主动丢弃的原因可能有很多种,如模块的业务逻辑压根不需要请求体 ,客户端发送了过大的请求体,另外为了兼容http1.1协议的pipeline请求,模块有义务主动丢弃不需要的请求体。总之为了保持良好的客户端兼容性,nginx必须主动丢弃无用的请求体。下面开始分析ngx_http_discard_request_body()函数:

ngx_int_t
ngx_http_discard_request_body(ngx_http_request_t *r)
{
 ssize_t size;
 ngx_event_t *rev; 

 if (r != r->main || r->discard_body) {
 return NGX_OK;
 } 

 if (ngx_http_test_expect(r) != NGX_OK) {
 return NGX_HTTP_INTERNAL_SERVER_ERROR;
 } 

 rev = r->connection->read; 

 ngx_log_debug0(NGX_LOG_DEBUG_HTTP, rev->log, 0, "http set discard body"); 

 if (rev->timer_set) {
 ngx_del_timer(rev);
 } 

 if (r->headers_in.content_length_n <= 0 || r->request_body) {
 return NGX_OK;
 } 

 size = r->header_in->last - r->header_in->pos; 

 if (size) {
 if (r->headers_in.content_length_n > size) {
  r->header_in->pos += size;
  r->headers_in.content_length_n -= size; 

 } else {
  r->header_in->pos += (size_t) r->headers_in.content_length_n;
  r->headers_in.content_length_n = 0;
  return NGX_OK;
 }
 } 

 r->read_event_handler = ngx_http_discarded_request_body_handler; 

 if (ngx_handle_read_event(rev, 0) != NGX_OK) {
 return NGX_HTTP_INTERNAL_SERVER_ERROR;
 } 

 if (ngx_http_read_discarded_request_body(r) == NGX_OK) {
 r->lingering_close = 0; 

 } else {
 r->count++;
 r->discard_body = 1;
 } 

 return NGX_OK;
}

由于函数不长,这里把它完整的列出来了,函数的开始同样先判断了不需要再做处理的情况:子请求不需要处理,已经调用过此函数的也不需要再处理。接着调用ngx_http_test_expect() 处理http1.1 expect的情况,根据http1.1的expect机制,如果客户端发送了expect头,而服务端不希望接收请求体时,必须返回417(Expectation Failed)错误。nginx并没有这样做,它只是简单的让客户端把请求体发送过来,然后丢弃掉。接下来,函数删掉了读事件上的定时器,因为这时本身就不需要请求体,所以也无所谓客户端发送的快还是慢了,当然后面还会将到,当nginx已经处理完该请求但客户端还没有发送完无用的请求体时,nginx会在读事件上再挂上定时器。
函数同样还会检查请求头中的content-length头,客户端如果打算发送请求体,就必须发送content-length头,同时还会查看其他地方是不是已经读取了请求体。如果确实有待处理的请求体,函数接着检查请求头buffer中预读的数据,预读的数据会直接被丢掉,当然如果请求体已经被全部预读,函数就直接返回了。

接下来,如果还有剩余的请求体未处理,该函数调用ngx_handle_read_event()在事件处理机制中挂载好读事件,并把读事件的处理函数设置为ngx_http_discarded_request_body_handler。做好这些准备之后,该函数最后调用ngx_http_read_discarded_request_body()接口读取客户端过来的请求体并丢弃。如果客户端并没有一次将请求体发过来,函数会返回,剩余的数据等到下一次读事件过来时,交给ngx_http_discarded_request_body_handler()来处理,这时,请求的discard_body将被设置为1用来标识这种情况。另外请求的引用数(count)也被加1,这样做的目的是客户端可能在nginx处理完请求之后仍未完整发送待发送的请求体,增加引用是防止nginx核心在处理完请求后直接释放了请求的相关资源。

ngx_http_read_discarded_request_body()函数非常简单,它循环的从链接中读取数据并丢弃,直到读完接收缓冲区的所有数据,如果请求体已经被读完了,该函数会设置读事件的处理函数为ngx_http_block_reading,这个函数仅仅删除水平触发的读事件,防止同一事件不断被触发。
再来看一下读事件的处理函数ngx_http_discarded_request_body_handler,这个函数每次读事件来时会被调用,先看一下它的源码:

void
ngx_http_discarded_request_body_handler(ngx_http_request_t *r)
{
 ... 

 c = r->connection;
 rev = c->read; 

 if (rev->timedout) {
 c->timedout = 1;
 c->error = 1;
 ngx_http_finalize_request(r, NGX_ERROR);
 return;
 } 

 if (r->lingering_time) {
 timer = (ngx_msec_t) (r->lingering_time - ngx_time()); 

 if (timer <= 0) {
  r->discard_body = 0;
  r->lingering_close = 0;
  ngx_http_finalize_request(r, NGX_ERROR);
  return;
 } 

 } else {
 timer = 0;
 } 

 rc = ngx_http_read_discarded_request_body(r); 

 if (rc == NGX_OK) {
 r->discard_body = 0;
 r->lingering_close = 0;
 ngx_http_finalize_request(r, NGX_DONE);
 return;
 } 

 /* rc == NGX_AGAIN */ 

 if (ngx_handle_read_event(rev, 0) != NGX_OK) {
 c->error = 1;
 ngx_http_finalize_request(r, NGX_ERROR);
 return;
 } 

 if (timer) { 

 clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module); 

 timer *= 1000; 

 if (timer > clcf->lingering_timeout) {
  timer = clcf->lingering_timeout;
 } 

 ngx_add_timer(rev, timer);
 }
}

函数一开始就处理了读事件超时的情况,之前说到在ngx_http_discard_request_body()函数中已经删除了读事件的定时器,那么什么时候会设置定时器呢?答案就是在nginx已经处理完该请求,但是又没有完全将该请求的请求体丢弃的时候(客户端可能还没有发送过来),在ngx_http_finalize_connection()函数中,如果检查到还有未丢弃的请求体时,nginx会添加一个读事件定时器,它的时长为lingering_timeout指令所指定,默认为5秒,不过这个时间仅仅两次读事件之间的超时时间,等待请求体的总时长为lingering_time指令所指定,默认为30秒。这种情况中,该函数如果检测到超时事件则直接返回并断开连接。同样,还需要控制整个丢弃请求体的时长不能超过lingering_time设置的时间,如果超过了最大时长,也会直接返回并断开连接。
如果读事件发生在请求处理完之前,则不用处理超时事件,也不用设置定时器,函数只是简单的调用ngx_http_read_discarded_request_body()来读取并丢弃数据。

(0)

相关推荐

  • nginx与apache限制ip并发访问 限制ip连接的设置方法

    nginx nginx限制ip并发数,也是说限制同一个ip同时连接服务器的数量 1.添加limit_zone 这个变量只能在http使用 vi /usr/local/nginx/conf/nginx.conf limit_zone one $binary_remote_addr 10m; 2.添加limit_conn 这个变量可以在http, server, location使用 我只限制一个站点,所以添加到server里面 vi /usr/local/nginx/conf/host/gaoji

  • Linux服务器nginx访问日志里出现大量http 400错误的请求分析

    服务器中的错误记录类似于这种: 124.65.133.242 – – [27/Oct/2014:14:30:51 +0800] "-" 400 0 "-" "-" 124.65.133.242 – – [27/Oct/2014:14:31:45 +0800] "-" 400 0 "-" "-" 124.65.133.242 – – [27/Oct/2014:14:31:45 +0800]

  • 详解Nginx服务器中配置全站HTTPS安全连接的方法

    HTTPS就等于HTTP加上TLS(SSL),HTTPS协议的目标主要有三个: 数据保密性.保证内容在传输过程中不会被第三方查看到.就像快递员传递包裹时都进行了封装,别人无法知道里面装了什么东西.     数据完整性.及时发现被第三方篡改的传输内容.就像快递员虽然不知道包裹里装了什么东西,但他有可能中途掉包,数据完整性就是指如果被掉包,我们能轻松发现并拒收.     身份校验.保证数据到达用户期望的目的地.就像我们邮寄包裹时,虽然是一个封装好的未掉包的包裹,但必须确定这个包裹不会送错地方. 启用

  • 修改配置解决Nginx服务器中常见的上传与连接错误

    nginx上传错误413 Request Entity Too Large 默认情况下使用nginx反向代理上传超过2MB的文件,会报错413 Request Entity Too Large,解决这个方法很简单,修改配置client_max_body_size值即可 修改nginx.conf #cat /usr/local/nginx-1.7.0/conf/nginx.conf | grep client_max_body_size client_max_body_size 10M; 如果需要

  • Nginx服务器中限制连接数与限制请求的模块配置教程

    限制连接数的ngx_http_limit_conn_module模块 我们经常会遇到这种情况,服务器流量异常,负载过大等等.对于大流量恶意的攻击访问,会带来带宽的浪费,服务器压力,影响业务,往往考虑对同一个ip的连接数,并发数进行限制.下面说说ngx_http_limit_conn_module 模块来实现该需求.该模块可以根据定义的键来限制每个键值的连接数,如同一个IP来源的连接数.并不是所有的连接都会被该模块计数,只有那些正在被处理的请求(这些请求的头信息已被完全读入)所在的连接才会被计数.

  • Nginx服务器中处理AJAX跨域请求的配置方法讲解

    Nginx 实现AJAX跨域请求 AJAX从一个域请求另一个域会有跨域的问题.那么如何在nginx上实现ajax跨域请求呢?要在nginx上启用跨域请求,需要添加add_header Access-Control*指令.如下所示: location /{ add_header 'Access-Control-Allow-Origin' 'http://other.subdomain.com'; add_header 'Access-Control-Allow-Credentials' 'true

  • Nginx静态文件响应POST请求 提示405错误的解决方法

    例1:用linux下的curl命令发送POST请求给Apache服务器上的HTML静态页 复制代码 代码如下: [root@localhost ~]# curl -d 11=1 http://www.jb51.net/index.html    <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">   <HTML>       <HEAD>           <TITLE>405 Meth

  • Nginx配置统计流量带宽请求及记录实时请求状态的方法

    流量带宽请求状态统计 ngx_req_status用来展示nginx请求状态信息,类似于apache的status,nginx自带的模块只能显示连接数等等信息,我们并不能知道到底有哪些请求.以及各url域名所消耗的带宽是多少.ngx_req_status提供了这些功能. 功能特性 按域名.url.ip等等统计信息 统计总流量 统计当前带宽\峰值带宽 统计总请求数量 1. 安装 # cd /usr/local/src/ # wget "http://nginx.org/download/nginx

  • 详解Nginx的核心配置模块中对于请求体的接受流程

    本篇文章主要会介绍nginx中请求的接收流程,包括请求头的解析和请求体的读取流程. 首先介绍一下rfc2616中定义的http请求基本格式: Request = Request-Line *(( general-header | request-header | entity-header ) CRLF) CRLF [ message-body ] </span> 第一行是请求行(request line),用来说明请求方法,要访问的资源以及所使用的HTTP版本: Request-Line  

  • 详解Nginx的配置函数对于请求体的读取

    nginx核心本身不会主动读取请求体,这个工作是交给请求处理阶段的模块来做,但是nginx核心提供了ngx_http_read_client_request_body()接口来读取请求体,另外还提供了一个丢弃请求体的接口-ngx_http_discard_request_body(),在请求执行的各个阶段中,任何一个阶段的模块如果对请求体感兴趣或者希望丢掉客户端发过来的请求体,可以分别调用这两个接口来完成.这两个接口是nginx核心提供的处理请求体的标准接口,如果希望配置文件中一些请求体相关的指

  • 详解Nginx如何配置Web服务器的示例代码

    概述 今天主要分享怎么将NGINX配置作为Web服务器,并包括以下部分: 设置虚拟服务器 配置位置 使用变量 返回特定状态码 重写HTTP响应 在高层次上,将NGINX配置作为Web服务器有一些问题需要了解,定义它处理哪些URL以及如何处理这些URL上的资源的HTTP请求. 在较低层次上,配置定义了一组控制对特定域或IP地址的请求的处理的虚拟服务器. 用于HTTP流量的每个虚拟服务器定义了称为位置的特殊配置实例,它们控制特定URI集合的处理. 每个位置定义了自己的映射到此位置的请求发生的情况.

  • 详解Nginx Location配置

    今天有一位同学问到 Nginx 的站点多路径匹配的问题? 1.www.domain.com/a 需要返回 /var/www/domain.com/a/index.html 2.www.domain.com/b 需要返回 /var/www/domain.com/b/index.html 如何配置 Nginx 使之生效? 解决这个问题,第一的反映是直接使用 Nginx 的 location 指令来解决,不过在给出答案之前,我们先来了解一下 Nginx location 指令的基础. Nginx 区块

  • 详解nginx websocket配置

    一·什么是websocket WebSocket协议相比较于HTTP协议成功握手后可以多次进行通讯,直到连接被关闭.但是WebSocket中的握手和HTTP中的握手兼容,它使用HTTP中的Upgrade协议头将连接从HTTP升级到WebSocket.这使得WebSocket程序可以更容易的使用现已存在的基础设施. WebSocket工作在HTTP的80和443端口并使用前缀ws://或者wss://进行协议标注,在建立连接时使用HTTP/1.1的101状态码进行协议切换,当前标准不支持两个客户端

  • 详解Nginx日志配置及日志切割

    日志配置 日志对于统计排错来说非常有利的.本文总结了nginx日志相关的配置如access_log.log_format.open_log_file_cache.log_not_found.log_subrequest.rewrite_log.error_log. nginx有一个非常灵活的日志记录模式.每个级别的配置可以有各自独立的访问日志.日志格式通过log_format命令来定义. ngx_http_log_module是用来定义请求日志格式的. 1. access_log指令 语法: a

  • 详解nginx如何配置HTTPS

    使用ssl模块配置同时支持http和https并存 一,生成证书 # 1.首先,进入你想创建证书和私钥的目录,例如: cd /etc/nginx/ # 2.创建服务器私钥,命令会让你输入一个口令: openssl genrsa -des3 -out server.key 1024 # 3.创建签名请求的证书(CSR): openssl req -new -key server.key -out server.csr # 4.在加载SSL支持的Nginx并使用上述私钥时除去必须的口令: cp se

  • 详解nginx upstream 配置和作用

    配置例子 upstream backend { server backend1.example.com weight=5; server backend2.example.com:8080; server unix:/tmp/backend3; server backup1.example.com:8080 backup; server backup2.example.com:8080 backup; } server { location / { proxy_pass http://backe

  • 详解Nginx如何配置继承模型

    要了解nginx的继承模型,首先需要知道nginx使用多个配置块进行操作. 在nginx中,这样的块被称为上下文,例如,放置在服务器上下文中的配置指令驻留在server { }块中,就像放置在http上下文中的指令驻留在http { } 块中一样. nginx中有6种可能的上下文,这里是从上到下的顺序: Global. Http. Server. If. Location. Nested Location. If in location. limit_except. 默认继承模型是指令仅向下继承

  • 详解Nginx防盗链和Nginx访问控制与Nginx解析php的配置

    详解Nginx防盗链和Nginx访问控制与Nginx解析php的配置 Nginx防盗链 配置如下,可以和上面的配置结合起来 location ~* ^.+\.(gif|jpg|png|swf|flv|rar|zip|doc|pdf|gz|bz2|jpeg|bmp|xls)$ { expires 7d; valid_referers none blocked server_names *.test.com ; if ($invalid_referer) { return 403; } access

随机推荐