https://www.opensips.org/Documentation/Development-Manual
目錄
16. 模塊開發
16.1 引言
根據OpenSIPS 的模塊化架構,添加新特性(新的參數,腳本函數,MI接口函數)的最簡單方式是把它合入一個新的OpenSIPS模塊。
一個OpenSIPS 模塊實際上就是一個動態庫(so文件),在OpenSIPS 啓動過程中可以動態加載,如果從OpenSIPS 腳本中加載模塊,那麼要用loadmodule 指令。
loadmodule "mynewmod.so"
加載新模塊時,OpenSIPS核心會查找類型爲struct module_exports的exports 變量。這個結構體變量是開發新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函數也將在新派生的進程中執行。