nginx http框架执行流程(5) - 丢弃请求体

2018-07-28 21:40:08

http 框架在读取完请求头后,之后的 http 处理引擎只关注写事件,"阻塞"了读事件,最常见的请求方法 get、head 都没有请求体。

如果想要操作请求体,必须再次监控连接上的读事件。

丢弃请求体

我们先来研究不处理请求体的情况,也就是读取并"丢弃",nginx 为我们提供了 ngx_http_discard_request_body() 函数,它是 nginx 对外提供的丢弃请求体函数的接口,它综合了下面介绍的3个函数。

ngx_int_t
ngx_http_discard_request_body(ngx_http_request_t *r){
    ssize_t       size;
    ngx_int_t     rc;
    ngx_event_t  *rev;

    // 子请求不与客户端直接通信,不会有请求体的读取
    // 已经设置了discard_body标志,表示已经调用了此函数
    // request_body指针不空,表示已经调用了此函数
    // 这三种情况就无需再启动读取handler,故直接返回成功
    // discard_body在本函数最末尾设置,防止重入
    if (r != r->main || r->discard_body || r->request_body) {
        return NGX_OK;
    }

    // 从请求获取连接对象,再获得读事件
    rev = r->connection->read;

    // 因为要丢弃数据,所以不需要检查超时,也就是说即使超时也不算是错误
    // 不检查读事件的超时,有数据就读
    if (rev->timer_set) {
        ngx_del_timer(rev);
    }

    // 如果头里的长度未设置、或者是0且不是chunked
    // 说明没有请求体数据,那么就无需再读,直接返回成功
    if (r->headers_in.content_length_n <= 0 && !r->headers_in.chunked) {
        return NGX_OK;
    }

    // 头里声明了body的数据长度
    // 或者body是chunked,即长度不确定
    // 这两种情况都需要读取数据并丢弃

    // 检查缓冲区里在解析完头后是否还有数据
    // 也就是说之前可能读取了部分请求体数据
    size = r->header_in->last - r->header_in->pos;

    // 有数据,或者是chunked数据
    // 有可能已经读取了一些请求体数据,所以先检查一下
    if (size || r->headers_in.chunked) {
        // 检查请求结构体里的缓冲区数据,丢弃
        // 有content_length_n指定确切长度,那么只接收,不处理,移动缓冲区指针
        // chunked数据需要解析数据
        rc = ngx_http_discard_request_body_filter(r, r->header_in);

        // 不是ok表示出错,不能再读取数据
        if (rc != NGX_OK) {
            return rc;
        }

        // content_length_n==0表示数据已经全部读完
        // 就已经完成了丢弃任务,否则就要加入epoll读事件继续读
        if (r->headers_in.content_length_n == 0) {
            return NGX_OK;
        }
    }

    // 走到这里,表明content_length_n>=0,还有数据要读取
    // 接下来就读取请求体数据并丢弃
    // 使用固定的4k缓冲区接受丢弃的数据
    // 一直读数据并解析,检查content_length_n,如果无数据可读就返回NGX_AGAIN
    // 因为使用的是et模式,所以必须把数据读完
    // 需要使用回调ngx_http_discarded_request_body_handler读取数据
    rc = ngx_http_read_discarded_request_body(r);

    // ok表示一次就成功读取了全部的body,完成丢弃工作
    if (rc == NGX_OK) {
        r->lingering_close = 0;
        return NGX_OK;
    }

    // 出错
    if (rc >= NGX_HTTP_SPECIAL_RESPONSE) {
        return rc;
    }

    /* rc == NGX_AGAIN */

    // 读事件not ready,无数据可读,那么就要在epoll里加入读事件和handler
    // 注意,不再需要加入定时器
    // 之后再有数据来均由ngx_http_discarded_request_body_handler处理
    // 里面还是调用ngx_http_read_discarded_request_body读数据

    r->read_event_handler = ngx_http_discarded_request_body_handler;

    // 注意,读事件的handler实际上是ngx_http_request_handler
    // 但最终会调用r->read_event_handler
    if (ngx_handle_read_event(rev, 0) != NGX_OK) {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    // 引用计数器增加,表示此请求还有关联的操作,不能直接销毁
    r->count++;

    // 设置丢弃标志,防止再次进入本函数
    r->discard_body = 1;

    return NGX_OK;
}


丢弃缓冲区数据

ngx_http_discard_request_body_filter() 就是通过指针移动模拟丢弃操作,并没有对数据做实际的操作。

static ngx_int_t
ngx_http_discard_request_body_filter(ngx_http_request_t *r, ngx_buf_t *b)
{
    size_t                    size;
    ngx_int_t                 rc;
    ngx_http_request_body_t  *rb;

    // chunked数据长度不确定,需要特殊处理
    if (r->headers_in.chunked) {

        // 这里暂不研究 chunked 请求体

    } else {
        // 不是chunked,请求体数据有确定的长度

        // 检查缓冲区里头之后的数据,即收到的请求体数据
        size = b->last - b->pos;

        // 收到的数据大于头里的content_length_n
        // 证明请求体接收完成
        if ((off_t) size > r->headers_in.content_length_n) {

            // 缓冲区指针移动,即消费content_length_n的数据
            b->pos += (size_t) r->headers_in.content_length_n;

            // content_length_n置0,表示无数据,丢弃成功
            r->headers_in.content_length_n = 0;

        } else {

            // 收到的数据不足,即还没有收完content_length_n字节数
            // 如果正好收完,也是在这里处理

            // 指针直接移动到最后,即消费所有收到的数据
            b->pos = b->last;

            // 头里的content_length_n减少,即还将要收多少数据
            // 如果正好收完,那么值就是0,丢弃成功
            r->headers_in.content_length_n -= size;
        }
    }

    return NGX_OK;
}


读取并丢弃数据

当连接可读时,nginx 会调用 ngx_http_read_discarded_request_body() 使用固定的4k缓冲区接受丢弃的数据。

static ngx_int_t
ngx_http_read_discarded_request_body(ngx_http_request_t *r)
{
    size_t     size;
    ssize_t    n;
    ngx_int_t  rc;
    ngx_buf_t  b;

    // 使用固定的4k缓冲区接受丢弃的数据
    u_char     buffer[NGX_HTTP_DISCARD_BUFFER_SIZE];

    // 读取用的缓冲区对象
    ngx_memzero(&b, sizeof(ngx_buf_t));

    // 标记为可写
    b.temporary = 1;

    // 一直读数据并解析,检查content_length_n
    // 如果无数据可读就返回NGX_AGAIN
    // 需要使用回调ngx_http_discarded_request_body_handler读取数据
    for ( ;; ) {

        // 判断content_length_n,为0就是已经读取完请求体
        // 就不需要再读了,读事件设置为block,返回成功
        if (r->headers_in.content_length_n == 0) {
            r->read_event_handler = ngx_http_block_reading;
            return NGX_OK;
        }

        // content_length_n大于0,表示还有数据需要读取
        // 看读事件是否ready,即是否有数据可读
        // 如果没数据那么就返回again
        // 需要使用回调ngx_http_discarded_request_body_handler读取数据
        if (!r->connection->read->ready) {
            return NGX_AGAIN;
        }

        // 决定要读取的数据长度,不能超过4k
        // #define NGX_HTTP_DISCARD_BUFFER_SIZE       4096
        size = (size_t) ngx_min(r->headers_in.content_length_n,
                                NGX_HTTP_DISCARD_BUFFER_SIZE);

        // 调用底层recv读取数据
        // 每次都从buffer的0位置放置数据,也就是丢弃之前读取的全部数据
        n = r->connection->recv(r->connection, buffer, size);

        // 出错也允许,因为丢弃数据不需要关心
        // 但需要置error标记
        if (n == NGX_ERROR) {
            r->connection->error = 1;
            return NGX_OK;
        }

        // again表示无数据可读
        // 后面需要使用回调ngx_http_discarded_request_body_handler读取数据
        if (n == NGX_AGAIN) {
            return NGX_AGAIN;
        }

        // 读到了0字节,即连接被客户端关闭,client abort
        // 也是ok
        if (n == 0) {
            return NGX_OK;
        }

        // 读取了n字节的数据,但不使用
        // 交给ngx_http_discard_request_body_filter来检查
        b.pos = buffer;
        b.last = buffer + n;

        // 检查请求结构体里的缓冲区数据,丢弃
        // 有content_length_n指定确切长度,那么只接收,不处理,移动缓冲区指针
        // chunked数据需要解析数据
        // content_length_n==0表示数据已经全部读完
        // 就已经完成了丢弃任务,否则就要加入epoll读事件继续读
        rc = ngx_http_discard_request_body_filter(r, &b);

        if (rc != NGX_OK) {
            return rc;
        }

        // 如果是ok,那么在for开始的地方检查content_length_n
    }
}


读事件处理函数

当需要读取请求体并丢弃,但是检查了缓冲区没有足够的请求体数据,而在一次主动调用 recv 也没有接收完需要的请求体时,就需要重新注册读事件(参照上面的 ngx_http_discard_request_body )。然后当客户端有新的请求体数据发来时,就会调用 ngx_http_discarded_request_body_handler() 回调函数。

void
ngx_http_discarded_request_body_handler(ngx_http_request_t *r)
{
    ngx_int_t                  rc;
    ngx_msec_t                 timer;
    ngx_event_t               *rev;
    ngx_connection_t          *c;
    ngx_http_core_loc_conf_t  *clcf;

    // 获取读事件相关的连接对象和请求对象
    c = r->connection;
    rev = c->read;

    //...

    // 这时epoll通知socket上有数据可以读取
    // 读取请求体数据并丢弃
    rc = ngx_http_read_discarded_request_body(r);

    // ok表示数据已经读完
    // 传递done给ngx_http_finalize_request,并不是真正结束请求
    // 因为有引用计数器r->count,所以在ngx_http_close_request里只是减1的效果
    if (rc == NGX_OK) {
        r->discard_body = 0;
        r->lingering_close = 0;
        ngx_http_finalize_request(r, NGX_DONE);
        return;
    }

    // 出错
    if (rc >= NGX_HTTP_SPECIAL_RESPONSE) {
        c->error = 1;
        ngx_http_finalize_request(r, NGX_ERROR);
        return;
    }

    /* rc == NGX_AGAIN */

    // again则需要再次加入epoll事件,等有数据来再次进入
    // rev的handler不变,直接加入
    // 注意,读事件的handler实际上是ngx_http_request_handler
    // 但最终会调用r->read_event_handler,即本函数
    if (ngx_handle_read_event(rev, 0) != NGX_OK) {
        c->error = 1;
        ngx_http_finalize_request(r, NGX_ERROR);
        return;
    }

    // ...
}

很多情况下客户端会把请求体连同请求头一起发过来,这些数据就存放在缓冲区 r->header_in 里,所以先要检查这里的数据来决定是否已经全部接收了请求体,如果只是部分接收了或者没有接收,那么需要监控读事件继续读取数据,都交给 ngx_http_discarded_request_body_handler() 处理。


备注

1.测试环境centos7 64位,nginx版本为 1.14.0。
2..原文地址http://www.freecls.com/a/2712/e5


©著作权归作者所有
收藏
推荐阅读
简介
天降大任于斯人也,必先苦其心志。