解析代码前,先从源码注释中大概了解srs配置信息的组成结构:
/**
* the config directive.
* the config file is a group of directives,
* all directive has name, args and child-directives.
* for example, the following config text:
vhost vhost.ossrs.net {
enabled on;
ingest livestream {
enabled on;
ffmpeg /bin/ffmpeg;
}
}
* will be parsed to:
* SrsConfDirective: name="vhost", arg0="vhost.ossrs.net", child-directives=[
* SrsConfDirective: name="enabled", arg0="on", child-directives=[]
* SrsConfDirective: name="ingest", arg0="livestream", child-directives=[
* SrsConfDirective: name="enabled", arg0="on", child-directives=[]
* SrsConfDirective: name="ffmpeg", arg0="/bin/ffmpeg", child-directives=[]
* ]
* ]
* @remark, allow empty directive, for example: "dir0 {}"
* @remark, don't allow empty name, for example: ";" or "{dir0 arg0;}
*/
从上可知,srs的配置信息实际上是存在一个由SrsConfDirective(指令,或称为:配置项)组成的树形结构中的,每个指令包含:指令名、参数列表、子指令集。
SrsConfDirective类型的定义一目了然:
class SrsConfDirective
{
public:
// 指令是从配置文件的多少行解析出来的
int conf_line;
// 指令名
std::string name;
// 指令的参数集
std::vector args;
// 子指令集合
std::vector directives;
public:
SrsConfDirective();
virtual ~SrsConfDirective();
public:
// 获取该指令的对应索引上的参数(感觉有点多余,一个函数不就可以了吗)
virtual std::string arg0();
virtual std::string arg1();
virtual std::string arg2();
public:
// 通过索引获取子指令
virtual SrsConfDirective* at(int index);
// 通过指令名获取子指令
virtual SrsConfDirective* get(std::string _name);
// 查询指令名和第一个参数都匹配的子指令
virtual SrsConfDirective* get(std::string _name, std::string _arg0);
public:
virtual bool is_vhost();
virtual bool is_stream_caster();
public:
// 从配置文件中(已读到buffer中)解析出配置信息,实际上直接调用的parse_conf
virtual int parse(_srs_internal::SrsConfigBuffer* buffer);
private:
// 正在解析的指令的类型
enum SrsDirectiveType {
// root的子指令
parse_file,
// root的多级子指令
parse_block
};
// 解析配置文件中的一个指令区域,第一个区域就是root区域
virtual int parse_conf(_srs_internal::SrsConfigBuffer* buffer, SrsDirectiveType type);
/**
* read a token from buffer.
* a token, is the directive args and a flag indicates whether has child-directives.
* @param args, the output directive args, the first is the directive name, left is the args.
* @param line_start, the actual start line of directive.
* @return, an error code indicates error or has child-directives.
*/
virtual int read_token(_srs_internal::SrsConfigBuffer* buffer, std::vector& args, int& line_start);
};
这里可以看到:name就是指令名,args就是指令参数集合,directives就是该指令的子指令集合;通过directives,整个配置构成了一个树形结构,根节点就是root。
三、配置解析
实际上、真正解析配置文件是由SrsConfDirective类中的两个方法实现:
virtual int parse_conf(_srs_internal::SrsConfigBuffer* buffer, SrsDirectiveType type);
virtual int read_token(_srs_internal::SrsConfigBuffer* buffer, std::vector
1、parse_conf是用来循环解析当前指令区域的所有指令,当指令区中的某个指令遇到子指令块时,会嵌套调用parse_conf去解析子指令块。parse_conf的实现逻辑实际上是一个状态机,具体解析命令名、命令参数、返回解析状态 是由read_token去完成的。
// see: ngx_conf_parse
int SrsConfDirective::parse_conf(SrsConfigBuffer* buffer, SrsDirectiveType type)
{
int ret = ERROR_SUCCESS;
while (true) {
/* 循环解析当前指令区域的所有指令(也就是说已
经有父指令了,因为当前指令区域就是这个父指
令的子指令集合区域)
*/
std::vector args;// 存放当前指令的指令名和参数
int line_start = 0;// 当前解析完的行号
ret = read_token(buffer, args, line_start);
/**
* ret maybe:
* ERROR_SYSTEM_CONFIG_INVALID error.
* ERROR_SYSTEM_CONFIG_DIRECTIVE directive terminated by ';' found
* ERROR_SYSTEM_CONFIG_BLOCK_START token terminated by '{' found
* ERROR_SYSTEM_CONFIG_BLOCK_END the '}' found
* ERROR_SYSTEM_CONFIG_EOF the config file is done
*/
if (ret == ERROR_SYSTEM_CONFIG_INVALID) {// 解析出错,直接退出
return ret;
}
if (ret == ERROR_SYSTEM_CONFIG_BLOCK_END) {
if (type != parse_block) {// 解析出错(莫名其妙多了一个"}"),直接退出
srs_error("line %d: unexpected \"}\", ret=%d", buffer->line, ret);
return ret;
}
return ERROR_SUCCESS;
}
if (ret == ERROR_SYSTEM_CONFIG_EOF) {// 根配置解析完毕
if (type == parse_block) {// 不是根配置,出错
srs_error("line %d: unexpected end of file, expecting \"}\", ret=%d", conf_line, ret);
return ret;
}
return ERROR_SUCCESS;
}
if (args.empty()) {// 连指令名都没有解析出来,出错
ret = ERROR_SYSTEM_CONFIG_INVALID;
srs_error("line %d: empty directive. ret=%d", conf_line, ret);
return ret;
}
// build directive tree.
SrsConfDirective* directive = new SrsConfDirective();// 构建当前解析的指令
directive->conf_line = line_start;
directive->name = args[0];
args.erase(args.begin());// 删除第一个arg,因为第一个是指令的name
directive->args.swap(args);
directives.push_back(directive);// 将当前指令添加到其父指令的子指集合中
if (ret == ERROR_SYSTEM_CONFIG_BLOCK_START) {// 解析当前指令的子指令
if ((ret = directive->parse_conf(buffer, parse_block)) != ERROR_SUCCESS) {
return ret;
}
}
}
// 继续解析当前指令区域的下一个子指令
return ret;
}
2、read_token用来直接从配置文件内容中解析出命令名、命令参数、并返回当前的解析状态(如果按标志配置,每行只有一个命令的话,read_token可以理解为每次解析一行配置文件)。返回的状态有下面这些:
ERROR_SYSTEM_CONFIG_EOF// 配置文件解析完毕
ERROR_SYSTEM_CONFIG_INVALID// 配置解析出错
ERROR_SYSTEM_CONFIG_DIRECTIVE// 一个配置项解析完毕
ERROR_SYSTEM_CONFIG_BLOCK_START// 开始一个新的配置块
ERROR_SYSTEM_CONFIG_BLOCK_END// 当前配置块解析完毕
// see: ngx_conf_read_token
int SrsConfDirective::read_token(SrsConfigBuffer* buffer, vector& args, int& line_start)
{
/*
1、首先明确,什么是一个token:
token是处在两个相邻空格,换行符,双引号,单引号等之间的字符串.
2、这里是一个字符一个字符的解析的
**/
int ret = ERROR_SUCCESS;
char* pstart = buffer->pos;// 当前解析到的位置
bool sharp_comment = false;//注释(#)
bool d_quoted = false;//标志位,表示已扫描一个双引号,期待另一个单引号
bool s_quoted = false;//标志位,表示已扫描一个双引号,期待另一个双引号
bool need_space = false;//标志位,表示需要空白字符
bool last_space = true;//标志位,表示上一个字符为token分隔符
while (true) {
if (buffer->empty()) {
ret = ERROR_SYSTEM_CONFIG_EOF;
if (!args.empty() || !last_space) {// 配置内容不完整
srs_error("line %d: unexpected end of file, expecting ; or \"}\"", buffer->line);
return ERROR_SYSTEM_CONFIG_INVALID;
}
srs_trace("config parse complete");// 配置解析完毕
return ret;
}
char ch = *buffer->pos++;
if (ch == SRS_LF) {// 换行
buffer->line++;
sharp_comment = false;
}
if (sharp_comment) {// 如果当前是注释行,直接跳过
continue;
}
if (need_space) {
if (is_common_space(ch)) {// 是空白字符
last_space = true;
need_space = false;
continue;
}
if (ch == ';') {
return ERROR_SYSTEM_CONFIG_DIRECTIVE;// 一个配置项结束
}
if (ch == '{') {
return ERROR_SYSTEM_CONFIG_BLOCK_START;// 一个新配置区域开始
}
srs_error("line %d: unexpected '%c'", buffer->line, ch);
return ERROR_SYSTEM_CONFIG_INVALID;
}
// last charecter is space.
if (last_space) {
if (is_common_space(ch)) {
continue;
}
pstart = buffer->pos - 1;
switch (ch) {
case ';':
if (args.size() == 0) {
srs_error("line %d: unexpected ';'", buffer->line);
return ERROR_SYSTEM_CONFIG_INVALID;
}
return ERROR_SYSTEM_CONFIG_DIRECTIVE;
case '{':
if (args.size() == 0) {
srs_error("line %d: unexpected '{'", buffer->line);
return ERROR_SYSTEM_CONFIG_INVALID;
}
return ERROR_SYSTEM_CONFIG_BLOCK_START;
case '}':
if (args.size() != 0) {
srs_error("line %d: unexpected '}'", buffer->line);
return ERROR_SYSTEM_CONFIG_INVALID;
}
return ERROR_SYSTEM_CONFIG_BLOCK_END;
case '#':
sharp_comment = 1;
continue;
case '"':
pstart++;
d_quoted = true;
last_space = 0;
continue;
case '\'':
pstart++;
s_quoted = true;
last_space = 0;
continue;
default:
last_space = 0;
continue;
}
} else {
// last charecter is not space
if (line_start == 0) {
line_start = buffer->line;
}
bool found = false;// 找到一个token
if (d_quoted) {
if (ch == '"') {
d_quoted = false;
need_space = true;
found = true;
}
} else if (s_quoted) {
if (ch == '\'') {
s_quoted = false;
need_space = true;
found = true;
}
} else if (is_common_space(ch) || ch == ';' || ch == '{') {
last_space = true;
found = 1;
}
if (found) {
int len = (int)(buffer->pos - pstart);
char* aword = new char[len];
memcpy(aword, pstart, len);
aword[len - 1] = 0;
string word_str = aword;// 解析出一个token
if (!word_str.empty()) {
args.push_back(word_str);
}
srs_freepa(aword);
if (ch == ';') {// 一个配置项解析完
return ERROR_SYSTEM_CONFIG_DIRECTIVE;
}
if (ch == '{') {// 一个新的子配置区域
return ERROR_SYSTEM_CONFIG_BLOCK_START;
}
}
}
}
return ret;
}
前面介绍了配置的组织和解析。前面的配置只包括从配置文件中解析出来的配置,实际上,配置除了配置文件还有启动命令参数中的配置。这些启动命令参数中的配置是存放在SrsConfig类型全局变量中的,SrsConfig同时通过成员属性root(SrsConfDirective*)去管理配置文件中的配置,更重要的是,SrsConfig可以去动态加载配置文件,原生配置文件的动态加载流程如下:
(1)发送一个信号SIGHUP到srs进程,srs这个信号的处理函数会将该信号写入管道
(2)srs中信号管理器运行着一个独立的协程,他会循环读对应的管道,一旦读到信号,就会修改SrsServer中对应的标志。比如读到SIGHUP,就会将signal_reload置为true。
(3)srs的main协程的循环处理函数SrsServer::do_cycle()中,会定期检测signal_reload标志,如果为true则会_srs_config->reload()去启动配置动态加载
(4)SrsConfig::reload中会首先加载配置文件,并解析得到一个新的SrsConfDirective中。然后检测新的SrsConfDirective中的配置项是不是在代码中注册过的。接着,SrsConfig::reload_conf比较存放新旧配置的两个SrsConfDirective,如果检测到配置有差异,就会调用所有订阅者ISrsReloadHandler的相应的回调方法,使得订阅者重新加载配置。