nginx处理post请求(http响应头部的收发)

来源:互联网 发布:sql语句查询多张表 编辑:程序博客网 时间:2024/05/24 03:08

        上一篇文章分析了nginx如何发送来自客户端的请求数据到后端服务器, 本篇文章开始将分析nginx如何接收来自后端服务器的响应。nginx接收来自后端服务器的响应分为两个过程,一个是接收来自后端服务器的http响应头部, 另一个是接收来自后端服务器的响应包体。

        有必要在最前面说明,也是很重要的几点。(1)nginx在收到来自客户端的全部请求数据后,采用边发送请求数据到后端服务器,一边接收来自后端服务器的响应, 收发同时进行,是一种全双工模式。也就是说,nginx要与后端服务器开始通信,必须接收到了来自客户端的所有请求数据,把压力都集中在nginx这端,跟后端服务器没关系;(2)nginx发送请求数据与接收来自后端服务器的响应可以同时进行,是一种全双工方式;(3)nginx接收到了后端服务器的响应,什么时候向客户端转发响应呢? 对于http响应头部,nginx只有在接收到了所有来自后端服务器的响应头部后,才会转发给客户端。对于http响应包体,则nginx边接收后端服务器的响应包体,边发给客户端。

        本篇文章先来看下nginx接收后端服务器http响应头部的过程,下一篇将详细分析接收包体的过程。

一、接收响应头部的启动

        nginx在与后端服务器建立 tcp连接时,已经把读事件的回调设置为了ngx_http_upstream_handler, 并把负载均衡的读事件回调设置为:ngx_http_upstream_process_header。

//与后端服务器建立连接,并注册读写事件的回调static void ngx_http_upstream_connect(ngx_http_request_t *r, ngx_http_upstream_t *u){//设置读写事件的回调    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;}
        因此,如果nginx发送完部分请求数据给后端服务器时,如果接受到了来自后端服务的响应,则读事件的回调ngx_http_upstream_handler会被触发,进而调用负载均衡模块的读事件回调:ngx_http_upstream_process_header, 开始接http响应头部。当然,如果一次不能接收完来自后端服务器的所有响应头部,则读事件会再次被触发,继续重复接收这个过程。

//事件模块的读写回调,事件被触发时会调用负载均衡模块对应的读写回调static void ngx_http_upstream_handler(ngx_event_t *ev){    if (ev->write) {//向后端服务器发送数据        u->write_event_handler(r, u);    }else {//接收后端服务器的响应,此时回调为ngx_http_upstream_process_header        u->read_event_handler(r, u);    }}
二、接收响应头部分析

        1、接收缓存区的开辟

        nginx要接收来自后端服务器的响应头部,肯定要有一个空间来存放这些响应头部。 这个缓冲区就是ngx_http_upstream_t结构中的buffer成员。这是一个固定的空间,即用来接收来自后端服务器的http响应头部,也接收来自后端服务器的响应包体。 当然,响应头部存放到最前面,响应包体紧跟响应头部存放。以此同时也初始化u->headers_in链表,这个链表是用于存放解析buffer中的所有响应头部的结果。因为buffer中存放的是直接来自后端服务器的http响应头部,这是一种factcgi格式的报文,解析fastcgi报文后,将得到的响应头部保存到链表中。

//nginx接收来自上游服务器的响应头部static void ngx_http_upstream_process_header(ngx_http_request_t *r, ngx_http_upstream_t *u){//如果没有开辟接收来自上游服务器响应包头缓冲区,则开辟    if (u->buffer.start == NULL){        u->buffer.start = ngx_palloc(r->pool, u->conf->buffer_size);        u->buffer.pos = u->buffer.start;        u->buffer.last = u->buffer.start;        u->buffer.end = u->buffer.start + u->conf->buffer_size;        u->buffer.temporary = 1;//初始化链表,这个链表是存放解析buffer后的响应头部        ngx_list_init(&u->headers_in.headers, r->pool, 8, sizeof(ngx_table_elt_t));    }}
        2、接收响应头部

        缓冲区空间开辟后,接下来就是循环接收来自后端服务器的响应头部。为什么要循环接收呢? 因为一次操作有可能不能够从内核缓冲区中接收到所有的响应头部到应用层缓冲区,因此需要循环接收。当然了,如果内核缓冲区中还没有接收到所有来自后端服务器的响应头部,那循环接收也没有用,此时需要把读事件重新注册到epoll红黑树中,以便下次读事件触发时,可以继续接收响应头部。

