一、参考链接:https://blog.csdn.net/marcky/article/details/6539387/
如果你正在从事nginx的模块开发,可能需要获取服务器端的ip地址。我在改进淘宝日志服务器的nginx模块时,就碰到需要从request中读取到接收这个请求的网卡ip。一开始,我试图如下方式直接从请求中获取地址:
struct sockaddr_in *sin = NULL; /* type of r is ngx_http_request_t */ sin = (struct sockaddr_in *)(r->connection->local_sockaddr);
经过几番尝试都失败了,读取的值(sin)始终是空,也就是说这个字段(local_sockaddr)根本没有被设置上ip相关的值。无奈之下查看一个请求的初始化过程的源码后,发现这个字段根本就没有被初始化。nginx为什么不帮我们做这件事情呢?难道需要我们自己通过getsockname函数来实现吗?这也有点太坑爹了吧。最后向叔度(nginx专家@淘宝)请教后,得知:在使用如上代码段获取本地地址之前,先调用一下如下函数就可以获取到了。
ngx_connection_local_sockaddr(r->connection, NULL, 0);
查看ngx_connection_local_sockaddr函数源码,可以看到这个函数的工作就是使用getsockname来获取本地地址。在处理一个http请求的时候,很少会使用到本地地址,所以nginx为了节省这段空间,故默认不初始化本地地址,而是在具体需要的时候,再通过提供的接口去主动获取,这样一来节省了空间,又在需要的时候能够快速的获取。这是一个不错的折中。
二、参考链接:https://www.cnblogs.com/lidabo/p/4177106.html
(此博主的其他系列文章也不错,可以相继查看)
上节说到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()接口读取的请求体会部分或者全部写入一个临时文件中,根据请求体的大小以及相关的指令配置,请求体可能完整放置在一块连续内存中,也可能分别放置在两块不同内存中,还可能全部存在一个临时文件中,最后还可能一部分在内存,剩余部分在临时文件中。下面先介绍一下和这些不同存储行为相关的指令:
![](/qrcode.jpg)
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,这个结构用来保存请求体读取过程用到的缓存引用,临时文件引用,剩余请求体大小等信息,它的定义如下。
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_read_client_request_body()函数的回调函数 } ngx_http_request_body_t;
做好准备工作之后,函数开始检查请求是否带有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变量只在当请求体已经被读取并且是全部保存在内存中,才能取得相应的数据。
三、参考链接:https://www.cnblogs.com/ljygoodgoodstudydaydayup/p/3838053.html
【Nginx】请求上下文
上下文与全异步web服务器的关系
请求上下文指在一个请求的处理过程中,把一些关键的信息保存下来的类似struct这样的结构体。每个http模块都可以有自己的上下文结构体,一般都是在刚开始处理请求时在内存池上分配它,之后当经由epoll、http框架再次调用到http模块的处理方法时,这个http模块可以由请求上下文结构体中获取信息。请求结束时就会销毁该请求的内存池,自然也就销毁了上下文结构体。
Nginx是全异步处理的web服务器,http模块可能会多次反复处理同一个请求,所以必须定义上下文结构体来保存处理过程的中间状态。Nginx框架不会维护这个上下文,只能由这个请求自己保存着上下文结构体。
使用http上下文
之所以使用上下文, 是因为Nginx是一个全异步处理的Web服务器, 1个请求并不会在epoll的一次调度中处理完成, 甚至会在成千上万的HTTP模块后才能完成请求的处理。
既然上下文是针对某请求的某模块的, 那么可以根据前文中, 存储某模块存储create_main_conf函数返回的结构体的方式了解到, 其实所有模块在每个请求中都对应了数组的一个元素, 所以可以根据数组下标来找到对应模块的数据.
使用上下文的方式就是调用下面这两个宏:
#define ngx_http_get_module_ctx(r, module) (r)->ctx[module.ctx_index]; #define ngx_http_set_ctx(r, c, module) r->ctx[module.ctx_index] = c;
下面提供一个例子:
//首先建立上下文的结构体 typedef struct { ngx_uint_t ben_count; }ngx_ben_test_ctx_t; //当请求第一次进入ben_test模块处理时,我们需要创建一个ngx_ben_test_ctx_t结构体,并设置到请求中去 static ngx_int_t ngx_http_ben_test(ngx_http_request_t *r) { //首先调用ngx_http_get_module_ctx获取此模块的上下文结构体(可以预料,既然这是第一次进入此模块处理,这个上下文应该就是NULL的) //传入的第一个参数是请求对象,第二个参数是模块对象 ngx_ben_test_ctx_t *ctx = ngx_http_get_module_ctx(r, ngx_http_ben_test_module); if (ctx == NULL) { //必须在内存池中申请对象,这样才能保证在一个请求结束和这些资源都被释放 ctx = ngx_palloc(r->pool, sizeof(ngx_ben_test_ctx_t)); if (ctx == NULL) return NGX_ERROR; //把上下文结构体存起来 ngx_http_set_ctx(r, ctx, ngx_http_ben_test_module); } //接下来就可以使用这个上下文了 ... } //如果Nginx多次回调ben_test模块的相应方法,那么每次使用宏得到上下文,继而修改再保存,这样,上下文就被正常使用起来了。
下面看一下请求结构体中的一个变量就明白了
struct ngx_http_request_s { ... void **ctx; ... }与我们上面提到的也确实是相符合的。