专栏名称: SegmentFault思否
SegmentFault (www.sf.gg)开发者社区,是中国年轻开发者喜爱的极客社区,我们为开发者提供最纯粹的技术交流和分享平台。
目录
相关文章推荐
OSC开源社区  ·  RAG市场的2024:随需而变,从狂热到理性 ·  20 小时前  
程序猿  ·  41岁DeepMind天才科学家去世:长期受 ... ·  昨天  
程序员的那些事  ·  清华大学:DeepSeek + ... ·  2 天前  
OSC开源社区  ·  2024: 大模型背景下知识图谱的理性回归 ·  3 天前  
程序猿  ·  “我真的受够了Ubuntu!” ·  3 天前  
51好读  ›  专栏  ›  SegmentFault思否

初探 nginx HTTP 处理流程

SegmentFault思否  · 公众号  · 程序员  · 2018-10-22 08:00

正文

运营研发团队:李乐

1. 初始化服务器

server指令用于配置 virtual server ,我们通常会在一台机器配置多个 virtual server ,监听不同端口号,映射到不同文件目录;nginx解析用户配置,在所有端口创建socket并启动监听。

nginx解析配置文件是由各个模块分担处理的,每个模块注册并处理自己关心的配置,通过模块结构体 ngx_module_t 的字段 ngx_command_t * commands 实现。

例如 ngx_http_module 是一个核心模块,其commands字段定义如下:

  1. struct ngx_command_s {

  2.    ngx_str_t             name;

  3.    ngx_uint_t            type;

  4.    char               *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);

  5. };

  6. static ngx_command_t  ngx_http_commands[] = {

  7.     { ngx_string("http"),

  8.      NGX_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS,

  9.      ngx_http_block,

  10.     },

  11. };

说明:

  • name:指令名称,解析配置文件时按照名称能匹配查找。

  • type:指令类型, NGX_CONF_NOARGS 该配置无参数, NGX_CONF_BLOCK 该配置是一个配置块, NGX_MAIN_CONF 表示配置可以出现在哪些位( NGX_MAIN_CONF NGX_HTTP_SRV_CONF NGX_HTTP_LOC_CONF )。

  • set:指令处理函数指针。

可以看到解析http指令的处理函数为 ngx_http_block ,实现如下:

  1. static char * ngx_http_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)

  2. {

  3.    //解析main配置

  4.    //解析server配置

  5.     //解析location配置

  6.    //初始化HTTP处理流程所需的handler

  7.    //初始化listening

  8.    if (ngx_http_optimize_servers(cf, cmcf, cmcf->ports) != NGX_OK) {

  9.        return NGX_CONF_ERROR;

  10.    }

  11. }

ngx_http_optimize_servers 方法循环所有配置端口,创建 ngx_listening_t 对象,并将其添加到 conf -> cycle -> listening (后续操作会遍历此数组,创建socket并监听)。方法主要操作如下图:

注意到这里设置了 ngx_listening_t 的handler为 ngx_http_init_connection ,当接收到socket连接请求时,会调用此handler处理。

那么什么时候启动监听呢?全局搜索关键字 cycle -> listening 可以找到。main方法会调用 ngx_init_cycle ,其完成了服务器初始化的大部分工作,其中就包括启动监听( ngx_open_listening_sockets )。

假设nginx使用epoll处理所有socket事件,什么时候将监听事件添加到epoll呢?全局搜索关键字 cycle -> listening 可以找到。 ngx_event_core_module 模块是事件处理核心模块,初始化此模块时会执行 ngx_event_process_init 函数,其中将监听事件添加到epoll。

  1. static ngx_int_t ngx_event_process_init(ngx_cycle_t *cycle)

  2. {

  3.    ls = cycle->listening.elts;

  4.    for (i = 0; i < cycle->listening.nelts; i++) {

  5.        //设置读事件处理handler

  6.        rev->handler = ngx_event_accept;

  7.        ngx_add_event(rev, NGX_READ_EVENT, 0);

  8.    }

  9. }

注意到接收到客户端socket连接请求事件的处理函数是 ngx_event_accept

2. HTTP请求解析

2.1 基础结构体

