nginx处理post请求之数据转发

        上一篇文章分析了nginx在处理post请求时,如何启动upstream这个负载均衡模块。它是一个http框架,由它来调度具体的http模块,例如fastcgi, proxyd反向代理等,这些模块负责将来自客户端 的请求包头,请求包体转为与后端服务器通信的格式。本篇文章来分析nginx是如何将已经转换后的报文发给后端服务器。

一、转发请求数据到后端服务器流程

//与后端服务器建立连接,并注册读写事件的回调
static void ngx_http_upstream_connect(ngx_http_request_t *r, ngx_http_upstream_t *u)
{
	//发送数据给后端服务器
	ngx_http_upstream_send_request(r, u)
}
        ngx_http_upstream_send_request函数负责将转换后的报文发给后端服务器,来看下这个函数的实现过程。
//发送数据给后端服务器
static void ngx_http_upstream_send_request(ngx_http_request_t *r, ngx_http_upstream_t *u)
{
    c = u->peer.connection;
	//向上游服务器发送请求内容,内容为request_bufs。如果一次性不能发送完,则会把
	//未发送的保存到output中的busy成员。第二次被调用时,传递参数为空,因此函数内部会把上次
	//没发送完的busy链表中的数据继续发给后端服务器
    rc = ngx_output_chain(&u->output, u->request_sent ? NULL : u->request_bufs);

	//标记为已经向上游服务器发送了请求
    u->request_sent = 1;

	//先移除写事件的超时事件
    if (c->write->timer_set)
	{
        ngx_del_timer(c->write);
    }

	//如果一次没有发送完所有请求数据,则把重新注册写事件超时到epoll
    if (rc == NGX_AGAIN) 
	{
        ngx_add_timer(c->write, u->conf->send_timeout);
        ngx_handle_write_event(c->write, u->conf->send_lowat);
        return;
    }

	/**********执行到此,表示已经向上游服务器发送完所有请求数据**********/
	/**********如果有读事件,则开始接收上游服务器的响应******************/
    ngx_add_timer(c->read, u->conf->read_timeout);

	//将写事件设置不做任何事情。因为nginx已经把所有请求都发给了上游服务器,不需要发送了,
    u->write_event_handler = ngx_http_upstream_dummy_handler;
    ngx_handle_write_event(c->write, 0);
}
        函数内部调用gx_output_chain向后端服务器发送数据,首次被调用时传递的参数是u->request_bufs,这个内容也就是经过转换后的,与后端服务器通信的报文。但如果一次发送不完, 需要再次被事件模块调度执行才能发送剩余数据时, 这个函数会再次被调用并且传递NULL空参数,这是为什么? 因为gx_output_chain函数内部会把上一次未发送完的数据报文保存到ngx_output_chain_ctx_t结构中的busy成员中。 这个函数再次被调用时传递的参数为空,就可以把busy成员中的剩余数据发送给后端服务。这个函数实现了具体的发送过程,比较复杂,文章最后面我们再来详细分析。这里先分析发送过程的整体框架,先把控整体,再来分析细节。现在分析两个场景:

        (1)如果一次调度这个函数不能发送完所有数据怎么办? nginx会重新注册写事件到epoll中,这样写事件被触发时,可以发送剩余的数据给后端服务器。来看下这个过程;

//事件模块的读写回调,事件被触发时会调用负载均衡模块对应的读写回调
static void ngx_http_upstream_handler(ngx_event_t *ev)
{
	//c表示nginx与上游服务器的连接
    c = ev->data;
	//r表示客户端与nginx的请求
    r = c->data;
	//u表示nginx与上游服务器的upstream
    u = r->upstream;
	//这时的c表示客户端与nginx的连接
    c = r->connection;
	
    if (ev->write) 
	{
		//向后端服务器发送数据
        u->write_event_handler(r, u);
    }
	else 
	{
		//接收后端服务器的响应
        u->read_event_handler(r, u);
    }
}
        对于负载均衡模块的写回调write_event_handler为: ngx_http_upstream_send_request_handler,用于在一次没有发送完所有的请求数据给后端服务器时,写事件再次触发时将调用这个函数发送剩余的数据。从中也可以看出,最终还是调用ngx_http_upstream_send_request这个函数发送请求数据给后端服务器。
