概述
在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_handler
static 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_handler
static 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框架是如何丢弃包体操作的。
最后
以上就是孝顺板凳为你收集整理的nginx接收包体处理的全部内容,希望文章能够帮你解决nginx接收包体处理所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复