//nginx接收来自上游服务器的响应头部static void ngx_http_upstream_process_header(ngx_http_request_t *r, ngx_http_upstream_t *u){//循环接收来自后端服务器的响应头部,并使用状态机解析    for ( ;; ){        n = c->recv(c, u->buffer.last, u->buffer.end - u->buffer.last);//表示还需要再次接收来自上游服务器的响应,重新注册读事件到epoll中        if (n == NGX_AGAIN) {            ngx_handle_read_event(c->read, 0);            return;        }//更新接收缓冲区        u->buffer.last += n;//调用http模块的方法,解析响应头部//fastcgi为: ngx_http_fastcgi_process_header        rc = u->process_header(r);//表示包头还没有完全接收到        if (rc == NGX_AGAIN) {            continue;        }    }}
        3、状态机解析响应头部
        对于接收到的来自后端服务器的响应头部,保存到了u->buffer中。这是一个fastcgi格式的响应报文,需要从fastcgi格式的报文中提取出http响应头部,因此需要经过状态机处理,从而解析出http响应头部。还记得nginx发送请求数据给后端服务器的过程吗? 请求数据是一种fastcgi的报文格式,同样的nginx接收后端服务的响应也应该是一种fastcgi报文格式。

        看下nginx接收到的fastcgi响应报文格式;


        从图中可以看出,每一个http响应包头前面都加上了fastcgi头部,而每一个http响应包头都有可能由多条http响应头部组成。我们暂且称每一个fastcgi头部 + http响应包头为一个组,那怎么解析这些fastcgi格式的响应报文呢? nginx使用扫描法,扫描每一组。每一个组使用两个状态机,一个状态机解析这个组内的fastcgi头部, 另一个状态机解析这个组内的http响应包头,从而获取到每一个http响应头部。

