nginx接收包体处理

来源:互联网 发布:石墨文档 mac 编辑:程序博客网 时间:2024/05/16 15:53

        在http中,一个请求通常由必选的请求行、请求头部、以及可选的包体组成。因此在接收完http头部后,使用状态机调度各个http模块协同处理请求,然后由各个http模块决定如何处理包体。 http框架提供了2种处理包体的方法供http模块调用(接收包体与丢弃包体), 这两种处理包体的方法对http各个模块来说是透明的,也就是说http模块不需要关心框架是如何处理包体的。  http模块只需要调用http框架提供的方法处理包体就可以了。是接收包体呢?还是丢弃包体? 这些行为都是由http模块自己的需要来决定的。

        http框架接收包体的入口为: ngx_http_read_client_request_body,  丢弃包体的入口为:ngx_http_discard_request_body。下面先分析http框架是如何处理请求包体的,丢弃包体则在下一篇文章分析。

一、首次接收包体初始化操作

        ngx_http_read_client_request_body是首次接收http包体函数,如果一次没全部接收完包体,则调用ngx_http_read_client_request_body_handler继续接收。在没有预先读取到包体的话,这个函数相当于只做了开辟接收缓冲区操作、以及在一次读包体没有完成时,设置读事件回调,以便下次继续读取包体,相当于一个初始化流程。而实际读取数据则将在后面分析。这个函数归纳起来,做了以下几件事情。

        1、函数将开辟接收包体缓冲区,保存到http请求结构中的request_body。接收到来自客户端的包体都会存放到这个空间。

        2、判断是否预先读取了部分包体, 如果在接收http请求头部时,也把包体数据也读取出来了,则处理预先读取的包体。

        3、设置请求对应的读事件的回调, 使得一次性没有读完全部包体时,再一次调度时能够继续读取剩余包体数据。

        看下开辟接收包体缓冲区的过程, 参数post_handler表示接收完http包体后,由http模块调用的回调。例如反向代理模块在接收完包体后,会把post_handler设置为:ngx_http_upstream_init, 把接收到的包体交由上游服务器处理。