//一次没有发送完所有的请求数据给后端服务器时,写事件再次触发时会调用这个函数发送剩余的数据
static void ngx_http_upstream_send_request_handler(ngx_http_request_t *r, ngx_http_upstream_t *u)
{
	//如果写超时,表示发送给上游服务器的请求超时了
    if (c->write->timedout) 
	{
		//重新连接服务器,会根据策略,可能连接到下一个可用的上游服务器
        ngx_http_upstream_next(r, u, NGX_HTTP_UPSTREAM_FT_TIMEOUT);
        return;
    }
	
	//再次调用这个函数发送请求
    ngx_http_upstream_send_request(r, u);
}
        (2)如果发送完了所有的请求数据给后端服务器,那该如何处理? 继续回到ngx_http_upstream_send_request函数进行分析。如果发送完所有的请求数据后,并且读事件已经就绪了,则立即读取来自后端服务的http响应头部;如果读事件还未就绪,则把负载均衡模块的write_event_handler写回调设置为不做任何事情的: ngx_http_upstream_dummy_handler,因为不需要再发送任何数据给后端服务器了。以此同时nginx将等待读事件被触发,当读事件被触发时,开始接收来自后端服务器的http响应头部。

二、发送函数分析

        调用ngx_output_chain函数负责把调用层传进来的数据发给后端服务器,如果调用层传进来的参数为空,则会把保存在内部隐藏层中的上一次未发送完的数据发送给后端服务器。在分析函数前,来看下这个函数维护的数据结构,分两种情况。一种是直接把调用层传进来的数据拷贝到内部隐藏层;另一种是把调用层传进来的参数先拷贝到过滤层,过滤层处理完成后,再把结果传给内部隐藏层;

        1、直接把调用层数据传给内部隐藏层;


        调用层链表,也就是调用ngx_output_chain函数传递的u->request_bufs链表或者空链表,也就是应用层要发给后端服务器的数据。通常如果调用层链表是一个空节点,或者调用层只有一个链表节点(要发给后端服务器的数据较少,一个链表节点足以存放所有数据),这两种情况下会直接把调用层的数据直接拷贝到内部隐藏层链表,也就是ngx_chain_writer_ctx_t成员out。这个内部隐藏层链表是做什么呢? 用于缓存要发送给后端服务器的报文,如果报文过大导致一次无法发送,则会保存上一次未发送完成的数据,这也是最终要发送给后端服务器的数据。另一个问题,为什么图中内部隐藏层链表节点数比调用层链表节点数更多呢? 因为内部隐藏层保留了上一次未发送完的数据,如果再次调用时,调用层又传递了新数据的话,那也会缓存这个新的数据,因此内部隐藏层数据节点肯定比调用层过多。

//向后端服务器发送数据
//参数in:调用层链表
ngx_int_t ngx_output_chain(ngx_output_chain_ctx_t *ctx, ngx_chain_t *in)
{
    if (ctx->in == NULL && ctx->busy == NULL)
	{
        if (in == NULL) 
		{
			//ngx_chain_writer,把数据直接传递给内部隐藏层进行发送
            return ctx->output_filter(ctx->filter_ctx, in);
        }

		//调用层链表由一个节点组成,通常这种情况是要发给后端服务器的数据较少,
		//一个节点足以存放所有数据。且不需要拷贝内存操作。
        if (in->next == NULL && ngx_output_chain_as_is(ctx, in->buf))
        {
        	//ngx_chain_writer,把数据直接传递给内部隐藏层进行发送
            return ctx->output_filter(ctx->filter_ctx, in);
        }
    }
}
        output_filter的回调为: ngx_chain_writer, 这个函数内部会把数据发给后端服务器,同时保存未发送完成的数据到内部隐藏层的out链表中,以便下一次写事件被触发时,可以发送剩余的数据给后端服务器。来看下这个函数的实现过程;
