Nginx源码阅读笔记-查询HTTP配置流程

2019-02-12
11分钟阅读时长

概述

前面已经分析过nginx解析配置文件的整体流程,接下来看查询HTTP配置的流程。

HTTP属于nginx的core顶层模块,下面又包括了三部分:

  • main部分配置:即在HTTP块但是又不在任何server、location块中的配置,如下图中的sendfile配置指令。
  • server块:在server块内部的配置。
  • location块:在location块内部分配置。

http_config

解析HTTP模块的入口函数是ngx_http_block,这一点可以从http指令相关的配置看出:

{ ngx_string("http),
  NGX_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS,
  ngx_http_block,
  0,
  0,
  NULL }

在这个解析函数的开始,就创建了ngx_http_conf_ctx_t结构体,所以看的出来这个结构体是HTTP模块的第一级配置,它的定义如下:

typedef struct {
  void        **main_conf;
  void        **srv_conf;
  void        **loc_conf;
} ngx_http_conf_ctx_t;

下面列举出来这几部分相关的函数以及数据结构:

入口函数 数据结构
http ngx_http_block ngx_http_conf_ctx_t
main ngx_http_core_main_conf_t
server ngx_http_core_server ngx_http_core_srv_conf_t
location ngx_http_core_location ngx_http_core_loc_conf_t

ngx_http_module

另外,由于HTTP块内的一些配置,作用域可以在多种块中,因此需要涉及到合并配置的流程,即:

  • 如果子作用域某配置项在解析过程中未被赋值,则将父作用域的 相同的配置项值拷贝至此配置项里;
  • 如果子作用域配置项在解析过程中被赋值了,则保留原 样;如果子作用域配置项和父作用域配置项都没有被初始化,则填入代码中预设的默认值。

相关的合并配置函数列举如下:

合并函数
server ngx_http_merge_servers
location ngx_http_merge_locations

以下具体看看一次HTTP请求如何查找到相关HTTP配置的流程,分为两步:

  • 根据Host查找server块
  • 根据URI查找location块

根据Host查找server块流程

前面分析nginx接收HTTP请求流程中分析到,nginx在接收HTTP请求流程中,将调用ngx_http_process_request_headers函数来处理请求头。

nginx使用一个ngx_http_header_t结构体,定义了哪些请求头需要进行特定的函数回调处理,函数ngx_http_process_request_headers会根据这个表来查询接收到的请求头都需要哪些回调函数来处理:

ngx_http_header_t  ngx_http_headers_in[] = {
  { ngx_string("Host"), offsetof(ngx_http_headers_in_t, host),
    ngx_http_process_host },

  { ngx_string("Connection"), offsetof(ngx_http_headers_in_t, connection),
    ngx_http_process_connection },
  ....
}

可以看到,针对Host这个header,会调用ngx_http_process_host函数,这个函数最终会调用ngx_http_set_virtual_server函数来根据Host头确定对应的server块。

nginx中,不同的server块可以监听同一个地址端口,只要对应的server_name不一样就可以了。

而相同的地址端口,在nginx中对应的是ngx_http_addr_conf_t,内部将同样地址端口的多个不同server_name再组织到一起来:

typedef struct {
  ngx_hash_combined_t        names;

  ngx_uint_t                 nregex;
  ngx_http_server_name_t    *regex;
} ngx_http_virtual_names_t;

struct ngx_http_addr_conf_s {
  /* the default server configuration for this address:port */
  ngx_http_core_srv_conf_t  *default_server;

  ngx_http_virtual_names_t  *virtual_names;

  unsigned                   ssl:1;
  unsigned                   http2:1;
  unsigned                   proxy_protocol:1;
};

显然,如果相同地址端口的server如果使用链表组织在一起,每一次都是线性时间的查找复杂度,这就太慢了。因此nginx定义了ngx_hash_combined_t这个数据结构,将相同地址端口的server_name组织到一起来:

typedef struct {
  ngx_hash_t            hash;
  ngx_hash_wildcard_t  *wc_head;
  ngx_hash_wildcard_t  *wc_tail;
} ngx_hash_combined_t;

该结构体中有三个成员,区分不同的server_name格式:

  • ngx_hash_t hash:精确匹配的哈希表,用于存储没有使用通配符的虚拟主机名,如”www.example.com“。
  • ngx_hash_wildcard_t wc_head:前置通配符哈希表,用于存储如”.example.org“和”.example.org“这样的前置通配符虚拟主机名。
  • ngx_hash_wildcard_t wc_tail:后置通配符哈希表,用于存储如”example.“这样的后置通配符虚拟主机名。

具体这个支持通配符的hash表,不在这里讲解,只谈host的查找顺序:

  • 首先查找精确匹配hash表,查找到则返回;
  • 接着查找前置通配符hash表,查找到则返回;
  • 最后查找后置通配符hash表,查找到则返回;
  • 如果以上都没有查找到,落到default_server的server块进行处理。

根据URI查找location块流程

根据Host查找到了server块,紧跟着就是根据URI来查找location块了。

location区分几种格式:

location = / {
    [ configuration A ]
}

location / {
    [ configuration B ]
}

location /documents/ {
    [ configuration C ]
}

location ^~ /images/ {
    [ configuration D ]
}

location ~* \.(gif|jpg|jpeg)$ {
    [ configuration E ]
}

在上面的配置例子中:

  • 配置A:精确匹配"/" URI,主机名后面不能带任何字符串。
  • 配置B:因为所有的地址都以 / 开头,所以这条规则将匹配到所有请求,但是正则和最长字符串会优先匹配。
  • 配置C:匹配任何以 /documents/ 开头的地址,匹配符合以后,还要继续往下搜索,只有后面的正则表达式没有匹配到时,这一条才会采用这一条。
  • 配置D:匹配任何以 /images/ 开头的地址,匹配符合以后,停止往下搜索正则,采用这一条。
  • 配置E:匹配所有以 gif,jpg或jpeg 结尾的请求,然而,所有请求 /images/ 下的图片会被 config D 处理,因为 ^~ 到达不了这一条正则。

具体根据URI匹配location的流程如下:

  • 首先先检查使用前缀字符定义的location,选择最长匹配的项并记录下来;
  • 如果找到了精确匹配的location,也就是使用了=修饰符的location,结束查找,使用它的配置。
  • 然后按顺序查找使用正则定义的location,如果匹配则停止查找,使用它定义的配置。
  • 如果没有匹配的正则location,则使用前面记录的最长匹配前缀字符location。

可以看到:

  • 不包含正则的 location 在配置文件中的顺序不会影响匹配顺序。而包含正则表达式的 location 会按照配置文件中定义的顺序进行匹配。
  • 设置为精确匹配 (with = prefix) 的 location 如果匹配请求 URI 的话,此 location 被马上使用,匹配过程结束。
  • 在其它只包含普通字符的 location 中,找到和请求 URI 最长的匹配。如果此 server {} 没有包含正则的 location 或者该 location 启用了 ^~ 的话,这个最 长匹配的 location 会被使用。如果此 server {} 中包含正则的 location,则先在 这些正则 location 中进行匹配,如果找到匹配,则使用匹配的正则 location,如果 没找到匹配,依然使用最大匹配的 location。

有了以上的准备,开始看具体的代码实现。

ngx_http_core_loc_conf_s结构体

ngx_http_core_loc_conf_s结构体对应一个location块的配置,相关的成员如下:

struct ngx_http_core_loc_conf_s {
  ngx_str_t           name;   /* URI 部分字符串 */
  ngx_http_regex_t    *regex; /* 正则引擎编译过的 正则表达式对象 */
  ...
  unsigned            named:1;        /* @ 修饰符 */
  unsigned            noname:1;       /* if () {} */
  unsigned            exact_match:1;  /* = 修饰符 */
  unsigned            noregex:1;      /* ^= 修饰符 */

  ...
  ngx_http_location_tree_node_t   *static_location;
  ngx_http_core_loc_conf_t        **regex_location;
  void                **loc_conf;
  ...
  ngx_queue_t         *locations; /* 连接 `location` 作用域,由
                                     ngx_http_location_queue_t 强制转
                                     换而来 */
};

可以看到,在ngx_http_core_loc_conf_s中使用了几个成员named、noname、exact_match、noregex区分了以上的情况。

ngx_http_location_queue_t

结构体ngx_http_location_queue_t用于临时保存location的队列:

typedef struct {
  ngx_queue_t                 queue;
  ngx_http_core_loc_conf_t    *exact; /* exact_match, regex, named, noname */
  ngx_http_core_loc_conf_t    *inclusive; /* 非 exact 的 location */
  ngx_str_t                   *name;
} ngx_http_location_queue_t;

ngx_http_location_tree_node_t

ngx_http_location_tree_node_t结构体是最终存储location的结构体,将location以树状组织在一起,实现location的快速查找:

struct ngx_http_location_tree_node_s {
  ngx_http_location_tree_node_t   *left;
  ngx_http_location_tree_node_t   *right;
  ngx_http_location_tree_node_t   *tree;

  ngx_http_core_loc_conf_t        *exact;     // 精确匹配的location配置
  ngx_http_core_loc_conf_t        *inclusive; // inclusive匹配的location配置

  u_char                          auto_redirect;
  u_char                          len;
  u_char                          name[1];
};

构建location查找树的流程

在函数ngx_http_block中(该函数即HTTP块的入口函数),将调用两个函数进行location的初始化:

  • ngx_http_init_locations:用于完成location的排序以及分类存放。
  • ngx_http_init_static_location_trees:用于将exact以及inclusive类型的location进一步处理,构造出可以快速访问的树状结构。

ngx_http_init_locations

  • 首先调用ngx_queue_sort(locations, ngx_http_cmp_locations)函数对location队列进行排序,排序的结果为:exact(sorted) -> inclusive(sorted) -> regex -> named -> noname,这里说明一下inclusive,它表示URI之间的包含关系,即”/abc/a“这个URI是包含”/abc“的。
  • 遍历排序过后的location队列,将其中的noname类型的location分离出队列。
  • 将named类型的location分离出来,放到配置的named_locations中。
  • 将含有正则的location分离出来,放到配置的regex_locations中。

可以看到,以上流程完成之后,原先的location队列就只剩下exact以及inclusive类型的location了。接着调用ngx_http_init_static_location_trees函数做进一步的处理。

ngx_http_init_static_location_trees

有以下几个流程:

  • ngx_http_join_exact_locations:将当前虚拟主机中 uri 字符串完全一致的 exact 和 inclusive 类型的 location 进行合并。
  • ngx_http_create_locations_list:将前缀一致的location放到list链表中。
  • ngx_http_create_locations_tree:构造location的树结构。

ngx_http_create_locations_list

static void
ngx_http_create_locations_list(ngx_queue_t *locations, ngx_queue_t *q)
{
  u_char                     *name;
  size_t                      len;
  ngx_queue_t                *x, tail;
  ngx_http_location_queue_t  *lq, *lx;

  // 由于本函数存在递归调用,所以这个判断是递归的终止条件
  if (q == ngx_queue_last(locations)) {
    return;
  }

  lq = (ngx_http_location_queue_t *) q;

  if (lq->inclusive == NULL) {
    // 如果不是inclusive类型的location,直接跳过,继续队列中下一个location的处理
    ngx_http_create_locations_list(locations, ngx_queue_next(q));
    return;
  }

  len = lq->name->len;
  name = lq->name->data;

  // 从该location的下一个元素开始遍历队列
  for (x = ngx_queue_next(q);
    x != ngx_queue_sentinel(locations);
    x = ngx_queue_next(x))
  {
    lx = (ngx_http_location_queue_t *) x;

    // 找到第一个不以q的location做为前缀的location就退出循环
    // 比如当前队列location为:/a /ab /abc /b
    // 这里的q就是/a,x就是/b,中间的/ab和/abc都是以/a为前缀的,不会终止循环
    if (len > lx->name->len
      || ngx_filename_cmp(name, lx->name->data, len) != 0)
    {
      break;
    }
  }

  q = ngx_queue_next(q);

  if (q == x) { // 如果x就是q的下一个元素,说明没有找到前缀匹配的,那么直接进入x进行下次递归调用
    ngx_http_create_locations_list(locations, x);
    return;
  }

  // 到了这里说明前面找到有前缀匹配的location了

  // 这里将与q相同前缀的节点,分离出队列
  ngx_queue_split(locations, q, &tail);
  // 然后加入到q的list链表中
  ngx_queue_add(&lq->list, &tail);

  if (x == ngx_queue_sentinel(locations)) {
    ngx_http_create_locations_list(&lq->list, ngx_queue_head(&lq->list));
    return;
  }

  // 将x从队列中分离出来
  ngx_queue_split(&lq->list, x, &tail);
  // 放回到location队列中
  ngx_queue_add(locations, &tail);

  // 对lq->list做相同的操作
  ngx_http_create_locations_list(&lq->list, ngx_queue_head(&lq->list));

  // 对从x开始的剩余节点做相同的操作
  ngx_http_create_locations_list(locations, x);
}

对该函数的几个说明:

  • 由于存在递归调用,所以函数开始要做q == ngx_queue_last(locations)的判断,做为递归的终止条件。
  • 对于非 inclusive 类型 (此时 locations 队列中也只包含 exact 和 inclusive 类型的 location 节点) 的 location 节点,直接跳过,不做任何整理。
  • 从lq开始遍历队列,直到查找到第一个不以q做为前缀的location才退出循环,退出循环时保存当前位置为x。比如当前队列location为:/a /ab /abc /b,这里的q就是/a,x就是/b,中间的/ab和/abc都是以/a为前缀的,不会终止循环。
  • 将与lq前缀匹配的队列元素,放到lq的list中,同时针对这个list递归调用ngx_http_create_locations_list函数。
  • 继续针对x开始的剩余队列节点递归调用ngx_http_create_locations_list函数。

如下图所示就是ngx_http_create_locations_list调用前后的效果:

ngx-location-create-locations-list

ngx_http_create_location_trees

ngx_http_create_location_trees在上面的基础上构造location查找树

static ngx_http_location_tree_node_t *
  ngx_http_create_locations_tree(ngx_conf_t *cf, ngx_queue_t *locations,
  size_t prefix)
{
  size_t                          len;
  ngx_queue_t                    *q, tail;
  ngx_http_location_queue_t      *lq;
  ngx_http_location_tree_node_t  *node;

  // 快速确定中间节点的位置,保存到q中
  q = ngx_queue_middle(locations);

  lq = (ngx_http_location_queue_t *) q;
  // 左边元素的数量
  len = lq->name->len - prefix;

  node = ngx_palloc(cf->pool,
    offsetof(ngx_http_location_tree_node_t, name) + len);
  if (node == NULL) {
    return NULL;
  }

  node->left = NULL;
  node->right = NULL;
  node->tree = NULL;
  node->exact = lq->exact;
  node->inclusive = lq->inclusive;

  node->auto_redirect = (u_char) ((lq->exact && lq->exact->auto_redirect)
    || (lq->inclusive && lq->inclusive->auto_redirect));

  node->len = (u_char) len;
  ngx_memcpy(node->name, &lq->name->data[prefix], len);

  // 从中间节点将location分为两部分
  ngx_queue_split(locations, q, &tail);

  // 如果分离完毕location队列为空
  if (ngx_queue_empty(locations)) {
    /*
     * ngx_queue_split() insures that if left part is empty,
     * then right one is empty too
     */
    // 直接跳到构造inclusive类型的子树
    goto inclusive;
  }

  // 构造左子树
  node->left = ngx_http_create_locations_tree(cf, locations, prefix);
  if (node->left == NULL) {
    return NULL;
  }

  ngx_queue_remove(q);

  if (ngx_queue_empty(&tail)) {
    goto inclusive;
  }

  // 构造右子树
  node->right = ngx_http_create_locations_tree(cf, &tail, prefix);
  if (node->right == NULL) {
    return NULL;
  }

inclusive:
  // 到这里构造inclusive类型的树保存到tree成员中

  // list为空说明没有inclusive类型的location了
  if (ngx_queue_empty(&lq->list)) {
    return node;
  }

  node->tree = ngx_http_create_locations_tree(cf, &lq->list, prefix + len);
  if (node->tree == NULL) {
    return NULL;
  }

  return node;
}

说明:

  • 调用ngx_queue_middle快速确定locaiton队列的中间节点。
  • 从中间节点将location分为两部分。
  • 分别构造左右子树放到成员left和right中。
  • 将inclusive类型的location放入到成员tree中。

如下图所示就是ngx-location-create-locations-tree调用前后的效果:

ngx-location-create-locations-tree

查找location流程

请求的 location 匹配,在请求处理的 FIND_CONFIG 阶段相对应的 checker ngx_http_core_find_config_phase 函数中完成。ngx_http_core_find_config_phase 函数调用 ngx_http_core_find_location 函数完成实际的匹配工作。

本质上就是根据前面构建好的树结构,进行二分查找,不再阐述。

参考资料