ZLMediaKit源码阅读:MediaServer main函数分析

1059 篇文章 276 订阅

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);通知程序有异常发生了,就会进入下面第三步
  1. 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.enablehook,admin_params

  • hook.enable默认为 false
  • hook,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);
}
  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
zlmediakit是一款开源的基于C++语言的流媒体服务器,其代码实现相对简单,源码分析后可以加深对流媒体服务器工作原理的理解。 首先,zlmediakit源码通过C++语言实现了一个基于TCP协议的流媒体服务器,可以用来实现音视频的实时传输和录像功能。源码中主要包含了底层网络通信模块、流媒体协议解析模块、音视频编解码模块和存储模块等功能。 在底层网络通信模块中,zlmediakit使用了epoll模型进行网络事件的监听与处理,通过TCP协议提供了稳定的连接,并支持同时处理多个客户端的请求。 在流媒体协议解析模块中,zlmediakit支持RTMP、RTSP、HLS等多种流媒体协议的解析和处理,可以接收来自客户端的音视频数据,并根据协议要求进行解析和分发。 在音视频编解码模块中,zlmediakit提供了基于FFmpeg的音视频编解码功能,支持常见的音视频编码格式,可以将输入的音视频数据进行解码或者编码,并通过网络传输给客户端。 在存储模块中,zlmediakit可以将音视频数据保存到本地文件或者直播服务器中,支持实时录像和回放功能。 总的来说,zlmediakit是一款功能强大且易于使用的流媒体服务器,其源码分析可以帮助我们了解流媒体服务器的工作原理,深入理解音视频传输与处理的过程,对于开发相关应用和解决流媒体相关问题具有一定的参考价值。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值