感叹于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) { ++_srs_pps_cids_set->sugar;
if (!trd) { _srs_context_default = v; return v; }
SrsContextId* cid = new SrsContextId(); *cid = v;
if (_srs_context_key < 0) { int r0 = srs_key_create(&_srs_context_key, _srs_context_destructor); srs_assert(r0 == 0); }
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() { ++_srs_pps_cids_get->sugar;
if (!srs_thread_self()) { return _srs_context_default; }
void* cid = srs_thread_getspecific(_srs_context_key);
if (!cid) { return _srs_context_default; }
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))); }
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
|
#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")
#define SRS_ERRNO_GEN(n, v, m, s) n = v,
enum SrsErrorCode { ERROR_SUCCESS = 0, SRS_ERRNO_MAP_SYSTEM(SRS_ERRNO_GEN) };
#undef SRS_ERRNO_GEN
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")
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
| #define srs_success NULL #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)形式即可得到配置