pjlib深入剖析和使用詳解

double2li發表於2017-05-19

1. PJSIP簡介

 PJSIP的實現是為了能在嵌入式裝置上高效實現SIP/VOIP.其主要特徵包括:
    1).極具移植性.(Extremely portable)
                
    2).非常小的足印.(Very small footprint)
        官方宣稱編譯後的庫<150Kb,我在PC上編譯後加上strip後大概173Kb,這對於嵌入
        式裝置,是個好訊息:)
    
    3).高效能.(High performance)
       這點我們後面可以看看是否如作者宣稱的:)
       
    4).支援眾多的特徵.(Many features)
      這點可以從http://www.pjsip.org/sip_media_features.htm#sip_features
看出.

       
    5).充足的SIP文件.(Extensive SIP documentation)
       這是我最初選擇該庫的原因,當然不是最終的原因,最終的原因是它的code:)
      
2.  PJSIP的組成.
  
    其實說是PJSIP不是特別貼切,這個庫實際上是幾個部分組成的.
 1).PJSIP – Open Source SIP Stack[開源的SIP協議棧]

    2).PJMEDIA – Open Source Media Stack[開源的媒體棧]
   
    3).PJNATH – Open Source NAT Traversal Helper Library[開源的NAT-T輔助庫]
  
    4).PJLIB-UTIL – Auxiliary Library[輔助工具庫]
   
    5).PJLIB – Ultra Portable Base Framework Library[基礎框架庫]

  3. PJLIB簡介

 要理解好PJSIP,就不得不先說說PJLIB,PJLIB算的上是這個庫中最基礎的庫,正是這個庫的優美實現,才讓PJSIP變得如此優越。
    PJLIB提供了一系列特徵,這是我們下面分析的重點,涉及到:
  1).非動態記憶體分配[No Dynamic Memory Allocations]
       實現了記憶體池,獲取記憶體是從與分配的記憶體池中獲取,高效能程式多會自己構造記憶體池,後面我們會解釋該記憶體池的使用以及基本的原理。根據作者的比較,是常規的 malloc(
)/free()函式的30倍。
       
    2).OS抽象[Operating System Abstraction]
       實現OS抽象的根本原因在與可移植性,毋庸置疑:).
       涉及到:
        a).執行緒[Threads.]
     b).執行緒本地儲存[Thread Local Storage.]
        c).互斥[Mutexes.]
     d).訊號燈[Semaphores.]
     e).原子變數[Atomic Variables.]
      f).臨屆區[Critical sections.]
      g).鎖物件[Lock Objects.]
       h).事件物件[Event Object.]
      i).時間管理[Time Data Type and Manipulation.]
       j).高解析的時間戳[High Resolution Timestamp.]
      等等,這些我們後面分析程式碼時一一看來:)
       
    3).底層的網路相關IO[Low-Level Network I/O]
     這涉及到:
       a).Socket抽象[Socket Abstraction.]
        b).網路地址解析[Network Address Resolution.]
      c).實現針對Socket的select API[Socket select() API.]
     
    4).時間管理[Timer Management]
       這主要涉及到兩個部分,一個是定時器的管理,還有就是時間解析的精度(舉例說來,就是能精確到哪個時間等級,比如 POSIX sleep(),就只能以秒為單位,而使用select()則可
以實現毫秒級別的計時)
 
    5).各種資料結構[Various Data Structures]
      主要有:
        a).針對字串的操作[String Operations]
      b).陣列輔助[Array helper]
       c).Hash表[Hash Tabl]
     d).連結串列[Linked List]
      e).紅黑平衡樹[Red/Black Balanced Tree]
      
    6).異常處理[Exception Construct]
        使用的是TRY/CATCH,知道C++/JAVA之類面嚮物件語言的人看過會宛而一笑:)
     
   7).LOG機制[Logging Facility]
      很顯然,一個良好的程式,好的LOG機制不可少。這能很方便的讓你去除錯程式,對此我
是深有體會,任何時候,不要忘記“好的程式,是架構出來的;而能跑的程式,是除錯出
來的:)”
       
    8).隨機數以及GUID的產生[Random and GUID Generation]
     GUID指的是”globally unique identifier”,只是一個標識而已,比如說你的省份證,
算的上是一個GUID,當然,準確說來是“china unique identifier”:).
      
    看了這麼多的特徵列舉,是不是很完備,的確。
  
    總算是初步列舉完了PJLIB的基本特徵了,後面我們來說說它的使用與實現:

4.  PJLIB的使用
   
    有了上述介紹,是不是很想知道這個庫的使用,沒關係,我們慢慢說來:)
  
    首先是標頭檔案和編譯出來的庫的位置,這就不必多說了,除非你沒有使用過手動編譯的庫
,如果不太瞭解步驟,google一下,啊:)
  
    1).為了使用這個庫,需要使用:
    #include <pjlib.h>
    當然,也可以選擇:
   #include <pj/log.h>
    #include <pj/os.h>
    這種分離的方式,不過,簡介其間,還是使用第一種吧:),畢竟,你不需要確認到你所
需的函式或者資料結構具體到哪個具體的標頭檔案:)
   
    2).確保在使用PJLIB之前呼叫 pj_init()來完成PJLIB庫使用前說必須的一些初始化.

    這是一個必不可少的步驟.
    ~~~~~~~~~~~~~~~~~~~~~~~
   
    3).使用PJLIB的一些建議
    作者對使用PJLIB的程式提出了一些建議,包括如下 :
       a).不要使用ANSI C[Do NOT Use ANSI C]
        觀點很明確,ANSI C並不會讓程式具有最大的移植性,應該使用PJSIP庫所提供的響
應機制來實現你所需要的功能.
       
        b).使用pj_str_t取代C風格的字串[Use pj_str_t instead of C Strings]
       原因之一是移植性,之二則是PJLIB內建的pj_str_t相關操作會更快(效能).
      
        c).從記憶體池分配記憶體[Use Pool for Memory Allocations]
        這很明顯,如果你知道為什麼會使用記憶體池的話(提示一下,效能以及易用性:))
      
        d).使用PJLIB的LOG機制做文字顯示[Use Logging for Text Display]
     很明顯:)
      
     還有些關於移植的一些問題,不在我們的討論範圍,如果你需要移植到其它平臺或者
環境,請參考http://www.pjsip.org/pjlib/docs/html/porting_pjlib_pg.htm

5. PJLIB的使用以及原理
    終於開始提及實現原理以及具體的編碼了:),前面的列舉還真是個瑣碎的事情,還是奔主題來:).  
    5.1快速記憶體池[Fast Memory Pool]
  前面說過,使用記憶體池的原因在於效能的考慮,原因是C風格的malloc()以及C++風格的new 操作在高效能或實時條件下表現並不太好,原因在於效能的瓶頸在於記憶體碎片問題.
   下面列舉其優點與需要主要的問題:
    優點:
 a).不像其它記憶體池,允許分配不同尺寸的chunks.
 b).快速.
      記憶體chunks擁有O(1)的複雜度,並且操作僅僅是指標的算術運算,其間不需要使用鎖住任何互斥量.
   c).有效使用記憶體.
      除了可能因為記憶體對齊的原因會浪費很少的記憶體外,記憶體的使用效率非常高.
  d).可預防記憶體洩漏.
     在C/C++程式中如果出現記憶體洩漏問題,其查詢過程哪個艱辛,不足為外人道也:(
     [曾經有次用別人的Code,出現了記憶體洩漏,在開發板上查詢N天,又沒工具可在開發板上使用,哪個痛苦,想自殺, 原因很簡單,你的記憶體都是從記憶體池中獲取的,就算你沒有釋放你獲取的記憶體,只要你記得把記憶體池destroy,那麼記憶體還是會還給系統.
       
    還有設計帶來的一些其它益處,比如可用性和靈活性:
    e).記憶體洩漏更容易被跟蹤.
      這是因為你的記憶體是在指定的記憶體池中分配的,只要能很快定位到記憶體池,記憶體洩漏的偵測就方便多了.
     f).設計上從記憶體池中獲取記憶體這一操作是非執行緒安全的.
     原因是設計者認為記憶體池被上層物件所擁有,執行緒安全應該由上層物件去保證,這樣的話,沒有鎖的問題會讓記憶體分配變得非常的快.
 g).記憶體池的行為像C++中的new的行為,當記憶體池獲取記憶體chunks會丟擲PJ_NO_MEMORY_EXCEPTION異常,當然,因為支援異常處理,也可以使用其它方式讓上層程式靈活的定義異常的處理.
   這是異常處理的基本出發點,但是這有大量的爭論,原因是這改變了程式的正常流程,誰能去保證這種流程是使用者所需要的呢,因此C++中的異常處理飽受爭議,請酌情使用]
 h). 可以在後端使用任何的記憶體分配器.預設情況下是使用malloc/free管理記憶體池的塊,
    a).使用合適的大小來初始化記憶體池.
      使用記憶體池時,需要指定一個初始記憶體池大小, 這個值是記憶體池的初始值,如果你想要高
效能,要謹慎選擇這個值哦,太大的化會浪費記憶體,過小又會讓記憶體池自身頻繁的去增加記憶體
,顯然這兩種情況都不可取.
  b). 注意,記憶體池只能增加,而不能被縮小(shrink),因為記憶體池沒有函式把記憶體chunks釋
放還給系統,這就要去記憶體池的構造者和使用者明確使用記憶體.
  
    恩,基本的原理都差不多了,後面我們來看看如何使用這個記憶體池.

  5.2記憶體池的使用[Using Memory Pool]
     記憶體池的使用相當的簡單,扳個手指頭就搞定了,如果你看明白了上面的原理和特徵:)

     a).建立記憶體池工廠[Create Pool Factory]
    PJLIB已經有了一個預設的實現:Caching Pool Factory,這個記憶體池工廠的初始化使用函式pj_caching_pool_init()

    
    b).建立記憶體池[Create The Pool]
   使用pj_pool_create(),其引數分別為記憶體工廠(Pool Factory),記憶體池的名字(name),初
始時的大小以及增長時的大小.
   
    c).根據需要分配記憶體[Allocate Memory as Required]
    然後,你就可以使用pj_pool_alloc(), pj_pool_calloc(), 或pj_pool_zalloc()從指定
的記憶體池根據需要去獲取記憶體了:)
   
    d).Destroy記憶體池[Destroy the Pool]
     這實際上是把預分配的記憶體還給系統.
  
    e).Destroy記憶體池工廠[Destroy the Pool Factory]
   這沒什麼好說的.
   
    #include <pjlib.h>

#define THIS_FILE “main.cpp”

int main()

{

char errmsg[PJ_ERR_MSG_SIZE];

pj_caching_pool cp;

pj_status_t status;

status=pj_init();

if(status!=PJ_SUCCESS)

{

pj_strerror(status,errmsg,sizeof(errmsg));

fprintf(stderr,”pj_init error
“);

return 1;

}

//create factory

pj_caching_pool_init(&cp,NULL,1024*1024);

pj_pool_t *pool;

 

//create pool

pool=pj_pool_create(&cp.factory,”pool1″,4000,4000,NULL);

if(!pool)

{

fprintf(stderr,”error createing pool
“);

return 1;

}

//alloc some size  memory

void *p;

p=pj_pool_alloc(pool,(pj_rand()+1)%512);

 

//free the pool

pj_pool_release(pool);

 

//free the factory

pj_caching_pool_destroy(&cp);

 

return 0;

 

}

2. 

PJSIP

基於一個開放的、成熟的SIP開源庫進行開發不但可以大大提高效率,也可增強與其他的SIP系統的相容性。PJSIP是用C編寫的,相當優秀的一個SIP協議棧,其主要特徵包括:

1. 極具移植性。支援的平臺有Windows、Windows Mobile、Linux、Unix、MacOS X、RTEMS、Symbian OS等。

2. 非常小的儲存空間。包含完整SIP功能的程式碼庫僅150K。

3. 高效能。採用優秀的記憶體分配機制,執行速度快。

4. 支援眾多SIP特徵和擴充套件。比如IM、presence、event subscription、call transfer、PIDF等。

5. 豐富的SIP文件和範例。

 