//将in链表的数据发送到后端服务器
ngx_int_t ngx_chain_writer(void *data, ngx_chain_t *in)
{
	//将in的内容拷贝到输出链表ctx->last中,也就是内部隐藏层的out链表。
	//之所以要保存的目的是一次不能发送完所有的数据到后端服务器时,可以在下一次把剩余的数据发给后端服务器
    for (size = 0; in; in = in->next) 
	{
		//将节点插入到out链表末尾
        cl = ngx_alloc_chain_link(ctx->pool);
        cl->buf = in->buf;
        cl->next = NULL;
        *ctx->last = cl;
        ctx->last = &cl->next;
    }

	//发送数据给后端服务器 ngx_writev_chain,返回值为已经发送到哪个节点。
	//下一次从这个节点开始发送剩余的数据给后端服务器,返回后的ctx->out指向未发送完成的链表节点
    ctx->out = c->send_chain(c, ctx->out, ctx->limit);
}
        如果一次没有发送完成,则下一次写事件被调度时,ngx_output_chain传入的请求数据为空,因此ngx_chain_writer再次被调度时,将会把内部隐藏层ngx_chain_writer_ctx_t的out成员的数据发送给后端服务器。

        2、调用层的数据经过过滤后,在发给内部隐藏层

        通常调用层成传进来的数据不是一个链表节点就可以存放所有数据,或者调用层的数据还保存到文件中,则这些数据不能直接传递给内部隐藏层,而需要经过过滤层进行处理。过滤层把数据处理完后,在发给内部隐藏层。因此对于内部隐藏层来讲这是透明的操作,内部隐藏层不管数据是直接来自调用层,还是过滤层处理后的结果,它才不关心数据来自何处,只要有数据到来,内部隐藏层就接收。这是一种典型的分层设计思想。


        从图中可以看出,过滤层链表ngx_output_chain_ctx_t成员in是一个链表,缓存来自调用层的数据, 而内部隐藏层ngx_chain_writer_ctx_t成员out也是一个链表,缓存来自过滤层的处理结果或者直接来自调用层的数据(调用层只传递一个链表节点,或者传递的数据为空)。 为什么要搞两个链表呢? 岂不是多此一举,其实不然,nginx这样设计肯定有它的目的。 因为过滤层链表缓存的是来自调用层的数据,经过过滤处理 ,然后把过滤结果传递给内部隐藏层,但有可能一次操作不能把调用层传进来的数据全部过滤完成, 而需要下一次被调用时继续过滤未过虑完的数据,因此需要维护这个过滤层链表;  而内部隐藏层链表是保存未发送给后端服务器的数据,也因此需要这个内部隐藏层链表。这两个链表分工不同,作用也就不同。

