首页 > Nginx > Nginx Rewrite重定向模块原理

Nginx Rewrite重定向模块原理

2013年5月13日 发表评论 阅读评论 9544次阅读    

今天花了点时间学习nginx rewrite模块的代码,其跟脚本解析是紧密相连的。
老毛病,贴个代码,以后补图,脑袋快撑不住了····
其他注释的代码在这里:https://github.com/kulv2012/ReadNginxSrc

先来看一下重定向模块的基本数据结构,ngx_http_rewrite_loc_conf_t,这个结构比较简单,主要就是codes数组,这个数组保存了一系列语法单元的函数,用来进行脚本解析的,也是脚本运算的结果函数数组。

typedef struct {
ngx_array_t  *codes;/* uintptr_t *///此结构保存着code_t的实现函数数组,用来做重定向的。
	//这里是句柄数组组成的,逻辑上分为一组一组的,每条rewrite语句占用一组,每一组可能包含好几条code()函数指针等数据。
	//如果匹配失败就通过next跳过本组。
ngx_uint_t    stack_size;//貌似是e->sp[]数组的大小,其用来存储正则简析时,存放类似堆栈的临时值。
ngx_flag_t    log;
ngx_flag_t    uninitialized_variable_warn;
} ngx_http_rewrite_loc_conf_t;

rewrite模块支持的指令都包含在ngx_http_rewrite_commands里面,包括rewrite,return,break,if,set等。模块初始化函数比较简单,只是将ngx_http_rewrite_handler函数句柄设置到SERVER_REWRITE_PHASE 和 REWRITE_PHASE过程中去。这样在ngx_http_core_run_phases的时候会调用这个句柄,也就是ngx_http_core_find_config_phase之后,会调用ngx_http_rewrite_handler进行重定向的过程。这里我们再回顾一下ngx_http_core_run_phases所使用的重要数据结构cmcf->phase_engine.handlers,各个阶段默认情况下的句柄分别为:ngx_http_init_phase_handlers里面设置的。在我编译的nginx环境下,其句柄数组结构为:

0 {checker = 0x42f71e <ngx_http_core_generic_phase>, handler = 0x45963a <ngx_http_realip_handler>, next = 1}
1 {checker = 0x42f792 <ngx_http_core_rewrite_phase>, handler = 0x45d41e <ngx_http_rewrite_handler>, next = 2}
2 {checker = 0x42fc03 <ngx_http_core_find_config_phase>, handler = 0, next = 0}
3 {checker = 0x42f792 <ngx_http_core_rewrite_phase>, handler = 0x45d41e <ngx_http_rewrite_handler>, next = 4}
4 {checker = 0x42f7cd <ngx_http_core_post_rewrite_phase>, handler = 0, next = 2}
5 {checker = 0x42f71e <ngx_http_core_generic_phase>, handler = 0x45963a <ngx_http_realip_handler>, next = 8}
6 {checker = 0x42f71e <ngx_http_core_generic_phase>, handler = 0x458b17 <ngx_http_limit_req_handler>, next = 8}
7 {checker = 0x42f71e <ngx_http_core_generic_phase>, handler = 0x457c9a <ngx_http_limit_conn_handler>, next = 8}
8 {checker = 0x42f889 <ngx_http_core_access_phase>, handler = 0x45711a <ngx_http_access_handler>, next = 11}//简单的IP禁止
9 {checker = 0x42f889 <ngx_http_core_access_phase>, handler = 0x456b81 <ngx_http_auth_basic_handler>, next = 11}//简单HTTP密码验证
10 {checker = 0x42f9a0 <ngx_http_core_post_access_phase>, handler = 0, next = 11}
11 {checker = 0x4305a8 <ngx_http_core_content_phase>, handler = 0x449afb <ngx_http_index_handler>, next = 14}
12 {checker = 0x4305a8 <ngx_http_core_content_phase>, handler = 0x455a22 <ngx_http_autoindex_handler>, next = 14}
13 {checker = 0x4305a8 <ngx_http_core_content_phase>, handler = 0x449241 <ngx_http_static_handler>, next = 14}
14 {checker = 0, handler = 0x8d200002, next = 0}

