OpenSIPS 3.1 开发手册(七)--新模块开发

https://www.opensips.org/Documentation/Development-Manual

目录

16.  模块开发

16.1  引言

16.2  编译模块

16.3  初始化模块

16.4  销毁模块

16.5  添加模块参数

16.6  添加模块函数

16.7  添加模块MI函数

16.8  添加模块统计

16.9  添加模块伪变量

16.10  添加模块专用进程


16.  模块开发

16.1  引言

        根据OpenSIPS 的模块化架构,添加新特性(新的参数,脚本函数,MI接口函数)的最简单方式是把它合入一个新的OpenSIPS模块
        一个OpenSIPS 模块实际上就是一个动态库(so文件),在OpenSIPS 启动过程中可以动态加载,如果从OpenSIPS 脚本中加载模块,那么要用loadmodule 指令。

loadmodule "mynewmod.so"


        加载新模块时,OpenSIPS核心会查找类型为struct module_exportsexports 变量。这个结构体变量是开发新OpenSIPS模块时最重要的变量。

        struct module_exports{
              char* name;                     /*!< null terminated module name */
              char *version;                  /*!< module version */
              char *compile_flags;            /*!< compile flags used on the module */
              unsigned int dlflags;           /*!< flags for dlopen */

              cmd_export_t* cmds;             /*!< null terminated array of the exported
                                           commands */
              param_export_t* params;         /*!< null terminated array of the exported
                                           module parameters */

              stat_export_t* stats;           /*!< null terminated array of the exported
                                           module statistics */

              mi_export_t* mi_cmds;           /*!< null terminated array of the exported
                                           MI functions */

              pv_export_t* items;             /*!< null terminated array of the exported
                                           module items (pseudo-variables) */

              proc_export_t* procs;           /*!< null terminated array of the additional
                                           processes reqired by the module */

              init_function init_f;           /*!< Initialization function */
              response_function response_f;   /*!< function used for responses,
                                           returns yes or no; can be null */
              destroy_function destroy_f;     /*!< function called when the module should
                                           be "destroyed", e.g: on opensips exit */
              child_init_function init_child_f;/*!< function called by all processes
                                            after the fork */
        };


        module_exports内容及注释非常清晰。

        接下来,我们将逐个讨论module_exports 的每个成员,解释它们在构建新模块中的用法。先看一个纯粹的实例,dialog 模块里的exports 变量:

        struct module_exports exports= {
              "dialog",        /* module's name */
              MODULE_VERSION,
              DEFAULT_DLFLAGS, /* dlopen flags */
              cmds,            /* exported functions */
              mod_params,      /* param exports */
              mod_stats,       /* exported statistics */
              mi_cmds,         /* exported MI functions */
              mod_items,       /* exported pseudo-variables */
              0,               /* extra processes */
              mod_init,        /* module initialization function */
              0,               /* reply processing function */
              mod_destroy,
              child_init       /* per-child init function */
        };

16.2  编译模块

        接下来,我们将遵循构建新模块的各种选项,构建一个名为ournewmod的新模块。

        在OpenSIPS的modules/目录下创建一个ournewmod 目录,然后,需要为我们的模块写一个Makefile,放在ournewmod 目录下。如果模块没有外部库依赖,那么最基本的Makefile内容是这样的:

# $Id$
#
# WARNING: do not run this directly, it should be run by the master Makefile

include ../../Makefile.defs
auto_gen=
NAME=ournewmod.so
LIBS=

include ../../Makefile.modules


        如果模块有外部库依赖,需要把它们链接进模块的Makefile。例如cachedb_memcached模块:

include ../../Makefile.defs
auto_gen=
NAME=cachedb_memcached.so
DEFS+=-I$(LOCALBASE)/include
LIBS=-L$(LOCALBASE)/lib -lmemcached

include ../../Makefile.modules

    如果新模块依赖外部库,那么不得将它放入缺省编译范围!


接下来,必须编辑Makefile.conf.template 文件(位于源码根目录下),它指定模块是否缺省编译,还有它的依赖关系。

Makefile.conf.template 里添加一行,格式如下:

modulename= Module Description | module dependency

        此外,我们还需要修改Makefile.conf.template里的exclude_modules 列表,把新模块的名字加进去,这样,缺省就不会编译这个模块。
 

16.3  初始化模块

        在初始化新模块的上下文中,有两类函数可以帮助我们:

mod_init

        这个函数必须在我们的module_exports exports 结构体变量中指定,对应成员init_f 。

        它是在单进程上下文中执行的( attendant 进程),执行时间点在OpenSIPS 配置文件完整解析之后(包含了相关模块参数),所有助手API(共享内存、锁、定时器进程,等等)也是在这个点初始化的。

        这个函数的目的是检查OpenSIPS 脚本配置模块的完整性,初始化需要的数据结构,等等。此外,一些关键资源(比如说定时器),只能在模块的mod_init()函数中初始化。
函数原型是:

/* MUST return 0 in case of success, anything else in case of error */
typedef int (*init_function)(void);

    由于这个函数是在单进程上下文中调用的,那么在OpenSIPS fork之后,每个OpenSIPS 进程都将收到attendat 进程资源的副本。因此,不要在mod_init函数里初始化OpenSIPS 单个进程私有的数据结构和连接。


child_init

         这个函数必须在我们的module_exports exports 结构体变量中指定,对应成员init_child_f 。

        它运行在所有OpenSIPS 进程上下文中,触发时间点是新进程刚被fork出来时。

        这个的目的是建立各种连接(db、cachedb,等等),这些资源每个OpenSIPS 进程的需求是不同的,所以需要独立初始化它们。函数原型:

/* MUST return 0 in case of success, anything else in case of error */
typedef int (*child_init_function)(int rank);


        这个函数接收一个整型参数,指示当前调用函数的OpenSIPS进程的类型。以下是所有可用选项:

#define PROC_MAIN      0  /* Main opensips process */
#define PROC_TIMER    -1  /* Timer attendant process */
#define PROC_MODULE   -2  /* Extra process requested by modules */
#define PROC_TCP_MAIN -4  /* TCP main process */
#define PROC_BIN      -8  /* Any binary interface listener */


 

        rank 参数为正值表示当前操作是在OpenSIPS的监听器(UDP、TCP或SCTP)上下文里发生的。


    如果我们初始化时必须执行耗时操作(比如说从数据加载大量数据),那么应当把它放在child_init() 里,而不是放在mod_init()里。这会加速OpenSIPS 的启动过程,尽快进入消息处理(至少需要处理的消息可能不会完全依赖于我们模块内部的数据填充)。


16.4  销毁模块

        这个函数必须在我们的module_exports exports 结构体变量中指定,对应成员destroy_function 。

        它运行在单进程上下文中( attendant 进程),时间点是OpenSIPS 关机处理时。

        这俱函数的目的是清理OpenSIPS 所使用的各种资源(共享内在、DB连接,等等)。此外,destroy_function 是执行模块状态持久化的良好时机,以便下次启动时恢复状态(比如说,dialog 模块在析构函数中所所有SIP dialog信息保存在DB中)。函数原型:

typedef void (*destroy_function)();

16.5  添加模块参数

        模块添加新参数是由模块里的exports 变量里的params 成员完成的。OpenSIPS启动时,将解析脚本,根据OpenSIPS 脚本的参数配置设置内部变量。参数定义如下( param_export_t ) :

struct param_export_ {
        char* name;             /*!< null terminated param. name */
        modparam_t type;        /*!< param. type */
        void* param_pointer;    /*!< pointer to the param. memory location */
};


The OpenSIPS modules can export both string and integer parameters.

       OpenSIPS 模块既可以导出字符串参数,也可以导出整型参数。

        以下示例将分别演示。注意: param_export_t结构体不接收任何的长度参数指示模块导出的参数数量,结构体必须以一个全0的行结束。

int enable_stats = 0;
static str db_url = {NULL,0};

static param_export_t mod_params[]={
        { "enable_stats",          INT_PARAM, &enable_stats         },
        { "db_url",                STR_PARAM, &db_url.s             },
        { 0,0,0 }
}

OpenSIPS 脚本中为ournewmod 模块设置这些参数的用法:

loadmodule "ournewmod.so"

modparam("ournewmod","enable_stats", 1)
modparam("ournewmod","db_url","mysql://vlad:mypw@localhost/opensips")


        此外,OpenSIPS 还支持在脚本中设置某个特定参数时触发一个内部函数。如果所提供参数需要转换为内部格式,或者一个参数可以多次设置,那么这一机制将针非常有利。

        找一个现成的实例说明一下,请参考下面的代码片段,NoSQL URL 可以重复设置多次,建立多个后端连接:
 

static param_export_t params[]={
        { "cachedb_url",                 STR_PARAM|USE_FUNC_PARAM, (void *)&set_connection},
        {0,0,0}
};

int set_connection(unsigned int type, void *val)
{
        LM_INFO("Our parameter has been set : value is %s\n",(char *)val);
        /* continue processing, eg : add our new parameter to a list to be further processed */
}

16.6  添加模块函数

        新模块添加函数是由exports 结构里的cmds 成员完成的。导出的函数结构定义:

struct cmd_export_ {
        char* name;             /* null terminated command name */
        cmd_function function;  /* pointer to the corresponding function */
        int param_no;           /* number of parameters used by the function */
        fixup_function fixup;   /* pointer to the function called to "fix" the
                                                           parameters */
        free_fixup_function
                                free_fixup; /* pointer to the function called to free the
                                                           "fixed" parameters */
        int flags;              /* Function flags */
};


        和exports 结构里的params 很相似,cmds 成员也必须以NULL结尾。

       OpenSIPS 启动时,尝试定位每个调用函数的位置,不论它是核心提供的,还是外围模块提供的。

       


函数重载很简单,你需要在 cmds结构中列出同名条目就行,但要区别param_no字段。


        模块导出的脚本函数原型:

typedef  int (*cmd_function)(struct sip_msg*, char*, char*, char*, char*, char*, char*);


        如你所见,所有OpenSIPS模块函数只接受字符串参数,一个函数最多支持六个参数。当前正在处理的SIP消息作为C函数的第一个参数传入,但它对脚本编写者是透明的(它只提供参数索引1到5)。


    cmd_export_ 结构体中的flags成员决定了OpenSIPS 脚本中可以调用函数的地方。


以下是当前的定义选项:

#define REQUEST_ROUTE 1   /*!< Request route block */
#define FAILURE_ROUTE 2   /*!< Negative-reply route block */
#define ONREPLY_ROUTE 4   /*!< Received-reply route block */
#define BRANCH_ROUTE  8   /*!< Sending-branch route block */
#define ERROR_ROUTE  16   /*!< Error-handling route block */
#define LOCAL_ROUTE  32   /*!< Local-requests route block */
#define STARTUP_ROUTE 64  /*!< Startup route block */
#define TIMER_ROUTE  128  /*!< Timer route block */
#define EVENT_ROUTE  256  /*!< Event route block */


        通过上述值的位掩码提供多类型的路由是支持的。

        这里需要理解的一个非常重要的概念是fixup_function。这个函数只在脚本最开始解析时调用一次,它是一种优化,可以进一步解析所提供的参数,以提高运行时效率。

        这里只提供一些fixup函数使用案例:

  • 由于所有的模块函数参数都是字符串,有些场景,我们的模块需要整型参数。可以用fixup function把字符串转换为整型值
  • 如果我们接受它,那么我们的函数可以接受伪变量作为参数。这时,可以用fixup函数查找伪变量空间,在运行时,我们仅需要当前SIP消息的上下文中评估pvar的值


        接下来,我们将利用load_balancer 模块的lb_is_destination 的实现,来深入理解这一概念。函数定义如下:

        {"lb_is_destination",(cmd_function)w_lb_is_dst4,     4,    fixup_is_dst,
                0, REQUEST_ROUTE|FAILURE_ROUTE|ONREPLY_ROUTE|BRANCH_ROUTE|LOCAL_ROUTE},

函数接收4个参数。文档描述的用法是lb_is_destination(ip,port,group,active) :

  • ip - 字符串或pvar,携带待检查的IP地址
  • port - 字符串或pvar,携带待检查的端口,如果为空,将跳过所有端口检查
  • group - 整数或pvar,携带待检查的 load_balancer组ID
  • active - 整数。如果1,我们只接受活跃目的地的检查


了解这些后,fixup_is_dst 实现如下 :

static int fixup_is_dst(void** param, int param_no)
{
        if (param_no==1) {
                /* the ip to test */
                return fixup_pvar(param);
        } else if (param_no==2) {
                /* the port to test */
                if (*param==NULL) {
                        return 0;
                } else if ( *((char*)*param)==0 ) {
                        pkg_free(*param);
                        *param = NULL;
                        return 0;
                }
                return fixup_pvar(param);
        } else if (param_no==3) {
                /* the group to check in */
                return fixup_igp(param);
        } else if (param_no==4) {
                /*  active only check ? */
                return fixup_uint(param);
        } else {
                LM_CRIT("bug - too many params (%d) in lb_is_dst()\n",param_no);
                return -1;
        }
}


        将为提供的每个参数调用fixup 函数,其中 param_no 参数描述正在解析的参数索引(从1开始,因为实际上第一个参数是当前处理的SIP消息,占用索引0)。

        上述fixup函数将用它们各自处理后的输出替换在主函数中接收的参数。因此,主函数不会接收到任何脚本中的纯文本参数,而是收到解析后的pvar值,或者是转换后的数字。以下是w_lb_is_dst4 在fixup之后的处理代码:

static int w_lb_is_dst4(struct sip_msg *msg,char *ip,char *port,char *grp,
                        char *active)
{
        int ret, group;

        if (fixup_get_ivalue(msg, (gparam_p)grp, &group) != 0) {
                LM_ERR("Invalid lb group pseudo variable!\n");
                return -1;
        }

        ret = lb_is_dst(*curr_data, msg, (pv_spec_t*)ip, (pv_spec_t*)port,
                        group, (int)(long)active);


        如你所见,输入的字符串参数在fixup之后,转换为它对应的值。
        此外,mod_fix.h 里提供了各种访问fixup结果的函数。在上例中,调用fixup_get_ivalue获取整数值(既可以从纯文本中提取,也可以从解析的伪变量空间中提取)。此外,注意active参数是怎样直接转换为long的, 由于这个参数只接收纯文本的整数,fixup直接帮我们转换了。


    模块中为脚本导出函数的返回代码非常重要。正值表示成功,负值表示失败。返回0将在函数终止后停止脚本执行T。如果没有绝对必要,不要返回0。


16.7  添加模块MI函数

        模块添加新的MI函数,由exports 结构体中的mi_cmds 成员完成。


    exports 构体中的mi_cmds 成员所指定的MI函数,将会被模块接口自动注册。


16.8  添加模块统计

        模块添加统计接口,由exports 结构体中的stats 成员完成。


    exports 构体中的mi_cmds 成员所指定的stats 函数,将会被模块接口自动注册。如果我们的新模块命名为mynewmod ,导出一个统计接口,命名为mycustomstat ,那么我们就能用opensipsctl 获取统计信息:
opensipsctl fifo get_statistics mynewmod mycustomstat


16.9  添加模块伪变量

        模块添加统计接口,由exports 结构体中的items 成员完成。
 

16.10  添加模块专用进程

        对于某些案例,我们的模块可能需要与非SIP的外部实体通信。对于这样的案例,我们会需要一个(或多个)进程来专门负责这类通信。RTPProxy 就是一个典型实例(它与外部的RTP Proxy引擎通信),还有mi_fifo和mi_datagram (从FIFO或UDP socket读取MI命令)。

        exports 结构体中的procs 成员可以完成这事。proc_export_t 结构体描述额外请求的进程:
 

struct proc_export_ {
        char *name;                             /* name of the new task */
        mod_proc_wrapper pre_fork_function;     /* function to be run before the fork */
        mod_proc_wrapper post_fork_function;    /* function to be run after the fork */
        mod_proc function;                      /* actual function that will be run in the context of the new process */
        unsigned int no;                        /* number of processes that will be forked to run the above function */
        unsigned int flags;                     /* flags for our new processes - only PROC_FLAG_INITCHILD makes sense here*/
};

typedef void (*mod_proc)(int no);
typedef int (*mod_proc_wrapper)();

    新进程里执行的函数必须永远执行。一旦函数终止执行,整个OpenSIPS 都将退出。



        pre_fork_function和post_fork_function 这两个函数为主进程派生进程提供各种辅助工具。


    这两个辅助函数都在OpenSIPS attendant 进程的上下文中执行。



        结构体中的no成员指示OpenSIPS需要fork出几个进程来处理指定的事务。当需要处理的工作量很大时,它就派上用场了,在你的模块逻辑里,为进程函数提供no参数,工作量将分散在fork出来的进程中。

        OpenSIPS 为特定功能fork出的进程数,不一定是静态的。 

        以下是一个实例,看看MI数据报是如何处理fork进程的。进程fork数量的缺省值是MI_CHILD_NO,但是它可以通过children_count 参数配置,代码如下:

static proc_export_t mi_procs[] = {
        {"MI Datagram",  pre_datagram_process,  post_datagram_process,
                        datagram_process, MI_CHILD_NO, PROC_FLAG_INITCHILD },
        {0,0,0,0,0,0}
};


static param_export_t mi_params[] = {
        {"children_count",      INT_PARAM,    &mi_procs[0].no           },


        结构体中的flags 成员值,可以是0或PROC_FLAG_INITCHILD。如果是PROC_FLAG_INITCHILD,那么,所有可加载模块的child_init函数也将在新派生的进程中执行。

猜你喜欢

转载自blog.csdn.net/yetyongjin/article/details/106516870
3.1