ngx_int_t ngx_http_read_client_request_body(ngx_http_request_t *r, ngx_http_client_body_handler_pt post_handler){//分配http接收包体缓冲区    rb = ngx_pcalloc(r->pool, sizeof(ngx_http_request_body_t));//保存到http请求结构中    r->request_body = rb;}
        如果包体的长度为0,表示没有包体。但是如果在nginx.conf配置文件中指定了包体只能存放到文件中,则nginx服务器也会创建一个空文件,即便没有来自客户端的包体数据。这个空文件在请求结束后会被删除。为什么没有包体数据还需要创建一个空文件,这块逻辑还不是很明白。由于没有数据要处理,则最后直接回调http模块提供的post_handler函数,交由http模块自己处理。

ngx_int_t ngx_http_read_client_request_body(ngx_http_request_t *r, ngx_http_client_body_handler_pt post_handler){    if (r->headers_in.content_length_n == 0) {//包体只存放到文件中        if (r->request_body_in_file_only) {ngx_create_temp_file(&tf->file, tf->path, tf->pool, tf->persistent, tf->clean, tf->access)}post_handler(r);}}
        如果在接收http请求头部时,也把包体数据也读取出来了,则处理预先读取的包体。大致逻辑是开辟一个缓冲区,读取包体数据。如果预先读取的数据已经是一个完整的包体了,说明包体已经全部读取完成了, 则直接回调http模块提供的post_handler函数,交由http模块自己处理。如果预先读取的数据还不是一个完整的包体,则说明还需要再次调度继续接收包体,这时应该把读事件注册到epoll中。当然,在一个缓冲区无法存放所有包体时,还会再创建一个缓冲区,构成一个只有两个节点的缓冲区链表。
ngx_int_t ngx_http_read_client_request_body(ngx_http_request_t *r, ngx_http_client_body_handler_pt post_handler){//已经预先读取过一部分http包体    if (preread) {//创建ngx_chain_t对象        rb->bufs = ngx_alloc_chain_link(r->pool);        rb->bufs->buf = b;        rb->bufs->next = NULL;        rb->buf = b;//如果预先读取的包体已经是一个完整的包体,则调用读取后的回调        if ((off_t) preread >= r->headers_in.content_length_n) {//包体只存放到临时文件中            if (r->request_body_in_file_only) {//将包体写入临时文件                ngx_http_write_request_body(r, rb->bufs));            }            post_handler(r);            return NGX_OK;        }        //以下是接收到的http包体不能够构成一个完整的包体        r->header_in->pos = r->header_in->last;        r->request_length += preread;        rb->rest = r->headers_in.content_length_n - preread;//缓冲区能够存放剩余包体内容        if (rb->rest <= (off_t) (b->end - b->last)){            rb->to_write = rb->bufs;//重新注册读事件回调            r->read_event_handler = ngx_http_read_client_request_body_handler;            return ngx_http_do_read_client_request_body(r);        }//缓冲区不能存放剩余包体内容,则需要开辟一个新的缓冲区,并加入到链表        next = &rb->bufs->next;    } }
        接下来会注册http请求的读事件read_event_handler回调为: ngx_http_read_client_request_body_handler
ngx_int_t ngx_http_read_client_request_body(ngx_http_request_t *r, ngx_http_client_body_handler_pt post_handler){//重新注册读事件回调    r->read_event_handler = ngx_http_read_client_request_body_handler;}
        在前面的逻辑已经分析了,ngx_connection_s读事件的回调设置为ngx_http_request_handler。  因此在读事件发生时,会回调请求结构的读回调。

//http请求处理读与写事件的回调,在ngx_http_process_request函数中将读写事件的回调//设置为ngx_http_request_handlerstatic void ngx_http_request_handler(ngx_event_t *ev){//如果同时发生读写事件,则只有写事件才会触发。写事件优先级更高    if (ev->write) {        r->write_event_handler(r);//在函数ngx_http_handler设置为ngx_http_core_run_phases    }else{        r->read_event_handler(r);//在函数ngx_http_process_request设置为ngx_http_block_reading    }//处理子请求    ngx_http_run_posted_requests(c);}

二、接收http请求包体

        初始化过程只是开辟了接收包体缓冲区。这个缓冲区是使用预先分配的方法创建的,由一个或者两个节点组成(最多只会有两个节点)。实际接收包体则由ngx_http_do_read_client_request_body函数完成。在分析函数代码之前,先考虑以下几个场景。

        1、nginx.conf配置中指定了http请求的包体数据只能存储在内存中, 并且一个数据节点能够存放所有的包体数据,则内存布局如下:


        2、在只有一个数据节点的情况下, 如果一个数据节点不能够存放所有的包体数据,则会把这个节点的所有数据都写入到临时文件中。写入到文件后,数据节点就可以用来重新接收来自客户端的包体。缓冲区满了后又会写入到文件中。依次执行这样的操作,直到接收完全部的包体数据。在这种情况下,即便指定了数据只能存放到内存中,但由于内存缓冲区有限,不能接收所有的包体,这时也会把所有数据写入到临时文件。内存布局如下:


        3、假设缓冲区由两个数据节点组成,这是有可能的。(例如: 在接收http请求头部时,预先读取了一部分包体数据,但发现当前数据节点不能存放剩余的包体数据,则会重新开辟一个数据节点)。在这种情况下,如果nginx.conf指定了数据只能存放到内存,并且2个节点的缓冲区能存放所有包体数据,则内存布局如下图:


         4、假设缓冲区由两个数据节点组成。在这种情况下,如果nginx.conf指定了数据只能存放到文件,则会把链表中的所有数据都写入到文件中。链表节点的缓冲区指向文件中相应的位置。内存布局如下图所示:

        5、假设缓冲区由两个数据节点组成。在这种情况下,如果nginx.conf并没有指定包体数据只能存放到文件、或者指定包体数据只能存放到内存中。这种情况下有可能一部分数据存放到内存,另一部分数据存放到文件中。如果第2个节点缓冲区满了,则只会把第2个节点的数据写入到文件中, 第1个节点的数据不变,仍然保存到内存中。这时第2个节点可以继续用来接收来自客户端的http包体数据,如果缓冲区满了,则又把第2个节点的数据写入到文件。依次循环执行这样的流程,直到读取完所有的包体。 内存布局如下图: 链表中,一个数据存放到内存,另一个数据存放到文件中。


        现在来分析下ngx_http_do_read_client_request_body函数是如何接收包体的。其实这个函数就是处理了上面的5个场景,把包体存放到预先开辟的节点中。

static ngx_int_t ngx_http_do_read_client_request_body(ngx_http_request_t *r){for ( ;; ) {//缓冲区满,则写入临时文件if (rb->buf->last == rb->buf->end) {ngx_http_write_request_body(r, rb->to_write)) ;//如果是2个节点,则除首次写文件操作外,每次只会把第2个节点的数据写入到文件中rb->to_write = rb->bufs->next ? rb->bufs->next : rb->bufs;//缓冲区文件写入临时文件后,就可以重复使用rb->buf->last = rb->buf->start;}//从内核中读取数据到缓冲区n = c->recv(c, rb->buf->last, size); //ngx_unix_recv//更新接收到的数据rb->buf->last += n;}//需要把剩余的包体数据写到文件,因为上面循环中写文件的条件是缓冲区满。//那如果缓冲区还没有满时,则就会走到这个逻辑,把剩余的数据写入到文件中。    if (rb->temp_file || r->request_body_in_file_only){        ngx_http_write_request_body(r, rb->to_write));        b = ngx_calloc_buf(r->pool);        b->in_file = 1;        b->file_pos = 0;        b->file_last = rb->temp_file->file.offset;        b->file = &rb->temp_file->file;    }}
        如果数据全部接收完成,则会设置读请求的回调为ngx_http_block_reading。这个函数不会做任何事情,表示包体已经读取完成,再有读事件时,将不做任何处理。以此同时将回调http模块的post_handler方法,说明包体已经全部接收完成了,后续的操作交由模块自己来处理。例如反向代理模块调用http框架接收完全部包体后,会回调反向代理这个模块的方法,将包体数据发给后端服务器。
static ngx_int_t ngx_http_do_read_client_request_body(ngx_http_request_t *r){//包体已经读取完成,重新注册读事件回调,表示再有读事件时,将不做任何处理    r->read_event_handler = ngx_http_block_reading;//调用接收包体完成后的回调,也就是说,在所有包体都接收完成后,才会调用,转发给上游服务器    rb->post_handler(r);}
三、再次调度时,接收剩余的包体

        如果一次没有接收完全部的包体数据,则会把请求的读方法设置为ngx_http_read_client_request_body_handler。这样再次被调度时,由这个函数负责接收剩余到包体数据。来看下这个函数的实现过程:

//接收包体回调, 读事件ngx_event_t的回调为ngx_http_request_handlerstatic void ngx_http_read_client_request_body_handler(ngx_http_request_t *r){//调用接收包体函数,接收剩余的包体数据    rc = ngx_http_do_read_client_request_body(r);}
        函数实现很简便,就是调度接收包体处理函数,来接收剩余的包体数据。这个函数会一直被调度执行,接收剩余的包体数据,直到接收完所有的http包体,函数的使命才完成。

        到此为止,框架如何接收http请求包体, 接收完包体后又是如何回调具体的http模块,交由模块继续处理接收完包体后的操作已经分析完成了。将包体存放到预先分配好的链表中,这个链表缓冲区有可能是内存,也有可能是文件。我觉得这块的处理逻辑还是很值得学习的,使用预分配方法,同时解决了数据即可能存放到内存,也可能存放到文件的场景。下一篇文章将分析http框架是如何丢弃包体操作的。


原创粉丝点击