//解析来自后端服务器发来的响应头部,从fastcgi格式转为nginx格式。//采用两个状态机,一个解析fastcgi头部,一个解析fastcgi响应包头static ngx_int_t ngx_http_fastcgi_process_header(ngx_http_request_t *r){//循环解析每一个组(fastcgi头部 + 多个http响应头部组成的数据)。因为每次从内核接收到的数据有可能包含多个组for ( ;; ) {//解析fastcgi的头部        if (f->state < ngx_http_fastcgi_st_data){            f->pos = u->buffer.pos;            f->last = u->buffer.last;//使用状态解析fastcgi的头部            rc = ngx_http_fastcgi_process_record(r, f);            u->buffer.pos = f->pos;            u->buffer.last = f->last;        }//这个循环用于解析这个组内的每一个http响应头部。因此该组内的http响应包头可能由多个http响应头部组成。//而ngx_http_parse_header_line状态机每次只能解析一个http响应头部,因此需要循环解析        for ( ;; ){//使用状态机解析来自后端服务器发来的http响应头部            rc = ngx_http_parse_header_line(r, &u->buffer, 1);            if (rc == NGX_OK) {//解析某个http响应头部成功后,保存到数组链表中                /* a header line has been parsed successfully */                h = ngx_list_push(&u->headers_in.headers);//保存http响应头部ngx_memcpy(h->key.data, r->header_name_start, h->key.len);//保存http响应头部的值ngx_memcpy(h->value.data, r->header_start, h->value.len);                h->hash = r->header_hash;//如果是负责均衡模块支持的头部,则把常用头部的指针指向数组链表中相应位置                hh = ngx_hash_find(&umcf->headers_in_hash, h->hash,                                   h->lowcase_key, h->key.len);                hh->handler(r, h, hh->offset);            }//解析完成所有的响应头部,则保存响应状态码            if (rc == NGX_HTTP_PARSE_HEADER_DONE) {                if (u->headers_in.status) {//后端服务器返回了状态码,则优先使用后端服务的响应状态码                } else if (u->headers_in.location) {//后端服务器返回了location,则需要给客户端返回302重定向                }else {//否则nginx构造200 0k返回给客户端                    u->headers_in.status_n = 200;                    ngx_str_set(&u->headers_in.status_line, "200 OK");                }            }        }    }}
        ngx_http_fastcgi_process_header这个解析函数, 即便抛开一些细枝末节,还得到了这么长的代码。上面的代码整体上就是解析的框架流程,注释也很清楚了,结合上面的这张图,仔细分析应该都能看得懂。需要注意的是,当解析成功某个http响应头部时,会临时把这个响应头部保存到u->headers_in链表中。这个链表只是一个过渡,最终还是需要拷贝到ngx_http_request_s这个http请求中的headers_out成员中,因此最终是把headers_out链表中的数据发送给客户端浏览器。为什么要多此一举呢? 直接把解析后的http响应头部保存到ngx_http_request_s这个http请求中的headers_out成员链表中不就可以了,省得在做一次拷贝操作。我想还是分层思想的作用吧! ngx_http_request_s这个http请求中的headers_out成员链表是nginx与客户端浏览器直接维护的数据结构, 而u->headers_in链表则是nginx与后端服务器之间维护的数据结构 ,两者是不同对象之间的交互,宁愿多花一点内存资源,分开对待才不会耦合在一起。

        如果nginx接收到了所有的http响应头部,则nginx构造响应码,用于给客户端发送响应状态,例如200 ok等。后端服务器是不会返回http 200 ok这样的响应行的,需要nginx自己构造给客户端的响应码。

        另外需要注意的时,使用状态机进行解析时,如果数据还没接受完整,则一次解析是解析不完的,需要反复多次进入状态机进行解析。 此时fastcgi的上下文结构ngx_http_fastcgi_ctx_t会记录当前解析到缓冲区中的哪个位置,以及解析到了哪种状态。下次解析时,可以从该缓冲区对应位置开始解析上一次的状态。

//使用状态机解析收到后端服务的fastcgi格式的响应头部。解析后的内容存放到f的相应字段//static ngx_int_t ngx_http_fastcgi_process_record(ngx_http_request_t *r,    ngx_http_fastcgi_ctx_t *f){//遍历缓冲区中的每一个字节,使用状态机进行解析。    for (p = f->pos; p < f->last; p++) {switch (state) {        case ngx_http_fastcgi_st_version://解析版本breakcase ngx_http_fastcgi_st_type://解析类型break..........case ngx_http_fastcgi_st_data://解析包体数据break}}//缓冲区中的内容还不够组成一个fastcgi的头部大小,则记录当前解析的状态。下一次从这个状态开始继续解析    f->state = state;}
        使用状态机解析fastcgi头部很简单,解析后的内容保存到了fastcgi的上下文结构ngx_http_fastcgi_ctx_t相应成员中,例如记录响应包头的长度等信息。fastcgi协议内容就不在本篇文章范畴了,读者不是很清楚的话可以找相应的资源去了解fastcgi协议。

        另一个状态机机ngx_http_parse_header_line,用来解析每一个http响应头部。函数内容也比较长,这里也就不在贴代码了,这里提下内部实现过程。函数内部循环的遍历每一个字符,从而提取出键值对key,value,并把key,value保存到http结构ngx_http_request_s中的4个成员中;

