nginx处理post请求(负载均衡启动)

   对于nginx源码分析系列的文章,到目前为止,已经接近尾声了。剩余反向代理模块,负载均衡模块还没分析完。从本篇开始的6篇文章,将详细分析nginx是如何处理http的post请求,通过post请求的分析,从而贯穿反向代理模块,负载均衡模块,以及fastcgi模块。

一、负载均衡的创建        

        NGX_HTTP_CONTENT_PHASE阶段的checker方法为:ngx_http_core_content_phase,这个阶段是很多http模块都喜欢介入处理http请求的阶段。所谓介入,就是http框架提供一些接口,而http模块则具体实现这些接口。因此http框架提供的接口相当于C++中的抽象基类,而具体的http模块则继承这个抽象基类,并实现抽象基类提供的接口。

//NGX_HTTP_CONTENT_PHASE阶段的checker方法
ngx_int_t ngx_http_core_content_phase(ngx_http_request_t *r, ngx_http_phase_handler_t *ph)
{
	//fastcgi模块的content_headler为:ngx_http_fastcgi_handler
    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;
    }
}

        在处理post请求时,如果请求的内容是php等动态资源,通常需要把请求转发到后端服务器。由后端服务器对这个动态资源进行处理,并返回给nginx,nginx然后把后端服务器的响应透传给客户端浏览器。通常nginx与后端服务器之间的数据传输会采用fastcgi协议,遵从fastcgi协议格式,nginx把来自客户端的请求头部,请求包体,按照fastcgi协议格式组装好报文,然后发给后端服务器。 后端服务器发送给nginx的响应头部,响应包体,也会组装成fastcgi协议格式的报文,之后发给nginx,nginx解析这个fastcgi格式的报文,并转为nginx与客户端通信的报文,最后把响应头部,响应包体发给客户端浏览器。


        而fastcgi这个http模块介入到NGX_HTTP_CONTENT_PHASE阶段的回调为ngx_http_fastcgi_handler,来看下这个函数做了些什么。

//fastcgi模块介入NGX_HTTP_CONTENT_PHASE阶段的回调函数
//功能: 1、创建负载均衡对象,并实现负载均衡提供的5个接口
//      2、创建pipe结构,用于nginx与客户端浏览器,nginx与后端服务器通信过程中报文从存储
//		3、接收来自客户端的响应包体,报文全部接收完成后,开启启动负载均衡模块
static ngx_int_t ngx_http_fastcgi_handler(ngx_http_request_t *r)
{
	//创建负载均衡对象
    ngx_http_upstream_create(r);

	//创建fastcgi模块的上下文结构
    f = ngx_pcalloc(r->pool, sizeof(ngx_http_fastcgi_ctx_t));
    ngx_http_set_ctx(r, f, ngx_http_fastcgi_module);

	//解析变量后,获取到后端服务器的ip,端口信息,保存到flcf相应成员中
    flcf = ngx_http_get_module_loc_conf(r, ngx_http_fastcgi_module);
    if (flcf->fastcgi_lengths)
	{
        ngx_http_fastcgi_eval(r, flcf);
    }
	
	//赋值负载均衡模块需要实现的5个回调
    u = r->upstream;
    u->create_request = ngx_http_fastcgi_create_request;
    u->reinit_request = ngx_http_fastcgi_reinit_request;
    u->process_header = ngx_http_fastcgi_process_header;
    u->abort_request = ngx_http_fastcgi_abort_request;
    u->finalize_request = ngx_http_fastcgi_finalize_request;

	//创建pipe结构,用于nginx与客户端浏览器,nginx与后端服务器通信过程中报文从存储
    u->pipe = ngx_pcalloc(r->pool, sizeof(ngx_event_pipe_t));

	//解析后端服务器的响应包体方法,通常后端服务器返回给nginx的响应包体需要解析后,
	//nginx才能把解析后的响应包体发给客户端浏览器
    u->pipe->input_filter = ngx_http_fastcgi_input_filter;
    u->pipe->input_ctx = r;

	//接收来自客户端剩余的所有包体,接收完成后调用ngx_http_upstream_init
	//开始与后端服务器建立连接并将包体发送给后端服务器。此时本身的nginx相当于一个客户端
    rc = ngx_http_read_client_request_body(r, ngx_http_upstream_init);
}
        可以看出这个函数主要在做启动负载均衡模块之前的初始化操作,也就是创建负载均衡对象,同时实现负载均衡模块提供的5个接口。之后接收来自客户端浏览器的请求包体,包体全部接收完成后,接下来开始启动负载均衡模块。这其实就是一个继承操作,fastcgi模块继承http框架,并实现框架的5个接口。如果还不是很明白,那可以看下反向代理模块介入到NGX_HTTP_CONTENT_PHASE阶段的回调为ngx_http_proxy_handler,也是创建负载均衡对象,然后实现框架需要的5个接口。也就是说fastcgi模块,proxy反向代理模块都继承自http框架,并实现框架需要的接口。从这里也可以看出c语言是如何实现C++多态功能的,我们也要学会这种多态的设计思想。
