英文版的看完就忘,打算把它翻译下来 ,不知道能坚持到第几章,加油吧,一边看一边翻译,难免有错请指正。至于排版实在是很费时间的事情,先这样吧。
Chapter 1:General Design
PJSIP是一个用C编写,占用资源少,高性能的sip协议栈。
1.1 Architecture
1.1.1 Communication Diagram
下面的图表展示了sip消息在pjsip组件中的流转。
1.1.2 Class Diagram
下面的是类图
1.2 The Endpoint
sip协议栈的核心就是sip endpoint,用不透明的类型pjsip_endpoint来表示。endpoint有以下的属性和功能。
#pool factory,用来为所有sip组件分配内存池
#timer heap instance,用来为所有所有sip组件设置timer
#transport manager instance,用来管理和控制sip传输,以及解析和打印消息
#PJLIB’s ioqueu,用来分派网络事务的proactor模式(个人注:网络编程中的一种设计模式)
#polling function,提供线程安全的轮询功能,应用的线程可以轮询timer和socket事务(PJSIP自身不具备创建线程的能力)
#PJSIP module,enpoint管理pjsip module,module是消息解析和传输之外的一种主要扩展
#endpoint从transport manager接收sip消息,然后分发到各个module
1.2.1 Pool Allocations and Deallocations
sip组件的所有内存分配都通过endpoint,来保证线程安全和约束整个应用的一致性。其中一个策略就是pool cache,未被使用的内存池不会销毁而是留作之后使用。
endpoint使用以下方式来分配和释放内存池
pjsip_endpt_create_pool()
pjsip_endpt_release_pool()
endpoint使用 pjsip_endpt_create() 来创建。endpoint一旦创建必须指明应用程序的pool factory。在endpoint整个生命周期内,endpoint都持有该pool factory的指针,并且创建和释放内存池。
1.2.2 Timer Management
endpoint有一个timer heap来管理timer。所有sip组件的timer创建和设置都是通过endpoint。
endpoint使用以下方式来管理timer
pjsip_endpt_schedule_timer()
pjsip_endpt_cancel_timer()
调用endpoint的轮询功能来检查timer的周期。
1.2.3 Polling the Stack
endpoint通过函数 pjsip_endpt_handle_events()来检查是否有timer和网络事务。应用程序可以指定等待某种事务的时间。
PJSIP协议栈从不创建线程。协议栈中正在运行的都可以认为是application创建的线程,比如API的调用,或者轮询功能的调用。
轮询功能可以根据timer heap的内容来优化等待时间。例如,如果知道一个timer在5ms之后结束,那么就不会花更多的时间等待socket events。当没有网络事务的时候,application就没必要等待那么久。timer的精确度依据不同平台有不同的变化。
1.3 Thread Safety and Thread Complications
1.3.1 Thread Safety
线程安全是一个复杂的问题。不过幸运是整个协议栈都适用以下设计的规范。
objects必须是线程安全的,而structure必须不是。
对象和结构在这里来看,区别并没有很清楚。但是给了几个例子帮助理解。
数据结构的例子:
PJLIB’s data structures, such as lists, arrays, hash tables, strings, and memory pools.
SIP messaging elements such as URIs, header fields, and SIP messages.
这些结构都不是线程安全的,它们的安全性是由包含它们的对象来确定的。如果数据结构是线程安全的,那么会对协议栈的性能产生影响,并且耗尽操作系统的资源。
相反的, sip的objects必须是线程安全的,以下是例子
PJLIB objects, such as ioqueue.
PJSIP objects, such as endpoint, transactions, dialogs, dialog usages, etc.
1.3.2 The Complications(线程复杂性)
糟糕的是,一些objects在头文件中暴露了它们的定义( e.g. pjsip_transaction and pjsip_dialog)。尽管这些objects暴露的API确定是安全的,但访问应用程序代码中的数据结构的时候,应用程序也必须获得相应的锁。比如objects mutex,调用 pj_mutex_lock() 。
更糟糕的是,dialog公开了用于锁dialog的不同的API。application应调用pjsip_dlg_inc_lock()和pjsip_dlg_dec_lock(),来代替pj_mutex_lock() 和pj_mutex_unlock()。这两者的区别是,dialog的inc/dec锁来确保函数调用的dialog不会被销毁,而dialog的销毁会引起pj_mutex_unlock()的崩溃。
看下面的例子
1 pj_mutex_lock(dlg->mutex);
2 pjsip_dlg_end_session(dlg, ...);
3 pj_mutex_unlock(dlg->mutex);
在以上的例子中,应用程序可能会在第三句崩溃, 因为pjsip_dlg_end_session在一定的条件下,会被销毁。如果发出的invite请求没有响应,那么此次事务将被立即销毁,同时dialog也被销毁。dialog的inc/dec锁通过临时增加dialog会话计数器来防止这个问题,这样dialog就不会在end_session()函数中被销毁。dialog可能在dec_lock()里被销毁。所以一个dialog恰当的锁应该是下面这样的顺序。
1 pjsip_dlg_inc_lock(dlg);
2 pjsip_dlg_end_session(dlg, ...);
3 pjsip_dlg_dec_lock(dlg)
不过,lock的顺序必须是正确的,否则会有死锁发生。比如,应用程序想要在一个dialog中对dialog和transaction都上锁,应用程序必须在transaction的mutex(互斥锁)之前获得dialog的mutex,否则其他的线程此时以相反的顺序获取同一个dialog和transaction,就会发生死锁。
1.3.3 The Relief
幸运的是,应用程序很少会去直接获取object的mutex,所以上面说的情况极少会出现。
应用程序应该通过object的API来获取可用的object,APIs保证了由于object被删除后的锁的正确性来避免死锁,以及程序崩溃。
当object(dialog或transaction)调用应用程序的回调时,这些回调通常在object的锁已被获取时调用。所以应用程序可以安全的的访问object的数据结构而不用去获取锁。