结构体 ngx_connection_t 存储socket连接相关信息;nginx预先创建若干个 ngx_connection_t 对象,存储在全局变量 ngx_cycle -> free_connections ,称之为连接池;当新生成socket时,会尝试从连接池中获取空闲connection连接,如果获取失败,则会直接关闭此socket。

指令 worker_connections 用于配置连接池最大连接数目,配置在events指令块中,由 ngx_event_core_module 解析。

  1. vents {

  2.   use epoll;

  3.   worker_connections  60000;

  4. }

当nginx作为HTTP服务器时,最大客户端数目 maxClient = worker_processes * worker_connections / 2 ;当nginx作为反向代理服务器时,最大客户端数目 maxClient = worker_processes * worker_connections / 4 。其 worker_processes 为用户配置的worker进程数目。

结构体 ngx_connection_t 定义如下:

  1. struct ngx_connection_s {

  2.     //空闲连接池中,data指向下一个连接,形成链表;取出来使用时,data指向请求结构体ngx_http_request_s

  3.    void               *data;

  4.    //读写事件结构体,两个关键字段:handler处理函数、timer定时器

  5.    ngx_event_t        *read;

  6.    ngx_event_t        *write;

  7.    ngx_socket_t        fd;   //socket fd

  8.    ngx_recv_pt         recv; //socket接收数据函数指针

  9.    ngx_send_pt         send; //socket发送数据函数指针

  10.    ngx_buf_t          *buffer; //输入缓冲区

  11.    struct sockaddr    *sockaddr; //客户端地址

  12.    socklen_t           socklen;

  13.    ngx_listening_t    *listening; //监听的ngx_listening_t对象

  14.    struct sockaddr    *local_sockaddr; //本地地址

  15.    socklen_t           local_socklen;

  16.    …………

  17. }

结构体 ngx_http_request_t 存储整个HTTP请求处理流程所需的所有信息,字段非常多,这里只进行简要说明:

  1. struct ngx_http_request_s {

  2.    ngx_connection_t                 *connection;

  3.    //读写事件处理handler

  4.    ngx_http_event_handler_pt         read_event_handler;

  5.    ngx_http_event_handler_pt         write_event_handler;

  6.    //请求头缓冲区

  7.    ngx_buf_t                        *header_in;

  8.     //解析后的请求头

  9.    ngx_http_headers_in_t             headers_in;

  10.    //请求体结构体

  11.    ngx_http_request_body_t          *request_body;

  12.    //请求行

  13.    ngx_str_t                         request_line;

  14.    //解析后请求行若干字段

  15.    ngx_uint_t                        method;

  16.    ngx_uint_t                        http_version;

  17.    ngx_str_t                         uri;

  18.    ngx_str_t                         args;

  19.    …………

  20. }

请求行与请求体解析相对比较简单,这里重点讲述请求头的解析,解析后的请求头信息都存储在 ngx_http_headers_in_t 结构体中。

ngx_http_request . c 文件中定义了所有的HTTP头部,存储在 ngx_http_headers_in 数组,数组的每个元素是一个 ngx_http_header_t 结构体,主要包含三个字段,头部名称、头部解析后字段存储在 ngx_http_headers_in_t 的偏移量,解析头部的处理函数。

  1. ngx_http_header_t  ngx_http_headers_in[] = {

  2.    { ngx_string("Host"), offsetof(ngx_http_headers_in_t, host),

  3.                 ngx_http_process_host },

  4.    { ngx_string("Connection"), offsetof(ngx_http_headers_in_t, connection),

  5.                 ngx_http_process_connection },

  6.    …………

  7. }

  8. typedef struct {

  9.    ngx_str_t                         name;

  10.    ngx_uint_t                        offset;

  11.    ngx_http_header_handler_pt        handler;

  12. } ngx_http_header_t;

解析请求头时,从 ngx_http_headers_in 数组中查找请求头 ngx_http_header_t 对象,调用处理函数handler,存储到 r -> headers_in 对应字段。以解析Connection头部为例, ngx_http_process_connection 实现如下:

  1. static ngx_int_t ngx_http_process_connection(ngx_http_request_t *r, ngx_table_elt_t *h, ngx_uint_t offset)

  2. {

  3.    if (ngx_strcasestrn(h->value.data, "close", 5 - 1)) {

  4.        r->headers_in.connection_type = NGX_HTTP_CONNECTION_CLOSE;

  5.    } else if (ngx_strcasestrn(h->value.data, "keep-alive", 10 - 1)) {

  6.        r->headers_in.connection_type = NGX_HTTP_CONNECTION_KEEP_ALIVE;

  7.    }

  8.    return NGX_OK;

  9. }