static ngx_int_t ngx_http_proxy_handler(ngx_http_request_t *r)
{
	//创建一个ngx_http_upstream_t对象
    ngx_http_upstream_create(r);   
	
	//赋值负载均衡模块需要实现的5个回调
    u = r->upstream;
    u->create_request = ngx_http_proxy_create_request;
    u->reinit_request = ngx_http_proxy_reinit_request;
    u->process_header = ngx_http_proxy_process_status_line;
    u->abort_request = ngx_http_proxy_abort_request;
    u->finalize_request = ngx_http_proxy_finalize_request;
    r->state = 0;
}
        至于这5个接口做了些什么,后面会详细分析, 逃都逃不掉。 这里只需要知道整体框架就可以了,没必要陷入到细节中,只看见树木,不见森林。先理清框架流程,然后在详细分析每一个流程,这也是阅读开源项目的一种方法。
二、启动负载均衡模块

        在接收完来自客户端浏览器的全部http请求包体后,会开始启动负载均衡模块。所谓的启动负载均衡,就是从后端服务器集群中选择一个服务器,然后创建socket,并与这个后端服务器建立tcp连接, 同时把来自客户端浏览器的http请求头部,http请求包体转为fastcgi协议格式,最后把fastcgi报文发给后端服务器。虽然启动负载均衡模块做了这些操作,但我更认为启动负载均衡模块,实际上是维护负载均衡模块的数据结构,搭建好整个框架。

        启动负载均衡的入口为ngx_http_upstream_init,这个函数只是删除nginx与客户端浏览器读事件的超时回调,为什么要删除这个读事件的超时回调?  因为nginx与上游服务器通信时,说明nginx已经接收完客户端所有的http请求包头与包体,不应该对客户端的读操作做什么实际的操作。这个函数本身没有做什么功能,最终是调用ngx_http_upstream_init_request来启动负载均衡模块。下面分别开下启动负载均衡模块都做了些什么;

        2.1、将来自客户端浏览器的http请求头部,http请求包体转为fastcgi协议格式


        ngx_http_fastcgi_create_request函数负责将来自客户端的请求头部,以及请求包体转为fastcgi格式的报文。从图中可以看出,对于客户端浏览器发来的http请求头部,会被加上一个fastcgi头部,组成一个fastcgi格式的http请求头部报文;  同样对于客户端浏览器发来的http请求包体,也会被加上一个fastcgi头部,构成一个fastcgi格式的http请求包体报文。现在来看下fastcgi模块是如何将来自客户端浏览器的http请求头部,http请求包体组成成fastcgi格式的报文。

//负载均衡模块初始化,与上游服务器建立一个tcp连接。
//同时将客户端发来的请求头部,包体转为fastcgi格式的内容
static void ngx_http_upstream_init_request(ngx_http_request_t *r)
{
	//指向客户端发来的请求包体
    if (r->request_body) 
	{
        u->request_bufs = r->request_body->bufs;
    }

	//构造发往上游服务器的请求内容,fastcgi为: ngx_http_fastcgi_create_request
    u->create_request(r);
}
        本来r->request_body存放的是来自客户端的http请求包体,执行完u->create_request(r)后,u->request_bufs存放的不再是http客户端的http请求包体, 而是由http请求头部,http请求包体组成的fastcgi格式的报文。这个fastcgi格式的报文就是最终要发送给后端服务器。对于fastcgi模块而言,create_request回调为:ngx_http_fastcgi_create_request。这个函数执行后,会创建一个fastcgi报文链表。


        看这张图可能有点懵逼,还是来看个例子吧! 假设客户端需要把mydata.txt这个文件的数据提交到后端服务器, 数据内容为01234567890123456789abcdefghigklmnopqrst一共40个字节。则这个fastcgi报文链表内容如下:


        从图中可以看出,http请求的方式为post, request-method这个变量名占14个字节,post占4个字节。同样的content-type占12个字节,text占4个字节。 对于来自客户端的请求包体,本例中共40个字节的包体, 假设一个fastcgi链表节点只能存放20个字节大小,因此需要两个fastcgi链表节点存放这些请求包体。

        现在来看下ngx_http_fastcgi_create_request函数的实现过程,这个函数就是为了将来自客户端的请求头部,请求包体转化为fastcgi格式的报文,并创建上面的这个fastcgi报文链表。

        2.1.1 统计fastcgi_param参数指定的请求头部长度以及来自客户端发来的请求头部长度,以便开辟合适的fastcgi报文空间

