背景

多个SIP注册服务器之间,如何同步分机的注册信息呢?

简单的方案就是使用共享数据库的方式同步注册信息,这个方案实现起来简单,但是分机的注册信息本身就是个需要频繁增删改查的,数据库很可能在大量注册分机的压力下,成为性能的瓶颈。

除了数据库之外,OpenSIPS和kamailio分别提供了不同的方案。

OpenSIPS提供的方案是使用cluster模块,cluster模块在多个实例之间同步分机的注册信息,注册信息的格式是OpenSIPS自定义的格式。 Kamailio的方案是DMQ模块, DMQ听起来高大上,放佛是依赖外部的一个服务。 但它其实就是扩展SIP消息,通过SIP消息来广播分机的注册信息。

KDMQ sip:notification_peer@192.168.40.15:5090 SIP/2.0
Via: SIP/2.0/UDP 192.168.40.15;branch=z9hG4bK55e5.423d95110000
To: <sip:notification_peer@192.168.40.15:5090>
From: <sip:notification_peer@192.168.40.15:5060>;tag=2cdb7a33a7f21abb98fd3a44968e3ffd-5b01
CSeq: 10 KDMQ
Call-ID: 1fe138e07b5d0a7a-50419@192.168.40.15
Content-Length: 116
User-Agent: kamailio (4.3.0 (x86_64/linneaus))
Max-Forwards: 1
Content-Type: text/plain

sip:192.168.40.16:5060;status=active
sip:192.168.40.15:5060;status=disabled
sip:192.168.40.17:5060;status=active

源码分析

该模块一共暴露了8个参数,其中7个参数都是简单类型,INT和STR,就直接取对应变量的地址就可以了。

其中notification_address参数是用来配置集群中其他节点的通信地址的,因为要配置多次,所以需要用一个函数来解析。

// dmq.c
static param_export_t params[] = {
	{"num_workers", PARAM_INT, &dmq_num_workers},
	{"ping_interval", PARAM_INT, &dmq_ping_interval},
	{"server_address", PARAM_STR, &dmq_server_address},
	{"server_socket", PARAM_STR, &dmq_server_socket},
	{"notification_address", PARAM_STR|PARAM_USE_FUNC, dmq_add_notification_address},
	{"notification_channel", PARAM_STR, &dmq_notification_channel},
	{"multi_notify", PARAM_INT, &dmq_multi_notify},
	{"worker_usleep", PARAM_INT, &dmq_worker_usleep},
	{0, 0, 0}
};

这些参数都没有加上static关键词,主要目的为了在dmq模块的其他c文件能使用。

// dmq.c
int dmq_num_workers = DEFAULT_NUM_WORKERS;
int dmq_worker_usleep = 0;
str dmq_server_address = {0, 0};
str dmq_server_socket = {0, 0};
str dmq_notification_channel = str_init("notification_peer");
int dmq_multi_notify = 0;
int dmq_ping_interval = 60;
str_list_t *dmq_notification_address_list = NULL;

dmq_add_notification_address

这个函数是在脚本解析阶段执行,每次配置一个notification_address,它就会被调用一次。

最终的目的就是组装dmq_notification_address_list这个链表结构,它里面存的就是集群里其他节点的SIP通信地址。

static int dmq_add_notification_address(modparam_t type, void *val)
{
	str tmp_str;
	int total_list = 0; /* not used */

	if(val == NULL) {
		LM_ERR("invalid notification address parameter value\n");
		return -1;
	}
	tmp_str.s = ((str *)val)->s;
	tmp_str.len = ((str *)val)->len;

	/*
	这个参数的格式是个SIP URL
	如: 
		sip:10.0.0.21:5060
		sip:10.0.0.21:5061;transport=tls
	所以需要做一次解析,其实也是验证数据的格式是否正确
	dmq_notification_uri 是个static类型的值,可以重复使用
	*/
	if(parse_uri(tmp_str.s, tmp_str.len, &dmq_notification_uri) < 0) {
		LM_ERR("could not parse notification address\n");
		return -1;
	}

	/* initial allocation */
	if(dmq_notification_address_list == NULL) {
		/* 
		初次分配,
		申请一块内容,用来存储第一个元素
		*/
		dmq_notification_address_list = pkg_malloc(sizeof(str_list_t));
		// 申请失败
		if(dmq_notification_address_list == NULL) {
			PKG_MEM_ERROR;
			return -1;
		}
		// 装载列表的第一项
		dmq_tmp_list = dmq_notification_address_list;
		dmq_tmp_list->s = tmp_str;
		dmq_tmp_list->next = NULL;
		LM_DBG("Created list and added new notification address to the list "
			   "%.*s\n",
				dmq_tmp_list->s.len, dmq_tmp_list->s.s);
	} else {
		// 思考这里传入的是双层指针,而不是指针,这是为什么?
		// 在函数参数中使用指向指针的指针,可以修改指针本身的值
		// append_str_list 就要修改last指针的指向
		// notification_address 每次被设置一次,链表就要增加一项,dmq_tmp_list就要向后移动一位
		dmq_tmp_list = append_str_list(
				tmp_str.s, tmp_str.len, &dmq_tmp_list, &total_list);
		if(dmq_tmp_list == NULL) {
			LM_ERR("could not append to list\n");
			return -1;
		}
		LM_DBG("added new notification address to the list %.*s\n",
				dmq_tmp_list->s.len, dmq_tmp_list->s.s);
	}
	return 0;
}