exosip2使用原理
- eXosip是Osip2协议栈的封装和调用。它实现了作为单个sip终端的大部分功能,如register、call、subscription等。
- eXosip使用UDP socket套接字实现底层sip协议的接收/发送。并且封装了sip消息的解释器。
- eXosip使用定时轮循的方式调用Osip2的transaction处理函数,这部分是协议栈运转的核心。透过添加/读取transaction消息管道的方式,驱动transaction的状态机,使得来自远端的sip信令能汇报给调用程序,来自调用程序的反馈能通过sip信令回传给远端。
- eXosip增加了对各个类型transaction的超时处理,确保所有资源都能循环使用,不会被耗用殆尽。
- eXosip使用jevent消息管道来向上通知调用程序底层发生的事件,调用程序只要读取该消息管道,就能获得感兴趣的事件,进行相关的处理。
- eXosip里比较重要的应用有j_calls、j_subscribes、j_notifies、j_reg、j_pub、osip_negotiation和authinfos。J_calls对应呼叫链表,记录所有当前活动的呼叫。J_reg对应注册链表,记录所有当前活动的注册信息。Osip_negotiation记录本地的能力集,用于能力交换。Authinfos记录需要的认证信息。
模块组成
底层连接管理
与网络连接有关。实现了连接的建立、数据的接收以及发送等相关的接口
- eXtl_udp.c:为使用UDP连接的实现
- extl_tcp.c:为使用TCP连接的实现
- eXtl_dtls.c、eXtl_tls.c:为使用安全socket连接的实现
内部功能模块实现
Jauth.c、jcall.c、jdialog.c、jevents.c、jnotify.c、jpublish.c、jreg.c、jrequest.c、jresponse.c、jsubscribe.c 等文件实现了内部对一些模块的管理,这些模块正如其文件名所表示的,jauth.c主要是认证,jcall.c 则是通话等等。
上层API 封装实现
eXcall_api.c、eXinsubscription_api.c、eXmessage_api.c…这几个以api 为后缀的文件,实现各个子模块的管理。应用程序可以调用这里提供的接口,方便的构造或者发送sip 消息。
其他
- inet_ntop.c 实现ip 地址的点分十进制与十六进制表示之间的转换。
- jcallback.c 实现一堆回调函数,这些回调函数就是用来注册到osip 库的。我们使用exosip 库,就是避免直接使用osip 库,因为一些工作exosip 已经帮我们做了,所以这样一来,可以简化上层的实现。
- udp.c:主要用来对通过UDP 连接接收到的消息进行分类处理。
- eXutils.c:实现一些杂项的函数。有ip 地址到字符串之间的转换,域名的解析等一些 辅助的功能函数。
- exconf.c 文件实现了exosip 的初始化相关的接口,包括后台任务的实现。实际上是 “configuration api”的实现。
- eXosip.c文件实现了与exconf.c 文件相似的功能。比如管道的使用,exosip 上事务的创建和查找,register 和subscribe 的更新,认证信息的处理等。
关键数据结构
eXtl_protocol
eXtl_protocol 是为实现网络通信专门定义的一个数据结构,包括了变量和方法两部分。其中:
- 变量包括了建立网络连接过程中使用的ip 地址、端口等;
- 方法部分封装了网络socket编程常用的系统调用接口。
struct eXtl_protocol {
int enabled;
int proto_port;
char proto_name[10];
char proto_ifs[20];
int proto_num;
int proto_family;
int proto_secure;
int proto_reliable;
int (*tl_init) (void);
int (*tl_free) (void);
int (*tl_open) (void);
int (*tl_set_fdset) (fd_set * osip_fdset, fd_set * osip_wrset, int *fd_max);
int (*tl_read_message) (fd_set * osip_fdset, fd_set * osip_wrset);
int (*tl_send_message) (osip_transaction_t * tr, osip_message_t * sip,
char *host, int port, int out_socket);
int (*tl_keepalive) (void);
int (*tl_set_socket) (int socket);
int (*tl_masquerade_contact) (const char *ip, int port);
int (*tl_get_masquerade_contact) (char *ip, int ip_size, char *port,
int port_size);
};
struct eXtl_protocol eXtl_udp = {
1,
5060,
"UDP",
"0.0.0.0",
IPPROTO_UDP,
AF_INET,
0,
0,
&udp_tl_init,
&udp_tl_free,
&udp_tl_open,
&udp_tl_set_fdset,
&udp_tl_read_message,
&udp_tl_send_message,
&udp_tl_keepalive,
&udp_tl_set_socket,
&udp_tl_masquerade_contact,
&udp_tl_get_masquerade_contact
};
struct eXtl_protocol eXtl_tcp = {
1,
5060,
"TCP",
"0.0.0.0",
IPPROTO_TCP,
AF_INET,
0,
0,
&tcp_tl_init,
&tcp_tl_free,
&tcp_tl_open,
&tcp_tl_set_fdset,
&tcp_tl_read_message,
&tcp_tl_send_message,
&tcp_tl_keepalive,
&tcp_tl_set_socket,
&tcp_tl_masquerade_contact,
&tcp_tl_get_masquerade_contact
};
struct eXtl_protocol eXtl_dtls = {
1,
5061,
"DTLS-UDP",
"0.0.0.0",
IPPROTO_UDP,
AF_INET,
0,
0,
&dtls_tl_init,
&dtls_tl_free,
&dtls_tl_open,
&dtls_tl_set_fdset,
&dtls_tl_read_message,
&dtls_tl_send_message,
&dtls_tl_keepalive,
&dtls_tl_set_socket,
&dtls_tl_masquerade_contact,
&dtls_tl_get_masquerade_contact
};
struct eXtl_protocol eXtl_tls = {
1,
5061,
"TLS",
"0.0.0.0",
IPPROTO_TCP,
AF_INET,
0,
0,
&tls_tl_init,
&tls_tl_free,
&tls_tl_open,
&tls_tl_set_fdset,
&tls_tl_read_message,
&tls_tl_send_message,
&tls_tl_keepalive,
&tls_tl_set_socket,
&tls_tl_masquerade_contact,
&tls_tl_get_masquerade_contact
};
代码中定义了四个该数据结构体的全局变量:eXtl_udp、eXtl_tcp、eXtl_tls 以及eXtl_dtls。分别针对使用UDP、TCP 以及安全加密连接进行了实现。
eXosip_call_t
eXosip_call_t定义了call 相关的信息,包括call 的id,call 的dialogs,call 上incoming的事务和outgoing 的事务。另外,还包括了前向和后向指针,所以,所有的call 可以通过该结构体串接起来。
typedef struct eXosip_call_t eXosip_call_t;
struct eXosip_call_t {
int c_id;
eXosip_dialog_t *c_dialogs;
osip_transaction_t *c_inc_tr;
osip_transaction_t *c_out_tr;
osip_transaction_t *c_cancel_tr;
int c_retry; /* avoid too many unsuccessful retry */
void *external_reference;
time_t expire_time;
eXosip_call_t *next;
eXosip_call_t *parent;
};
eXosip_dialog_t
exosip_dialog_t 包含了dialog 相关的信息。
eXosip_reg_t
用来管理Register模块
eXosip_subscribe_t
用来管理subscribe模块
eXosip_pub_t
用来管理publish模块
eXosip_notify_t
用来管理notify模块
jinfo_t
这个结构体关联了dialog、call、subscribe以及notify几个结构体
eXosip_event_t
与 event 有关的结构体。这个结构体主要用来在应用层和exosip 之间通信。Exosip 在处理事务的过程中,如果需要将结果反馈给上层应用,则会生成如上结构类型的事件,并将其放到exosip 的事件队列中。应用层会不断循环从事件队列中读取事件,然后进行应用层的处理。
/**
* Structure for event description
* @struct eXosip_event
*/
struct eXosip_event
{
eXosip_event_type_t type; /**< type of the event */
char textinfo[256]; /**< text description of event */
void *external_reference; /**< external reference (for calls) */
osip_message_t *request; /**< request within current transaction */
osip_message_t *response; /**< last response within current transaction */
osip_message_t *ack; /**< ack within current transaction */
int tid; /**< unique id for transactions (to be used for answers) */
int did; /**< unique id for SIP dialogs */
int rid; /**< unique id for registration */
int cid; /**< unique id for SIP calls (but multiple dialogs!) */
int sid; /**< unique id for outgoing subscriptions */
int nid; /**< unique id for incoming subscriptions */
int ss_status; /**< current Subscription-State for subscription */
int ss_reason; /**< current Reason status for subscription */
};
eXosip_t
exosip_t 是exosip 中最重要的结构体之一。从图可以看出,这个结构体比较大,其中包含了exosip 中用到的各个子模块的结构。比如call、reg、pub 等等。代码中定义了一个该结构类型的全局变量,通过该全局变量,就可以对exosip 前的状态进行掌控(许多相关的信息要么包含在该结构上,要么可以通过该结构找到)。
- extl 是eXtl_protocol 类型的指针,保存了网络接口类。
- j_osip 保存了osip 初始化时返回的osip 结构体。
- j_transactions 一般是等待释放的事务。在事务经过osip处理完后,不再需要时,exosip会将其放在j_transactions上,等待释放。
struct eXosip_t {
struct eXtl_protocol *eXtl;
char transport[10];
char *user_agent;
eXosip_call_t *j_calls; /* my calls */
#ifndef MINISIZE
eXosip_subscribe_t *j_subscribes; /* my friends */
eXosip_notify_t *j_notifies; /* my susbscribers */
#endif
osip_list_t j_transactions;
eXosip_reg_t *j_reg; /* my registrations */
#ifndef MINISIZE
eXosip_pub_t *j_pub; /* my publications */
#endif
#ifdef OSIP_MT
void *j_cond;
void *j_mutexlock;
#endif
osip_t *j_osip;
int j_stop_ua;
#ifdef OSIP_MT
void *j_thread;
jpipe_t *j_socketctl;
jpipe_t *j_socketctl_event;
#endif
osip_fifo_t *j_events;
jauthinfo_t *authinfos;
int keep_alive;
int keep_alive_options;
int learn_port;
#ifndef MINISIZE
int http_port;
char http_proxy[256];
char http_outbound_proxy[256];
int dontsend_101;
#endif
int use_rport;
int dns_capabilities;
int dscp;
char ipv4_for_gateway[256];
char ipv6_for_gateway[256];
#ifndef MINISIZE
char event_package[256];
#endif
struct eXosip_dns_cache dns_entries[MAX_EXOSIP_DNS_ENTRY];
struct eXosip_account_info account_entries[MAX_EXOSIP_ACCOUNT_INFO];
struct eXosip_http_auth http_auths[MAX_EXOSIP_HTTP_AUTH];
CbSipCallback cbsipCallback;
};
exosip的初始化
Exosip 的初始化有两部分组成,这主要是从使用exosip 的角度看。
(1)对exosip 全局结构体变量的配置。这步通过调用接口eXosip_init 完成。主要完成工作如下:
- 初始化条件变量和互斥信号量。
- 调用
osip_init
初始化osip 库,并将生成osip 结构体给exosip,同时也让osip 的application_contexgt 指针指向exosip,也就是二者相互指向。 - 调用eXosip_set_callbacks 设置osip 的回调函数,所以回调函数都是exosip 自己实现。
- 调用jpipe 创建通信用的pipe。对于windows 平台,是通过socket 接口模拟实现的。
- 初始化其上的事务和事件队列。这不同于osip 的事务和事件队列。
- 调用extl 指向的结构体的init 函数指针,初始化网络接口。
初始化完后,全局exosip结构在内存中的状态基本如下图:
其中的橘红色标识了初始化过程中涉及到的部分。
(2)在socket 接口上进行监听
这步通过调用eXosip_listen_addr 接口完成。主要完成工作如下:
-
将eXosip 全局变量的eXtl 指针指向eXtl_udp 全局变量
-
根据参数,配置extl_protocol 和exosip 上有关ip 端口地址等信息。另外,调用extl_udp的tl_open 函数指针,完成在本机指定的端口上监听连接的工作。需要注意的是,虽然是监听,但是使用的UDP 来建立连接的,所以消息的recv 和发送在同一个socket 上完成。在osip中设置的out_socket 并不会起作用。
-
调用osip_thread_create 创建exosip 后台任务,用于驱动osip 的状态机。(在osip 中,在发送sip 消息部分,提到将9 个函数放到一个线程中执行,exosip 就是这样做的)
这部分工作完成后,内存状态如下图所示:
上图中浅绿色部分显示了这部分工作所作的配置。J_thread保存了任务的句柄。
示例代码
#include "stdio.h"
#include <eXosip2/eXosip.h>
#include <iostream>
#include <rpc/types.h>
using namespace std;
int main(void)
{
int listenport = 8888;
//库处理结果
int result = OSIP_SUCCESS;
TRACE_INITIALIZE (6, stdout);
eXosip_t *ctx = eXosip_malloc();
result = eXosip_init(ctx);
if (OSIP_SUCCESS != result)
{
printf("Can't initialize eXosip!\n");
return 1;
}
printf("eXosip_init successfully!\n");
//监听
result = eXosip_listen_addr(ctx, IPPROTO_UDP,NULL,listenport,AF_INET,0);
if (OSIP_SUCCESS != result){
printf("eXosip_listen_addr failure.\n");
return 1;
}
printf("eXosip_listen_addr successfully.\n");
// 结束
eXosip_quit(ctx);
ctx = nullptr;
return 0;
}
这样,在初始化完成后,我们基本上完成了对内存中所用数据结构的配置,同时启动了一个后台任务负责osip状态机的驱动。用户就可以发送和接收SIP事件了
数据收发整体框架
接收过程
在初始化过程中我们创建了一个后台任务,现在可以看看这个后台任务都做了哪些操作。任务的执行函数为_eXosip_thread,在该接口中,循环不断的调用eXosip_execute。在每一次的eXosip_execute
执行中,完成如下的工作:
- 首先计算出底层osip离当前时间最近的超时时间。也就是查看底层所有的超时事件,找出其中的最小值,然后将其与当前时间做差,结果就是最小的超时间隔了。这步是通过调用接口osip_timers_gettimeout完成的。主要检查osip全局结构体上的ict、ist、nict、nist以及ixt上所有事务的事件的超时时间。 如果ict事务队列上没有事件,则说明没有有效的数据交互存在,返回值为默认的一年,实际上就是让后面的接收接口死等。如果有事务队列上的事件的超时时间小于当前值,则说明已经超时了,需要马上处理,此时将超时时间清为零,并返回。
- 调用eXosip_read_message接口从底层接收消息并处理。如果返回-2,则任务退出。
- 执行osip的状态机。具体为执行osip_timers_ict(ist|nict|nist)_execute和osip_ict(ist|nict|nist)_execute这几个函数。最后还检查释放已经终结的call、registrations以及publications。
- 如果keep_alive设置了,则调用_eXosip_keep_alive检查发送keep_alive消息。
这样,当远端的终端代理发送sip消息过来时,会被之前创建的监听端口捕获(sip协议默认的端口为5060)。在调用eXosip_read_message接口时会将其接收上来。
接收上来的数据存放在buffer中交给接口_eXosip_handle_incoming_message来处理。在其中首先调用osip_parse进行消息的解析,这是osip的核心功能之一。数据解析后,会生成一个osip_event类型的事件。接着调用osip_message_fix_last_via_header将接收到该消息的ip地址和端口根据需要设置到数据头的via域中。这在消息返回时有可能发挥作用。为了能够让消息正确的被处理,调用osip_find_transaction_and_add_event接口将其添加到osip的事务队列上。处理在这之后发生了分叉,如果osip接纳了该事件,接口直接返回,因为这说明该事件在osip上已经有匹配的事务了,或者说该事件是某一个事务过程的一部分。这样在后面执行状态机的接口时,该事件会被正确的处理。如果osip没有拿走该事件,则说明针对该事件还没有事务与之对应。此时,我们首先检查其类型,如果是request,则说明很可能是一个新的事件到来(这将触发服务端的状态机的建立),调用eXosip_process_newrequest接口进行处理。如果是response,则调用接口eXosip_process_response_out_of_transaction处理。
在eXosip_process_newrequest接口中,如果是合法的事件,则会为其创建一个新的事务。也就是说这是新事务的第一个事件。经过一大堆的处理后,该事件可能就被osip消化了,或者被exosip消化了。如果需要上报给应用,由应用拿来对一些信息进行存储或者进行图形显示之类,则会将该事件添加到exosip的事件队列上。如下图所示:
应用程序在exosip初始化完之后需要调用如下类似的代码,不断从事件队列上读取事件,并进行处理。
//开启循环消息,实际应用中可以开启多线程同时接收信号
eXosip_event_t* osipEventPtr = NULL;
while (true)
{
// Wait the osip event.
osipEventPtr = ::eXosip_event_wait(0, 200);
eXosip_lock();
//一般处理401/407采用库默认处理
eXosip_default_action(osipEventPtr);
eXosip_unlock();
// If get nothing osip event,then continue the loop.
if (NULL == osipEventPtr)
{
continue;
}
// 事件处理
switch (osipEventPtr->type)
{
//需要继续验证REGISTER是什么类型
case EXOSIP_REGISTRATION_NEW:
{
//注册事件处理
}
break;
case EXOSIP_MESSAGE_NEW:
{
//消息事件处理
}
break;
case XXXXX:
{
//事件处理
}
break;
case XXXXX:
{
//事件处理
}
break;
default:
cout << "未处理消息 : " << osipEventPtr->type<<endl;
break;
}
eXosip_event_free(osipEventPtr);
osipEventPtr = NULL;
每当发送一个SPI消息,都会收到一个事件。每个事件包含受影响事务的原始请求,以及触发有效事件的最后应答,
你可以从这些信息获取所有的头部,并存储起来,以方便后续的操作或者显示。
例如:当收到一个呼叫传输REFER请求时,你可以检索“refer-To”头部:
发送过程
要发送数据时,需要根据消息类型,调用exosip对应模块的api接口函数来完成。如果要发送的sip消息不属于当前已有的任何事务,则类似接收过程,调用osip的相关接口创建一个新的事务,同时根据消息生成一个事件,加到事务的事件队列上。最后,唤醒exosip后台进程,使其驱动osip状态机,执行刚添加的事件,从而完成数据的状态处理和发送。当然,也有一些消息并不通过osip状态机,而是由exosip直接调用回调函数cb_snd_message完成发送。
小结
使用eXosip时,第一步工作是初始化eXosip和libosip库(解析器和状态机)。在使用libeXosip2前必须必须先做这一步。
#include <eXosip2/eXosip.h>
int i;
i = eXosip_init(); // 初始化eXosip和osip协议栈
if (i != 0)
return -1;
i = eXosip_listen_addr (IPPROTO_UDP, NULL, 5060, AF_INET, 0); // 打开信号socket
if (i != 0) // 传输层初始化失败
{
eXosip_quit();
return -1;
}
然后可以发送消息,等待并处理eXosip事件。
eXosip_event_t *je = NULL;
while (1)
{
je = eXosip_event_wait(0,50); // 侦听是否有消息到来
eXosip_lock();
eXosip_default_action(je);
eXosip_unlock ();
if (je == NULL) // 没有接收到消息
continue;
switch (je->type)
{
case EXOSIP_MESSAGE_NEW:
break;
case EXOSIP_CALL_INVITE:
break;
case EXOSIP_CALL_ACK:
break;
case EXOSIP_CALL_CLOSED:
break;
case ...:
break;
default:
break;
}
eXosip_event_free(je);
}
每当发送一个SIP消息,都会收到一个事件。每个事件包含受影响事务的原始请求,以及触发有效事件的最后应答,
你可以从这些信息获取所有的头部,并存储起来,以方便后续的操作或者显示。
例如:当收到一个呼叫传输REFER请求时,你可以检索“refer-To”头部:
osip_header_t *referto_head = NULL;
i = osip_message_header_get_byname(evt->sip, "refer-To", 0, &referto_head);
if (referto_head == NULL || referto_head->value == NULL)
eXosip_event包含呼叫标识符、注册信息、传入订阅、呼出订阅等。这些标识符将会被API用于控制呼叫、注册、传入或呼出订阅,这些API将会建立默认的带SIP头的信息头(From, To, Call-ID, CSeq, Route, Record-Route,Max-Forward…),并为你发送信息。
关系
exosip与上层应用以及osip之间的流程关系
应用需要接收数据时流程如下:
上图为接收过程的示意图。Exosip后台任务不断从网络另一端读取sip消息,交给osip的parser模块解析,并将其转换为events,添加到事务队列上。同时,exosip后台任务在不断的驱动osip的状态机,这样,事务队列上的事件就会被处理。如果需要响应对端,状态机会根据回调函数的设置,直接完成数据的发送。同样,如果要将当前处理反馈给应用,则将其发送到事件队列上(这里是exosip的事件队列),并通过e-ctl管道通知应用进行处理。
应用需要发送数据时,流程如下图所示:
此时,应用调用exosip提供的辅助函数(上图中虚线示意此关系),构造osip事件,将其添加到osip的事务队列上。同时,应用通过s-ctl管道通知exosip后台任务执行状态机。在exosip执行状态机的过程中,sip消息会被发送到网络另一端的终端。
数据结构体之间的关系
在exosip中,有一个比较大的数据结构图是exosip_t,这个在前面的数据结构说明中就介绍了。这个结构体中包含了一些子模块的数据结构,比如call,register等等。程序中,定义了一个exosip_t类型的全局变量,这样,通过该全局变量,就可以获取各个子模块的信息。
在许多的开源软件中,经常会看到这种设计方法。
上图中,像call、register、subscribe等等,都有指向自身类型的前后指针,因此,实际运行中,多个实例都是串接在一起,形成一个双向链。