这里以ngx_http_script_compile为线索,看一下nginx的变量原理中还有哪些值得挖掘的地方。
ngx_http_script_compile函数被调用,一般都是用来处理变量的,特别是在配置处理阶段,出现变量的时候(即"$"开头的配置),一般都会使用这个函数来做处理,生成所谓的“运行时处理机“。在函数的开始,有个ngx_http_script_init_arrays函数,从字面来看,我们也能大体知道它的作用,这里先暂时放一下,后面再讨论。
核心的处理就在一个for循环里。其中sc包含了我们需要的绝大部分信息,那么这个所谓的sc到底是个什么来历呢?我们以proxy_pass为例子:
ngx_http_proxy_pass:
...
/* n是对proxy_pass中出现的变量进行计数,看有几个变量要处理 */
if (n) {
ngx_memzero(&sc, sizeof(ngx_http_script_compile_t));
sc.cf = cf;
sc.source = url; // url就是proxy_pass后面配置的字符串,含有变量
/*
* 这里需要交代的是,nginx这套”运行时处理机“在处理结果的处理上是分长度和内容
* 两部分的,也就是说,获得变量实际值对应长度和内容的处理子(也就是处理函数),分别
* 保存在lengths和values中。
*/
sc.lengths = &plcf->proxy_lengths;
sc.values = &plcf->proxy_values;
sc.variables = n;
/* 这两个值是作为一次compile的结束标记,在lengths和values的最后添加一个空处理子,即NULL指针。
* 后面会讲到:在运行时处理时,即处理lengths和values的时候,碰到NULL,这次处理过程就宣告结束 */
sc.complete_lengths = 1;
sc.complete_values = 1;
if (ngx_http_script_compile(&sc) != NGX_OK) {
return NGX_CONF_ERROR;
}
return NGX_CONF_OK;
}
接下来看ngx_http_script_compile的核心处理部分:
for (i = 0; i < sc->source->len; /* void */ ) {
name.len = 0;
if (sc->source->data[i] == '$') {
// 以'$'结尾,是有错误的,因为这里处理的都是变量,而不是正则(正则里面末尾带$是有意思的)
if (++i == sc->source->len) {
goto invalid_variable;
}
#if (NGX_PCRE)
{
ngx_uint_t n;
/*
* 注意,在这里所谓的变量有两种,一种是$后面跟字符串的,一种是跟数字的。
* 这里判断是否是数字形式的变量。
*/
if (sc->source->data[i] >= '1' && sc->source->data[i] <= '9') {
n = sc->source->data[i] - '0';
if (sc->captures_mask & (1 << n)) {
sc->dup_capture = 1;
}
/*
* 在sc->captures_mask中将数字对应的位置1,那么captures_mask的作用是什么?
* 在后面对sc结构体分析时会提到。
*/
sc->captures_mask |= 1 << n;
if (ngx_http_script_add_capture_code(sc, n) != NGX_OK) {
return NGX_ERROR;
}
i++;
continue;
}
}
#endif
/*
* 这里是个有意思的地方,举个例子,假设有个这样一个配置proxy_pass $host$uritest,
* 我们这里其实是想用nginx的两个内置变量,host和uri,但是对于$uritest来说,如果我们
* 不加处理,那么在函数里很明显会将uritest这个整体作为一个变量,这显然不是我们想要的。
* 那怎么办呢?nginx里面使用"{}"来把一些变量包裹起来,避免跟其他的字符串混在一起,在此处
* 我们可以这样用${uri}test,当然变量之后是数字,字母或者下划线之类的字符才有必要这样处理
* 代码中体现的很明显。
*/
if (sc->source->data[i] == '{') {
bracket = 1;
if (++i == sc->source->len) {
goto invalid_variable;
}
// name用来保存一个分离出的变量
name.data = &sc->source->data[i];
} else {
bracket = 0;
name.data = &sc->source->data[i];
}
for ( /* void */ ; i < sc->source->len; i++, name.len++) {
ch = sc->source->data[i];
// 在"{}"中的字符串会被分离出来(即break语句),避免跟后面的字符串混在一起
if (ch == '}' && bracket) {
i++;
bracket = 0;
break;
}
/*
* 变量中允许出现的字符,其他字符都不是变量的字符,所以空格是可以区分变量的。
* 这个我们在配置里经常可以感觉到,而它的原理就是这里所显示的了
*/
if ((ch >= 'A' && ch <= 'Z')
|| (ch >= 'a' && ch <= 'z')
|| (ch >= '0' && ch <= '9')
|| ch == '_')
{
continue;
}
break;
}
if (bracket) {
ngx_conf_log_error(NGX_LOG_EMERG, sc->cf, 0,
"the closing bracket in \"%V\" "
"variable is missing", &name);
return NGX_ERROR;
}
if (name.len == 0) {
goto invalid_variable;
}
// 变量计数
sc->variables++;
// 得到一个变量,做处理
if (ngx_http_script_add_var_code(sc, &name) != NGX_OK) {
return NGX_ERROR;
}
continue;
}
/*
* 程序到这里意味着一个变量分离出来,或者还没有碰到变量,一些非变量的字符串,这里不妨称为”常量字符串“
* 这里涉及到请求参数部分的处理,比较简单。这个地方一般是在一次分离变量或者常量结束后,后面紧跟'?'的情况
* 相关的处理子在ngx_http_script_add_args_code会设置。
*/
if (sc->source->data[i] == '?' && sc->compile_args) {
sc->args = 1;
sc->compile_args = 0;
if (ngx_http_script_add_args_code(sc) != NGX_OK) {
return NGX_ERROR;
}
i++;
continue;
}
// 这里name保存一段所谓的”常量字符串“
name.data = &sc->source->data[i];
// 分离该常量字符串
while (i < sc->source->len) {
// 碰到'$'意味着碰到了下一个变量
if (sc->source->data[i] == '$') {
break;
}
/*
* 此处意味着我们在一个常量字符串分离过程中遇到了'?',如果我们不需要对请求参数做特殊处理的话,
* 即sc->compile_args = 0,那么我们就将其作为常量字符串的一部分来处理。否则,当前的常量字符串会
* 从'?'处,截断,分成两部分。*/
if (sc->source->data[i] == '?') {
sc->args = 1;
if (sc->compile_args) {
break;
}
}
i++;
name.len++;
}
// 一个常量字符串分离完毕,sc->size统计整个字符串(即sc->source)中,常量字符串的总长度
sc->size += name.len;
// 常量字符串的处理子由这个函数来设置
if (ngx_http_script_add_copy_code(sc, &name, (i == sc->source->len))
!= NGX_OK)
{
return NGX_ERROR;
}
}
// 本次compile结束,做一些收尾善后工作。
return ngx_http_script_done(sc);
上面我们分析了一个compile过程的主要工作,很显然,还有细节没有讨论到。在compile过程中,共需要处理4类:$1这样的capture变量,普通的变量($uri),args变量,常量(即常量字符串)。其实这些变量的处理过程总体来说并不算多麻烦,而有些细节确实难点。我们这里总结下,之前的博客有对变量和脚本引擎机制做过探讨了,这里把一下没有谈论到的难点和细节,或者之前不是太清楚的分析再来探讨一下,希望你我都能有所收获。
对于流程,一般gdb跟一下,配合debug日志,基本上可以理清,难点就在于一些结构中的成员,特别是有些标记位的使用,却是贯穿整个系统,在理解上有不少难度,这是我们这里讨论的重点,对于这些地方搞懂了,流程就不是什么大问题了。
首先看ngx_http_script_compile_t结构,这个结构在compile的时候被使用过。
typedef struct {
ngx_conf_t *cf; // 配置信息
ngx_str_t *source; // 需要compile的字符串
/*
* 保存普通变量在变量表中的index,关于什么是变量表,后面会讨论
*/
ngx_array_t **flushes;
ngx_array_t **lengths; // 处理变量长度的处理子数组
ngx_array_t **values; // 处理变量内容的处理子数组
ngx_uint_t variables; // 普通变量的个数,而非其他三种(args变量,$n变量以及常量字符串)
/*
* 下面三个变量放在一起讨论,他们都跟pcre的正则处理相关,这三个用到的地方比较少
*/
ngx_uint_t ncaptures; // 当前处理时,出现的$n变量的最大值,如配置的最大为$3,那么ncaptures就等于3
/*
* 以位移的形式保存$1,$2...$9等变量,即响应位置上置1来表示,主要的作用是为dup_capture准备,
* 正是由于这个mask的存在,才比较容易得到是否有重复的$n出现。
*/
ngx_uint_t captures_mask;
/*
* 这个标记位主要在rewrite模块里使用,在ngx_http_rewrite中,
* if (sc.variables == 0 && !sc.dup_capture) {
* regex->lengths = NULL;
* }
* 没有重复的$n,那么regex->lengths被置为NULL,这个设置很关键,在函数
* ngx_http_script_regex_start_code中就是通过对regex->lengths的判断,来做不同的处理,
* 因为在没有重复的$n的时候,可以通过正则自身的captures机制来获取$n,一旦出现重复的,
* 那么pcre正则自身的captures并不能满足我们的要求,我们需要用自己handler来处理。
*/
unsigned dup_capture:1;
ngx_uint_t size; // 待compile的字符串中,”常量字符串“的总长度
/*
* 对于main这个成员,有许多要挖掘的东西。main一般用来指向一个
* ngx_http_script_regex_code_t的结构,那么这个main到底起到了什么作用呢?
* 这里有对它进行分析。
*/
void *main;
unsigned compile_args:1; // 是否需要处理请求参数
unsigned complete_lengths:1; // 是否设置lengths数组的终止符,即NULL
unsigned complete_values:1; // 是否设置values数组的终止符
unsigned zero:1; // values数组运行时,得到的字符串是否追加'\0'结尾
unsigned conf_prefix:1; // 是否在生成的文件名前,追加路径前缀
unsigned root_prefix:1; // 同conf_prefix
unsigned args:1; // 待compile的字符串中是否发现了'?'
} ngx_http_script_compile_t;
在函数ngx_http_script_add_var_code中,用到了ngx_http_core_main_conf_t(后面以cmcf代替)中的variables,即所谓的全局变量数组,
由于是cmcf,以为着在所有的server块,location块,包括upstream块里面,都是可见的,即一方修改,便会在其他地方呈现出变化的道理。
在各个module中的preconfiguration函数里,都会将该module预设的一些全局变量,放到cmcf->variables_keys中。另外一个重要的成员就是
cmcf->variables,前面提到cmcf->variables_keys是所有预设的变量(和通过set指令设置的),而cmcf->variables则是配置中实际用到的变量。放到cmcf->variables中的变量实际上是先占个位置,这些变量的更多信息,来源于cmcf->variables_keys,所以在配置解析结束之后,通过ngx_http_variables_init_vars函数来填充这个变量的各个重要信息。
在r中,也有一个variables成员,它是个数组,而且数组成员的个数跟cmcf->variabels是一样的,区别在于cmcf->variabels的成员类型是:
struct ngx_http_variable_s {
ngx_str_t name; /* 变量的字符串值 */
ngx_http_set_variable_pt set_handler; /* 使用变量中的值设置request的某个成员的值 */
ngx_http_get_variable_pt get_handler; /* 根据request中成员(如uri,args等)的值来设置,r->variables中对应变量的内容 */
uintptr_t data; /* 在set和get操作中使用,一般是r中某个成员在request结构中的offset */
ngx_uint_t flags; /* 一些在set和get中控制特定动作的标志,后面会讲到 */
ngx_uint_t index; /* 某个变量在r->variabels或者cmcf->variabels中数组中的下标 */
};
而r->variables的成员类型是:
typedef struct {
unsigned len:28; /* 变量值的长度 */
unsigned valid:1; /* 变量是否有效 */
unsigned no_cacheable:1; /* 变量是否是可缓存的,一般来说,某些变量在第一次得到变量值后,后面再次用到时,可以直接使用上
* 次的值,而对于一些所谓的no_cacheable的变量,则需要在每次使用的时候,都要通过get_handler之
* 类操作,再次获取
*/
unsigned not_found:1; /* 变量没有找到,一般是指某个变量没用能够通过get获取到其变量值 */
unsigned escape:1; /* 变量值是否需要作转义处理*/
u_char *data; /* 变量值 */
} ngx_variable_value_t;
这两个结构的关系很密切,一个所谓变量,一个所谓变量值,所以nginx建立在变量上的这套处理机制,就像一个预定好的方程,类似的运算过程,以不同的变量值来运行,获得我们想要的不同结果。