输入参数offset在此处并没有什么作用。注意到第二个输入参数 ngx_table_elt_t ,存储了当前请求头的键值对信息:

  1. typedef struct {

  2.    ngx_uint_t        hash;  //请求头key的hash值

  3.    ngx_str_t         key;

  4.    ngx_str_t         value;

  5.    u_char           *lowcase_key;  //请求头key转为小写字符串(可以看到HTTP请求头解析时key不区分大小写)

  6. } ngx_table_elt_t;

再思考一个问题,从 ngx_http_headers_in 数组中查找请求头对应 ngx_http_header_t 对象时,需要遍历,每个元素都需要进行字符串比较,效率低下。因此nginx将 ngx_http_headers_in 数组转换为哈希表,哈希表的键即为请求头的key,方法 ngx_http_init_headers_in_hash 实现了数组到哈希表的转换,转换后的哈希表存储在 cmcf -> headers_in_hash 字段。

2.2 解析HTTP请求

第1节提到,在创建socket启动监听时,会添加可读事件到epoll,事件处理函数为 ngx_event_accept ,用于接收socket连接,分配connection连接,并调用 ngx_listening_t 对象的处理函数( ngx_http_init_connection )。

  1. void ngx_event_accept(ngx_event_t *ev)

  2. {

  3.    s = accept4(lc->fd, (struct sockaddr *) sa, &socklen, SOCK_NONBLOCK);

  4.    //客户端socket连接成功时,都需要分配connection连接,如果分配失败则会直接关闭此socket。

  5.    //而每个worker进程连接池的最大连接数目是固定的,当不存在空闲连接时,此worker进程accept的所有socket都会被拒绝;

  6.    //多个worker进程通过竞争执行epoll_wait;而当ngx_accept_disabled大于0时,会直接放弃此次竞争,同时ngx_accept_disabled减1。

  7.    //以此实现,当worker进程的空闲连接过少时,减少其竞争epoll_wait次数

  8.    ngx_accept_disabled = ngx_cycle->connection_n / 8 - ngx_cycle->free_connection_n;

  9.    c = ngx_get_connection(s, ev->log);

  10.    ls->handler(c);

  11. }

socket连接成功后,nginx会等待客户端发送HTTP请求,默认会有60秒的超时时间,即60秒内没有接收到客户端请求时,断开此连接,打印错误日志。函数 ngx_http_init_connection 用于设置读事件处理函数,以及超时定时器。

  1. void ngx_http_init_connection(ngx_connection_t *c)

  2. {

  3.    c->read = ngx_http_wait_request_handler;

  4.    c->write->handler = ngx_http_empty_handler;

  5.    ngx_add_timer(rev, c->listening->post_accept_timeout);

  6. }

全局搜索 post_accept_timeout 字段,可以查找到设置此超时时间的配置指令, client_header_timeout ,其可以在http、server指令块中配置。

函数 ngx_http_wait_request_handler 为解析HTTP请求的入口函数,实现如下:

  1. static void ngx_http_wait_request_handler(ngx_event_t *rev)

  2. {

  3.    //读事件已经超时

  4.    if (rev->timedout) {

  5.        ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT, "client timed out");

  6.        ngx_http_close_connection(c);

  7.        return;

  8.    }

  9.    size = cscf->client_header_buffer_size;   //client_header_buffer_size指令用于配置接收请求头缓冲区大小

  10.    b = c->buffer;

  11.    n = c->recv(c, b->last , size);

  12.    //创建请求对象ngx_http_request_t,HTTP请求整个处理过程都有用;

  13.    c->data = ngx_http_create_request(c);

  14.    rev->handler = ngx_http_process_request_line; //设置读事件处理函数(此次请求行可能没有读取完)

  15.    ngx_http_process_request_line(rev);

  16. }

函数 ngx_http_create_request 创建并初始化 ngx_http_request_t 对象,注意这赋值语句 r -> header_in = c -> buffer

解析请求行与请求头的代码较为繁琐,终点在于读取socket数据,解析字符串,这里不做详述。HTTP请求解析过程主要函数调用如下图所示:

注意,解析完成请求行与请求头,nginx就开始处理HTTP请求,并没有等到解析完请求体再处理。处理请求入口为 ngx_http_process_request

3. 处理HTTP请求

3.1 HTTP请求处理的11个阶段

nginx将HTTP请求处理流程分为11个阶段,绝大多数HTTP模块都会将自己的handler添加到某个阶段(将handler添加到全局唯一的数组phases中),注意其中有4个阶段不能添加自定义handler,nginx处理HTTP请求时会挨个调用每个阶段的handler。

  1. typedef enum {

  2.    NGX_HTTP_POST_READ_PHASE = 0, //第一个阶段,目前只有realip模块会注册handler,但是该模块默认不会运行(nginx作为代理服务器时有用,后端以此获取客户端原始ip)

  3.    NGX_HTTP_SERVER_REWRITE_PHASE,  //server块中配置了rewrite指令,重写url

  4.    NGX_HTTP_FIND_CONFIG_PHASE,   //查找匹配的location配置;不能自定义handler;

  5.    NGX_HTTP_REWRITE_PHASE,       //location块中配置了rewrite指令,重写url

  6.    NGX_HTTP_POST_REWRITE_PHASE,  //检查是否发生了url重写,如果有,重新回到FIND_CONFIG阶段;不能自定义handler;

  7.    NGX_HTTP_PREACCESS_PHASE,     //访问控制,比如限流模块会注册handler到此阶段

  8.    NGX_HTTP_ACCESS_PHASE,        //访问权限控制,比如基于ip黑白名单的权限控制,基于用户名密码的权限控制等

  9.    NGX_HTTP_POST_ACCESS_PHASE,   //根据访问权限控制阶段做相应处理;不能自定义handler;

  10.    NGX_HTTP_TRY_FILES_PHASE,     //只有配置了try_files指令,才会有此阶段;不能自定义handler;

  11.    NGX_HTTP_CONTENT_PHASE,       //内容产生阶段,返回响应给客户端

  12.    NGX_HTTP_LOG_PHASE            //日志记录

  13. } ngx_http_phases;

nginx使用结构体 ngx_module_s 表示一个模块,其中字段ctx,是一个指向模块上下文结构体的指针(上下文结构体的字段都是一些函数指针);nginx的HTTP模块上下文结构体大多都有字段postconfiguration,负责注册本模块的handler到某个处理阶段。11个阶段在解析完成http配置块指令后初始化。

  1. static char * ngx_http_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)

  2. {

  3.    //解析http配置块

  4.    //初始化11个阶段的phases数组,注意多个模块可能注册到同一个阶段,因此phases是一个二维数组

  5.    if (ngx_http_init_phases(cf, cmcf) != NGX_OK) {

  6.        return NGX_CONF_ERROR;

  7.    }

  8.    //遍历索引HTTP模块,注册handler

  9.    for (m = 0; ngx_modules[m]; m++) {

  10.        if (ngx_modules[m]->type != NGX_HTTP_MODULE ) {

  11.            continue;

  12.        }

  13.        module = ngx_modules[m]->ctx;

  14.        if (module->postconfiguration) {

  15.            if (module->postconfiguration(cf) != NGX_OK) {

  16.                return NGX_CONF_ERROR;

  17.            }

  18.        }

  19.    }

  20.    //将二维数组转换为一维数组,从而遍历执行数组所有handler

  21.    if (ngx_http_init_phase_handlers(cf, cmcf) != NGX_OK) {

  22.        return NGX_CONF_ERROR;

  23.    }

  24. }

以限流模块 ngx_http_limit_req_module 模块为例,postconfiguration方法简单实现如下:

  1. static ngx_int_t ngx_http_limit_req_init(ngx_conf_t *cf)

  2. {

  3.    h = ngx_array_push(&cmcf->phases[NGX_HTTP_PREACCESS_PHASE].handlers);

  4.    *h = ngx_http_limit_req_handler;  //ngx_http_limit_req_module模块的限流方法;nginx处理HTTP请求时,都会调用此方法判断应该继续执行还是拒绝请求

  5.    return NGX_OK;

  6. }

GDB调试,断点到 ngx_http_block 方法执行所有HTTP模块注册handler之后,打印phases数组:

  1. p cmcf->phases[*].handlers

  2. p *(ngx_http_handler_pt*)cmcf->phases[*].handlers.elts

11个阶段注册的handler如下图所示:

3.2 11个阶段初始化

上面提到HTTP的11个处理阶段handler存储在phases数组,但由于多个模块可能注册handler到同一个阶段,使得phases是一个二维数组,因此需要转换为一维数组,转换后存储在 cmcf -> phase_engine 字段,phase_engine的类型为 ngx_http_phase_engine_t ,定义如下:

  1. typedef struct {

  2.    ngx_http_phase_handler_t  *handlers;   //一维数组,存储所有handler

  3.    ngx_uint_t                 server_rewrite_index;  //记录NGX_HTTP_SERVER_REWRITE_PHASE阶段handler的索引值

  4.    ngx_uint_t                 location_rewrite_index; //记录NGX_HTTP_REWRITE_PHASE阶段handler的索引值

  5. } ngx_http_phase_engine_t;

  6. struct ngx_http_phase_handler_t {

  7.    ngx_http_phase_handler_pt  checker;  //执行handler之前的校验函数

  8.    ngx_http_handler_pt        handler;

  9.    ngx_uint_t                 next;   //下一个待执行handler的索引(通过next实现handler跳转执行)

  10. };

  11. //cheker函数指针类型定义

  12. typedef ngx_int_t (*ngx_http_phase_handler_pt)(ngx_http_request_t *r, ngx_http_phase_handler_t *ph);

  13. //handler函数指针类型定义

  14. typedef ngx_int_t (*ngx_http_handler_pt)(ngx_http_request_t *r);

数组转换函数 ngx_http_init_phase_handlers 实现如下:

  1. static ngx_int_t ngx_http_init_phase_handlers(ngx_conf_t *cf, ngx_http_core_main_conf_t *cmcf)

  2. {

  3.    use_rewrite = cmcf->phases[NGX_HTTP_REWRITE_PHASE].handlers.nelts ? 1 : 0;

  4.    use_access = cmcf ->phases[NGX_HTTP_ACCESS_PHASE].handlers.nelts ? 1 : 0;

  5.    n = use_rewrite + use_access + cmcf->try_files + 1 /* find config phase */; //至少有4个阶段,这4个阶段是上面说的不能注册handler的4个阶段

  6.    //计算handler数目,分配空间

  7.    for (i = 0; i < NGX_HTTP_LOG_PHASE; i++) {

  8.        n += cmcf->phases[i].handlers.nelts;

  9.    }

  10.    ph = ngx_pcalloc(cf->pool, n * sizeof(ngx_http_phase_handler_t) + sizeof(void *));

  11.    //遍历二维数组

  12.    for (i = 0; i < NGX_HTTP_LOG_PHASE; i++) {

  13.        h = cmcf->phases[i].handlers.elts;

  14.        switch (i) {

  15.        case NGX_HTTP_SERVER_REWRITE_PHASE:

  16.            if (cmcf->phase_engine.server_rewrite_index == (ngx_uint_t) -1) {

  17.                cmcf->phase_engine.server_rewrite_index = n;   //记录NGX_HTTP_SERVER_REWRITE_PHASE阶段handler的索引值

  18.            }

  19.            checker = ngx_http_core_rewrite_phase;

  20.            break;

  21.        case NGX_HTTP_FIND_CONFIG_PHASE:

  22.            find_config_index = n;   //记录NGX_HTTP_FIND_CONFIG_PHASE阶段的索引,NGX_HTTP_POST_REWRITE_PHASE阶段可能会跳转回此阶段

  23.            ph->checker = ngx_http_core_find_config_phase;

  24.            n++;

  25.            ph++;

  26.            continue;   //进入下一个阶段NGX_HTTP_REWRITE_PHASE

  27.        case NGX_HTTP_REWRITE_PHASE:

  28.            if (cmcf->phase_engine.location_rewrite_index == (ngx_uint_t) -1) {

  29.                cmcf->phase_engine.location_rewrite_index = n;   //记录NGX_HTTP_REWRITE_PHASE阶段handler的索引值

  30.            }

  31.            checker = ngx_http_core_rewrite_phase;

  32.            break;

  33.        







请到「今天看啥」查看全文