static ngx_int_t ngx_http_fastcgi_create_request(ngx_http_request_t *r)
{
	//1、使用脚本引擎解析后,会将各个变量保存下来。这里统计这些变量名与变量值的长度
    if (flcf->params_len) 
	{
		while (*(uintptr_t *) le.ip)
		{
			//获取变量名的长度
            lcode = *(ngx_http_script_len_code_pt *) le.ip;
            key_len = lcode(&le);

			//获取变量值的长度
            for (val_len = 0; *(uintptr_t *) le.ip; val_len += lcode(&le)) 
			{
                lcode = *(ngx_http_script_len_code_pt *) le.ip;
            }
            le.ip += sizeof(uintptr_t);

			//累加变量名与变量值的长度
            len += 1 + key_len + ((val_len > 127) ? 4 : 1) + val_len;
        }
    }
	
	//2、如果需要把来自客户端的请求头部也发给后端服务器,则这里统计来自客户端的所有http请求头部的长度
	if (flcf->upstream.pass_request_headers) 
	{
		for (i = 0; /* void */; i++) 
		{
			//遍历每一个http请求头部,对每一个http请求头部,都在fastcgi_param指令指定的,
			//以HTTP_开头的所有请求头部构成的哈希表中进行查找,查找到说明重复了,排重处理
			if (ngx_hash_find(&flcf->headers_hash, hash, lowcase_key, n)) 
			{
				ignored[header_params++] = &header[i];
				continue;
			}
		}
	}
}

        假设使用fastcgi_param指定了某些http请求头部, 例如下面在location中指定了三个http请求头部,则在使用脚本引擎解析fastcgi_param时,会保存已经解析完成的这些请求头部的变量名, 变量值。  这些请求头部是需要发给后端服务器的,因此需要统计这些长度,以便开辟足够大的缓冲区,存放转换后的fastcgi格式报文。

location / 
{
    fastcgi_param REQUEST_METHOD  $request_method;
    fastcgi_param CONTENT_TYPE    $content_type;
    fastcgi_param CONTENT_LENGTH  $content_length;
}
        如果来自客户端发来的http请求头部也需要发给后端服务器,则也需要统计这些请求头部的长度,以便开辟足够的空间,存放转换后的fastcgi格式报文。但有一个问题, 如果fastcgi_param指定了某个请求头部,例如content_lenth,  同时来自客户端发来的请求头部链表中也包含了该请求头部content_lenth。因为在fastcgi_param指令中已经计算过了这个请求头部的长度,因此在这里需要忽略这个请求头部,不能重复计算。怎么排除重复的请求头部呢? 在解析fastcgi_param指令时,会把以HTTP_开头的请求头部加入到flcf->headers_hash哈希表中,这样遍历来自客户端请求头部链表中的每一个请求头部,在这个哈希表中进行查找,查找到了,说明是重复的请求头部,这个请求头部就不应该再重复计算长度了。

       2.1.2 计算得到fastcgi报文缓冲区大小后, 接下来就是开辟这个缓冲区了,存放需要转发给后端服务器的fastcgi格式报文。

static ngx_int_t ngx_http_fastcgi_create_request(ngx_http_request_t *r)
{
	//计算fastcgi格式的报文的缓冲区总大小
    size = sizeof(ngx_http_fastcgi_header_t)
           + sizeof(ngx_http_fastcgi_begin_request_t)
           + sizeof(ngx_http_fastcgi_header_t)  /* NGX_HTTP_FASTCGI_PARAMS */
           + len + padding
           + sizeof(ngx_http_fastcgi_header_t)  /* NGX_HTTP_FASTCGI_PARAMS */
           + sizeof(ngx_http_fastcgi_header_t); /* NGX_HTTP_FASTCGI_STDIN */
			
	//开辟空间
    b = ngx_create_temp_buf(r->pool, size);
    cl = ngx_alloc_chain_link(r->pool);
}
        2.1.3 开辟完缓冲区后,接下来要把fastcgi_param指令指定的http请求头部,以及来自客户端浏览器发来的所有http请求头部放入到这个缓冲区链表中。

        2.1.4 当然如果来自客户端的http请求包体也需要发给后端服务器,则也需要为包体开辟缓冲区,并插入到fastcgi报文链表末尾。如果请求包体长度太长的话,则会开辟多个这样的fastcgi报文链表节点。

static ngx_int_t ngx_http_fastcgi_create_request(ngx_http_request_t *r)
{
	//是否将客户端原始的请求包体数据转发到后端服务器
    if (flcf->upstream.pass_request_body)
	{
		body = r->upstream->request_bufs;
        r->upstream->request_bufs = cl;
		//遍历每一个请求包体链表节点,插入到fastcgi报文链表末尾(尾插法)
        while (body) 
		{
		
		}
	}
}
        总体上ngx_http_fastcgi_create_request函数就实现了这些功能,当然细枝末节的东西还是得读者去分析源码了。一句话, 这个函数就是为了将fastcgi_param指令指定的头部以及来自客户端发来的http请求头部,请求包体,转为fastcgi格式,并插入到fastcgi链表中。函数执行后,ngx_http_upstream_s结构中的request_bufs成员就是这个fastcgi报文链表头指针,存放了要发给后端服务的报文。

        2.2、后端服务器的选择

        通常后端服务器是由多台设备组成的一个集群, 在与后端服务器建立tcp连接之前, 需要从后端服务器集群中选择出一个服务器。nginx提供了两种策略,用于从后端服务器集群中选择一个服务器, 第一种策略为加权轮询, 另一种策略为ip哈希。 当然了第三方模块还实现了其它的方式,这里就不在陈述了。 后端服务器的选择也是比较复杂的一块内容,打算用一篇文章来详细分析加权轮询与ip哈希两种策略。 为了不影响对主流程的分析, 这里就先跳过这部份内容。 读者只需要知道, 后端服务器的选择就是为了得到一台服务器的ip地址与端口就可以了, 有了ip与端口就可以和它建立TCP连接。这也是分析源码的一种方式, 对于不是很清楚的模块,把它当做一个黑盒子, 内部实现先不管, 只需要知道它提供了什么功能就可以了。 在ngx_event_connect_peer函数内会选择出一个后端服务。

ngx_http_upstream_init_request

        --->ngx_http_upstream_connect

               ---->ngx_event_connect_peer

//获取一个后端服务器的连接地址
ngx_int_t ngx_event_connect_peer(ngx_peer_connection_t *pc)
{
	//获取一个后端服务器的地址,加权轮询策略的回调为:ngx_http_upstream_get_round_robin_peer
	//得到的服务器地址信息保存在了pc->sockaddr
    rc = pc->get(pc, pc->data);
}

        2.3、与后端服务器建立tcp连接


        在选择出一个后端服务器后,得到了后端服务器的ip与端口,接下来就可以与后端服务器建立tcp连接了。当然获取到的后端服务器的地址有可能是一个域名,需要进行域名解析,这里就不详细分析域名解析逻辑。建立tcp连接后,此时nginx相对于后端服务器来讲,其实就相当于一个客户端,将会构造一个上图这样的客户端数据结构。从图中可以看出,负载均衡结构相当于nginx与后端服务器之间的请求,而这个请求是基于某个tcp连接的, 对这个tcp连接来说, 又对应两个事件,一个读事件,另一个写事件。

        建立tcp连接比较简单,大概做了以下操作; (1)创建一个与后端服务器通信的socket;  (2)获取tcp连接对象(该对象关联了一个读事件,一个写事件), 这个tcp连接对象是nginx与后端服务器之间的连接对象,而不是nginx与客户端的连接对象;  (3)注册与后端服务器通信的读写事件回调, 将读写事件添加到epoll中,等待事件被触发; (4)与后端服务器进行连接。

        ngx_event_connect_peer函数用于与后端服务器建立tcp连接, 来看下这个函数的实现过程。

