说明:此系列文章是根据PJSIP Dev’ Guide文档翻译而来,所有版权归原作者所有。这里的翻译仅仅是用作学习、交流等用途。本人近期在学习SIP,鉴于网上SIP资料比较少,自己就翻译了此文章,限于自己的SIP及英语水平,错误之处在所难免,希望高手多多指教,也希望有此爱好者一起学习交流。
第一章 通用设计
1.1架构
1.1.1通信图
下面的示意图显示了(SIP)消息是如何在PJSIP组件之间来回传递的。

图表1:协作图
1.1.2类图
下面的示意图显示了“类图”:

图表2:类图
1.2 Endpoint
SIP协议栈的核心就是SIP Endpoint,由不透明的类型pjsip_endpoint来表示。Endpoint具体有以下的属性和职责:
- 它有内存池工厂,为所有SIP组件分配内存池;
- 它有定时器堆实例,为所有SIP组件调度定时器;
- 它有传输管理器实例。传输管理器有SIP传输商品,且控制消息解析和显示;
- 它拥有单实例的PJSIP ioqueue类型。Ioqueue是用来分派网络事件的proactor 模式;
- 它提供线程安全的轮询功能,这样应用程序中的线程可以轮询定时器和网络事件(PJSIP本身不创建任何线程);
- 它管理模块。PJSIP模块是扩展协议栈的主要方法,而协议栈扩展并不局限于消息的解析和显示;
- 它从传输管理器接受到来的消息,并将这些消息分派到模块。
一些基本功能将会在下面的部分讲述,其余的会在后面的章节讲述。
1.2.1 内存池的分配和释放
SIP组件的所有内存分配都是通过endpoint来完成的,以在整个应用程序中保证线程安全及强制策略的一致性。可应用策略的一个例子是内存池缓存,这里未使用的内存将保留以备将来使用,而不是释放。
Endpoint提供以下方法来分配和释放内存池:
pjsip_endpt_create_pool()
pjsip_endpt_release_pool()
当创建endpoint时(使用pjsip_endpt_create()),应用程序必须指定endpoint使用的内存池工厂。Endpont将在自己的生命期内保持此内存池工厂的指针,并由此来分配和释放内存池。
1.2.2定时器管理
Endpoint保留一个单实例的定时器堆来管理定时器。所有定时器的创建和所有SIP组件的定时器调度皆由endpoint来完成。
Endpoint提供以下方法来管理定时器:
pjsip_endpt_schedule_timer()
pjsip_endpt_cancel_timer()
当endpont的轮询函数被调用时,endpoint将检查定时器是否过期。
1.2.3轮询协议栈
Endpoint提供了一个单一的函数调用(pjsip_endpt_handle_events())来检查定时器和网络事件的出现。应用程序可以指定它将等待多长时间后去检查这些事件的出现。
PJSIP协议栈从不创建线程。整个协议栈中的代码执行完全代表着应用程序创建的线程,无论是在一个API被调用时,或是应用程序调用轮询方法时。
轮询功能是可以基于定时器堆的内容来优化等待时间的(The polling function is also able to optimize the waiting time based on the timer heap’s contents.)。例如,如果它知道一个定时器将在下一个5秒过期,它等待网络事件的时间将不会超过这5秒;在无网络事件出现时这样做将无必要地延长等待时间。当然定时器的精度在每个平台上都不同。
1.3线程安全和线程复杂性
1.3.1线程安全
线程安全的讨论是一个相当复杂的事情。但是,比较幸运的是,下面的设计原则在整个协议栈中的一致应用:
对象必须是线程安全的;而数据结构必须不是线程安全的。
具体到现在的话题,很自然,对象和简单数据结构的区别不是非常清楚。但是一些例子将会使你对此更明白一点。
数据结构的例子有:
- PJSIP的数据结构,如链表(lists)、数组(arrays)、哈希表(hash tables)、字符串(strings)、以及内存池。
- SIP的消息元素,如URLs、header fields、以及SIP消息。
这些数据结构不是线程安全的;这些数据结构的线程安全由包含它们的对象来保证。如果使数据结构也线程安全,这将严重影响协议栈的性能,并消耗操作系统的资源。
相比之下,SIP对象必须是线程安全的。我们称之为对象的例子有:
- PJLIB对象,如ioqueue
- PJSIP对象,如endpoint、transactions、dialogs、dialog usages,等等
1.3.2线程复杂性
使事情变糟的是,一些对象在头文件中暴露了它们的声明(例如pjsip_transaction和pjsip_dialog)。尽管这些对象暴露的API是保证线程安全的,应用程序代码在访问这些数据结构之前仍然必须在对象的互斥变量(mutex)上调用pj_mutex_lock()来获取正确的锁。
使事情变得更糟的是,一个dialog提供不同的API来锁定dialog。这样应用程序应该调用pjsip_dlg_inc_lock()和pjsip_dlg_dec_lock(),而不是pj_mutex_lock()和pj_mutex_unlock()。这两种方法的区别是,dialog的inc/dec锁保证dialog不会在函数调用过程中被销毁;不然由于gialog已经被销毁,会使pj_mutex_unlock()崩溃。
考虑下面的例子:
pj_mutex_lock(dlg->mutex);
psip_dlg_end_session(dlg,…);
pj_mutex_unlock(dlg->mutex);
在上面的例子中(假想的),程序可能会在第三行代码处崩溃,因为psip_dlg_end_session()有可能在一定情况下销毁dialog。例如,出去的初始INVITE事务没有得到任何回应,因此事务会马上被销毁,造成dialog也被销毁。Dialog的inc/dec锁通过临时增加dialog会话的计数器来避免这个问题,因而在end_session()中dialog不会被销毁。Dialog可能会在dec_lock()方法中销毁。因此正确锁定dialog的顺序应该像这样:
pj_mutex_lock(dlg->mutex);
psip_dlg_end_session(dlg,…);
pj_mutex_unlock(dlg->mutex);
最后,真正使情事变糟的是,锁定的顺序必须正确,否则可能发现死锁。例如,应用程序在dialog中想既锁定dialog,又锁定transaction,应用程序必须在获取transaction锁之前获取dialog锁,否则当另一个线程正在以相反的顺序获取相同dialog和transaction的锁时,死锁将会发生。
1.3.3解决方法(The Relief)
幸运的是,应用程序很少需要直接获取对象的锁。因此几乎不会出现以上所述的问题。
如果可用,应用程序应该使用对象的API 来存取对象。由于会对对象进行检查,对象的API保证加锁的正确性及避免死锁和崩溃的出现。
当一个对象调用应用程序的回调函数时(如dialog和transaction),这此回调函数在对象的锁获取后正常调用,因此应用程序可以安全访问对象的数据结构而不用获取对象的锁。
首发张文杰的博客:http://zhangwenjie.net(转载请保留,谢谢)(未完待续)
Windows
pjsip, SIP