janus
中的plugin
是其非常重要的一部分内容,今天我们就来对这块内容做一下分析,看看janus
是如何实现plugin
的,以及它的工作原理是怎样的。
janus的架构模型
janus
的最大特色就是可以以插件的方式对业务模块进行管理。比如当你想实现新业务时,按照janus Plugin
的要求写一个plugin
,然后将它放到指定目录下,这样janus
在启动时就可以将它加载到内存中。
下面这张图是janus的整体架构图:
从上面这张图我们可以看到,janus
在设计时被分成了两层,即核心层
和插件层
。核心层主要用于资源的分配(如线程的启动与分配)、底层事件处理、各种WebRTC协议的实现及处理等;插件层用于业务处理,各种传输类型命令的处理等。
从中我们可以知道,这样的架构设计及管理方式特别适合变化比较快的业务模型。因为我们可以随时生成一个新的janus插件,并将它加载到内存中。
OK,了解了janus
的架构,我们再来看看janus是如何实现插件管理的吧。
Linux 系统下动态库的动态加载
要想真正理解janus的插件管理,我们首先要知道Linux系统是如何动态加载库的,这是我们理解 janus 插件管理的基础。
在Linux系统中,动态加载库其实很容易,只要用两个API 就可以了,即 dlopen 和 dlsym 。它们的定义如下:
#include <dlfcn.h> /** * path: 被加载到内存中的动态库路径 * mode: RTLD_LAZY,用时加载;RTLD_NOW,立即加载; * return: handle,即被加载的动态库的内存地址 */ void* dlopen(const char* path, int mode); /** * handle: dlopen 的返回值 * symbol: 指向动态库中的符号,如函数,变量等 * return: 返回在内存中的符号地址 */ void* dlsym(void* handle, const char* symbol);
其中,dlopen 用于将动态库加载到内存中;dlsym 用于查找被加载到内存中的动态库的函数或变量的地址。
接下来我们就使用这两个API 来演示一下如何在 Linux 系统下动态加载库。
要想做这个演示,首先我们要写一个动态库。这个动态库特别简单,就一个函数 add() , 用于加法运算。代码如下:
//源码 #include <stdio.h> int add(int a, int b){ return a+b; } //编译 gcc -shared -o add.so add.c
通过上面的操作我们就有了一个add.so的动态库。接下来我们再来看看如何使用 dlopen 及 dlsym 将上面生成的库动态库加载到内存中,并调用它的 add() 方法吧。具体代码如下:
#include<stdio.h> #include <dlfcn.h> typedef int(*FUNC)(int,int); int main(int argc, char *argv[]){ void* handler = dlopen("./add.so", RTLD_NOW); if(!handler){ printf("Failed to load so!\n"); return -1; } FUNC func = (FUNC)dlsym(handler, "add"); int r = func(10, 20); printf("the result is : %d\n", r); return 0; } //编译 gcc -g -o loadso mytest.c
上面的这段代码是不是很简单?短短的几行代码就向你展示了在 Linux/Mac 系统下动态加载并调用动态库中方法的具体步骤。有了上面的知识,我们再来看janus的实现就很容易理解它是如何做的了。
janus 加载Plugin
janus实现加载插件的代码量很大,但核心代码就那么几行,只要我们将核心代码抽取出来,我们就会发现其实它与我们上面讲的代码几乎是一模一样的。下面我们来看看janus 是如何动态加载库的吧。
为了方便janus专门创建了一个目录用于存放插件。在janus启动时它会遍历该目录,并将目录中的插件一个个动态加载到内存中。经整理后的代码如下:
while((pluginent = readdir(dir))) { ...... g_snprintf(pluginpath, 1024, "%s/%s", path, pluginent->d_name); void *plugin = dlopen(pluginpath, RTLD_NOW | RTLD_GLOBAL); ...... create_p *create = (create_p*) dlsym(plugin, "create"); ...... janus_plugin *janus_plugin = create(); ...... }
上面的这段核心代码是不是与我们上面 Linux 系统下动态库的动态加载 一节介绍的几乎是一样的?所以我们只要把基础知识撑握好了,再看一些复杂的实现时也就不会感觉很难了。
从上面的代码中我们还可以看到,janus 中的每个插件都实现了create 函数。该函数会返回一个包含多个函数的结构体。这些函数是我们实现janus插件必须要实现的,它们包括:
init()
: 该函数是插件的初始化函数,像读取配置文件等操作都应该在这个函数中实现。destroy()
: 插件被关闭的时候被调用get_api_compatibility()
: 该方法只需要返回janus的API版本即可,用于控制不同janus版本是否兼容get_version()
: 返回版本号(例如 3)get_version_string()
: 返回字符串格式的版本号(例如, “v1.0.1”)get_description()
: 返回插件的详细信息get_name()
: 返回插件的短名子get_package()
:返回插件的唯一包标识 (例如., “janus.plugin.myplugin”);create_session()
: 在你和客户端之间创建一个sessionhandle_message()
: 处理对方发送给你的消息handle_admin_message()
: 来自Admin API的消息setup_media()
: 创建socket,建立与客户端peerConnection
之间的连接incoming_rtp()
: 接收客户端发过来的rtp
包incoming_rtcp()
: 接收客户端发过来的rtcp
消息incoming_data()
: 接收客户端通过SCTP DataChannel
发过来的数据data_ready()
: 检查数据是否可以通过SCTP DataChannel
发送了slow_link()
: 客户端发过来很多NACK
,说是此时网络质量变差了hangup_media()
: 客户端关闭了PeerConnection
query_session()
: 查询在你与客户端之间的session
信息destroy_session()
: 销毁session
对于一个插件来讲,上面的方法中除了 incoming_rtp
、incoming_rtcp
、incoming_data
可以不实现外,其它的方法都必须实现。只有这样当插件被janus
核心层加载之后,才可以被顺利的调用。
小结
本文我重点向你介绍了janus
是如何使用和管理plugin
的,同时向你简要的介绍了janus
的架构模型以及要实现一个janus
插件要实现哪些函数。
当然,我们这里只是对plugin
做了一个大体的讲解,很多细节这里并没有讲到,我会在后面的文档中做更详细的介绍 。