59

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

 5 years ago
source link: https://www.codedump.info/post/20190212-nginx-http-config/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

概述

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

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

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

jE3eQz2.jpg!web

解析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

3uM7neM.png!web

另外,由于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调用前后的效果:

Jja6Jrj.png!web

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调用前后的效果:

I3y2YfU.png!web

查找location流程

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

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

参考资料


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK