PJSIP开发手册之总体设计(一)

来源:互联网 发布:win10如何忘记网络 编辑:程序博客网 时间:2024/05/15 04:17

PJSIP是用C语言写的。

第一章  总体设计

结构

通信图

下面的图展示了SIP消息是如何在SIP组件之间传递的。


类图

下面的图展示的是PJSIP的类

 

EndPoint

EndPoint是SIP栈的核心,封装在pjsip_endpoint数据结构中。Endpoint有以下属性和职责:

它有内存池工厂,负责给所有的SIP组件分配内存池。

它有定时器堆实例,为所有的SIP组件调度定时器

它有SIP传输管理实例,传输管理有SIP传输和控制消息的解析和打印

它拥有PJLIB io队列的唯一实例。Io队列是用来分发网络事件

它提供线程安全的轮询功能,这样的应用中的线程可以轮询定时器和socket事件(PJSIP本身不创建任何线程)

它管理模块,PJSIP模块是扩展除了消息解析和传输之外协议栈功能的主要方法

它负责从Transport Manager那里接收SIP消息,再分发给各个模块

内存池分配和释放

Endpoint负责所有的SIP组件的内存分配,为了保证整个应用的线程安全和策略的一致性。比如内存池缓存策略,就是把不使用的内存池缓存下来为将来的使用,而不是释放它

Endpoint提供分配和释放内存池的函数:

pjsip_endpt_create_pool()

pjsip_endpt_release_pool()

当endpoint被创建(pjsip_endpt_create())之后,应用为endpoint指定内存池工厂。Endpoint在它整个生命周期内都将保管这个内存池工厂的指针,并且使用它去创建和释放内存池。

定时器管理

Endpoint拥有单一一个定时器堆实例,去管理定时器,完成所有定时器的创建和调度。

Endpoint提供以下函数去管理定时器:

pjsip_endpt_schedule_timer()

pjsip_endpt_cancel_timer()

Endpoint在轮询函数被调用时,会检查定时器的期限。

轮询栈

Endpoint提供一个函数(pjsip_endpt_handle_events())去检查定时器超时和网络事件的发生。应用可以指定等待这些事件发生的最长时间。

PJSIP栈不会创建线程。所有栈的执行都会在应用的主线程中,不管是使用API调用还是应用调用轮询函数。

轮询功能是基于定时器堆的内容来有优化等待时间的。例如,它知道一个定时器将在下一个5ms超时,它等待socket事件的时间就不会超过5ms;这样就减少了当没有socket事件发生时,应用所需要的等待时间。定时器的精确度依平台而定。

线程安全与线程并发

线程安全

线程安全是个复杂的问题。但是,幸运的是,以下的设计原则在整个栈中都一致使用了:

对象一定要是线程安全的,但是数据结构不一定是线程安全的。

通常,对象和简单数据之间没有明显的区别。但是下面的例子可以让你了解它们之间的区别:

数据结构是:

  • PJLIB的数据结构,像列表、数组、哈希表、字符串和内存池
  • SIP消息元素,像URIs,头部域和SIP消息

数据结构都是线程不安全的,它们的线程安全是由包含它们的对象保证的。如果把数据结构也设置为线程安全的。我们称之为对象的有:

PJLIB对象,像io队列的,那么将会严重影响栈的性能和浪费操作系统的资源

  • 然而,SIP对象必须是线程安全
  • PJSIP对象,像endpoint,transaction,dialogs和dialogusages等等

线程并发

但是糟糕的是一些对象在头文件里暴露了它们的声明(如pjsip_transaction和pjsip_dialog)。虽然这些对象暴露的API是保证线程安全的。但是应用还是必须保证在访问这些数据结构之前获取到相应的锁,例如调用pj_mutex_lock()

更糟糕的是,一个dialog暴露了除了pj_mutex_lock()和pj_mutex_unlock()之外的不同的APi来锁一个dialog。应用应该调用pjsip_dlg_inc_lock()和pjsip_dlg_dec_lock()。这两种方法之前的区别,是dialog inc/dec锁方法在调用过程中并不会销毁dialog,不然dialog销毁会导致pj_mutex_unlock()崩溃。

考虑下面的例子:


上面的例子(假象的),应用执行到第3行可能会崩溃,由于pjsip_dlg_end_session()在某种情况下,可能会销毁dialog,例如,当发出的初始的INVITE事务没有收到任何响应,因此事务将被销毁,同时dialog也将被销毁。Inc/dec方法通过临时增加会话计数来阻止dialog的销毁,因此在end_session()方法中将不会销毁dialog。Dialog可能在dec_lock()方法中被销毁。正确的顺序应该如下:


最后,真正糟糕的是,加锁必需在正确的顺序,否则将会造成死锁。例如,如果应用想要锁定同一个dialog中的dialog和一个事务,必须在获得transaction的锁之前先获得dialog的锁,否则当另一个线程以相反的顺序获取同一个dialog和transaction的锁时会导致死锁的发生。

解决方法

幸运的是,应用只有在极少的情况下需要直接获取对象的锁,所以上面的问题很少会发生。

应用应该使用对象的API去访问对象。APIs保证正确的执行顺序,避免死锁和崩溃。

回调函数通常都是在已经获取到对象的锁之后,才会被对象(transaction或dialog)调用。应用不用获取对象的锁,也可以安全地访问对象的数据结构。

0 0
原创粉丝点击