//http请求结构struct ngx_http_request_s {    u_char   * header_name_start; //http头部名,例如:content-length:40,则为content-length    u_char   * header_name_end;  //头部名的结束位置    u_char   * header_start;  //http头部值开始位置,例如:content-length:40,则为40    u_char   * header_end;  //头部值的结束位置}
        在nginx接收到来自客户端的请求头部这一篇文章中也使用到这个状态机进行解析http请求头部, 读者也可以参考这篇文章进行分析。 虽然解析过程比较长,但很容易看懂的,粗劣看下这个代码, 细节的代码还是留给读者自己分析。
//使用状态机获取http请求头部中的每一个键值对,underscores_in_headers表示key是否支持下划线//该函数一次只解析一个http请求头部,因此需要反复调用ngx_int_t ngx_http_parse_header_line(ngx_http_request_t *r, ngx_buf_t *b, ngx_uint_t allow_underscores){//变量每一个字符进行接for (p = b->pos; p < b->last; p++) {}}
三、将解析后的http响应头部保存到http请求结构中的响应头部链表

        使用状态机解析完http响应头部后,保存到了u->headers_in链表中。这个链表只是一个过渡,最终还是需要拷贝到ngx_http_request_s这个http请求中的headers_out成员中。这个headers_out链表中的数据才是最终要发给客户端的响应头部。回到ngx_http_upstream_process_header函数进行分析, 在解析完所有的来自后端服务的响应头部后,会调用ngx_http_upstream_process_headers函数将响应头部从u->headers_in链表拷贝到请求结构的headers_out链表。

//将已经解析后的来自上游服务器的响应头部设置到headers_out成员中static ngx_int_t ngx_http_upstream_process_headers(ngx_http_request_t *r, ngx_http_upstream_t *u){//遍历来自后端服务器响应的http头部    for (i = 0; /* void */; i++) {//该http响应头部存在不需要发给客户端的响应头部哈希表中,则跳过        if (ngx_hash_find(&u->conf->hide_headers_hash, h[i].hash, h[i].lowcase_key, h[i].key.len))        {            continue;        }//该http响应头部也是负载均衡模块支持的响应头部,则使用负载均衡模块这个头部对应的拷贝回调        hh = ngx_hash_find(&umcf->headers_in_hash, h[i].hash, h[i].lowcase_key, h[i].key.len);        if (hh){            if (hh->copy_handler(r, &h[i], hh->conf) != NGX_OK) {                return NGX_DONE;            }            continue;        }//直接保存到http请求结构中的响应头部链表        ngx_http_upstream_copy_header_line(r, &h[i], 0);    }}

四、发送http响应头部

        不管是上游网速优先还是下游网速优先, 都需要把http响应头部发发给客户端浏览器,这是一个公共的操作。发送http响应包体前,必须先发送http响应头部。发送http响应头部其实没有什么特殊的地方,直接调用过滤器模块就可以把http响应头部发送给客户端浏览器了。过滤器模块在前面的文章已经分析过了,这里就不在重复叙述了。那什么时候nginx会触发http响应头部给客户端呢? 答案是在nginx在接收到了全部的来自后端服务器的响应头部后,才会触发向客户端浏览器发送http响应头部这个操作。如果nginx只接收到了部分的来自后端服务器的响应头部,是不会触发这个逻辑的。

//nginx接收来自上游服务器的响应头部static void ngx_http_upstream_process_header(ngx_http_request_t *r, ngx_http_upstream_t *u){//subrequest_in_memory值为0,表示需要转发来自上游服务器的响应到客户端    if (!r->subrequest_in_memory){        ngx_http_upstream_send_response(r, u);        return;    }}static void ngx_http_upstream_send_response(ngx_http_request_t *r, ngx_http_upstream_t *u){//调用过滤器模块发送响应头部给客户端, 此时一定是接收完全部来自后端服务器的响应头部    ngx_http_send_header(r);}
        到此为止,nginx如何接收来自后端服务器的响应头部, 以及把响应头部发给下游客户端已经分析完成了,下一篇文章将分析nginx如何接收来自后端服务器的响应包体。