//获取一个后端服务器的连接地址后,与它建立tcp连接
ngx_int_t ngx_event_connect_peer(ngx_peer_connection_t *pc)
{
	//创建与后端服务通信的socket
    s = ngx_socket(pc->sockaddr->sa_family, SOCK_STREAM, 0);

	//获取一个空闲连接,用于与后端服务器的连接
    c = ngx_get_connection(s, pc->log);
	
	//从网卡中读写数据的方法(内核空间读数据到应用层空间,或者应用层空间写数据到内核)
	c->recv = ngx_recv;
    c->send = ngx_send;
    c->recv_chain = ngx_recv_chain;
    c->send_chain = ngx_send_chain;
	
	//将连接的读写事件添加到epoll
    ngx_add_conn;

	//连接后端服务器
    rc = connect(s, pc->sockaddr, pc->socklen);
}
        而读写事件的注册则是在ngx_http_upstream_connect函数中完成的。 函数中会把与后端服务器通信的读、写事件的回调都设置为: ngx_http_upstream_handler,把负载均衡模块的写事件回调设置为:  ngx_http_upstream_send_request_handler, 用于把fastcgi格式的报文发给后端服务器; 负载均衡模块的读事件回调设置为:  ngx_http_upstream_process_header,用于读取来自后端服务器的http响应头部。
//与后端服务器建立连接,并注册读写事件的回调
static void ngx_http_upstream_connect(ngx_http_request_t *r, ngx_http_upstream_t *u)
{
	//获取一个后端服务器的连接地址,并与后端服务器进行连接
    rc = ngx_event_connect_peer(&u->peer);
	
	//这个请求为客户端与nginx的请求
	c->data = r;			

	//设置读写事件的回调
    c->write->handler = ngx_http_upstream_handler;
    c->read->handler = ngx_http_upstream_handler;

	//设置upstream机制的读写事件回调
    u->write_event_handler = ngx_http_upstream_send_request_handler;
    u->read_event_handler = ngx_http_upstream_process_header;
}
        那事件模块的读写回调与负载均衡模块的读写回调有什么关系呢? 无论事件模块捕获到的是读事件,还是写事件, 最终都会调用负载模块的读事件,或者写事件回调。如果与后端服务器通信的读写事件同时发生, 则写事件的优先级更高, 优先写入数据,发送给后端服务器。
static void ngx_http_upstream_handler(ngx_event_t *ev)
{
	//c表示nginx与上游服务器的连接
    c = ev->data;
	//u表示nginx与上游服务器的upstream
    u = r->upstream;

	//写事件优先级更高
    if (ev->write) 
	{
        u->write_event_handler(r, u);
    }
	else 
	{
        u->read_event_handler(r, u);
    }
}
        到此为止,负载均衡的启动过程已经完成了,接下来就是把转换后的fastcgi格式的报文发送给后端服务器。ngx_http_upstream_send_request函数负责将fastcgi格式的报文发给后端服务器。 下一篇文章将分析nginx如何把fastcgi报文发送给后端服务器。

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Nginx处理请求的过程可以简单概括为以下几个步骤: 1. 接收请求:当Nginx服务器启动并监听指定的端口时,它会等待客户端的连接请求。一旦有客户端发起连接请求Nginx就会接受该连接并建立起客户端与服务器之间的通信通道。 2. 解析请求头:一旦建立连接,Nginx会读取客户端发送的请求头信息。请求头包含了客户端的请求方式(GET、POST等)、目标URL、HTTP版本和其他相关信息。 3. 处理请求Nginx根据配置文件中的规则(如反向代理配置、负载均衡配置等)来决定如何处理请求。它可能将请求转发给后端服务器、进行缓存、重定向或者处理其他特定的操作。 4. 处理响应:一旦Nginx确定了如何处理请求,它会向后端服务器转发请求,并等待后端服务器返回响应。一旦收到响应,Nginx会对响应进行处理,如修改响应头、重定向或者进行其他的操作。 5. 发送响应:经过处理后的响应将由Nginx发送回客户端。Nginx会将响应内容分成较小的数据包,并使用非阻塞IO方式进行发送,以提高性能和并发能力。 6. 关闭连接:一旦响应发送完毕,Nginx会关闭与客户端的连接,释放相关的资源,并等待下一个连接请求的到来。 需要注意的是,Nginx是一个高性能的服务器软件,它使用事件驱动和异步非阻塞的架构来处理请求。这意味着Nginx可以同时处理多个并发连接,并能够高效地处理大量的请求和响应。通过适当的配置和调优,Nginx可以提供高可靠性、高并发性和高性能的服务。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值