因此模块的重点就在ngx_http_rewrite_handler函数了。不过我们先来关注一下指令的初始化解析的过程。


零、ngx_http_rewrite_handler重定向回调:

ngx_http_rewrite_init函数在初始化时将这个函数加入SERVER_REWRITE_PHASE 和 REWRITE_PHASE过程中。这样每次进入ngx_core_run_phrases()后会调用这个地方进行重定向。重定向完毕后,如果不是break,就将进入下一次find config 阶段,后者成功后又将进行重新的rewrite,就像有个新的请求到来一样。

这个函数首先取得ngx_http_rewrite_module的location配置,然后申请一个ngx_http_script_engine_t脚本解析的结构体,这个结构体是提供给脚本解析的各个回调函数的参数。然后为e->sp变量堆栈数组申请空间,用来存放各个code的结果空间大小为stack_size,设置解析的指针表e->ip = rlcf->codes->elts,这个比较重要,codes我们上面介绍过了,是一组组函数指针。我们来看一下ngx_http_script_engine_t这个结构的代码:

typedef struct {
    u_char                     *ip;
	/*关于pos && code: 每次调用code,都会将解析到的新的字符串放入pos指向的字符串处,
	然后将pos向后移动,下次进入的时候,会自动将数据追加到后面的。
	对于ip也是这个原理,code里面会将e->ip向后移动。移动的大小根据不同的变量类型相关。
	ip指向一快内存,其内容为变量相关的一个结构体,比如ngx_http_script_copy_capture_code_t,
	结构体之后,又是下一个ip的地址。比如移动时是这样的 :
	code = (ngx_http_script_copy_capture_code_t *) e->ip;
    e->ip += sizeof(ngx_http_script_copy_capture_code_t);//移动这么多位移。
	*/
    u_char                     *pos;//pos之前的数据就是解析成功的,后面的数据将追加到pos后面。
    ngx_http_variable_value_t  *sp;//这里貌似是用sp来保存中间结果,比如保存当前这一步的进度,到下一步好用e->sp--来找到上一步的结果。

    ngx_str_t                   buf;//存放结果,也就是buffer,pos指向其中。
    ngx_str_t                   line;//记录请求行URI  e->line = r->uri;

    /* the start of the rewritten arguments */
    u_char                     *args;

    unsigned                    flushed:1;
    unsigned                    skip:1;
    unsigned                    quote:1;
    unsigned                    is_args:1;
    unsigned                    log:1;

    ngx_int_t                   status;
    ngx_http_request_t         *request;//所属的请求
} ngx_http_script_engine_t;

关于ip和code上面注释比较详细了,不多说,ngx_http_rewrite_handler初始化ngx_http_script_engine_t后,就开始循环的调用e->ip进行解析,重定向匹配,比如某个location {}里面有多个rewrite指令的 话,那么他们的解析函数组会按顺序安排好的,举个例子,对于下面的指令:

rewrite ^(.*)$ http://$http_host.mp4 break;

其解析后的函数数组为:

ngx_http_rewrite_handler

  1. ngx_http_script_regex_start_code 解析完了正则表达式。并求出总长度,设置到了e上了
    1. 1 ngx_http_script_copy_len_code 7
    2. 2 ngx_http_script_copy_var_len_code 18
    3. 3 ngx_http_script_copy_len_code 4 === 29
  2. ngx_http_script_copy_code 拷贝"http://" 到e->buf
  3. ngx_http_script_copy_var_code 拷贝"115.28.34.175:8881"
  4. ngx_http_script_copy_code 拷贝".mp4"
  5. ngx_http_script_regex_end_code

这样就能够首先调用ngx_http_script_regex_start_code函数,这个函数会匹配 ^(.*)$ 看看是否匹配成功,如果成功,那它就会调用后面的1.1到1.3的长度计算函数,来计算后面http://$http_host.mp4的解析后的长度。
然后ngx_http_script_regex_start_code函数返回,下一个函数就是ngx_http_script_copy_code函数,这个函数会拷贝rewrite后面目标的前面静态字符串,也就是http://,下一个是ngx_http_script_copy_var_code,这个是用来拷贝变量的。具体看下面的代码:

/*ngx_http_rewrite_init函数在初始化时将这个函数加入SERVER_REWRITE_PHASE 和 REWRITE_PHASE过程中。
//这样每次进入ngx_core_run_phrases()后会调用这个地方进行重定向。
重定向完毕后,如果不是break,就将进入下一次find config 阶段,
后者成功后又将进行重新的rewrite,就像有个新的请求到来一样。
*/
static ngx_int_t ngx_http_rewrite_handler(ngx_http_request_t *r)
{
    ngx_http_script_code_pt       code;
    ngx_http_script_engine_t     *e;
    ngx_http_rewrite_loc_conf_t  *rlcf;

    rlcf = ngx_http_get_module_loc_conf(r, ngx_http_rewrite_module);
    if (rlcf->codes == NULL) {//如果没有处理函数,直接返回,因为这个模块肯定没有一条rewrite。也就是不需要
        return NGX_DECLINED;//如果返回OK就代表处理完毕,不用处理i后面的其他过程了。
    }
	//新建一个脚本引擎,开始进行codes的解析。
    e = ngx_pcalloc(r->pool, sizeof(ngx_http_script_engine_t));
    if (e == NULL) {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }
	//下面的stack_size到底在哪里设置的/代码里面都找不到。
	//功能是用来存放计算的中间结果。
    e->sp = ngx_pcalloc(r->pool,  rlcf->stack_size * sizeof(ngx_http_variable_value_t));
    if (e->sp == NULL) {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    e->ip = rlcf->codes->elts;
    e->request = r;
    e->quote = 1;
    e->log = rlcf->log;
    e->status = NGX_DECLINED;
	/*对于这样的: rewrite ^(.*)$ http://$http_host.mp4 break; 下面的循环是i这样走的
	ngx_http_rewrite_handler
		1. ngx_http_script_regex_start_code 解析完了正则表达式。并求出总长度,设置到了e上了
			1.1 ngx_http_script_copy_len_code		7
			1.2 ngx_http_script_copy_var_len_code 	18
			1.3 ngx_http_script_copy_len_code		4	=== 29

		2. ngx_http_script_copy_code		拷贝"http://" 到e->buf
		3. ngx_http_script_copy_var_code	拷贝"115.28.34.175:8881"
		4. ngx_http_script_copy_code 		拷贝".mp4"
		5. ngx_http_script_regex_end_code
	*/

    while (*(uintptr_t *) e->ip) {//遍历每一个函数指针,分别调用他们。
        code = *(ngx_http_script_code_pt *) e->ip;
        code(e);//执行对应指令的函数,比如if等,
    }
    if (e->status == NGX_DECLINED) {
        return NGX_DECLINED;
    }
    if (r->err_status == 0) {
        return e->status;
    }
    return r->err_status;
}

ngx_http_rewrite_handler相对比较简单,只是循环调用了一下脚本引擎根据当前location指令内容设置的各个回调函数,那么,这些回调函数是怎么设置进去的呢?答案是:指令解析的时候。下面逐一来看看。


一、rewrite 语句解析

rewrite语句语法为:

Syntax: rewrite regex replacement [ flag ]

当ngx_conf_parse 函数解析到rewrite指令时,会调用ngx_http_rewrite函数去解析上面的指令行,ngx_http_rewrite函数完成如下几个功能:

  1. 解析正则表达式,提取子模式,命名子模式存入variables等;
  2. 解析第四个参数last,break等。
  3. 调用ngx_http_script_compile将目标字符串解析为结构化的codes句柄数组,以便解析时进行计算;
  4. 根据第三步的结果,生成lcf->codes 组,后续rewrite时,一组组的进行匹配即可。失败自动跳过本组,到达下一组rewrite。

我们一步步来看ngx_http_rewrite函数,其开头调用ngx_http_script_start_code申请了一个脚本开始节点,用来做什么的呢,上面我们在ngx_http_rewrite_handler看到,这是用来做脚本解析的,其设置的code为ngx_http_script_regex_start_code函数完成了一个重要的功能:

  • 调用正则表达式引擎编译URL参数行,如果匹配失败,则e->ip += code->next;让调用方调到下一个表达式块进行解析。
  • 如果成功,调用code->lengths,从而获取正则表达式替换后的字符串长度,以备在此函数返回后的code函数调用中能够存储新字符串长度。

接着调用ngx_http_regex_compile函数,完成正则表达式的编译,也就是下面正则表达式的^(/xyz.*)$部分解析,该函数完成了下面的功能:

  • 1. 调用ngx_regex_compile编译正则表达式,并且得到相关的数据,比如子模式数目,命名子模式数目,列表等。
  • 2. 将正则表达式信息存储到ngx_http_regex_t里面,包括正则句柄,子模式数目
  • 3. 将命名子模式 加入到cmcf->variables_keys和cmcf->variables中。以备后续通过名字查找变量值。

ngx_http_regex_compile函数完成后,正则表达式部分的数据结构以及准备好了,后面简单做了一下是否要重定向,是否要调整站点302,以及last,break,redirect等指令,设置相关的标志,对于break参数,我们会设置regex->break_cycle ,在脚本模块会看到这个参数的用处。这个参数决定了rewrite后,如果URL有变化,nginx还是会设置url_changed=0,来避免进行重定向。下面看一下代码:

/*
1. 解析正则表达式,提取子模式,命名子模式存入variables等;
2.	解析第四个参数last,break等。
3.调用ngx_http_script_compile将目标字符串解析为结构化的codes句柄数组,以便解析时进行计算;
4.根据第三步的结果,生成lcf->codes 组,后续rewrite时,一组组的进行匹配即可。失败自动跳过本组,到达下一组rewrite
*/
static char * ngx_http_rewrite(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{//碰到"rewrite"指令时调用这里。
//比如rewrite ^(/xyz/aa.*)$ http://$http_host/aa.mp4 break;
    ngx_http_rewrite_loc_conf_t  *lcf = conf;

    ngx_str_t                         *value;
    ngx_uint_t                         last;
    ngx_regex_compile_t                rc;
    ngx_http_script_code_pt           *code;
    ngx_http_script_compile_t          sc;
    ngx_http_script_regex_code_t      *regex;
    ngx_http_script_regex_end_code_t  *regex_end;
    u_char                             errstr[NGX_MAX_CONF_ERRSTR];
	//在本模块的codes的尾部,这里应该算一块新的指令组的头部,增加一个开始回调ngx_http_script_regex_start_code
	//这里申请的是ngx_http_script_regex_code_t,其第一个成员code为经常被e->ip指向的函数指针,被当做code调用的。
    regex = ngx_http_script_start_code(cf->pool, &lcf->codes, sizeof(ngx_http_script_regex_code_t));
    if (regex == NULL) {
        return NGX_CONF_ERROR;
    }
    ngx_memzero(regex, sizeof(ngx_http_script_regex_code_t));
    value = cf->args->elts;
    ngx_memzero(&rc, sizeof(ngx_regex_compile_t));

    rc.pattern = value[1];//记录 ^(/xyz/aa.*)$
    rc.err.len = NGX_MAX_CONF_ERRSTR;
    rc.err.data = errstr;

    /* TODO: NGX_REGEX_CASELESS */
	//解析正则表达式,填写ngx_http_regex_t结构并返回。正则句柄,命名子模式等都在里面了。
    regex->regex = ngx_http_regex_compile(cf, &rc);
    if (regex->regex == NULL) {
        return NGX_CONF_ERROR;
    }
	//ngx_http_script_regex_start_code函数匹配正则表达式,计算目标字符串长度并分配空间。
	//将其设置为第一个code函数,求出目标字符串大小。尾部还有ngx_http_script_regex_end_code
    regex->code = ngx_http_script_regex_start_code;
    regex->uri = 1;
    regex->name = value[1];//记录正则表达式

    if (value[2].data[value[2].len - 1] == '?') {//如果目标结果串后面用问好结尾,则nginx不会拷贝参数到后面的
        /* the last "?" drops the original arguments */
        value[2].len--;
    } else {
        regex->add_args = 1;//自动追加参数。
    }

    last = 0;
    if (ngx_strncmp(value[2].data, "http://", sizeof("http://") - 1) == 0
        || ngx_strncmp(value[2].data, "https://", sizeof("https://") - 1) == 0
        || ngx_strncmp(value[2].data, "$scheme", sizeof("$scheme") - 1) == 0)
    {//nginx判断,如果是用http://等开头的rewrite,就代表是垮域重定向。会做302处理。
        regex->status = NGX_HTTP_MOVED_TEMPORARILY;
        regex->redirect = 1;//标记要做302重定向。
        last = 1;
    }

    if (cf->args->nelts == 4) {//处理后面的参数。
        if (ngx_strcmp(value[3].data, "last") == 0) {
            last = 1;
        } else if (ngx_strcmp(value[3].data, "break") == 0) {
            regex->break_cycle = 1;//需要break,这里体现了跟last的区别,参考ngx_http_script_regex_start_code。
            //这个标志会影响正则解析成功之后的代码,让其设置了一个url_changed=0,也就骗nginx说,URL没有变化,
            //你不用重新来跑find config phrase了。不然还得像个新连接一样跑一遍。
            last = 1;
        } else if (ngx_strcmp(value[3].data, "redirect") == 0) {
            regex->status = NGX_HTTP_MOVED_TEMPORARILY;
            regex->redirect = 1;
            last = 1;
        } else if (ngx_strcmp(value[3].data, "permanent") == 0) {
            regex->status = NGX_HTTP_MOVED_PERMANENTLY;
            regex->redirect = 1;
            last = 1;
        } else {
            ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                               "invalid parameter \"%V\"", &value[3]);
            return NGX_CONF_ERROR;
        }
    }

ngx_http_rewrite函数下一步就是要进行脚本解析了,上面的部分解析完正则表达式。脚本解析的数据存放在ngx_http_script_compile_t类型的变量sc里面,然后调用ngx_http_script_compile()函数,这个函数我们在Nginx 脚本引擎解析源码注释 里面看到了,它传入一个字符串,进行编译解析,比如http://$host/aaa.php;将计算长度的lcode放入sc->lengths,计算值的放入sc->values。由于前面初始化的时候设置了sc.values = &lcf->codes;因此ngx_http_script_compile函数会将结果存放在模块的当前location配置结构里面,也就是:ngx_http_rewrite_loc_conf_t:codes,这样在ngx_http_rewrite_handler里面就可以这样取出这个codes了:rlcf = ngx_http_get_module_loc_conf(r, ngx_http_rewrite_module);
最后,为该指令增加一个结束的语法函数ngx_http_script_regex_end_code进行扫尾工作,比如如果需要redirect,设置一下头部header的location,该302了。
另外ngx_http_rewrite函数最后一行也很关键,通过它连接了不同的语句之间的跳转关系,将regex->next设置为下一个codes数组项的位置。这个的用处主要体现在正则表达式的匹配上,ngx_http_script_regex_start_code函数如果调用ngx_http_regex_exec进行正则匹配的时候,当前指令不匹配,那么就需要跳转到下一条指令进行匹配,这个是通过e->ip += code->next;进行的,也就是跳过本指令组的函数,因为一条rewrite指令在codes数组里面占用好几项的。
最后看一下ngx_http_rewrite的余下代码:

    ngx_memzero(&sc, sizeof(ngx_http_script_compile_t));
    sc.cf = cf;
    sc.source = &value[2];//字符串 http://$http_host/aa.mp4
    sc.lengths = &regex->lengths;//输出参数,里面会包含一些如何求目标字符串长度的函数回调。如上会包含三个: 常量 变量 常量
    sc.values = &lcf->codes;//将子模式存入这里。
    sc.variables = ngx_http_script_variables_count(&value[2]);
    sc.main = regex;//这是顶层的表达式,里面包含了lengths等。
    sc.complete_lengths = 1;
    sc.compile_args = !regex->redirect;

    if (ngx_http_script_compile(&sc) != NGX_OK) {
        return NGX_CONF_ERROR;
    }

    regex = sc.main;//这里这么做的原因是可能上面会改变内存地址。
    regex->size = sc.size;
    regex->args = sc.args;

    if (sc.variables == 0 && !sc.dup_capture) {//如果没有变量,那就将lengths置空,这样就不用做多余的正则解析而直接进入字符串拷贝codes
        regex->lengths = NULL;
    }
    regex_end = ngx_http_script_add_code(lcf->codes, sizeof(ngx_http_script_regex_end_code_t), &regex);
    if (regex_end == NULL) {
        return NGX_CONF_ERROR;
    }
	/*经过上面的处理,后面的rewrite会解析出如下的函数结构: rewrite ^(.*)$ http://$http_host.mp4 break;
	ngx_http_script_regex_start_code 解析完了正则表达式。根据lengths求出总长度,申请空间。
			ngx_http_script_copy_len_code		7
			ngx_http_script_copy_var_len_code 	18
			ngx_http_script_copy_len_code		4	=== 29

	ngx_http_script_copy_code		拷贝"http://" 到e->buf
	ngx_http_script_copy_var_code	拷贝"115.28.34.175:8881"
	ngx_http_script_copy_code 		拷贝".mp4"
	ngx_http_script_regex_end_code
	*/

    regex_end->code = ngx_http_script_regex_end_code;//结束回调。对应前面的开始。
    regex_end->uri = regex->uri;
    regex_end->args = regex->args;
    regex_end->add_args = regex->add_args;//是否添加参数。
    regex_end->redirect = regex->redirect;

    if (last) {//参考上面,如果rewrite 末尾有last,break,等,就不会再次解析后面的数据了,那么,就将code设置为空。
        code = ngx_http_script_add_code(lcf->codes, sizeof(uintptr_t), &regex);
        if (code == NULL) {
            return NGX_CONF_ERROR;
        }
        *code = NULL;
    }
	//下一个解析句柄组的地址。
    regex->next = (u_char *) lcf->codes->elts + lcf->codes->nelts - (u_char *) regex;
    return NGX_CONF_OK;
}

解析完成之后,在当前的location 上下文中,ngx_http_rewrite_loc_conf_t:codes成员变量是个数组,记录了当前location的所有语法函数列表,这些函数就组成了nginx rewrite,if,break等语句的解析方法:依次调用这些函数就行;

比如rewrite语句:rewrite ^(/xyz.*)$ http://$http_host/kulv.php break;  语法解析后的codes将增加如下几项:

ngx_http_script_regex_start_code 正则匹配,如果成功,用regex->lengths语法计算目标字符串长度,否则移动e->ip进入下一条指令的的语法函数组:e->ip += code->next;
ngx_http_script_copy_code 拷贝简单字符串:http://
ngx_http_script_copy_var_code 拷贝变量:$http_host,变量从ngx_http_script_var_code_t:index中取得在cmcf->variables中的下标。
ngx_http_script_copy_code 拷贝简单字符串:/kulv.php
ngx_http_script_regex_end_code 检查是否是重定向,如果是旧302

其实后面的目标字符串http://$http_host/kulv.php 在nginx中叫做复杂表达式。ngx_http_complex_value函数可以搞定它,根据val复杂表达式结构,获取其代表的目标值,存入value.

其关系如下图所示。

http_rewrite_module


总之,一句话描述rewrite就是:在配置解析阶段,通过解析rewrite指令的参数建立对应的语法函数列表,设置到location的codes数组上;在处理过程中ngx_http_rewrite_handler函数依次调用这些codes来解析对应的语句,拼接出rewrite后的目标URL,然后进行重定向

二、if 指令解析

在ngx_conf_parse碰到if指令时,会调用ngx_http_rewrite_if函数进行解析。if指令比rewrite指令稍微复杂一点。为了篇幅我们再下一篇文章介绍吧。

Share
分类: Nginx 标签: , ,
  1. 本文目前尚无任何评论.
  1. 本文目前尚无任何 trackbacks 和 pingbacks.

注意: 评论者允许使用'@user空格'的方式将自己的评论通知另外评论者。例如, ABC是本文的评论者之一,则使用'@ABC '(不包括单引号)将会自动将您的评论发送给ABC。使用'@all ',将会将评论发送给之前所有其它评论者。请务必注意user必须和评论者名相匹配(大小写一致)。