nginx配置文件解析

【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>> hot3.png

        在我们使用nginx的过程中,配置文件可以说是我们接触最为频繁的一个部分,在我们配置完相应的配置项之后,一般都会使用./sbin/nginx -t命令来测试配置文件是否有参数错误,然后再重新加载nginx。本文首先会以一个示例对nginx配置文件的配置方式进行讲解,然后会从源码的角度对nginx配置文件的解析原理进行阐述。

1. 使用示例

1.1 配置文件使用示例

daemon off;
error_log  stderr info;

events {
    worker_connections  1024;
}

http {
    proxy_cache_path /nginx-cache levels=1:2 keys_zone=one:10m max_size=10g inactive=60m;
    sendfile        on;
    keepalive_timeout  65;

    server {
        listen       8080;
        server_name  localhost;

        location / {
            root   html;
        }
    }
}

        在抛开每个配置项具体的含义的情况下,关于nginx的配置规则,这里主要有如下几点需要说明:

  • nginx的配置项主要分为两种类型:配置块和配置项。比如这里的http {}就是一个配置块,而daemon off;就是一个配置项;
  • 每个配置项都是由一个模块负责解析的,该模块将会定义这个配置项的配置方式,主要有开关标记、时间、空间和一般配置项等类型:
    • 开关标记:比如这里的sendfile指令后面就只能有一个参数,而且这个参数必须为on或者off;
    • 时间:比如这里的proxy_cache_path指令后面有一个inactive=60m,这里的60m表示60分钟,其单位可以是yMwdhMs ,含义分别表示年、月、星期、天、小时、分钟、秒、结尾标记;
    • 空间:比如这里sendfile指令的max_size=10g;,这里的10g就表示空间大小,其单位可以是kmg,含义分别表示空间大小单位的kbmbgb
    • 一般配置:比如这里的root html;,其后的html就是一个一般的配置项;
  • 每个模块在指定其可以解析的配置项的时候,还会指定其配置项的参数个数,nginx在解析之前都会检查每个配置项的个数是否为指定个数,如果不是,则不会进行后续的检查。比如这里的keepalive_timeout 65;,其配置项个数必须为2(这里的配置项名也计算在内);
  • 每个配置项都必须以分号;结尾,表示这是一个完整的配置项;
  • 每个配置项的每个参数都必须以空格分隔,如果配置项中也有空格,则使用转义字符进行转义;

1.2 nginx配置项配置示例

        关于nginx是如何指定每个配置项的处理方式的,这里我们以daemon off;这个配置项进行举例。在nginx中,每个配置项都会从属于某个模块,而该模块将会配置改配置项所对应的ngx_command_t结构体,该结构体中会指定该配置项的类型、参数个数、配置项的解析方式等等属性。如下是daemon这个配置项的配置方式:

static ngx_command_t ngx_core_commands[] = {
    {ngx_string("daemon"),
     NGX_MAIN_CONF | NGX_DIRECT_CONF | NGX_CONF_FLAG,
     ngx_conf_set_flag_slot,
     0,
     offsetof(ngx_core_conf_t, daemon),
     NULL}
};

        关于上述每个配置项的含义如下:

  • ngx_string("daemon"):指定了当前配置项的名称,也即daemon
  • NGX_MAIN_CONF|NGX_DIRECT_CONF|NGX_CONF_FLAG:这里的NGX_MAIN_CONF指定了当前配置项是nginx核心模块的配置,也即其配置的位置必须是在配置文件的最外层,NGX_DIRECT_CONF指定了解析当前配置项最后生成的结构体对象在nginx的cycle对象中的寻址方式,这里是直接寻址,NGX_CONF_FLAG指定了当前配置项的参数配置方式,即其是FLAG类型的,也即其值必须为on或者off
  • ngx_conf_set_flag_slot:这是一个方法,该方法定义了该如何解析这个配置项,以及会生成什么样的结构体对象来保存解析的结果。需要注意的是,在nginx进行配置文件的解析的时候,当匹配到当前配置文件中的某个配置项是与某个模块中的配置项相一致的时候,就会调用该模块中当前属性定义的方法来将该配置项的解析权限下放。通过这种方式,各个模块就可以根据自身的功能要求来自定义该配置项的配置方式和解析方式;
  • 0:其属性名为conf,表示当前的配置解析后的配置对象的存储位置,一般被NGX_HTTP_MODULE模块使用,由于该模块划分了main、server和location三个部分,而这三个部分都有各自独立的内存池部分,因而这里conf属性就是指定其所属的内存池部分的,其一般情况下会取三个值NGX_HTTP_MAIN_CONF_OFFSET、NGX_HTTP_SRV_CONF_OFFSET和NGX_HTTP_LOC_CONF_OFFSET,分别对应这三个内存池部分,当然,其取值也可以为0,为0则表示当前是核心模块在使用的;
  • offsetof(ngx_core_conf_t, daemon):该配置项的含义为偏移量,其实就是当前的属性在定义该配置项的结构体中的位置。因为在nginx解析配置项的时候,每个模块一般都会自定义一个结构体用于存储解析出来的属性参数,而当前的配置项的参数可能是该结构体中的某个属性,这里的offset就是指定了该属性在该结构体中的位置的,这里也就是daemon属性在ngx_core_conf_t结构体中的位置。有的时候,我们解析出来的配置项是一个更复杂的结构体,用偏移量已无法表示,此时偏移量就可以置为0;
  • NULL:这里的属性名是post,其为一个指针,主要的作用是在我们解析配置数据的时候,可能需要依赖某些数据,这里的post就是指向这些数据的,大多数时候都不需要指定该参数值。

        上面我们通过一个示例对ngx_command_t结构体的各个属性进行了讲解,对于配置的解析,我们需要着重强调的是前三个参数,在nginx解析配置文件的时候,比如解析到了daemon off;这个配置项,此时就会在所有的模块的所有ngx_command_t结构体数组中进行查找,看是否有一个的名称与类型(这里的NGX_MAIN_CONF)与当前解析的配置项相匹配,如果匹配,则使用该ngx_command_t结构体中的set属性所指向的方法(比如这里的ngx_conf_set_flag_slot()方法)对当前的配置项进行解析。

2. 源码解析

        nginx对于配置项的解析,主要是通过ngx_conf_file.c文件的ngx_conf_parse()方法进行的。如下是该方法的源码:

char *ngx_conf_parse(ngx_conf_t *cf, ngx_str_t *filename) {
  char *rv;
  ngx_fd_t fd;
  ngx_int_t rc;
  ngx_buf_t buf;
  ngx_conf_file_t *prev, conf_file;
  enum {
      parse_file = 0,
      parse_block,
      parse_param
  } type;

#if (NGX_SUPPRESS_WARN)
  fd = NGX_INVALID_FILE;
  prev = NULL;
#endif

  if (filename) {

    /* open configuration file */
    // 打开配置文件
    fd = ngx_open_file(filename->data, NGX_FILE_RDONLY, NGX_FILE_OPEN, 0);
    if (fd == NGX_INVALID_FILE) {
      ngx_conf_log_error(NGX_LOG_EMERG, cf, ngx_errno,
                         ngx_open_file_n " \"%s\" failed",
                         filename->data);
      return NGX_CONF_ERROR;
    }

    prev = cf->conf_file;

    cf->conf_file = &conf_file;
    // 检查配置文件的状态
    if (ngx_fd_info(fd, &cf->conf_file->file.info) == NGX_FILE_ERROR) {
      ngx_log_error(NGX_LOG_EMERG, cf->log, ngx_errno,
                    ngx_fd_info_n
                        " \"%s\" failed", filename->data);
    }

    cf->conf_file->buffer = &buf;

    buf.start = ngx_alloc(NGX_CONF_BUFFER, cf->log);
    if (buf.start == NULL) {
      goto failed;
    }

    buf.pos = buf.start;
    buf.last = buf.start;
    buf.end = buf.last + NGX_CONF_BUFFER;
    buf.temporary = 1;

    cf->conf_file->file.fd = fd;
    cf->conf_file->file.name.len = filename->len;
    cf->conf_file->file.name.data = filename->data;
    cf->conf_file->file.offset = 0;
    cf->conf_file->file.log = cf->log;
    cf->conf_file->line = 1;

    type = parse_file;

    if (ngx_dump_config
#if (NGX_DEBUG)
      || 1
#endif
        ) {
      if (ngx_conf_add_dump(cf, filename) != NGX_OK) {
        goto failed;
      }

    } else {
      cf->conf_file->dump = NULL;
    }

  } else if (cf->conf_file->file.fd != NGX_INVALID_FILE) {

    type = parse_block;

  } else {
    type = parse_param;
  }


  for (;;) {
    /**
     * 这里主要是读取配置文件的某一个数据单元进行解析,所谓的数据单元指的是:
     * 1. 以分号结尾的某一个配置项;
     * 2. 以大括号开头的配置项;
     * 3. 以大括号结尾的字符;
     * 4. 整个配置文件读取完毕的大括号;
     * 其中对于第一种和第二种情况,在解析出具体的配置数据之后,会在所有的module中查找当前有哪一个配置项
     * 与这个配置项相匹配,如果匹配,则交由该模块的handler处理该配置项,这里需要说明的是,对于配置块,
     * 在在对应的module的handler中会进一步解析该配置块中包含的配置信息,
     * 而解析的方式也还是通过ngx_conf_read_token()方法。
     * 对于第三种和第四种情况,其实本质上第三种情况只会在具体的handler中处理时才会出现,
     * 即用于表征解析某个配置块完毕了;
     * 而第四种情况,则是在最终所有的配置项都解析完毕之后,主流程中会判断当前已经没有可解析的字符,
     * 并且最后一个字符是反大括号时才会返回。
     */
    rc = ngx_conf_read_token(cf);
    if (rc == NGX_ERROR) {
      goto done;
    }

    // 对于当前配置块解析完成的情况,直接走到最后一步
    if (rc == NGX_CONF_BLOCK_DONE) {

      if (type != parse_block) {
        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "unexpected \"}\"");
        goto failed;
      }

      goto done;
    }

    // 对于整个配置文件都解析完成的情况,也直接走到最后
    if (rc == NGX_CONF_FILE_DONE) {

      if (type == parse_block) {
        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                           "unexpected end of file, expecting \"}\"");
        goto failed;
      }

      goto done;
    }

    // 这里表示当前的配置项其后接的是一个左大括号,而大括号内的内容,则交由具体的handler进行
    if (rc == NGX_CONF_BLOCK_START) {

      if (type == parse_param) {
        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                           "block directives are not supported "
                           "in -g option");
        goto failed;
      }
    }

    // 走到这里说明当前的返回值是NGX_OK或者NGX_CONF_BLOCK_START,也即具体的配置项或者配置块。
    // 这里的handler默认是空的,如果不为空,那么所有的配置项的处理都将交由该handler进行处理
    if (cf->handler) {
      if (rc == NGX_CONF_BLOCK_START) {
        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "unexpected \"{\"");
        goto failed;
      }

      rv = (*cf->handler)(cf, NULL, cf->handler_conf);
      if (rv == NGX_CONF_OK) {
        continue;
      }

      if (rv == NGX_CONF_ERROR) {
        goto failed;
      }

      ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, rv);

      goto failed;
    }

    // 这里开始查找当前解析出来的配置项是哪一个module所指定的,然后交由该module对应的处理方法进行处理
    rc = ngx_conf_handler(cf, rc);

    if (rc == NGX_ERROR) {
      goto failed;
    }
  }

  failed:

  rc = NGX_ERROR;

  done:

  // 进行解析完成的清理工作,比如清理缓冲区,释放文件句柄等
  if (filename) {
    if (cf->conf_file->buffer->start) {
      ngx_free(cf->conf_file->buffer->start);
    }

    if (ngx_close_file(fd) == NGX_FILE_ERROR) {
      ngx_log_error(NGX_LOG_ALERT, cf->log, ngx_errno,
                    ngx_close_file_n
                        " %s failed",
                    filename->data);
      rc = NGX_ERROR;
    }

    cf->conf_file = prev;
  }

  if (rc == NGX_ERROR) {
    return NGX_CONF_ERROR;
  }

  return NGX_CONF_OK;
}

        对于上面的解析过程,这里有几个方法需要注意一下:

  • ngx_conf_read_token():这个方法的主要作用是读取一个token,需要注意的是,对于nginx而言,配置项有两种类型:配置块和配置项,比如http {}daemon off;。这两个都属于token,而且需要注意的是对于配置块http而言,只有该字符串才属于token,而其内部的各个配置项,则是交由http这个配置项对应的ngx_command_t.set()方法进行解析的,另外,daemon off;整个属于一个配置项。
  • ngx_conf_handler():这个方法的主要作用就是对解析出来的token进行配置项的匹配,也即查找所有模块定义的ngx_command_t数组,看其与哪一个相匹配,然后就将该配置项的解析交由该ngx_command_t.set()方法进行处理;

        关于ngx_conf_read_token()方法,其主要作用就是不断读取配置文件的各个字符,然后进行组装,读者有兴趣可以自行阅读其源码。这里我们主要看ngx_conf_handler()方法的实现原理:

static ngx_int_t ngx_conf_handler(ngx_conf_t *cf, ngx_int_t last) {
  char *rv;
  void *conf, **confp;
  ngx_uint_t i, found;
  ngx_str_t *name;
  ngx_command_t *cmd;

  name = cf->args->elts;

  found = 0;

  for (i = 0; cf->cycle->modules[i]; i++) {

    cmd = cf->cycle->modules[i]->commands;
    if (cmd == NULL) {
      continue;
    }

    for ( /* void */ ; cmd->name.len; cmd++) {

      if (name->len != cmd->name.len) {
        continue;
      }

      // 遍历每个module的每个command,以查找哪个command与当前解析出来的配置项相匹配
      if (ngx_strcmp(name->data, cmd->name.data) != 0) {
        continue;
      }

      found = 1;

      if (cf->cycle->modules[i]->type != NGX_CONF_MODULE
          && cf->cycle->modules[i]->type != cf->module_type) {
        continue;
      }

      /* is the directive's location right ? */

      if (!(cmd->type & cf->cmd_type)) {
        continue;
      }

      // 检查当前的command是否是用于配置块的,并且检查解析出来的配置是配置块
      if (!(cmd->type & NGX_CONF_BLOCK) && last != NGX_OK) {
        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                           "directive \"%s\" is not terminated by \";\"",
                           name->data);
        return NGX_ERROR;
      }

      // 检查command类型是配置块,并且解析出来的配置项如果不是配置块,则返回异常
      if ((cmd->type & NGX_CONF_BLOCK) && last != NGX_CONF_BLOCK_START) {
        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                           "directive \"%s\" has no opening \"{\"",
                           name->data);
        return NGX_ERROR;
      }

      /* is the directive's argument count right ? */

      // 这里的NGX_CONF_ANY表示该配置块可以有可变的多个参数
      if (!(cmd->type & NGX_CONF_ANY)) {

        // NGX_CONF_FLAG表示当前配置项的值必须为on或者off
        if (cmd->type & NGX_CONF_FLAG) {
          // 检查当前的配置项的数目必须为2
          if (cf->args->nelts != 2) {
            goto invalid;
          }

          // NGX_CONF_1MORE表示配置项的个数大于等于2
        } else if (cmd->type & NGX_CONF_1MORE) {
          // 检查配置项的个数必须大于等于2
          if (cf->args->nelts < 2) {
            goto invalid;
          }

          // NGX_CONF_2MORE表示配置项的个数必须大于等于3
        } else if (cmd->type & NGX_CONF_2MORE) {
          // 检查配置项的个数
          if (cf->args->nelts < 3) {
            goto invalid;
          }

          // NGX_CONF_MAX_ARGS表示nginx每个配置项最多能够使用的配置个数,默认为8
        } else if (cf->args->nelts > NGX_CONF_MAX_ARGS) {

          goto invalid;

          // 这里的argument_number是一个固定的数组,表示当前配置项的个数必须为某个具体的值
        } else if (!(cmd->type & argument_number[cf->args->nelts - 1])) {
          goto invalid;
        }
      }

      /* set up the directive's configuration context */

      conf = NULL;

      // 查找配置对象,NGX_DIRECT_CONF常量单纯用来指定配置存储区的寻址方法,只用于core模块
      if (cmd->type & NGX_DIRECT_CONF) {
        conf = ((void **) cf->ctx)[cf->cycle->modules[i]->index];

        // X_MAIN_CONF常量有两重含义,其一是指定指令的使用上下文是main(其实还是指core模块),
        // 其二是指定配置存储区的寻址方法。
      } else if (cmd->type & NGX_MAIN_CONF) {
        conf = &(((void **) cf->ctx)[cf->cycle->modules[i]->index]);

        // 除开core模块,其他类型的模块都会使用第三种配置寻址方式,也就是根据cmd->conf的值
        // 从cf->ctx中取出对应的配置。举http模块为例,cf->conf的可选值是
        // NGX_HTTP_MAIN_CONF_OFFSET、NGX_HTTP_SRV_CONF_OFFSET、NGX_HTTP_LOC_CONF_OFFSET,
        // 分别对应“http{}”、“server{}”、“location{}”这三个http配置级别。
      } else if (cf->ctx) {
        confp = *(void **) ((char *) cf->ctx + cmd->conf);

        if (confp) {
          conf = confp[cf->cycle->modules[i]->ctx_index];
        }
      }

      // 这里通过匹配得到的command,调用其对应的handler,从而进行相应的配置项的处理逻辑
      rv = cmd->set(cf, cmd, conf);

      if (rv == NGX_CONF_OK) {
        return NGX_OK;
      }

      if (rv == NGX_CONF_ERROR) {
        return NGX_ERROR;
      }

      ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                         "\"%s\" directive %s", name->data, rv);

      return NGX_ERROR;
    }
  }

  if (found) {
    ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                       "\"%s\" directive is not allowed here", name->data);

    return NGX_ERROR;
  }

  ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                     "unknown directive \"%s\"", name->data);

  return NGX_ERROR;

  invalid:

  ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                     "invalid number of arguments in \"%s\" directive",
                     name->data);

  return NGX_ERROR;
}

        这里ngx_conf_handler()方法主要完成了三部分的工作:

  • 首先会尝试查找与当前配置项相匹配的ngx_command_t结构体;
  • 然后检查当前当前配置项配置的位置是否与ngx_command_t结构体中定义的一致,并且会检查当前配置项的参数个数是否与配置的一致;
  • 最后,如果前置检查都完成了,则会调用ngx_command_t.set()方法完成对当前配置项的解析。

        另外需要说明的是,上面的代码主要是解析某个配置项的,对于配置块的解析,其实本质上也是调用这一块的方法完成,因为在nginx解析配置项的时候,其是一步一步进行的,比如当前解析到http这个配置块的初始位置,此时其是记录了当前的解析点的,因而在调用http对应的ngx_command_t结构体的set()方法的时候,其会根据当前解析到的位置继续往下解析,而后面的解析工作其实还是一个一个配置项的解析,因而还是调用上述的方法进行。

3. 小结

        本文首先对nginx配置文件的基本配置方式进行了讲解,然后使用一个示例介绍了ngx_command_t结构体各个属性的含义,最后从源码的角度讲解了nginx是如何解析各个配置项的。

你可能感兴趣的:(nginx配置文件解析)