nginx request body读取流程详解

简介:

nginx request body读取流程详解
前面的文章中我们分别讲解了nginx是如何读取请求行和请求头数据的,在读取完请求头之后,nginx并不会直接读取请求体,而是直接进入http模块的11个阶段开始处理请求的数据。在这个过程中,如果当前请求匹配的location配置了proxy_pass,那么就会在NGX_HTTP_CONTENT_PHASE阶段读取客户端发送来的request body数据,以转发给上游服务器。本文主要是对nginx是如何读取客户端的数据进行讲解的。

  1. request body读取入口
    nginx读取数据是通过ngx_http_read_client_request_body()进行的,如下是该方法的源码:

/**

  • 接收http请求的包体
    */

ngx_int_t ngx_http_read_client_request_body(ngx_http_request_t *r, ngx_http_client_body_handler_pt post_handler) {
size_t preread;
ssize_t size;
ngx_int_t rc;
ngx_buf_t *b;
ngx_chain_t out;
ngx_http_request_body_t *rb;
ngx_http_core_loc_conf_t *clcf;

r->main->count++;

// 如果当前请求是子请求,或者已经接收过包体,或者需要忽略包体,则直接调用post_handler()方法,然后返回
if (r != r->main || r->request_body || r->discard_body) {

r->request_body_no_buffering = 0;
post_handler(r);
return NGX_OK;

}

// 主要是向用户发送100 continue以期待获取更多数据
if (ngx_http_test_expect(r) != NGX_OK) {

rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
goto done;

}

// 申请ngx_http_request_body_t的内存
rb = ngx_pcalloc(r->pool, sizeof(ngx_http_request_body_t));
if (rb == NULL) {

rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
goto done;

}

rb->rest = -1;
rb->post_handler = post_handler;
r->request_body = rb;

// 如果content-length小于0,并且不是大文件,则直接回调post_handler(),并且返回
if (r->headers_in.content_length_n < 0 && !r->headers_in.chunked) {

r->request_body_no_buffering = 0;
post_handler(r);
return NGX_OK;

}

// preread表示缓冲区还有多少数据未处理。这里这么计算的原因在于,r->header_in中关于请求行和header的
// 数据都已经处理了,剩下的部分如果还有数据,必然是请求包体的,也即当前方法需要处理的部分
preread = r->header_in->last - r->header_in->pos;

// 已经读取到了部分数据
if (preread) {

out.buf = r->header_in;
out.next = NULL;

// 将out中的数据尝试写入到临时文件中
rc = ngx_http_request_body_filter(r, &out);
if (rc != NGX_OK) {
  goto done;
}

// 对处理的数据长度进行累加
r->request_length += preread - (r->header_in->last - r->header_in->pos);
// 这里是判断r->header_in中是否已经读取到的数据长度大于剩余需要读取的长度
if (!r->headers_in.chunked
    && rb->rest > 0
    && rb->rest <= (off_t) (r->header_in->end - r->header_in->last)) {
  b = ngx_calloc_buf(r->pool);
  if (b == NULL) {
    rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
    goto done;
  }

  b->temporary = 1;
  b->start = r->header_in->pos;
  b->pos = r->header_in->pos;
  b->last = r->header_in->last;
  b->end = r->header_in->end;
  rb->buf = b;

  r->read_event_handler = ngx_http_read_client_request_body_handler;
  r->write_event_handler = ngx_http_request_empty_handler;

  rc = ngx_http_do_read_client_request_body(r);
  goto done;
}

} else {

if (ngx_http_request_body_filter(r, NULL) != NGX_OK) {
  rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
  goto done;
}

}

// rb->rest为0说明没有包体数据,或者包体数据已经读取完毕
if (rb->rest == 0) {

r->request_body_no_buffering = 0;
post_handler(r);
return NGX_OK;

}

// 这里rb->rest肯定是大于0的,因而如果其小于0,说明读取包体异常了
if (rb->rest < 0) {

ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0, "negative request body rest");
rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
goto done;

}

clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
size = clcf->client_body_buffer_size;
size += size >> 2;

// 计算读取数据缓冲区的大小
if (!r->headers_in.chunked && rb->rest < size) {

size = (ssize_t) rb->rest;
if (r->request_body_in_single_buf) {
  size += preread;
}

} else {

size = clcf->client_body_buffer_size;

}

// 创建读取数据的缓冲区
rb->buf = ngx_create_temp_buf(r->pool, size);
if (rb->buf == NULL) {

rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
goto done;

}

r->read_event_handler = ngx_http_read_client_request_body_handler;
r->write_event_handler = ngx_http_request_empty_handler;

rc = ngx_http_do_read_client_request_body(r);

done:

if (r->request_body_no_buffering && (rc == NGX_OK || rc == NGX_AGAIN)) {

if (rc == NGX_OK) {
  r->request_body_no_buffering = 0;
} else {
  r->reading_body = 1;
}

r->read_event_handler = ngx_http_block_reading;
post_handler(r);

}

if (rc >= NGX_HTTP_SPECIAL_RESPONSE) {

r->main->count--;

}

return rc;
}
上述读取request body的流程主要分为如下几个步骤:

判断当前请求如果是子请求,或者已经调用当前方法读取过body,或者被设置为需要忽略body,则直接返回;
申请ngx_http_request_body_t结构体的内存,这个结构体的作用是存储当前读取到的body数据的;
判断当前请求的Content-Length是否为0,是则不进行body的读取;
判断当前请求的读取缓冲区中是否已经读取了一部分的body数据,如果已经读取了,则调用ngx_http_request_body_filter()方法将已经读取到的数据存储到临时文件中,否则只是调用ngx_http_request_body_filter()方法计算当前还剩余多少数据未读取,计算的到的值存储在rb->rest中。这里可能存在部分已经读取的数据的原因在于,前面在读取请求行和header数据的时候可能已经多读取了一部分数据,而这部分数据就是body数据;
判断当前剩余需要读取的数据长度为0,则直接返回;
申请存储body数据的缓冲区;
将当前请求的read_event_handler设置为ngx_http_read_client_request_body_handler()方法,而write_event_handler设置为ngx_http_request_empty_handler()方法。在ngx_http_read_client_request_body_handler()内部,其会调用ngx_http_do_read_client_request_body()方法以执行真正的请求体读取过程。这里的ngx_http_request_empty_handler()方法是一个空方法,由于当前正处于读取数据阶段,因而意外触发的写事件不需要处理;
调用ngx_http_do_read_client_request_body()以执行真正的body读取过程。
这里需要说明的是,第七步中将read_event_handler设置为ngx_http_read_client_request_body_handler(),这个read_event_handler()方法的调用方式在前面讲解nginx如何驱动http模块的11个阶段的文章中已经进行了介绍。简而言之,进入http模块的11个阶段的流程之后,如果触发了读事件,那么就会调用read_event_handler()方法,这里设置了之后,每次触发了读事件就会调用ngx_http_read_client_request_body_handler()方法,从而触发body数据的继续读取流程。

上面的流程中,最主要的两个方法是ngx_http_request_body_filter()和ngx_http_do_read_client_request_body()方法,第一个方法在缓冲区数据满了的时候会将数据写入到缓冲区中,第二个方法则用于读取客户端的body数据。

  1. 存储缓冲区数据至临时文件
    存储缓冲区数据到临时文件的方法为ngx_http_request_body_filter()方法,如下是该方法的源码:

static ngx_int_t ngx_http_request_body_filter(ngx_http_request_t r, ngx_chain_t in) {
if (r->headers_in.chunked) {

return ngx_http_request_body_chunked_filter(r, in);

} else {

// 我们这里主要讲解小块body的处理方式
return ngx_http_request_body_length_filter(r, in);

}
}

/**

  • 这里主要是将in中的数据写入到临时文件中
    */

static ngx_int_t ngx_http_request_body_length_filter(ngx_http_request_t r, ngx_chain_t in) {
size_t size;
ngx_int_t rc;
ngx_buf_t *b;
ngx_chain_t cl, tl, out, *ll;
ngx_http_request_body_t *rb;

rb = r->request_body;

// rest为-1表示还未读取过任何包体数据,因而将rest设置为content_length_n的值
if (rb->rest == -1) {

rb->rest = r->headers_in.content_length_n;

}

out = NULL;
ll = &out;

for (cl = in; cl; cl = cl->next) {

if (rb->rest == 0) {
  break;
}

tl = ngx_chain_get_free_buf(r->pool, &rb->free);
if (tl == NULL) {
  return NGX_HTTP_INTERNAL_SERVER_ERROR;
}

b = tl->buf;
ngx_memzero(b, sizeof(ngx_buf_t));

b->temporary = 1;
b->tag = (ngx_buf_tag_t) &ngx_http_read_client_request_body;
b->start = cl->buf->pos;
b->pos = cl->buf->pos;
b->last = cl->buf->last;
b->end = cl->buf->end;
b->flush = r->request_body_no_buffering;

size = cl->buf->last - cl->buf->pos;
if ((off_t) size < rb->rest) {
  cl->buf->pos = cl->buf->last;
  rb->rest -= size;
} else {
  cl->buf->pos += (size_t) rb->rest;
  rb->rest = 0;
  b->last = cl->buf->pos;
  b->last_buf = 1;
}

*ll = tl;
ll = &tl->next;

}

// 这里的ngx_http_top_request_body_filter指向的是ngx_http_request_body_save_filter方法,
// 这里主要是检查现有的缓冲区是否写满了,如果满了,则将缓冲区的数据写入到临时文件中
rc = ngx_http_top_request_body_filter(r, out);

// 释放busy中ngx_buf_t链表占用的缓冲区,不过不会释放其tag为ngx_http_read_client_request_body的
// 缓冲区,而会将这些缓冲区添加到free链表的头部
ngx_chain_update_chains(r->pool, &rb->free, &rb->busy, &out,

                      (ngx_buf_tag_t) &ngx_http_read_client_request_body);

return rc;
}
上述方法主要完成了如下几部分工作:

遍历out链表,依次将其存储的数据长度从rb->rest中扣除;
调用ngx_http_top_request_body_filter()判断如果缓冲区中没有剩余空间,则将数据存储到临时文件中;
调用ngx_chain_update_chains()方法将rb->busy中需要释放的空间释放到rb->free中;
这里我们主要看看ngx_http_top_request_body_filter()方法是如何存储数据到临时文件的,这个方法实际上指向的是ngx_http_request_body_save_filter()方法,如下是该方法的源码:

/**

  • 这里主要是判断是否还有剩余的可用空间,如果没有可用空间,则会将现有的数据写入到包体中
    */

ngx_int_t ngx_http_request_body_save_filter(ngx_http_request_t r, ngx_chain_t in) {
ngx_buf_t *b;
ngx_chain_t *cl;
ngx_http_request_body_t *rb;

rb = r->request_body;

// 将in的数据拼接到rb->bufs的末尾
if (ngx_chain_add_copy(r->pool, &rb->bufs, in) != NGX_OK) {

return NGX_HTTP_INTERNAL_SERVER_ERROR;

}

if (r->request_body_no_buffering) {

return NGX_OK;

}

if (rb->rest > 0) {

// 这里rest大于0,说明还有数据未接收,但是这里的buf->last等于buf->end,说明缓冲区中没有剩余可用于
// 存储数据的空间了,因而这里调用ngx_http_write_request_body()方法将已经接收到的包体写入到临时
// 文件中
if (rb->buf && rb->buf->last == rb->buf->end 
    && ngx_http_write_request_body(r) != NGX_OK) {
  return NGX_HTTP_INTERNAL_SERVER_ERROR;
}

return NGX_OK;

}

// 走到这里,说明客户端的包体数据已经接收完毕了
if (rb->temp_file || r->request_body_in_file_only) {

if (ngx_http_write_request_body(r) != NGX_OK) {
  return NGX_HTTP_INTERNAL_SERVER_ERROR;
}

if (rb->temp_file->file.offset != 0) {
  cl = ngx_chain_get_free_buf(r->pool, &rb->free);
  if (cl == NULL) {
    return NGX_HTTP_INTERNAL_SERVER_ERROR;
  }

  b = cl->buf;
  ngx_memzero(b, sizeof(ngx_buf_t));

  // 标记数据已经写入到文件中了
  b->in_file = 1;
  b->file_last = rb->temp_file->file.offset;
  b->file = &rb->temp_file->file;
  rb->bufs = cl;
}

}

return NGX_OK;
}
在ngx_http_request_body_save_filter()方法中,其首先会将in中的数据拷贝到rb->bufs缓冲区中。然后会判断当前缓冲区是否还有剩余空间,如果没有,则调用ngx_http_write_request_body()方法将数据写入到临时文件中,并且更新表征当前request body的rb结构体中与文件相关的属性。关于ngx_http_write_request_body()是如何将数据写入到临时文件的,其逻辑比较简单,这里不再赘述。

  1. 读取request body数据
    前面我们讲到,nginx读取request body是通过ngx_http_do_read_client_request_body()方法进行的,如下是该方法的源码:

static ngx_int_t ngx_http_do_read_client_request_body(ngx_http_request_t *r) {
off_t rest;
size_t size;
ssize_t n;
ngx_int_t rc;
ngx_chain_t out;
ngx_connection_t *c;
ngx_http_request_body_t *rb;
ngx_http_core_loc_conf_t *clcf;

c = r->connection;
rb = r->request_body;

for (;;) {

for (;;) {
  // 判断rb->buf中是否还有可用的空间
  if (rb->buf->last == rb->buf->end) {
    if (rb->buf->pos != rb->buf->last) {
      out.buf = rb->buf;
      out.next = NULL;
      // 将数据写入到临时文件中
      rc = ngx_http_request_body_filter(r, &out);
      if (rc != NGX_OK) {
        return rc;
      }
    } else {
      rc = ngx_http_request_body_filter(r, NULL);
      if (rc != NGX_OK) {
        return rc;
      }
    }

    if (rb->busy != NULL) {
      if (r->request_body_no_buffering) {
        if (c->read->timer_set) {
          ngx_del_timer(c->read);
        }

        if (ngx_handle_read_event(c->read, 0) != NGX_OK) {
          return NGX_HTTP_INTERNAL_SERVER_ERROR;
        }

        return NGX_AGAIN;
      }

      return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    // 更新rb->buf的指针数据
    rb->buf->pos = rb->buf->start;
    rb->buf->last = rb->buf->start;
  }

  // size表示rb->buf中剩余的可用空间
  size = rb->buf->end - rb->buf->last;
  // 计算新的未读取的数据长度
  rest = rb->rest - (rb->buf->last - rb->buf->pos);
  if ((off_t) size > rest) {
    size = (size_t) rest;
  }

  // 从连接的句柄中读取size大小的数据到rb->buf->last位置
  n = c->recv(c, rb->buf->last, size);
  // NGX_AGAIN表示还未读取完
  if (n == NGX_AGAIN) {
    break;
  }

  // n等于0表示客户端关闭了连接
  if (n == 0) {
    ngx_log_error(NGX_LOG_INFO, c->log, 0, "client prematurely closed connection");
  }

  // 如果读取异常,则返回BAD_REQUEST
  if (n == 0 || n == NGX_ERROR) {
    c->error = 1;
    return NGX_HTTP_BAD_REQUEST;
  }

  rb->buf->last += n;
  r->request_length += n;

  // n等于rest表示剩余的数据都读取完了,此时将数据写入到临时文件中
  if (n == rest) {
    out.buf = rb->buf;
    out.next = NULL;
    rc = ngx_http_request_body_filter(r, &out);
    if (rc != NGX_OK) {
      return rc;
    }
  }

  // 如果rb->rest为0,则退出当前内层循环
  if (rb->rest == 0) {
    break;
  }

  // 如果当前rb->buf中还有可用空间,则退出当前内层循环
  if (rb->buf->last < rb->buf->end) {
    break;
  }
}

// 如果没有需要读取的数据,则跳出外层循环
if (rb->rest == 0) {
  break;
}

// 如果连接句柄上没有可以读取的数据,则继续将读取事件添加到事件调度器中,并且监听连接句柄上的读取事件
if (!c->read->ready) {
  if (r->request_body_no_buffering && rb->buf->pos != rb->buf->last) {
    /* pass buffer to request body filter chain */

    out.buf = rb->buf;
    out.next = NULL;
    
    rc = ngx_http_request_body_filter(r, &out);
    if (rc != NGX_OK) {
      return rc;
    }
  }

  clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
  ngx_add_timer(c->read, clcf->client_body_timeout);
  if (ngx_handle_read_event(c->read, 0) != NGX_OK) {
    return NGX_HTTP_INTERNAL_SERVER_ERROR;
  }

  return NGX_AGAIN;
}

}

// 走到这里,说明数据读取完毕了,因而这里会删除读取事件,并且将读取事件的处理方法设置为一个空方法
if (c->read->timer_set) {

ngx_del_timer(c->read);

}

if (!r->request_body_no_buffering) {

r->read_event_handler = ngx_http_block_reading;
rb->post_handler(r);

}

return NGX_OK;
}
这里ngx_http_do_read_client_request_body()方法使用了一个双重无限for循环,对于内层循环,其主要分为四部分的工作:

检查当前读取缓冲区是否有剩余的空间,如果没有,则还是调用ngx_http_request_body_filter()方法将数据写入到临时文件中;
计算下一次应该读取的数据长度;
调用c->recv()方法读取当前请求句柄上的数据,返回值如果大于0,则表示此次读取到的数据大小,如果等于0,则表示客户端断开了连接,如果为NGX_AGAIN,则表示需要再次长度读取,如果为NGX_ERROR,则表示读取异常了;
根据上一步调用的返回值以判断应该作何处理,对于NGX_AGAIN,直接跳出当前循环,对于0和NGX_ERROR,直接给客户端返回400响应码,对于大于0的情况,则更新当前缓冲区的相关属性,并且调用ngx_http_request_body_filter()尝试将满了的数据存储到临时文件中。
本质上来讲,内层循环如果读取到了数据,并且还有数据需要读取,那么其是不会跳出循环的,并且循环会一直尝试读取数据,这样做的优点在于,客户端传送的body数据一般都比较大,读取事件可能会经常触发,通过循环读取可以尽快的读取到连接句柄上接收到的数据。对于内层循环返回NGX_AGAIN时,表示此次读取的数据长度为0,此时可能句柄比较空闲,因而此时会break到外层循环(当然,还有其他的情况也都可以break到外层循环)。外层循环中,只要当前剩余需要读取的数据不为0,并且连接事件不是ready状态,则会将当前事件再次添加到事件框架中,以等待下次驱动读事件读取剩余的数据。

  1. 小结
    本文首先对nginx读取request body数据的入口进行了讲解,然后讲解了读取的整体流程,接着着重讲解了读取过程中临时文件的存储方式,以及读取数据的具体细节。

原文地址https://my.oschina.net/zhangxufeng/blog/3215381

相关文章
|
5月前
|
应用服务中间件 nginx
nginx优化:URI过长或request header过大导致400或414报错
当出现URI过长或请求头过大导致400或414报错时,可以通过以下方式对Nginx进行优化: 1. 调整client_max_body_size参数:该参数用于限制请求体的大小。默认情况下,Nginx的client_max_body_size参数设置为1M。如果请求体超过这个大小,Nginx会返回400错误。您可以根据实际需求适当增加这个值,例如设置为10M或更大。 ``` http { client_max_body_size 10M; } ``` 2. 调整large_client_header_buffers参数:该参数用于调整请求头缓冲区的大
712 0
|
7月前
|
关系型数据库 MySQL 应用服务中间件
Mac PHP-Nginx-Mysql 本地开发日常启动流程
Mac PHP-Nginx-Mysql 本地开发日常启动流程
40 1
|
1月前
|
算法 应用服务中间件 网络安全
windows下采用 nginx配置websocket支持wss流程
windows下采用 nginx配置websocket支持wss流程
|
7月前
|
应用服务中间件 nginx
nginx输入请求的header和body到日志
nginx输入请求的header和body到日志
306 1
|
3月前
|
应用服务中间件 nginx
上传文件失败413 Request Entity Too Large,nginx配置文件大小的限制
上传文件失败413 Request Entity Too Large,nginx配置文件大小的限制
|
4月前
|
JSON 应用服务中间件 nginx
在Nginx日志中记录请求的header和请求body
在Nginx日志中记录请求的header和请求body
139 0
|
7月前
|
应用服务中间件 nginx
解决nginx 出现 413:Request Entity Too Large
解决nginx 出现 413:Request Entity Too Large
104 0
|
7月前
|
应用服务中间件 nginx
nginx输入请求的body到日志
nginx输入请求的body到日志
261 0
|
8月前
|
JavaScript 应用服务中间件 开发工具
Nuxt.js(Vue SSR)创建项目到服务器(Nginx+PM2)部署详细流程(下)
Nuxt.js(Vue SSR)创建项目到服务器(Nginx+PM2)部署详细流程(下)
404 0
|
8月前
|
资源调度 JavaScript 前端开发
Nuxt.js(Vue SSR)创建项目到服务器(Nginx+PM2)部署详细流程(上)
Nuxt.js(Vue SSR)创建项目到服务器(Nginx+PM2)部署详细流程
398 0