libosip2是开源的SIP协议栈的实现。它的核心特性为sip协议数据的解析和事务的管理。数据包的收发、RTP流的处理等,并不在osip中完成。应用程序使用osip时需要单独去实现这些模块。也因为如此,osip并不与sip终端挂钩,它可以用于sip proxy,sip server等许多需要处理sip消息的场合。
libosip2模块过程
语法分析器
目录src/libosip2下未解析器的源代码,完成对SIP协议相关字段的构造和解析。比如,将紧凑的存储与内存buffer中的SIP数据解析到清晰定义的数据结构体中,每一个字段代表SIP协议中有意义的一条头域。
在这一部分,需要完成URI的解析、SIP Message的解析处理、SDP的解析处理等。
有限状态机
libosip2实现了对SIP协议定义的处理四种事务的状态机:ict(Invite Client (outgoing) Transaction),ist(Invite Server (incoming) Transaction),nict(Non-Invite Client (outgoing) Transaction) ,nist(Non-Invite Server (incoming) Transaction)。Src/osip2下,ict.c、ict_fsm.c、ist.c、ist_fsm.c、nict.c、nict_fsm.c、nist.c、nist_fsm.c、fsm_misc.c这几个文件实现了状态机的处理。
事务和对话管理
主要是dialog和transaction的使用和管理
操作系统移植层接口
下述几个文件实现了操作系统层的porting接口。
- port_condv.c条件变量相关的接口,实现了linux,vxworks以及windows平台的支持
- port_fifo.c,先进先出类型消息队列(FIFO)的定义。基于信号量和互斥锁实现多任务环境的保护。这一点不同于osip_list.c中定义的list。list模块只是简单实现了一个链表,以及对链表的操作,其中并没有保护。因此如果有多个任务同时对同一个链表进行操作,那么可能会出现一些意想不到的错误。其实,fifo模块中的元素添加和删除也是基于list模块的接口来完成的。
- port_sema.c 互斥锁和信号量的实现。同样支持多个平台。
- port_thread.c 任务(在linux下为线程模式实现)接口的实现。
- osip_time.c 与时间相关的处理。
关键数据结构及其说明
osip
osip结构体的定义如下图所示:
从上述结构体的定义可以看出,osip主要有两部分构成
- 其一是事务链表,各个状态的事务的事件都挂载在该链表上;
- 其二是回调函数。
- 回调函数中前三个是内部使用,也就是osip库在处理消息的过程中,如果匹配某个状态,就执行对应的回调函数。用户可以使用这些回调函数显示一些状态,处理一些错误等。、
- 最后一个是消息发送回调函数,也就是osip需要发送sip数据包时,就是通过该回调函数完成的。
transition_t
transition_t结构体定义了事务,如下图:
state为事务的状态,type是事务对应事件的类型,method为函数指针,定义了当前状态收到type类型事件后应该执行的动作。next和parent为构成链表和队列时的前向和后向指针。
这是一个通用的结构体,也就是说,ict、ist、nict,nist都是用该结构体表示事务的。
___osip_message_config_t
___osip_message_config_t结构体主要用于sip消息的解析,定义如下:
hname为sip消息头的某一个域的名称,比如cseq、from、to或者via等。Setheader是处理该部分的函数。最后一项是一个标识,当被设置为1时,表明当前头域数据如果无效的话,可以忽略,不影响整个消息头的处理。
osip_transaction
osip_transaction的定义如下图:
这也是一个事务的定义,与之前的transaction_t的区别在于:之前定义的事务主要用在状态机处理中,主要包括了事务的状态,事件的类型以及要调用的处理函数;而这里定义的osip_transaction,则包含了事务相关的所有信息,存放在osip的事务队列上。
该结构体包含了与事务处理相关的信息,包括事务ID,事务队列(其上是事务的事件),事务关联的sip信息(via、from、to、callid以及cseq五元素),关联的sip message(orig_request、last_response、ack),以及该事务在状态机中的上下文信息(ict_context、ist_context、nict_context、nist_context)。
osip_transaction_t是RFC中的事务的定义,它表示的是一个会话的某个Dialog之间的某一次消息发送及其完整的响应,例如invite-100-180-200-ack这是一个完整的事务,bye-200这也是一个完整的事务,体现在SIP消息中,就是Via中的branch的值相同表示属于一个事务的消息(当然,事务是在Dialog中的,所以From、To的tag,Call-id值也是相同的)。事务对于UAC,UAS的终端类型不同及消息的不同,分为四类,前面说的invite的事务,主叫uac中会关联一个ict事务,被叫uas会关联一个ist事务,而除了invite之外,都归类定义主叫nict,被叫nist。在osip中,它是靠有限状态机来实现的上述四种事务(osip_fsm_type_t ctx_type中定义)的,它的主要属性值有callid,transactionid,分别来标识dialog和transaction,其中还有一个时间戳birth_time标识事务创建时间,可由超时处理函数用来判断和决定超时情况下的事务的进行和销毁。它的state属性也是非常重要的,根据上述的事务类型不同,其值也不同,它是前面提到的状态机的“状态”,在实际状态机的逻辑执行中是一个关键值。
ixt_t
该结构体用于管理需要重传的2xx响应消息。Dialog为其关联的对话,msg2xx为重传的数据buffer,ack指向ack 消息,start为重传开始的时间,interval为重传间隔,剩下的为目的地址,目的端口,所用socket描述符以及重传计数
osip_dialog
对话结构体的定义如下图:
osip_dialog中包含的主要都是与对话相关的sip头中的信息,这些信息可以区分不同的对话,比如call_id,local_tag,remote_tag等。另外,还指出了对话的类型,是caller还是callee。
osip_dialog_t是SIP RFC中的dialog或叫call leg的定义,它标识了uac和uas的一对关系,并一直保持到会话(session)结束。一个完整的dialog主要包括from,to,callid,fromtag,totag,state等(可查看源码),其中fromtag,totag,callid在一个dialog成功建立后才完整,体现在SIP消息中,就是From、To的tag,Call-id字段的值相同时,这些消息是属于它们对应的一个Dialog的。例如将要发起invite时,只有fromtag,callid填充有值,在收到to远端的响应时,收到totag填充到dialog中,建立成功一个dialog,后继的逻辑均是使用这个dialog进行处理(如transaction事务处理)。state表示本dialog的状态,与transaction的state有很大的关联,共同由Enum结构state_t定义。
osip_message_t
消息结构体的定义如下图:
可以看到这是一个非常大的结构体。该结构体用来保存与sip消息相关的大部分信息。一般都是一个消息头对应其中的一项。比如,像sip头数据中的call_id、from、to、via、等等,都能在该结构体中找到。
在程序中,接收到的sip消息都是以紧凑的方式放在buffer中的,解析器模块的功能就是将其进行解析分类,放到这个结构体的具体对应项上,这样便于在程序中使用。同时,如果需要发送数据时,解析器会根据该结构体中的信息重新将sip信息以紧凑方式放到buffer中供发送模块使用。简单来说,sip协议中定义的各个头,在接收发送处理中都是一个接一个在内存中存放的,而在osip中对其的使用是按照上面的结构体来的,我们在程序中不再需要移动指针从buffer中来找各个sip头数据。
osip_event
这个结构体用来表示事务上的事件。Type指出事件的类型,transactionid指出事务的id,sip指向上面介绍的osip_message结构体,也就是事件对应的sip消息。
初始化所做的工作
使用osip库之前,需要先进行初始化。这通过调用接口osip_init完成。在该接口中主要完成如下两项工作:
- 创建多任务相关的互斥信号量。
- 如果是第一次初始化,则创建全局状态机,否则只是简单增加引用计数。这步通过调用__osip_global_init接口完成。在__osip_global_init中,首先加载状态机,并为相关变量指针分配内存。如下图:
-
每当有新的事务到来时,就在其对应的事务链上进行检查,匹配状态和类型,然后执行method指向的执行体,并将事务的状态按照状态机跳转到新的状态。
-
接着调用parse_init初始化解析模块,完成后的内存状态如下:
-
Pconfig为__osip_message_config_t类型的静态数组,对于sip消息中不同的头域,通过pconfig数组即可找到处理函数。
-
最后为事务队列的处理定义多任务环境下使用的互斥信号量。
- 创建osip结构体,并初始化事务队列。完成后的内存状态为
- 在这里,回调函数还没有被设置,所以整个数组都是空的。另外,osip指针指向刚创建的结构体,这个指针会作为参数返回给调用osip_init的函数。
综合来看,osip初始化后,内存中就存在四个状态机、pconfig以及osip三部分了。
如果只是仅仅使用osip来进行sip信令的解析和生成工作,那么初始化到这里就算完成了。如果要使用状态机进行事务的管理,则需要设置一些回调函数,在sip事务状态发生变化时,这些回调函数会被调用,用来通知用户。Callback函数有很多,但是主要可以分为以下四类:
- 用于发送sip消息的网络接口。这通过osip结构体的cb_send_message函数指针指向。
- 当一个sip事务被terminate的时候调用的回调函数。这由osip结构体的kill_callbacks数组保存
- 当消息通过网络接口发送失败的时候调用的回调函数。这由osip结构体的tp_error_callbacks数组保存。
- sip事务处理过程中需要通知用户的回调函数。这部分由osip结构体的msg_callbacks数组保存。
上面的回调函数中,有一些是必须设置的,比如第一项,有些则是可选的,比如第四项中的部分回调函数。对于可选的回调函数,简单的处理可以是不做任何处理,或者仅仅打印通知信息。
sip消息收发的整体处理框架
sip消息的发送
发送SIP消息,需要用到如下三个主要的数据结构:osip_messag_t,保存待发送消息;osip_dialog_t,保存dialog信息;osip_transaction_t,保存事务信息。
- 首先,调用osip_malloc新分配一个dialog类型的结构体,使用osip_to_init,osip_to_parse,osip_to_free这类parser函数按RFC设置call-id,from,to,local_cseq等必要字段(原则是:后面生成实际SIP消息结构体要用到的字段就需要设置)。
- 接着,使用osip_message_init初始化一个sip msg,并根据dialog来填充该结构体(不同的消息填充的数据不同,实际应该填充的信息可参考RFC中的描述)。如果要给SIP消息添加Body,例如SDP段,则需要使用osip_message_set_body,osip_message_set_content_type函数,设置的值是纯文本。另外,如果是SDP,Osip有提供简单的解析和生成便捷函数,例如sdp_message_to_str,sdp_message_a_attribute_add,但只是简单的字符操作,要填充合法的字段需要自己参考SDP的RFC文档。
- 最后,就是事务的创建和触发,这通过调用osip_transaction_init完成。该接口创建了一个新的事务,并自动根据事务类型、dialog和sipmsg进行初始化,最重要的是它使用了__osip_add_ict等函数,将本事务插入到全局的osip_t结构体的全局FIFO链表中去了,不同的事务类型对应不同的FIFO。在前面关于osip结构体的描述可知,有四个FIFO,分别对应ICT,NICT,IST,NIST。
int osip_transaction_init(
osip_transaction_t ** transaction, /*返回的事务结构体指针*/
osip_fsm_type_t ctx_type, /*事务类型ICT/NICT/IST/NIST*/
osip_t * osip, /*前文说的全局变量*/
osip_message_t * request) /*前面生成的sipmsg*/
- 事务创建好后,就可以按照状态机的设置,进行状态转换的处理。这步需要事件来触发。应用可以调用osip_new_outgoing_sipmessage对sip message进行处理,产生事件,保存到结构体osip_event_t中。这一步省却了手动去设置。另外,还调用evt_set_type_outgoing_sipmessage设置事件的type_t,并将sip message挂到事件结构体的sip属性值上。有了根据消息分析出的事件后,使用osip_fifo_add(trn->transactionff, ev)将事件插入到事务的事件FIFO中。
现在条件都具备了,那么消息是如何发出的呢?
- 实际上,SIP消息的发送和响应是一个事务,不能单独隔离开来,所以消息的发送需要事务状态机来控制。我们上面设置了状态机的状态和事件,要触发它,就是要执行状态机了:
osip_ict_execute
osip_nict_execute
osip_ist_execute
osip_nist_execute
- 上面四个函数分别用来遍历前面提到的osip全局结构体上的四个事务FIFO。首先取出事务,再依次取出事务内的事件FIFO上的事件,使用osip_transaction_execute依次执行。最终会调用到osip结构体的cb_send_message回调函数。在osip初始化时,我们为这个函数指针指定了具体的处理函数,此时就会调用该处理函数发送数据。一般我们在实现这个回调函数时,也是按照网络socket编程,用send系统调用实现的。
- 如果某个事务不能正常终结怎么办呢?例如发出了Invite没有收到任何响应,按RFC定义,不同的事务有不同的超时时间,osip_timers_ict[nict|ist|nist]_execute这些函数就是来根据取出的事务的时间戳与当前时间取差后与规定的超时时间比对,如果超时,就自动设置超时“事件”,并将事务“状态”设为终结,使用初始化时设定的消息超时事件回调函数处理即可(如果设置了);如果网络质量不稳定,经常丢失消息,需要使用osip_retransmissions_execute函数来自动重发消息而不是等待超时。
- 为了即时响应SIP消息的处理,并推动状态机,上述的九个函数需要不停执行,可以将它放入单独线程中。
sip消息的接收
有了前面的发送SIP消息的理解,接收消息的处理就方便理解了,收到SIP消息,使用osip_parse进行解析,得到一个osip_message_t的sip msg,使用evt_set_type_incoming_sipmessage得到事务的“事件”,同上,将sip msg挂到事件结构体的sip字段,随后立即使用osip_find_transaction_and_add_event来根据“事件”查找事务(有兴趣可以深入看一下,事务的查找是通过SIP消息Via中的branch来匹配的),否则新建事务,然后推动状态机执行。
关键数据结构之间的关联
数据结构之间的关联
tr为事务结构体,ev为事件结构体,osip_msg为sip消息结构体。
从上图可以看出,事务有事件队列,用于存放该事务的事件,同时,事件上载有osip_message消息结构体。另外,事务结构体中包含了ict、ist、nict以及nist几部分,提供或保存状态机处理的辅助信息。
初始时
开始时,内存中主要存有初始化时完成的三个结构体。此时的osip上面还没有任何的事务,不过完成了部分必要的回调函数的设置。
运行一段时间
Osip运行一段时间(有sip请求和相应处理)后内存中数据结构之间的关系可能如下图:
此时osip上的事务队列上可能就已经有事务了,而且某些事物的事件队列上也可能还有事件等待处理。
有消息到来
- 当有消息到来,osip首先调用解析器,完成对消息包的解析。
- 之后,消息就从buffer中的紧凑模式转变到osip_message结构中了,同时,普通信令消息也被分解成了osip自己可以理解的消息事件并将其归类,确定其应该属于上述四类中的哪一类。
- 接着,查看对应的事务队列,确定是否有现成的事务可以匹配。如果有,就根据当前的sip消息提取出一个事件放到该事务的事件队列上;如果没有,则创建一个新的事务,放到事务队列中,同样提取出事件放到事件队列上。如下图所示:
上图为有事务匹配的图示。图中红色块则表示发生的变化
上图展示了新建事务的图示。
ip message处理中用到的主要数据结构之间的关系
这些数据结构体在解析sip消息的过程中发挥着重要作用。从上图可以看出,其中的大部分为list(淡绿色)或者char *(深蓝色)类型的。