ngx_int_t ngx_output_chain(ngx_output_chain_ctx_t *ctx, ngx_chain_t *in)
{
	//将链表in的内容拷贝到ctx->in链表的末尾,也就是保存到过滤链表
    if (in) 
	{
        ngx_output_chain_add_copy(ctx->pool, &ctx->in, in);
    }
	
	//这个循环是为了多次进行过滤操作
    for ( ;; ) 
	{
		//对过滤链表ctx->in中的每一个节点内容进行过滤处理,过滤后保存到out结果链表。
		//这里说的过滤只是判断是否需要为这个链表节点拷贝一份数据到内存, 为了方便理解,暂且称之为过滤。
		//ctx->in链表中的数据不就是在内存吗?为什么还需要拷贝一份数据到内存。没错,如果ctx->in链表中的数据在
		//内存中的话,那肯定不需要在拷贝数据到内存了,但ctx->in链表中的节点有可能指向的是文件,而文件的数据
		//是没有拷贝到内存中的,因此对于文件则需要拷贝到内存中来。
        while (ctx->in) 
		{
			//计算每一个链表节点的大小
            bsize = ngx_buf_size(ctx->in->buf);
			//如果数据不是在文件中,则这个函数返回0,表示数据已经在内存中了,因此不需要拷贝数据到内存中。
			//直接把该节点插入到out结果链表末尾,相当于这个节点就过滤完成了。否则需要申请buf空间
            if (ngx_output_chain_as_is(ctx, ctx->in->buf)) 
			{
                cl = ctx->in;
                ctx->in = cl->next;
                *last_out = cl;
                last_out = &cl->next;
                cl->next = NULL;
                continue;
            }

            if (ctx->buf == NULL)
			{
				//创建bsize大小的bufer缓冲区,存放到ctx->buf,通常返回的结果不等于NGX_OK
                rc = ngx_output_chain_align_file_buf(ctx, bsize);
                if (rc != NGX_OK) 
				{
                    if (ctx->free) 
					{
						//从空闲链表中获取buf空间
                        cl = ctx->free;
                        ctx->buf = cl->buf;
                        ctx->free = cl->next;
                        ngx_free_chain(ctx->pool, cl);
                    }
					else if (out || ctx->allocated == ctx->bufs.num) 
					{
						//申请的buf空间总数超过限制,则先把缓存中的数据发送完后,在来发送剩余的数据
						//发送完成后,可以重复利用这些缓存
                        break;

                    }
					else if (ngx_output_chain_get_buf(ctx, bsize) != NGX_OK) 
					{
						//重新申请一个buff空间,最多不能申请超过ctx->bufs.num个数
                        return NGX_ERROR;
                    }
                }
            }

			//将ctx->in->buf缓冲区的内容拷贝到ctx->buf缓冲区中
            rc = ngx_output_chain_copy_buf(ctx);
			//已经将该过滤节点的数据移动到了out结果链表中,因此可以删除该过滤节点
            if (ngx_buf_size(ctx->in->buf) == 0)
			{
                ctx->in = ctx->in->next;
            }

			//创建一个链表节点
            cl = ngx_alloc_chain_link(ctx->pool);
            cl->buf = ctx->buf;
            cl->next = NULL;
			//将链表节点插入到out链表末尾
            *last_out = cl;
            last_out = &cl->next;
            ctx->buf = NULL;
        }

		//发送数据给后端服务器 ngx_chain_writer
        last = ctx->output_filter(ctx->filter_ctx, out);

		//将out中已经发送完的数据移动到free空闲链表,之后需要buf空间的话就可以直接从free空闲链表中获取
		//而对应没有发送完的数据则保存到busy链表,以便下一次写事件触发时发送剩余的数据
        ngx_chain_update_chains(&ctx->free, &ctx->busy, &out, ctx->tag);
		
		//此时out此时一定是空,因为ngx_chain_update_chains函数内部把out未发送完的数据保存到busy后,将out设置为空
        last_out = &out;
    }
}

        函数有点长,不过注释得很清楚了。这里所说的过滤,是我方便理解而称之为过滤。说白了就是判断调用层传进来的数据是否需要做一个内存拷贝操作。当调用层传进来的链表节点指向的数据是保存到文件中时,则需要把文件中的数据拷贝到内存中,拷贝结束后将这个节点插入到过滤结果链表。而如果调用层传进来的链表指向的数据已经保存到了内存,则不需要在做内存拷贝操作了,直接将该节点插入到过滤链结果链表末尾。 

        需要注意的是,如果需要执行内存拷贝操作,则会有两种方式获取一个新的buf空间。一种是从空闲链表free中获取,那这个空闲链表从何而来呢? 当发送数据给后端服务器完成后,会将已经发送完的链表节点插入到空闲链表中,从ngx_chain_update_chains函数可以看出这个过程,这样就可以复用这个缓冲区。另一种方式是,如果空闲链表没有空间容纳新的数据了,则重新从内存池中申请一个空间,但申请缓冲区的总个数不能超过限制,超过限制时,需要等缓冲区中的数据发给后端服务器完成后,在复用这片空间,而不是重新开辟。

         到此为止, 对于nginx是如何发生fastcgi报文给后端服务器,以及一次没发送完所有数据时,写事件再次被触发时是如何发送剩余数据给后端服务器的,这块逻辑应该清晰了吧!  做个总结,如果调用层传进来的数据比较小,一个节点足以存放所有数据,则直接把数据发给内部隐藏层处理; 如果调用层传进来的数据很大时,需要经过过滤层过滤处理,再把过滤后的结果传递给内部隐藏层。 内部隐藏层把数据发送给后端服务器,以及保存一次未发送完的数据,以便写事件触发时再次发送。通过发送逻辑的分析,能很好的体现分层设计的思想。

        下一篇文章将分析nginx是如何接收来自后端服务器响应的。


  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 要查看nginx接收到的post请求参数,可以通过以下几个步骤进行操作: 1. 设置nginx配置文件:修改nginx.conf文件,找到http块中的server段落,并在其中添加以下配置项: ``` location / { ... proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://your_backend_server; proxy_intercept_errors on; error_page 502 = @fallback; } location @fallback { internal; proxy_pass http://your_backend_server; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } ``` 其中,`your_backend_server`是指向后端真实服务器的地址。 2. 重新加载nginx配置:修改完配置文件后,执行以下命令重新加载配置: ``` sudo nginx -s reload ``` 3. 查看post请求参数:通过访问nginx服务器的访问地址进行post请求,例如: ``` curl -X POST -d "param1=value1&param2=value2" http://your_nginx_server ``` 其中,`your_nginx_server`是指向nginx服务器的地址。这样,nginx会将post请求转发至后端真实服务器,并将请求参数一同发送过去。 在后端服务器上,可以使用相应的编程语言(如Python中的Flask框架)来接收和解析post请求参数,并对其进行处理。 通过以上步骤,就可以在nginx中查看post请求参数了。 ### 回答2: 要使用Nginx查看POST请求参数,你可以按照以下步骤进行操作: 1. 首先,确保你已经正确配置了Nginx的服务器块(server block)。你可以使用文本编辑器打开Nginx的配置文件,通常位于/etc/nginx/nginx.conf或/usr/local/nginx/conf/nginx.conf。 2. 在server block中,找到对应的location段落并添加以下指令: ``` location /your_post_url { proxy_set_header Content-Type application/x-www-form-urlencoded; proxy_pass_request_body on; proxy_set_body $request_body; } ``` 这将设置NginxPOST请求请求体传递给上游服务器,并将请求体中的参数存储在$request_body变量中。 3. 保存并关闭配置文件,然后重启Nginx服务以使配置生效。你可以使用以下命令重启Nginx: ``` sudo service nginx restart ``` 4. 现在,当有POST请求发送到Nginx时,它将将请求数据传递给上游服务器,并将请求体中的参数存储在$request_body变量中。你可以在Nginx的访问日志中查看这些参数。默认情况下,Nginx的访问日志位于/var/log/nginx/access.log。你可以使用以下命令查看日志文件: ``` sudo tail -f /var/log/nginx/access.log ``` 注意,这只会实时显示日志文件的最新内容。你可以通过Ctrl+C停止查看。 通过上述步骤,你可以使用Nginx查看POST请求的参数。请确保在测试环境中进行配置和调试,并确保对服务器的访问进行适当的身份验证和安全措施。 ### 回答3: 在Nginx中查看POST请求参数,可以通过以下步骤: 1. 首先,需要在Nginx的配置文件中开启请求参数的记录。找到nginx.conf文件,一般位于/etc/nginx/或/usr/local/nginx/的conf目录下。在http {}段中添加以下代码: ``` http { log_format postdata '$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$request_body"'; access_log /var/log/nginx/access.log postdata; ... } ``` 2. 配置完成后,重启Nginx服务以使配置生效。 3. 当有POST请求发生时,Nginx会将请求参数记录在上述配置文件中指定的日志文件中。 4. 查询POST请求参数,可以打开配置文件中指定的日志文件,通常位于/var/log/nginx/access.log。该文件记录了所有请求的详细信息,包括请求方式(POST)、请求URL、请求状态码、请求体大小、来源页、用户代理以及请求参数等信息。 在日志文件中,POST请求参数通常以$request_body的形式记录在一行中,通过分析该行,即可查看POST请求所提交的参数。 需要注意的是,这种方式适用于需要在Nginx层面查看POST请求参数的场景,如果想在应用程序中获取POST请求参数,应该通过编程的方式来处理
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值