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;
}
}
}
}