欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Nginx中变量的实现(上)

程序员文章站 2022-05-22 21:17:58
...

上一篇主要描述的是变量的使用,所以没涉及任何代码,而这一篇主要描述变量的实现原理,避免不了会涉及到一些底层代码,对于不了解c语言的同学读起来可能会有点吃力,这部分同学可以尝试一下两篇结合着读,比如先读一个知识点的用法,然后再回到这篇来看一下实现原理,以此来加深理解。如果在读的过程中你有发现任何问题,还请反馈给我,我会非常感激。

1.ngx内部如何表示变量

    nginx内部用了两种结构体来表示变量,一个用来表示变量的名字,另一个用来表示变量的值,分别是:

src/http/ngx_http_variables.h/ngx_http_variable_t;
src/http/ngx_http_variables.h/ngx_http_variable_value_t;

 
nginx内部在定义变量的时候,实际上就是在创建ngx_http_variable_t结构体。而代表变量值的结构体ngx_http_variable_value_t,则是通过绑定在ngx_http_variable_t结构体上的get_handler()方法创建或获取的。比如set指令:

set $a “I am var”;

 
该指令用来定义一个变量“$a”,并且赋值为“I am var”。

    按照上面的描述,当解析到这个set指令的时候,在nginx内部会创建一个ngx_http_variable_t结构体表示变量名“a”,而当需要获取这个变量值的时候,会调用这个变量上绑定的get_handler()方法,该方法会创建或者从缓存中获取一个ngx_http_variable_value_t结构体,对应的变量值“I am var”就存在于这个值结构体中。

把这两个结构体的代码贴过来看看它更详细的表示。

表示变量名的结构体有如下字段:

typedef struct ngx_http_variable_s  ngx_http_variable_t;
struct ngx_http_variable_s {
    ngx_str_t                name;
    ngx_http_set_variable_pt set_handler;
    ngx_http_get_variable_pt get_handler;
    uintptr_t                data;
    ngx_uint_t               flags;
    ngx_uint_t               index;
};

 
表示变量值的结构体有如下字段:

typedef ngx_variable_value_t  ngx_http_variable_value_t;
typedef struct {
    unsigned    len:28;
    unsigned    valid:1;
    unsigned    no_cacheable:1;
    unsigned    not_found:1;
    unsigned    escape:1;
    u_char     *data;
} ngx_variable_value_t;


其中ngx_http_variable_s#name字段用来存放变量的名字“a”,而变量值则用到了ngx_http_variable_value_t结构体中的两个字段len和data,data用来指向变量值字符串的首地址,len则表示字符的长度。
 
1.1 nginx中的变量值都是字符型的吗?
    从上面两个结构体中可以看到,用来存放变量值的字段是一个无符号char类型的指针(c中一般用它来表示字符串),该类型的指针类似于java语言中的String类型,例如:  

u_char  *data = “hahaha”;
String   data = “hahaha”:

 
这两句的作用都是定义并初始化一个字符串,表面上看我们会认为nginx中的变量“应该”只有字符变量这一种类型,但是上一篇我们也提到了一个非字符型变量“${binary_remote_addr}”的例子,能够出现这种情况其实来自于C语言的灵活性。下面通过一个片段代码来说明它是怎么做到的: 

src/http/ngx_http_variables.c/ngx_http_variable_binary_remote_addr()
v->len = sizeof(in_addr_t);
v->data = (u_char *) &sin->sin_addr;

 
其中v是ngx_http_variable_value_t对象,可以看到nginx把代表网络地址(sin->sin_addr)的这个变量的本身的地址转换成了一个u_char类型的指针,并赋值给v->data字段,这种转换在c语言中是合法的,然后v->len字段表示了这个网络地址所占的字节个数(sin->sin_addr其实就是in_addr_t类型的),最终结果就是用一个char类型的指针指向了内存中的一块空间,而这个空间里面存放的且不是我们认知上的字符数据。如果读者对c语言不太了解,可能仍然没办法理解为什么一个字符类型的变量可以表示非字符类型的数据,这个其实涉及到了c语言中的类型强转,下面简单介绍一下。

    熟悉java语法的读者应该都知道,在java中数据类型相互转换是有严格约束的,即使看上去相同的数字,如果类型不同,也没办法相互转换,比如:    

Integer aa = 23;
Short bb = 23;

 
这两个变量都是在表示23这个数字,但是在java中试图把aa强转成Short和试图把bb强转成Integer是行不通的,例如:

bb = (Short)aa;
aa = (Integer)bb;

 
这两种强转都会报错,正确的做法是使用这两种数据类型自带的xxxValue()方法,比如:

bb = aa.shortValue();
aa = bb.intValue();

 
但是对于两个毫不相干的数据类型是不可能有类似的xxxValue()方法的,比如数据类型Integer和HashMap,在java现有的框架内,这两种类型根本没有相互强转的可能性,所以java在编译阶段就会杜绝这种情况发生。

但是在c语言中这些所谓的强转约束几乎是不存在的,你可以把任意类型转换成其它类型,比如:  

unsigned int a = 255;
unsigned char b =12;
b = (unsigned char)a;
printf("a=%d b=%d\n",a,b);


看,我们把一个int类型数据强转成了一个char类型数据,并且编译没有报错。你甚至可以把一个指针类型转换成int类型,比如:

unsigned int a = 255;
char *aa = "abc";
a = (int)aa;
printf("a=%d aa=%s\n",a,aa);

 
这些在c中都是合法的,并且也可以输出结果。

   上面的两个例子虽然可以强转成功,但最终结果且不一定如你所料。对于第一个例子我们可看到最终a和b都打印出了正确的结果255,但是当我们把变量a设置为256的时会发现这次的输出结果b变成了0。另一个例子中a显然不会输出“abc”这个字符串,而是一个很大的数字。我们暂且称其为强转的“副作用”,这是c语言中类型相互强转的“代价”。出现这种情况是因为在c语言的类型强转中,对应的数据并没有发生任何变动,改变的只是对原有数据的解释。这个其实有点指鹿为马的意思,比方说A和B两个人同时看到一个动物,A说这是一匹马,而B说这是一只鹿,最终不管A和B怎么解释这个动物,动物本身是不会发生任何改变的,改变的只有A和B两个人各自的认知。

   我们再举一个更实际的例子来说明c语言中类型强转的特性,在c中一个无符号的int类型数据可以如下表示:

unsigned int a = 255;

 
而在内存中他实际上是使用二进制表示的,并且占用了4个字节内存,表示如下:

1111 1111 0000 0000 0000 0000 0000 0000
(从左到右,左边是低地址,右边是高地址,后续都是这个顺序)

 
而一个无符号的char类型的数据在内存中只占用了1个字节内存,表示如下:

unsigned char b = 255;
1111 1111


根据转换规则可以知道,当int类型转换成char类型的时候,int类型数据本身并没有变,只是char类型变量在解释int类型的原始数据时候看不到其后面三个字节的数据,因为char类型的眼里只能看到一个字节的长度。所以我们可以得出结论,当int类型的值在char类型的可表示范围内是可以正确转换的,一旦超出char类型的表示范围则会发生意想不到的事。比如256这个数字在内存中的二级制表示如下:

0000 0000 1000 0000 0000 0000 0000 0000

 

可以看到它的前八位都是0,而char类型数据只能看到一个字节八位的数据,所以这时候256转成char的时候就变成了0。

   通过上面的描述,读者对c中的类型强转应该会有一个大致的了解,专门把这个知识点拿出来解释一遍其实是想告诉读者,虽然nginx中变量值使用字符类型来定义的,但你仍然可以利用c语言的特性,用它来表示任何类型的变量值,比方说你可以用它来存放一个结构体:  

ngx_http_variable_t  my_struct;
v->len = sizeof(ngx_http_variable_t);
v->data = (u_char *) &my_struct;

 
如此我们就用data表示了一个结构体类型。但是大部分情况下是不建议这么做的,因为从nginx整个框架来看,其实它无意把变量分成多种类型,大部分情况下字符类型就够了,所以我们在开发的时候最好也遵守这个规则。
 

2.如何定义一个变量
    在nginx中使用一个变量之前需要先定义(创建),否则nginx会无法启动。而定义变量的方式又有两种:一种是nginx中的内置变量,这些变量都是“隐性”创建的,不需要使用者明确去定义,各个模块会各自创建自己的内置变量,并在合适的时机将其注册到nginx中。另一种是通过指令的方式“显示”的在nginx的配置文件中定义某个变量,比如ngx_http_rewrite模块中的set指令、ngx_http_geo模块中的geo指令,他们在内部一般会通过nginx提供的公共api来创建和注册变量。

    不管使用哪种方式,最终都是创建变量对应的结构体,并且把变量对应的结构体注册到相应的容器(cmcf->variables_keys ,后续会用它来代表该容器)中,然后由该容器统一管理。

在nginx中每定义一个变量都要创建一个ngx_http_variable_t结构体,比如:

set $a “I am a”;
set $b “I am b”;

 
当nginx解析到这两条指令的时候就会为这两个变量分别创建一个对应的ngx_http_variable_t结构体,然后将其注册到对应的容器中。但是对应的变量值结构体ngx_http_variable_value_t并不会在此时创建,我们之前也说过,变量值是通过变量对应的get_handler()方法动态生成的,这个在后面的小节会讲到。

2.1 ngx_http_add_variable()方法
    nginx为各个模块提供了一个公共方法ngx_http_add_variable()来创建并注册变量,这个方法一般用在需要在配置文件创建自定义变量的模块中,是其它模块将自己的变量放入到nginx框架中的一个入口,比如ngx_http_rewrite模块的set指令、ngx_http_geo的geo指令等。

该方法的原型如下: 

ngx_http_variable_t * ngx_http_add_variable(ngx_conf_t *cf, ngx_str_t *name, ngx_uint_t flags)

 
其中name是要创建的变量的名字,会被设置到ngx_http_variables_t#flags字段中;而flags则用来为将要创建的变量打标记,会被设置到ngx_http_variables_t#flags字段中。

目前在nginx中可用的标记(ngx_http_variables_t#flags)有四种:

      NGX_HTTP_VAR_CHANGEABLE:该标记表示变量是可变的,也就是说一旦某个变量在创建的时候打上了这个标记,随后再次试图调用该方法创建同名变量时,该变量会被覆盖。反之,如果没有这个标记,那么在随后试图创建同名变量时nginx都会报错,并打印一条错误日志: 

nginx: [emerg] the duplicate "xx" variable in /path /conf/nginx.conf:48

 
这条日志会提示你这是一个重复的变量,这就是该标记的意义所在。Nginx中大部分内置变量会被打上这个标记,比如“$uri”这个内置变量。

      NGX_HTTP_VAR_NOCACHEABLE:该标记用来表示变量是否可缓存,如果有则表示该变量不可缓存,反之则表示可以缓存。对于不可缓存的变量,每次获取变量值都会调用该变量对应的handler方法(比如以“arg_”开头的动态变量),也就是ngx_http_variables_t结构体中get_handler字段对应的方法。如果变量在创建的时候没有打这个标记,则表示这个变量是可以缓存的,比如rewirte模块的set指令定义的变量就没有打这个标记。

   还有另外两个标记分别是NGX_HTTP_VAR_INDEXED和NGX_HTTP_VAR_NOHASH,一个表示变量是可索引的,另一个表示变量不需要放到hash结构体中。这两个标记主要在ssi模块中会用到,等后续讲解ssi模块时再做详细介绍,这里就不再赘述了。

    nginx中大部分内置变量的创建和注册并没有使用ngx_http_add_variable() 方法,比如http核心模块中的变量,它在src/http/ngx_http_variables.c文件中定义了一个ngx_http_variable_t类型的数组,每个变量需要的必要信息都直接硬编码到了代码里,然后会在某个合适的时机把数组中所有的变量全部注册到存放变量的容器中,这个容器跟ngx_http_add_variable()方法中用到的是同一个。

    nginx在创建内置变量的时候没有使用ngx_http_add_variable()方法也确实因为没有那个必要,因为定义一个变量的最关键一个未知因素----变量名字,在内置变量中是已经被确认了的,它不像自定义变量那样,需要在配置文件解析时才能确认变量的名字。

 
3.使用变量
    在nginx配置中如何使用一个变量在上一篇文章中已经介绍过了,我们这里就不再重复介绍了,这里主要介绍在nginx内部是如何做的。比如像这样一个指令:

return 200 “$uri”;


通过其配置可以知道,当用户请求到该指令所在的范围内的时候,它会打印出这个变量所代表的当前uri值。本小节会重点介绍一下在打印之前,这个变量值在nginx内部是如何获取到的。

3.1注册(索引)变量
    在前面介绍如何定义变量的时候提到了ngx_http_add_variable()方法,我们说该方法的作用是创建变量,并将变量注册到一个容器中。这里的“注册”听起来似乎没有什么不妥之处,毕竟这是该变量第一次出现在系统内。但是如果要让这个变量能够被使用,还需要一个“二次注册”,也可以称之为“索引”变量。这个“二次注册”也是要把变量注册到一个容器中,但是这个容器(其实就是一个数组cmcf->variables)跟创建变量时注册的容器(cmcf->variables_keys)是不一样的,他是为了专门索引变量(为变量创建索引)而存在的,后续在使用变量的都会通过这个索引值。

nginx专门为索引变量提供了一个ngx_http_get_variable_index()方法,该方法的声明如下:

ngx_int_t ngx_http_get_variable_index(ngx_conf_t *cf, ngx_str_t *name)

 
这个方法的逻辑比较简单,下面我们简单描述一下它的基本逻辑:
    1.先判断这个容器是否存在,如果不存在则创建该容器。
    2.如果该容器存在,则检查当前要索引的变量name是否已经存在该容器中,如果变量name已经存在该容器中,则直接返回该变量在容器中的索引值。
    3.如果变量name不在该容器中,则创建一个代表该变量的ngx_http_variable_t结构体,然后将其放入到该容器中。
    4.返回新建变量结构体在容器中的索引值。
 
3.2通过ngx_http_get_indexed_variable()方法获取变量值
    当变量被索引完后会返回一个索引值,ngx_http_get_indexed_variable()方法就是根据这个索引值来获取变量值的。

来看一下这个方法的原型:  

ngx_http_variable_value_t * ngx_http_get_indexed_variable(ngx_http_request_t *r, ngx_uint_t index)

 
这个方法有两个入参:一个是index,也就是要获取的变量的索引值;另外一个是nginx内部用来表示一个请求的结构体对象(ngx_http_request_t),它里面保存了当前请求的所有信息,而这个方法中用到了里面的一个r->variables字段,variables实际上就是一个数组,数组大小和用来索引变量用的容器大小是一样的,不同的是一个存放的是变量值(ngx_http_variable_value_t对象),一个存放的是变量名(ngx_http_variable_t对象)。另一个需要注意的是,对于同一个变量,它们的名字和变量值在各自的容器中的索引值是相等的,如此一来,nginx就可以通过索引变量时获取的索引值,从r->variables数组中获取对应的变量值。

简单描述一下这个方法的大概逻辑:
    1.该方法首先通过索引值去r->variables数组中获取变量值(ngx_http_variable_value_t结构体),然后通过相应的标记来检查这个变量值是否可用,如果可用则直接返回,如果不可用则走下面的逻辑。
    2.变量值不可用,所以需要调用该变量对应的get_handler()方法来动态生成变量值,而该方法是绑定在变量名结构体对象中的,所以这里需要用这个索引值去变量名(ngx_http_variable_t)所在的容器(后续用cmcf->variables表示这个容器)中获取这个方法。
    3.取到变量名对应的get_handler()方法并调用,如果调用成功,那么生成的变量值就会被设置到r->variables容器的对应位置,否则就返回空了。
    4.在返回生成的变量值之前还需要做一个flags的判断,这个flags就是我们在2.1中提到的ngx_http_variables_t#flags标记,它会检查这个变量在定义的时候是否设置了NGX_HTTP_VAR_NOCACHEABLE标记,如果有则表示该变量值不可缓存,那么该变量值对应的v.no_cacheable就会被设置为1,后续再获取变量值的时候,nginx可以利用该字段值来确定是否要再次调用对应的get_handler()方法。
    5.返回生成的变量。
 
3.3通过ngx_http_get_flushed_variable()方法获取变量值
     这个方法跟3.2中的方法一样都是在获取变量值,不同的是该方法有一个flush的概念,所谓flush顾名思义就是刷新缓存,但并不意味着通过该方法获取的变量都没有走缓存,该方法会用到我们上面提到的v.no_cacheable字段值来判定是否可以使用缓存值,而v.no_cacheable字段是否被设置则取决于变量定义时是否打上了NGX_HTTP_VAR_NOCACHEABLE标记(ngx_http_variable_t#flags)。目前只要某个变量被打上这个标记,并且用ngx_http_get_flushed_variable()方法获取变量值,那么获取到的变量值就都是实时生成的,比如动态变量“arg_xxx”就设置了这个标记。

该方法原型跟3.2基本类似,具体定义如下:  

ngx_http_variable_value_t *  ngx_http_get_flushed_variable(ngx_http_request_t *r, ngx_uint_t index)

 
该方法的大致逻辑如下:
    1.首先通过索引值从r->variables数组中取出该索引对应的变量值v
    2.判断变量是否合法(利用v->valid和v->not_found判断),不合法则直接调用ngx_http_get_indexed_variable()方法获取变量值。
    3.如果变量合法,则判断v->no_cacheable值,值为0表示可以走缓存则直接返回变量值v;值为1表示不可以走缓存,为变量值v打上不合法标记。
    4.直接调用ngx_http_get_indexed_variable()方法获取变量值,该方法会中会用到变量值是否合法标记,如果合法则走缓存,不合法则回调对应的get_handler()方法来获取变量值。

3.4通过ngx_http_get_variable()方法获取变量值
     目前只在ngx_http_ssi_filter_module模块中用到该方法来获取变量值,不过该方法并不像前面其它两个方法那样通过索引值获取变量值,该方法通过变量名字从一个hash表(cmcf->variables_hash)中获取该变量值,这个hash表的数据来源于cmcf->variables_keys容器,所有定义好(配置文件中的和内置的)的变量都会被注册到这个容器中。

来看一下方法定义:  

ngx_http_variable_value_t * ngx_http_get_variable(ngx_http_request_t *r, ngx_str_t *name, ngx_uint_t key)

 
入参name就是要查找的变量名字,入参key是name的存放在hash结构中的hash值。

该方法的大致逻辑如下:
    1.调用ngx_hash_find () 方法从cmcf->variables_hash容器中查找name的变量值。
    2.找到变量v后接着判断该变量有没有标记NGX_HTTP_VAR_INDEXED,如果有表示可以根据变量索引值获取变量,则直接调用ngx_http_get_flushed_variable()方法获取变量值并返回;如果没有则调用该变量对应的get_handler()方法获取变量值并返回。
    3.如果没有在容器中找个对应的变量则去判断该变量是否是动态变量,如果是则执行对应的方法,比如变量是以“http_”开头的动态变量,则使用ngx_http_variable_unknown_header_in()获取变量值;如果不是动态变量则表示根不能不存在这个变量。

 
4.两个重要的容器
    在nginx的变量设计中有两个至关重要的容器,他们的作用前面我们也多多少少介绍了一点,目前为止,我们上面介绍的方法基本都在围绕这两个容器在工作,这一节我们简单总结一下nginx这么做的目的。

4.1容器cmcf->variables_keys
    该容器存在的目的是为了收集nginx中定义的变量,包括自定义变量(比如set指令定义的变量)和内置变量(不包括动态内置变量),只有存在于该容器中的变量才能被使用。类似于一个放满工具的仓库,你只能使用仓库中已经存在的工具,对于仓库中没有的工具你是无能为力的。

    该容器另一个作用是变量排重,它会保证整个容器中只有一个同名变量,当你试图向该容器中放一个已经存在的变量时它会返回错误,并且后台会打印一条错误日志“conflicting variable name xxx”。从这里可以间接知道,整个nginx中变量名字都是唯一的,但这并不表示我们不能多次定义变量(比如set指令),这个其实涉及到了变量的另一个标记NGX_HTTP_VAR_CHANGEABLE,该标记的意思在“如何定义一个变量”已有介绍,这里就不再赘述。
    向容器中放变量基本有两种方式:一种是像内置变量那样,直接调用nginx提供的ngx_hash_add_key()方法,该方法不做任何变量业务处理(比如检查NGX_HTTP_VAR_CHANGEABLE标记),在该方法的眼里只有数据;另一种就像set指令那样使用nginx专门为变量提供的ngx_http_add_variable()方法。

    还有一个比较特殊的地方是该容器的生命周期,它只存在于配置解析阶段,当nginx启动成功该容器就不存在了,相当于把仓库毁掉了。

4.2容器cmcf->variables
    该容器存在的目的是为了收集在nginx中使用到的变量。如果一个变量被定义了,但是并没有被使用,那么他一般不会存在于该容器中,比如nginx中的内置变量。该容器基本上算是cmcf->variables_keys容器的一个子集,它更像你旅行时候带着的旅行箱,一旦你已经上路了(比如nginx已经启动了),你也就只能使用旅行箱的东西了。

    目前向该容器中添加变量的方式只有一种:使用nginx提供的ngx_http_get_variable_index()方法。你当然可以自行写代码向该容器中放入变量,但我强烈建议你不要这么做,因为这个方法中封装了nginx设计变量的必要逻辑。

    该容器本身就是一个数组,而所谓的索引值其实就是变量在该数组中的位置下标。另外一个就是容器的大小,它虽然是cmcf->variables_keys容器的一个子集,但它能存放的元素个数跟cmcf->variables_keys是一样的。

 

 

 

 好了,上篇内容就到这里了,主要介绍了nginx为实现变量搞得一些“基础材料”。下篇内容主要描述nginx是如何使用这些“基础材料”来炒出变量这道菜的,想学nginx“炒菜”的同学请移步下篇