main函数入口在server/main.cpp中
int main(int argc,char *argv[]) {
return start_main(argc,argv);
}
可以看出,它实际上是调用了start_main
函数。
start_main又做了什么呢?
第一步,它解析函数入口参数
CMD_main cmd_main;
try {
cmd_main.operator()(argc, argv);
} catch (ExitException &) {
return 0;
} catch (std::exception &ex) {
cout << ex.what() << endl;
return -1;
}
一般都是直接使用默认的(不传参数),如下:
bool bDaemon = cmd_main.hasKey("daemon");
LogLevel logLevel = (LogLevel) cmd_main["level"].as<int>();
logLevel = MIN(MAX(logLevel, LTrace), LError);
g_ini_file = cmd_main["config"];
string ssl_file = cmd_main["ssl"];
int threads = cmd_main["threads"];
第二步:设置日志输出到控制台和文件
Logger::Instance().add(std::make_shared<ConsoleChannel>("ConsoleChannel", logLevel));
auto fileChannel = std::make_shared<FileChannel>("FileChannel", exeDir() + "log/", logLevel);
fileChannel->setMaxDay(cmd_main["max_day"]); //日志最多保存天数
Logger::Instance().add(fileChannel);
exeDir()为二进制当前程序位置。
感兴趣的话可以看下exeDir()是怎么实现的,如下:
string exePath(bool isExe /*= true*/) {
char buffer[PATH_MAX * 2 + 1] = {0};
int n = -1;
n = readlink("/proc/self/exe", buffer, sizeof(buffer));
string filePath;
if (n <= 0) {
filePath = "./";
} else {
filePath = buffer;
}
return filePath;
}
原理:linux系统中有个符号链接:/proc/self/exe 它代表当前程序,所以可以用readlink读取它的源路径就可以获取当前程序的绝对路径
第三步: 设置如果崩溃的话就开启捕获崩溃
System::systemSetup();
看下它是怎么实现的
(1)首先解开进程资源限制
struct rlimit rlim,rlim_new;
if (getrlimit(RLIMIT_CORE, &rlim)==0) { //core文件的最大字节数
rlim_new.rlim_cur = rlim_new.rlim_max = RLIM_INFINITY; // 不限制
if (setrlimit(RLIMIT_CORE, &rlim_new)!=0) { // 设置为不限制
rlim_new.rlim_cur = rlim_new.rlim_max = rlim.rlim_max;
setrlimit(RLIMIT_CORE, &rlim_new);
}
InfoL << "core文件大小设置为:" << rlim_new.rlim_cur;
}
if (getrlimit(RLIMIT_NOFILE, &rlim)==0) { // 获取进程默认可以打开的最大文件描述符数
rlim_new.rlim_cur = rlim_new.rlim_max = RLIM_INFINITY; //设置为不限制
if (setrlimit(RLIMIT_NOFILE, &rlim_new)!=0) {
rlim_new.rlim_cur = rlim_new.rlim_max = rlim.rlim_max;
setrlimit(RLIMIT_NOFILE, &rlim_new);
}
InfoL << "文件最大描述符个数设置为:" << rlim_new.rlim_cur;
}
2、然后捕获异常信号:SIGSEGV和SIGABRT
signal(SIGSEGV, sig_crash);
signal(SIGABRT, sig_crash);
当有信号发生时,自动调用sig_crash,这里的效果是
- 第一次发生SIGSEGV或者SIGABRT异常时,就调用sig_crash函数。
- sig_crash函数里面会读取异常的栈帧,并使用addr2line找出是哪一行发生了异常,并压入一个vector里面,然后使用
NoticeCenter::Instance().emitEvent(kBroadcastOnCrashDump,sig,stack);
通知程序有异常发生了,就会进入下面第三步
- NoticeCenter::Instance().addListener监听异常事件,当异常发生时,就将响应栈帧情况写入一个[程序路径/crash.pid]文件中
NoticeCenter::Instance().addListener(nullptr,kBroadcastOnCrashDump,[](BroadcastOnCrashDumpArgs){
stringstream ss;
ss << "## crash date:" << getTimeStr("%Y-%m-%d %H:%M:%S") << endl;
ss << "## exe: " << exeName() << endl;
ss << "## signal: " << sig << endl;
ss << "## stack: " << endl;
for (size_t i = 0; i < stack.size(); ++i) {
ss << "[" << i << "]: ";
for (auto &str : stack[i]){
ss << str << endl;
}
}
string stack_info = ss.str();
ofstream out(StrPrinter << exeDir() << "/crash." << getpid(), ios::out | ios::binary | ios::trunc);
out << stack_info;
out.flush();
cerr << stack_info << endl;
});
4. 下一次相同异常发生时,不会捕获了,而是直接崩溃。因为异常不会函数sig_crash
在第一次被调用时设置了
signal(sig, SIG_DFL); //恢复默认行为
第四步:读取配置文件
loadIniConfig(g_ini_file.data());
默认读取:
第五步:加载证书
//不是文件夹,加载证书,证书包含公钥和私钥
SSL_Initor::Instance().loadCertificate(ssl_file.data());
默认加载:
第六步:开启各个服务
根据刚刚读到的配置开启各个服务,注意,如果某个服务开启失败,就会退出当前程序
uint16_t shellPort = mINI::Instance()[Shell::kPort];
uint16_t rtspPort = mINI::Instance()[Rtsp::kPort];
uint16_t rtspsPort = mINI::Instance()[Rtsp::kSSLPort];
uint16_t rtmpPort = mINI::Instance()[Rtmp::kPort];
uint16_t rtmpsPort = mINI::Instance()[Rtmp::kSSLPort];
uint16_t httpPort = mINI::Instance()[Http::kPort];
uint16_t httpsPort = mINI::Instance()[Http::kSSLPort];
uint16_t rtpPort = mINI::Instance()[RtpProxy::kPort];
//简单的telnet服务器,可用于服务器调试,但是不能使用23端口,否则telnet上了莫名其妙的现象
//测试方法:telnet 127.0.0.1 9000
auto shellSrv = std::make_shared<TcpServer>();
//rtsp[s]服务器, 可用于诸如亚马逊echo show这样的设备访问
auto rtspSrv = std::make_shared<TcpServer>();;
auto rtspSSLSrv = std::make_shared<TcpServer>();;
//rtmp[s]服务器
auto rtmpSrv = std::make_shared<TcpServer>();;
auto rtmpsSrv = std::make_shared<TcpServer>();;
//http[s]服务器
auto httpSrv = std::make_shared<TcpServer>();;
auto httpsSrv = std::make_shared<TcpServer>();;
//GB28181 rtp推流端口,支持UDP/TCP
auto rtpServer = std::make_shared<RtpServer>();
try {
//rtsp服务器,端口默认554
if (rtspPort) { rtspSrv->start<RtspSession>(rtspPort); }
//rtsps服务器,端口默认322
if (rtspsPort) { rtspSSLSrv->start<RtspSessionWithSSL>(rtspsPort); }
//rtmp服务器,端口默认1935
if (rtmpPort) { rtmpSrv->start<RtmpSession>(rtmpPort); }
//rtmps服务器,端口默认19350
if (rtmpsPort) { rtmpsSrv->start<RtmpSessionWithSSL>(rtmpsPort); }
//http服务器,端口默认80
if (httpPort) { httpSrv->start<HttpSession>(httpPort); }
//https服务器,端口默认443
if (httpsPort) { httpsSrv->start<HttpsSession>(httpsPort); }
//telnet远程调试服务器
if (shellPort) { shellSrv->start<ShellSession>(shellPort); }
//创建rtp服务器
if (rtpPort) { rtpServer->start(rtpPort); }
} catch (std::exception &ex) {
WarnL << "端口占用或无权限:" << ex.what() << endl;
ErrorL << "程序启动失败,请修改配置文件中端口号后重试!" << endl;
sleep(1);
#endif
return -1;
}
第七步:设置http相关接口
注意,http服务在第六步就已经打开了
installWebApi();
InfoL << "已启动http api 接口";
installWebHook();
InfoL << "已启动http hook 接口";
第八步:设置一些信号
- 配置文件热更新
signal(SIGHUP, [](int) { mediakit::loadIniConfig(g_ini_file.data()); });
- 等待ctrl + c时,打印SIGINT:exit,然后退出
//设置退出信号处理函数
static semaphore sem;
signal(SIGINT, [](int) {
InfoL << "SIGINT:exit";
signal(SIGINT, SIG_IGN);// 设置退出信号
sem.post();
});// 设置退出信号
sem.wait(); //阻塞等待
第九步:销毁资源
unInstallWebApi();
unInstallWebHook();
//休眠1秒再退出,防止资源释放顺序错误
InfoL << "程序退出中,请等待...";
sleep(1);
InfoL << "程序退出完毕!";
installWebApi()在干啥?
做了三件事:
(1)简单来说就是:就是监听是否有HTTP请求事件到来
addHttpListener()
(2)可以理解为:string api_secret = 从配置文件找到api.secret
的内容
#define API_FIELD "api."
const string kSecret = API_FIELD"secret";
GET_CONFIG(string,api_secret,API::kSecret);
(3)注册相应API和事件到来时的回调函数
//获取线程负载
//测试url http://127.0.0.1/index/api/getThreadsLoad
api_regist("/index/api/getThreadsLoad", .....
//获取后台工作线程负载
//测试url http://127.0.0.1/index/api/getWorkThreadsLoad
api_regist("/index/api/getWorkThreadsLoad",
//获取服务器配置
//测试url http://127.0.0.1/index/api/getServerConfig
api_regist("/index/api/getServerConfig"
......
实现原理
关于addHttpListener
static inline void addHttpListener(){
GET_CONFIG(bool, api_debug, API::kApiDebug);
//注册监听kBroadcastHttpRequest事件
NoticeCenter::Instance().addListener(nullptr, Broadcast::kBroadcastHttpRequest ....
关于GET_CONFIG是怎么实现的
#define GET_CONFIG(type, arg, key) \
static type arg = ::toolkit::mINI::Instance()[key]; \
LISTEN_RELOAD_KEY(arg, key, { \
RELOAD_KEY(arg, key); \
});
::toolkit::mINI::Instance()可以简单理解为一个map,所以上面第二行相当于:
static std::string api_secret = map[ API::kSecret];
下面两行的功能是: 监听相应key是否有变化,如果变化了,就重新更新api_secret 。 具体原理稍后解释
api_regist怎么实现的?
随便找一个
void api_regist(const string &api_path, const function<void(API_ARGS_MAP_ASYNC)> &func) {
s_map_api.emplace(api_path, toApi(func));
}
发现它是不断的往s_map_api压数据,那s_map_api又是个什么东西呢?
using HttpApi = function<void(const Parser &parser, const HttpSession::HttpResponseInvoker &invoker, SockInfo &sender)>;
//http api列表
static map<string, HttpApi> s_map_api;
可以看出它是一个map,其key是字符串,value是一个函数指针。 另外,是一个重载函数,它可以将不同的回调函数都转成HttpAPI格式的指针,作为这个map的value。具体的转换工作是由 toApi(func)干的
#define HTTP_FIELD "http."
const string kCharSet = HTTP_FIELD"charSet";
static HttpApi toApi(const function<void(API_ARGS_MAP_ASYNC)> &cb) {
return [cb](const Parser &parser, const HttpSession::HttpResponseInvoker &invoker, SockInfo &sender) {
.......
};
}
installWebHook的工作?
做了三件事
(1)从配置文件中获取hook.enable
和hook,admin_params
hook.enable
默认为 falsehook,admin_params
默认为 secret=035c73f7-bb6b-4889-a715-d9eb2d1925cc
GET_CONFIG(bool,hook_enable,Hook::kEnable);
GET_CONFIG(string,hook_adminparams,Hook::kAdminParams);
配置文件内容:
[hook]
#在推流时,如果url参数匹对admin_params,那么可以不经过hook鉴权直接推流成功,播放时亦然
#该配置项的目的是为了开发者自己调试测试,该参数暴露后会有泄露隐私的安全隐患
admin_params=secret=035c73f7-bb6b-4889-a715-d9eb2d1925cc
#是否启用hook事件,启用后,推拉流都将进行鉴权
enable=0
(2)监听各种事件
//监收到rtsp/rtmp推流事件广播,通过该事件控制推流鉴权
NoticeCenter::Instance().addListener(nullptr, Broadcast::kBroadcastMediaPublish, ......
//播放rtsp/rtmp/http-flv事件广播,通过该事件控制播放鉴权
NoticeCenter::Instance().addListener(nullptr,Broadcast::kBroadcastMediaPlayed
.....
(3)告诉某台主机【我】已经启动了,并定时上报保活
//上报服务器启动
reportServerStarted();
//定时上报保活
reportServerKeepalive();
实现原理
reportServerStarted:上报前服务器启动
(1)读取相关配置
GET_CONFIG(bool,hook_enable,Hook::kEnable);
GET_CONFIG(string,hook_server_started,Hook::kOnServerStarted);
if(!hook_enable || hook_server_started.empty()){
return;
}
配置文件相关内容如下:
[hook]
#是否启用hook事件,启用后,推拉流都将进行鉴权
enable=1
#服务器启动报告,可以用于服务器的崩溃重启事件监听
on_server_started=https://127.0.0.1/index/hook/on_server_started
如果要继续执行,enable必须为1&&on_server_started不为空
(2)将配置文件中所有配置转成一个json
ArgsType body;
for (auto &pr : mINI::Instance()) {
body[pr.first] = (string &) pr.second;
}
(3)执行do_http_hook
do_http_hook(hook_server_started,body, nullptr);
do_http_hook就是把body发送到hook_server_started指定的URL上
do_http_hook实现:发送HTTP请求
(1)读取相关配置
GET_CONFIG(string, mediaServerId, General::kMediaServerId);
GET_CONFIG(float, hook_timeoutSec, Hook::kTimeoutSec);
内容如下:
[hook]
#hook api最大等待回复时间,单位秒
timeoutSec=10
[general]
#流媒体服务器的ID(GUID),用于触发hook时区别是哪台服务器
mediaServerId=your_server_id
值得说明一下的是mediaServerId,它不需要自己去配置,而是在程序启动时会随机生成一个标识当前服务器
#define GENERAL_FIELD "general."
const string kMediaServerId = GENERAL_FIELD"mediaServerId";
......
static onceToken token([](){
.....
mINI::Instance()[kMediaServerId] = makeRandStr(16);
(2)使用刚刚读到的mediaServerId修改body,表示当前是哪台服务器上报
const_cast<ArgsType &>(body)["mediaServerId"] = mediaServerId;
(3)生成一个Http请求头,开始上报
(4)具体是怎么上报的我们先不管,也就是不关心requester->startRequester的实现原理。这里我们需要知道的是如果这个请求发送完毕之后(成功、失败、超时),就会自己去执行startRequester的第二个参数,它是一个lambda回调
requester->startRequester(....., [url, func, bodyStr, requester, pTicker](const SockException &ex,
它做了两件事情:
(4.1)一些清理工作
onceToken token(nullptr, [&]() mutable{
requester.reset();
});
(4.2)解析响应,并打印
parse_http_response(ex, res, [&](const Value &obj, const string &err) {
if (!err.empty()) {
WarnL << "hook " << url << " " << pTicker->elapsedTime() << "ms,failed" << err << ":" << bodyStr;
} else if (pTicker->elapsedTime() > 500) {
DebugL << "hook " << url << " " << pTicker->elapsedTime() << "ms,success:" << bodyStr;
}
});
reportServerKeepalive:保活
(1)读取配置相关
GET_CONFIG(bool, hook_enable, Hook::kEnable);
GET_CONFIG(string, hook_server_keepalive, Hook::kOnServerKeepalive);
if (!hook_enable || hook_server_keepalive.empty()) {
return;
}
配置文件相关内容如下:
[hook]
#是否启用hook事件,启用后,推拉流都将进行鉴权
enable=1
#服务器启动报告,可以用于服务器的崩溃重启事件监听
on_server_started=https://127.0.0.1/index/hook/on_server_started
如果要继续执行,enable必须为1&&on_server_started不为空
(2)生成一个定时任务
GET_CONFIG(float, alive_interval, Hook::kAliveInterval);
g_keepalive_timer = std::make_shared<Timer>(alive_interval, []() {
getStatisticJson([](const Value &data) mutable {
ArgsType body;
body["data"] = data;
//执行hook
do_http_hook(hook_server_keepalive, body, nullptr);
});
return true;
}, nullptr);
配置文件相关内容如下:
[hook]
#keepalive hook触发间隔,单位秒,float类型
alive_interval=10.0
当定时任务触发时,会定时发送一些统计数据到指定URL
void getStatisticJson(const function<void(Value &val)> &cb) {
auto obj = std::make_shared<Value>(objectValue);
auto &val = *obj;
val["MediaSource"] = (Json::UInt64)(ObjectStatistic<MediaSource>::count());
val["MultiMediaSourceMuxer"] = (Json::UInt64)(ObjectStatistic<MultiMediaSourceMuxer>::count());
val["TcpServer"] = (Json::UInt64)(ObjectStatistic<TcpServer>::count());
val["TcpSession"] = (Json::UInt64)(ObjectStatistic<TcpSession>::count());
val["UdpServer"] = (Json::UInt64)(ObjectStatistic<UdpServer>::count());
val["UdpSession"] = (Json::UInt64)(ObjectStatistic<UdpSession>::count());
val["TcpClient"] = (Json::UInt64)(ObjectStatistic<TcpClient>::count());
val["Socket"] = (Json::UInt64)(ObjectStatistic<Socket>::count());
val["FrameImp"] = (Json::UInt64)(ObjectStatistic<FrameImp>::count());
val["Frame"] = (Json::UInt64)(ObjectStatistic<Frame>::count());
val["Buffer"] = (Json::UInt64)(ObjectStatistic<Buffer>::count());
val["BufferRaw"] = (Json::UInt64)(ObjectStatistic<BufferRaw>::count());
val["BufferLikeString"] = (Json::UInt64)(ObjectStatistic<BufferLikeString>::count());
val["BufferList"] = (Json::UInt64)(ObjectStatistic<BufferList>::count());
val["RtpPacket"] = (Json::UInt64)(ObjectStatistic<RtpPacket>::count());
val["RtmpPacket"] = (Json::UInt64)(ObjectStatistic<RtmpPacket>::count());
cb(*obj);
}