15.Nginx解析配置文件之ngx_conf_read_token

本文介绍Nginx配置文件解析过程中的ngx_conf_read_token函数,详细分析了如何从配置文件中提取token,包括处理行注释、转义序列等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Nginx的可配置选项非常的多, 意味着配置文件可以相当复杂,那么解析起来也是困难重重,今天学习一下与配置解析有关的函数ngx_conf_read_token;

顾名思义,可以看出这是一个用于从配置文件中提取token的函数, 什么是token呢?

举栗子,下面这个配置片段中,user是一个token,www-data是一个token,worker_processes、auto、pid、/run/nginx.pid、events、worker_connections、768都是token;

multi_accept、on就不是token了,因为它们在#后面,是行注释的一部分

user www-data;
worker_processes auto;
pid /run/nginx.pid;

events {
    worker_connections 768;
    # multi_accept on;
}

从下面的代码分析中,我们也可以看出ngx_conf_read_token并不能一次提取所有token,在解析完一个配置行(以分号结束)或者遇到配置块的开始(以左花括号开始)时就会返回;因此我们想要解析完整个配置文件, 可能需要进行多次调用

/* core/ngx_conf_file.c */

/* 从配置文件解析token
*/
static int ngx_conf_read_token(ngx_conf_t *cf)
{
    u_char      *start, ch, *src, *dst;
    int          len;
    int          found, need_space, last_space, sharp_comment;
    int          quoted, s_quoted, d_quoted;
    ssize_t      n;
    ngx_str_t   *word;
    ngx_buf_t   *b;

    found = 0;                          // 用来表示是否找到一个完整的token
    need_space = 0;                     // 当接下来扫描到的字符需要是空格/水平制表符/回车符/换行符时置为1
    last_space = 1;                     // 当扫描到空格/水平制表符/回车符/换行符时置为1
    sharp_comment = 0;                  // 用来表示当前扫描的字符是否在一个#开头的行注释中
    quoted = s_quoted = d_quoted = 0;   // quoted: 当扫描到一个转义序列的\时置为1
                                        // s_quoted: 当扫描到一个单引号时置为1
                                        // d_quoted: 当扫描到一个双引号时置为1
    cf->args->nelts = 0;                // cf->args数组用来存放解析到的token
    b = cf->conf_file->buffer;          // 缓冲区b, 里面存放配置内容
    start = b->pos;                     // b->pos用来标记当前将要扫描的字符位置
                                        // start用来标记一个token的起始字符

#if 0
ngx_log_debug(cf->log, "TOKEN START");
#endif

    for ( ;; ) {

        if (b->pos >= b->last) {
            // b->last用来标记缓冲区内有效内容的结束位置,
            // 这里意味着已经读取的配置内容解析完毕
            
            if (cf->conf_file->file.offset
                                 >= ngx_file_size(&cf->conf_file->file.info)) {
                // 当配置文件的当前偏移量等于配置文件大小时, 说明配置文件已经读取完毕,
                // 也就意味着配置文件全部解析完毕
                return NGX_CONF_FILE_DONE;
            }

            if (b->pos - start) {
                // 意味着当前token还没解析完毕, 只扫描了它的部分字符,
                // 此时将这部分字符拷贝到缓冲区头
                ngx_memcpy(b->start, start, b->pos - start);
            }
            // 继续读取配置文件到缓冲区
            n = ngx_read_file(&cf->conf_file->file,
                              b->start + (b->pos - start),
                              b->end - (b->start + (b->pos - start)),
                              cf->conf_file->file.offset);

            if (n == NGX_ERROR) {
                return NGX_ERROR;
            }
            // 更新b->pos, 使之指向刚读取内容的起始位置
            b->pos = b->start + (b->pos - start);
            // start指向缓冲区起始地址, 也就是指向当前要解析的token的起始字符
            start = b->start;
            // 更新b->last, 使之指向刚读取内容的结束位置
            b->last = b->pos + n;
        }
        // 读取当前扫描的字符, 并让b->pos后移
        ch = *b->pos++;

#if 0
ngx_log_debug(cf->log, "%d:%d:%d:%d:%d '%c'" _
              last_space _ need_space _
              quoted _ s_quoted _ d_quoted _ ch);
#endif

        if (ch == LF) {
            // 如果当前扫描的字符为换行符
            
            // cf->conf_file的line成员加1,
            // line成员用来记录已扫描的配置文件行数
            cf->conf_file->line++;

            if (sharp_comment) {
                // 如果sharp_comment为1, 那么这里的换行符意味着#行注释的结束,
                // 置sharp_comment为0
                sharp_comment = 0;
            }
        }

        if (sharp_comment) {
            // 如果sharp_comment为1, 那么这里的字符就是行注释的一部分, 直接跳过
            continue;
        }

        if (quoted) {
            // 如果quoted为1, 意味着上一个扫描的字符为\, 当前字符就是转义序列的一部分, 
            // 置quoted为0, 表示当前扫描到的转义序列结束, 然后直接跳过
            quoted = 0;
            continue;
        }

        if (need_space) {
            // 当need_space为1时, 意味着当前扫描到的字符需要是空格
            
            if (ch == ' ' || ch == '\t' || ch == CR || ch == LF) {
                // 如果当前扫描到的字符为空格/水平制表符/回车符/换行符时,
                // 置need_space为0, 因为已经满足了要求;
                // 置last_space为1, 标记当前扫描到了这些字符;
                // 然后直接跳过
                last_space = 1;
                need_space = 0;
                continue;
            }

            if (ch == ';' || ch == '{') {
                return NGX_OK;
            }

            ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
                          "unexpected '%c' in %s:%d",
                          ch, cf->conf_file->file.name.data,
                          cf->conf_file->line);

            return NGX_ERROR;
        }

        if (last_space) {
            // 如果上一个扫描的字符为空格/水平制表符/回车符/换行符
            
            if (ch == ' ' || ch == '\t' || ch == CR || ch == LF) {
                // 当前扫描的字符依旧为空格/水平制表符/回车符/换行符, 那么直接跳过
                continue;
            }
            
            // 如果当前扫描的字符不为空格/水平制表符/回车符/换行符, 意味着当前扫描的字符为下一个token
            // 的起始字符, start指向当前扫描的字符
            start = b->pos - 1;

            switch (ch) {

            case ';':
            case '{':
                // 如果当前扫描到的字符为分号或者左花括号
                
                if (cf->args->nelts == 0) {
                    // 如果此次解析到的token数量为0, 返回NGX_ERROR;
                    // 因为分号或左花括号之前必须出现至少一个token, 否则说明配置有误
                    ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
                                  "unexpected '%c' in %s:%d",
                                  ch, cf->conf_file->file.name.data,
                                  cf->conf_file->line);
                    return NGX_ERROR;
                }

                // 意味着配置行的结束或者配置块的开始, 返回NGX_OK
                return NGX_OK;

            case '}':
                // 如果当前扫描到的字符为右花括号
            
                if (cf->args->nelts > 0) {
                    // 如果此次解析到的token数量大于0, 返回NGX_ERROR;
                    // 因为右花括号必须独占一行, 否则说明配置有误
                    
                    ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
                                  "unexpected '}' in %s:%d",
                                  cf->conf_file->file.name.data,
                                  cf->conf_file->line);
                    return NGX_ERROR;
                }

                // 意味着配置块的结束, 返回NGX_CONF_BLOCK_DONE
                return NGX_CONF_BLOCK_DONE;

            case '#':
                // 如果当前扫描到的字符为井号,
                // 意味着遇到行注释的开始, 置sharp_comment为1;
                // 然后直接跳过
                sharp_comment = 1;
                continue;

            case '\\':
                // 如果当前扫描到的字符为反斜杠,
                // 意味着遇到转义序列的开始, 置quoted为1;
                // 置last_space为0; 然后直接跳过
                quoted = 1;
                last_space = 0;
                continue;

            case '"':
                // 如果当前扫描到的字符为双引号, 加上前一个扫描的字符为空格,
                // 那么我们可以推断该双引号为左双引号;
                // 那么下一个token的开始字符必定为当前扫描字符的下一个字符, start向后移;
                // 置d_quoted为1; 置last_space为0; 然后直接跳过
                start++;
                d_quoted = 1;
                last_space = 0;
                continue;

            case '\'':
                // 如果当前扫描到的字符为单引号, 与上面的双引号同理, 只是需要置s_quoted为1
                start++;
                s_quoted = 1;
                last_space = 0;
                continue;

            default:
                // 如果当前扫描到的字符为其他字符, 说明是token中的字符, 置last_space为0即可
                last_space = 0;
            }

        } else {
            // 如果上一个扫描的字符不为空格/水平制表符/回车符/换行符
            
            if (ch == '\\') {
                // 如果当前扫描到的字符为反斜杠, 意味着遇到转义序列的开始, 置quoted为1;
                // 然后直接跳过
                quoted = 1;
                continue;
            }

            // 如果上一个扫描的字符不为空格/水平制表符/回车符/换行符, 那么当前扫描字符为单引号或双引号时,
            // 我们需要结合s_quoted或d_quoted来判断是左还是右
            if (d_quoted) {
                // 如果d_quoted为1, 说明之前遇到一个双引号
                
                if (ch == '"') {
                    // 如果当前扫描的字符为双引号, 那么必为右双引号;
                    // 置d_quoted为0; 
                    // 右双引号之后需要出现空格, 置need_space为1;
                    // 右双引号意味着一个token的结束, 置found为1
                    d_quoted = 0;
                    need_space = 1;
                    found = 1;
                }

            } else if (s_quoted) {
                // 如果s_quoted为1, 说明之前遇到一个单引号
                
                if (ch == '\'') {
                    // 如果当前扫描的字符为单引号, 那么必为右单引号;
                    // 置s_quoted为0;
                    // 右单引号之后需要出现空格, 置need_space为1;
                    // 右单引号意味着一个token的结束, 置found为1
                    s_quoted = 0;
                    need_space = 1;
                    found = 1;
                }

            } else if (ch == ' ' || ch == '\t' || ch == CR || ch == LF
                       || ch == ';' || ch == '{') {
                // 如果当前扫描的字符为空格/水平制表符/回车符/换行符/分号/左花括号,
                // 意味着一个token的结束, 置found为1; 置last_space为1
                last_space = 1;
                found = 1;
            }

            if (found) {
                // 如果found为1, 说明解析到一个token
                
                // 从cf->args数组中申请存放一个元素的内存空间
                if (!(word = ngx_push_array(cf->args))) {
                    return NGX_ERROR;
                }

                // 从cf->pool内存池中申请一块大小为b->pos - start + 1的内存空间用来存放token字符串(包含结束符);
                // 其实我觉得这里应该b->pos - start就足矣
                // 举栗子, "ab"_ 此时,start指向a, b->pos指向下划线模拟的空格, 只需要b->pos - start = 3个字节即可存放token
                if (!(word->data = ngx_palloc(cf->pool, b->pos - start + 1))) {
                    return NGX_ERROR;
                }

                // 遍历缓冲区中扫描到的token字符串, 将其拷贝到刚才申请到的内存中;
                // 当然这里也需要验证该token的合法性和过滤其中的无用字符
                for (dst = word->data, src = start, len = 0;
                     src < b->pos - 1;
                     len++)
                {
                    if (*src == '\\') {
                        // 如果token的当前字符为反斜杠
                        
                        switch (src[1]) {
                        case '"':
                        case '\'':
                        case '\\':
                            // 如果token的下一个字符为双引号/单引号/反斜杠,
                            // 那么直接退出循环
                            src++;
                            break;

                        case 't':
                            // 如果token的下一个字符为t, 意味着与当前字符组成一个水平制表符,
                            // 那么直接拷贝一个水平制表符
                            *dst++ = '\t';
                            src += 2;
                            continue;

                        case 'r':
                            // 如果token的下一个字符为r, 意味着与当前字符组成一个回车符,
                            // 那么直接拷贝一个回车符
                            *dst++ = '\r';
                            src += 2;
                            continue;

                        case 'n':
                            // 如果token的下一个字符为n, 意味着与当前字符组成一个换行符,
                            // 那么直接拷贝一个换行符
                            *dst++ = '\n';
                            src += 2;
                            continue;
                        }

                    }
                    // 将缓冲区中token的当前字符拷贝至申请的内存中
                    *dst++ = *src++;
                }
                // 在申请的内存中加上结束符;
                // 记录token长度为len
                *dst = '\0';
                word->len = len;

#if 0
ngx_log_debug(cf->log, "FOUND %d:'%s'" _ word->len _ word->data);
#endif

                if (ch == ';' || ch == '{') {
                    // 如果当前扫描到的字符为分号/左花括号, 那么意味着配置行的结束或配置块的开始,
                    // 返回NGX_OK
                    return NGX_OK;
                }

                // 置found为0, 以查找下一个token
                found = 0;
            }
        }
    }
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值