PJSIP 開源庫由一系列功能庫所組成,如圖1所示,PJLIB 是系統抽象層,PJLIB-UTIL提供有用的工具函式,PJNATH 解決NAT 穿越問題,PJMEDIA 和PJMEDIA-CODEC 負責SDP 協商、媒體編碼和媒體傳輸,PJSIP 是核心SIP 協議棧,PJSIP-SIMPLE 實現Presence和即時訊息,PJSIP-UA 提供SIP 使用者代理庫,PJSUA 位於最高層,整合了下層模組的全部功能。PJSIP 的每個功能庫根據其所在的層次以及負責的功能都提供了豐富的程式設計介面,方便開發人員使用。

PJSIP 協議棧內部包含多個SIP 訊息處理層,如下圖2所示,從下往上依次TRANSPORT層、ENDPOINT 層、TRANSACTION 層、UA 層和DIALOG 層。每個訊息處理層以模組的形式註冊到協議棧中,開發者也可以編寫並新增自己的訊息處理模組,對SIP 訊息進行解析或修改。當TRANSPORT MANAGER 收到SIP 訊息包時,會把該SIP EVENT 通知上層的ENDPOINT,而ENDPOINT 會找到對應的接收者,先把EVENT 傳給TRANSACTION LAYER,然後再傳給UA LAYER(傳遞的順序由每個模組的優先權決定),如果 UA LAYER指定要處理TRANSACTION 的EVENT,TRANSACTION LAYER 也會把解析後的EVENT傳給UA LAYER。

               PJSIP是一個高度封裝的庫,實際上它是通過PJSUA子庫來實現應用的。一個完整的PJSUA生命週期,首先需要初始化,通過函式init()來實現。在這個函式中,將建立代理、初始化變數和堆疊,以及建立一個UDP傳輸並在最後啟動代理;第二步將為UA新增使用者,如果需要的話,還要向伺服器註冊使用者;當使用者新增成功後,此時可以建立一個呼叫連線,發起會話;當會話連線成功後,就可以使用SRTP協議實時傳輸加密後的資料,進行通話。最後的過程是掛起或銷燬呼叫。  

1.1 整體結構

        1.1.1 通訊結構圖

  

 

1.2 Endpoint的介紹

Sip協議棧的中心環節就是Endpoint,被封裝成了pjsip_endpoint型別來使用。來看看它的特性和職責吧。

1.它採用記憶體池來為所有的sip元件分配記憶體。

2.它有一個時間堆,為所有的sip元件分配時間。

3.它還有一個通訊協議管理模組,這個管理模組包括通訊協議的管理,以及對訊息的解析的列印。還記得sip訊息的結構嗎?

4.它還有一個單一的PJLIB的io佇列,這個io佇列什麼功能呢?就是對處理不完的網路事件放到佇列中。

5.它還有一個執行緒安全的輪詢函式。用於來對應用程式的執行緒進行事件和socket事件的輪詢。

6.它管理著pjsip模組,pjsip模組不建立任何執行緒。Pjsip是擴充套件協議棧的主要方法,並不侷限於訊息的解析和顯示。

7.從通訊管理模組接收訊息,並且把訊息分發到各個模組。

下面具體介紹一下這些功能。

1.2.1 記憶體的分配和釋放

所有元件記憶體的分配都有endpoint進行,這樣就可以保證執行緒的安全,保證整個應用體系執行統一的決定。這些決定很多,舉一個例子吧。例如,快取。

Endpoint提供了兩個函式來分配和釋放記憶體池

.pjsip_endpt_create_poll()

.pjsip_endpt_create_poll()

對於endpoint的建立可以用pjsip_endpt_create()函式。

當呼叫這個函式之後,endpoint就被建立了,建立記憶體池的工廠由應用程式指定來讓endpoint使用。這個記憶體工廠指標將存在於endpoint的生命週期中,endpoint就用他來建立和釋放記憶體池。

1.2.2 時間管理

Endpoint通過控制一個定時器堆來管理定時器。所有元件的定時器的建立和管理都由endpoint進行。

提供的函式有

.pjsip_endpt_schedule_timer();

.pjsip_endpt_cancel_timer();

當endpoint的輪詢函式被呼叫的時候,endpoint來檢查時間的終結。

1.2.3 輪詢處理事件

Endpoint提供了一個函式叫做pjsip_endpt_handle_events()來檢查時間和網路時間。應用程式可以確定它多久等到網路事件的發生。

1.3 執行緒安全和執行緒併發


相關文章