在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, 把接收到的包体交由上游服务器处理。
1
2
3
4
5
6
7
8
9ngx_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; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14ngx_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); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43ngx_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; } }
1
2
3
4
5
6ngx_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; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18//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个场景,把包体存放到预先开辟的节点中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35static 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; } }
1
2
3
4
5
6
7
8static 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。这样再次被调度时,由这个函数负责接收剩余到包体数据。来看下这个函数的实现过程:
1
2
3
4
5
6
7
8//接收包体回调, 读事件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框架是如何丢弃包体操作的。
最后
以上就是孝顺板凳最近收集整理的关于nginx接收包体处理的全部内容,更多相关nginx接收包体处理内容请搜索靠谱客的其他文章。
发表评论 取消回复