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()接口的实现,它的定义如下:

[cpp] view plaincopy

  1. <span style="font-family:SimSun;font-size:18px;">ngx_int_t
  2. ngx_http_read_client_request_body(ngx_http_request_t *r,
  3. ngx_http_client_body_handler_pt post_handler)</span>

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

[cpp] view plaincopy

  1. <span style="font-family:SimSun;font-size:18px;">static ngx_int_t
  2. ngx_http_proxy_handler(ngx_http_request_t *r)
  3. {
  4. ...
  5. rc = ngx_http_read_client_request_body(r, ngx_http_upstream_init);
  6. if (rc >= NGX_HTTP_SPECIAL_RESPONSE) {
  7. return rc;
  8. }
  9. return NGX_DONE;
  10. }</span>

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

[cpp] view plaincopy

  1. <span style="font-family:SimSun;font-size:18px;">ngx_int_t
  2. ngx_http_core_content_phase(ngx_http_request_t *r,
  3. ngx_http_phase_handler_t *ph)
  4. {
  5. ...
  6. if (r->content_handler) {
  7. r->write_event_handler = ngx_http_request_empty_handler;
  8. ngx_http_finalize_request(r, r->content_handler(r));
  9. return NGX_OK;
  10. }
  11. ...
  12. }</span>

上面的代码中,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,这个结构用来保存请求体读取过程用到的缓存引用,临时文件引用,剩余请求体大小等信息,它的定义如下。

[cpp] view plaincopy

  1. <span style="font-family:SimSun;font-size:18px;">typedef struct {
  2. ngx_temp_file_t                  *temp_file;
  3. ngx_chain_t                      *bufs;
  4. ngx_buf_t                        *buf;
  5. off_t                             rest;
  6. ngx_chain_t                      *to_write;
  7. ngx_http_client_body_handler_pt   post_handler;
  8. } 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变量只在当请求体已经被读取并且是全部保存在内存中,才能取得相应的数据。

时间: 2024-10-12 00:29:25

nginx请求体读取的相关文章

nginx请求体读取(二)

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

nginx源码分析--请求体的读取(1)

首先请求体的读取已经进入了HTTP请求的11个阶段,针对有些模块需要对请求体做一些处理,那么这个模块就需要在这个阶段注册函数,其中读取请求体的函数ngx_http_read_client_request_body()是存在的,只不过不同的模块可能对请求体做不同的处理,读取请全体的函数是在某个模块的conent_handler函数中包含的,比如比如proxy模块,fastcgi模块,uwsgi模块等这些模块对请求体感兴趣,那么读取请求体的函数在这些模块的content_handler中注册. 上节

springboot请求体中的流只能读取一次的问题

场景交代 在springboot中添加拦截器进行权限拦截时,需要获取请求参数进行验证.当参数在url后面时(queryString)获取参数进行验证之后程序正常运行.但是,当请求参数在请求体中的时候,通过流的方式将请求体取出参数进行验证之后,发现后续流程抛出错误: Required request body is missing ... 经过排查,发现ServletInputStream的流只能读取一次(参考:httpServletRequest中的流只能读取一次的原因). 这就是为什么在拦截器

SpringCloud Gateway获取post请求体(request body)

获取spring cloud gateway POST请求体的时候,会有很多坑,网上大多数解决方案是 /** 这种方法在spring-boot-starter-parent 2.0.6.RELEASE + Spring Cloud Finchley.SR2 body 中生效, 但是在spring-boot-starter-parent 2.1.0.RELEASE + Spring Cloud Greenwich.M3 body 中不生效,总是为空 */ private String resolv

新章节——请求体搜索(full body search)

轻量级的搜索--?query string search?--对命令行的即席查询来说是十分有用的.然而为了驾驭搜索的强大功能,你应该使用带请求体的search API,之所以这样,是因为很多的参数是在JSON的请求体中的而不是查询字符串. 请求体查询--从现在开始就是"search"--不仅仅自身处理查询,而且允许你高亮结果片段,也可以对结果集进行合并和分析,也能返回did-you-mean?suggestions这样的提示,帮助提示你的用户快速找到更好的结果. 原文:http://w

python写http post请求的四种请求体

Web自动化测试(25) HTTP 协议规定 POST 提交的数据必须放在消息主体(entity-body)中,但协议并没有规定数据必须使用什么编码方式.常见的四种编码方式如下: 1.application/x-www-form-urlencoded 这应该是最常见的 POST 提交数据的方式了.浏览器的原生 form 表单,如果不设置 enctype 属性,那么最终就会以 application/x-www-form-urlencoded 方式提交数据.请求类似于下面这样(无关的请求头在本文中

ElasticSearch(七)--请求体查询

简单查询lite search (字符串查询)是一种有效的命令行ad hoc 查询,但是想要善用搜索,必须使用请求体查询request  body search API.之所以这么称呼,是因为大多数的参数以JSON格式所容纳,而不是查询字符串. 请求体查询不但可以处理查询,而且还可以高亮返回结果中的片段. 1.空查询 GET _search {} 同字符串查询一样,你可以查询一个,或多个索引及类型 GET /index_2014*/type1,type2/_search {} 也可以使用from

RIDE 接口自动化请求体参数中文时报错:“UnicodeDecodeError: &#39;ascii&#39; codec can&#39;t decode byte 0xd7 in position 9......”

在进行robotframework  接口自动化,在请求体参数中输入中文会报以下错误: UnicodeDecodeError: 'ascii' codec can't decode byte 0xd7 in position 9: ordinal not in range(128)..... 改mimetypes.py文件,路径位于python的安装路径下的Lib\mimetypes.py文件.在import下添加如下几行:解放参考如下: if sys.getdefaultencoding() 

SolrCloud的Java客户端请求体过大导致的400错误

由于业务原因,Solr客户端发起的查询请求体数据太多,默认采用get方式超过 tomcat maxHttpHeaderSize. 方案: 增加 tomcat maxHttpHeaderSize 到32k,默认8k 修改solr客户端默认提交方式post maxBooleanClauses 建议1-5000