感叹于SRS项目封装的精妙特此记录

这里并不会按照SRS的逻辑框架,或面面俱到的陈述所有特点,仅是记录其优势以及卓越的设计方法。

日志系统(srs_kernel_log.hpp)

SRS支持打印到console和file;支持设置level,支持连接级别的日志,支持可追溯日志;基类日志系统中定义了两个虚基类ISrsLog 和 ISrsContext,系统内的业务逻辑都是基于这两个接口实现的。

level设置

verbose: 非常详细的日志,性能会很低,日志会非常多。SRS默认是编译时禁用这些日志,提高性能。
info:较为详细的日志,性能也受影响。SRS默认编译时禁用这些日志。
trace: 重要的日志,比较少,SRS默认使用这个级别。
warn: 警告日志,SRS在控制台以黄色显示。若SRS运行较稳定,可以只打开这个日志。建议使用trace级别。
error: 错误日志,SRS在控制台以红色显示。
当设置低级日志时自动打印高级,譬如设置为trace,那么trace/warn/error日志都会打印出来。
注意级别数字设计:分别为1、2、4、8、16、32。方便进行位处理等

日志开销控制

1
2
3
4
#ifndef SRS_VERBOSE
#undef srs_verbose//终止定义
#define srs_verbose(msg, ...) (void)0
#endif

通过这样的宏技巧,实现编译时的裁剪。编译器会检查是否定义了SRS_VERBOSE。如果未定义,所有对应的日志调用(例如 srs_verbose(…))都会被直接替换成 (void)0,即空语句,步进不打印,而且不产生任何开销。

宏定义接口

1
2
3
#define srs_trace(msg, ...) srs_logger_impl(SrsLogLevelTrace, NULL, _srs_context->get_id(), msg, ##__VA_ARGS__)

void srs_logger_impl(SrsLogLevel level, const char* tag, const SrsContextId& context_id, const char* fmt, ...)

通过宏定义封装,实现最简单的信息处理,##__VA_ARGS__中的##当可变参数为空的时候会将多余的,去掉。__VA_ARGS__会将可变参数的值copy到占用位置。

日志系统写文件设计(ISrsLog派生类)

  • if (level < level_ || level >= SrsLogLevelDisabled)
  • SrsThreadLocker(mutex_);
  • srs_log_header(log_data, LOG_MAX_SIZE, utc, level >= SrsLogLevelWarn, tag, context_id, srs_log_level_strings[level], &size);
  • vsnprintf(log_data + size, LOG_MAX_SIZE - size, fmt, args);
  • snprintf(log_data + size, LOG_MAX_SIZE - size, “(%s)”, strerror(errno))
  • write_log(fd, log_data, size, level);
1
2
3
4
fd = ::open(filename.c_str(),
O_RDWR | O_CREAT | O_APPEND,
S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH
);

打开文件的函数也是设计的,,难啊。。下面说一下怎么做的

前三个标志位是:读写模式打开、不存在则自动创建、末尾追加(append)模式;后面的标志位是设定权限为664

日志系统写console设计(ISrsLog派生类)

与上一小节基本相同。

SrsThreadContext设计(ISrsContext派生类)

这个类毫无疑问是SRS日志系统的核心,引入了上下文管理,分配并管理所有线程的上下文ID,从而将分散的日志条目整理成一个业务流。每当一个新的连接(协程)建立时,都会调用_srs_context->generate_id() 生成一个新ID。实现的核心机制是线程的局部存储。

设置协程ID

这个函数接收当前协程句柄trd,以及设置的SrsContextId。当trd为空的时候,函数仅更新默认信息,将传入SrsContextId赋值给_srs_context_default;当trd不为空即处在一个有效的协程中,在堆上开辟一个SrsContextId对象,并得到其指针(cid)。接下使用st_key_create(仅第一次执行时)来创建全局key。并最后调用st_thread_setspecific2,拿着全局key、协程句柄trd、协程ID指针cid,完成绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const SrsContextId& srs_context_set_cid_of(srs_thread_t trd, const SrsContextId& v)
{
// 1. 性能统计,仅是计算set_id被调用的次数,进行系统负载和性能衡量
++_srs_pps_cids_set->sugar;

// 2. 判断传入的协程句柄是否为 NULL。如果是,意味着这不是为某个具体协程设置ID,而是要设置那个全局的默认ID _srs_context_default
if (!trd) {
_srs_context_default = v;
return v;
}

// 3. 在堆上为ID创建副本
SrsContextId* cid = new SrsContextId();
*cid = v;

// 4. 懒加载:首次调用时创建全局Key
if (_srs_context_key < 0) {
int r0 = srs_key_create(&_srs_context_key, _srs_context_destructor);//调用st_key_create(keyp, destructor),设定消除方法
srs_assert(r0 == 0);
}

// 5. 将ID副本的指针与指定协程绑定
int r0 = srs_thread_setspecific2(trd, _srs_context_key, cid);
srs_assert(r0 == 0);

return v;
}

获取协程ID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const SrsContextId& SrsThreadContext::get_id()
{
// 1. 性能统计。仅是计算get_id被调用的次数,进行系统负载和性能衡量
++_srs_pps_cids_get->sugar;

// 2. 检查当前是否在ST协程中运行
if (!srs_thread_self()) { // 返回(srs_thread_t)st_thread_self();获得正在执行的协程句柄
return _srs_context_default;//返回默认ID
}

// 3. 从协程局部存储中获取ID指针
void* cid = srs_thread_getspecific(_srs_context_key); // 调用st_thread_getspecific,拿到协程key,数据结构近似map<coroutine_handle, map<key, void*>>

// 4. 检查是否成功获取
if (!cid) {
return _srs_context_default;
}

// 5. 解引用并返回ID
return *(SrsContextId*)cid;
}

随机数生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
std::string srs_random_str(int len)
{
static string random_table = "01234567890123456789012345678901234567890123456789abcdefghijklmnopqrstuvwxyz";

string ret;
ret.reserve(len);
for (int i = 0; i < len; ++i) {
ret.append(1, random_table[srs_random() % random_table.size()]);//每次只加一个数
}

return ret;
}
long srs_random()
{
static bool _random_initialized = false;
if (!_random_initialized) {//单例化调用逻辑
_random_initialized = true;
::srandom((unsigned long)(srs_update_system_time() | (::getpid()<<13)));//利用时间和进程PID创建的随机数
}

return random();
}

RAII(Resource Acquisition Is Initialization)设计模式

设计一个类impl_SrsContextRestore设置新cid,保存先前的cid,并在析构时恢复先前。具体的应用场景是这样:当定时器处理超时协程时,打印日志的上下文需要临时切换到超时协程,cid需要做一个impl_SrsContextRestore初始化,并在结束后恢复,这样使日志输出更加清晰。

1
#define SrsContextRestore(cid) impl_SrsContextRestore _context_restore_instance(cid)

错误处理机制

XX宏使用

使用XX宏来定义错误,并通过两次解包装得到枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 这是“数据列表”宏
// 它接受一个“操作”宏作为参数,这里我们叫它 XX
#define SRS_ERRNO_MAP_SYSTEM(XX) \
XX(ERROR_SOCKET_CREATE, 1000, "SocketCreate", "Create socket fd failed") \
XX(ERROR_SOCKET_SETREUSE, 1001, "SocketReuse", "Setup socket reuse option failed") \
XX(ERROR_SOCKET_BIND, 1002, "SocketBind", "Bind socket failed")

// 1. 定义“操作”宏,我们叫它 SRS_ERRNO_GEN
// 这个宏接收四个参数 (n, v, m, s),并将其转换为 "n = v," 的形式
#define SRS_ERRNO_GEN(n, v, m, s) n = v,

// 2. 将这个“操作”宏作为参数,传给“数据列表”宏
enum SrsErrorCode {
ERROR_SUCCESS = 0,
SRS_ERRNO_MAP_SYSTEM(SRS_ERRNO_GEN) // 看这里!
// ... 其他列表
};

// 3. 立即取消定义,避免污染后续代码
#undef SRS_ERRNO_GEN

// 4. XX被替换
SRS_ERRNO_GEN(ERROR_SOCKET_CREATE, 1000, "SocketCreate", "Create socket fd failed")
SRS_ERRNO_GEN(ERROR_SOCKET_SETREUSE, 1001, "SocketReuse", "Setup socket reuse option failed")
SRS_ERRNO_GEN(ERROR_SOCKET_BIND, 1002, "SocketBind", "Bind socket failed")

//5. 产生枚举
enum SrsErrorCode {
ERROR_SUCCESS = 0,
ERROR_SOCKET_CREATE = 1000,
ERROR_SOCKET_SETREUSE = 1001,
ERROR_SOCKET_BIND = 1002,
// ...
};

SrsCplxError类设计

  • 私有成员变量 (Private Members):
    code, wrapped, msg, func, file, line 等所有核心数据都被声明为 private。这是一种良好的封装实践,意味着外部代码不能直接修改一个错误对象的状态,必须通过公共接口。

  • 私有构造函数 (Private Constructor):
    SrsCplxError() 是私有的。这意味着不能像这样 SrsCplxError* err = new SrsCplxError(); 来创建一个实例。这个设计强制所有使用者必须通过类提供的静态工厂方法(create, wrap)来创建对象,确保了对象在创建时总是被正确地初始化。

  • 公共静态方法 (Public Static Methods):
    这是整个错误处理框架的公共API。

success(): 返回 NULL,代表成功。description(err), summary(err), error_code(err) 等: 这些是获取错误信息的辅助函数。注意,获取描述的实例方法 description() 是私有的,而静态方法 description(SrsCplxError* err) 是公有的。这是一种设计选择,引导用户统一通过静态方法来处理错误对象(无论是 NULL 还是有效指针)。

  • 最后使用宏来包裹函数,完成语法糖设计,最后设置断言
1
2
3
4
5
6
7
8
9
10
11
// Error helpers, should use these functions to new or wrap an error.
#define srs_success NULL // SrsCplxError::success()
#define srs_error_new(ret, fmt, ...) SrsCplxError::create(__FUNCTION__, __FILE__, __LINE__, ret, fmt, ##__VA_ARGS__)
#define srs_error_wrap(err, fmt, ...) SrsCplxError::wrap(__FUNCTION__, __FILE__, __LINE__, err, fmt, ##__VA_ARGS__)
#define srs_error_copy(err) SrsCplxError::copy(err)
#define srs_error_desc(err) SrsCplxError::description(err)
#define srs_error_summary(err) SrsCplxError::summary(err)
#define srs_error_code(err) SrsCplxError::error_code(err)
#define srs_error_code_str(err) SrsCplxError::error_code_str(err)
#define srs_error_code_longstr(err) SrsCplxError::error_code_longstr(err)
#define srs_error_reset(err) srs_freep(err); err = srs_success

核心配置文件

配置树

设置SrsConfDirective为配置树根节点(vector<SrsConfDirective*> directives),在其下面安置各种子配置,重复迭代形成配置树结构,使用get方法进行树查找。

热加载

使用观察者模式实现。

对get的封装

SrsConfig对于海量的get_XX,封装了其复杂的树查找过程。最后仅需调用_srs_config->get_hls_fragment(vhost_name)形式即可得到配置


本站由 Edison.Chen 创建。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

undefined