一、通用設計
1.1 架構
1.1.1 通信圖
下面的圖展示了SIP消息在PJSIP組件間從后端到前端如何傳遞的。
1.1.2 類圖
下面的圖顯示類視圖
1.2 Endpoint
SIP 協議棧的核心是SIP endpoint,它由透明的pjsip_endpoint的表示,endpoint具有下面的屬性和職責
l 內存儲工廠,為所有的SIP組件分配內存
l 具備定時器堆實列,為所有的SIP組件調度定時器
l 傳輸管理起實例,傳輸管理器負責傳輸SIP消息並且控制消息的解析和打印。
l 擁有單實例PJLIB io 隊列,IO隊列采用proactor模式分發事件。
l 提供了線程安全的輪詢功能,應用程序的線程能夠查詢定時器和socket事件(PJSIP自身不常見任何線程)
l 管理PJSIP模塊,PJSIP模塊主要以為着擴展SIP stack,而不僅僅限於消息的解析和打印。
l 從傳輸模塊接收SIP消息,並且分發到各個模塊中。
1.2.1 內存池分配和釋放
為了保證線程的安全性以及在整個應用中策略強一致性,所有的SIP內存都需要在endpoint中完成分配。比如:內存池緩存,未使用的內存被保留在以后使用而不是銷毀。
Endpoint提供下面的函數分配和釋放內存池
pjsip_endpt_create_pool()
pjsip_endpt_release_pool()
當endpoint被pjsip_endpt_create()創建時,應用一定要指明由endpoint使用的內存池工廠。在整個生命周期內Endpoint持有內存池工廠的指針,將來備用創建和釋放內存。
1.2.2 定時器管理
Endpoint 擁有一個獨立定時器堆實例,所有SIP組件的定時器創建和調度都需要通過endpoint 完成。Endpoint提供下面的函數管理定時器
pjsip_endpt_schedule_timer()
pjsip_endpt_cancel_timer()
endpoint的poll函數被調用時,檢查定時器是否過期。
1.2.3 輪詢棧
Endpoint 提供獨立的函數(pjsip_endpt_handle_events())為了檢查定時器和網絡事件的出現,應用可以指定准備等待多久去檢查事件的出現。
Pjsip 棧從不創建線程,整個棧的運行過程都依賴於應用所創建的線程,不管是API被調用或者是輪詢函數被調用。
輪詢功能優化等待時間依賴於定時器堆的內容,比如:當它找到定時器將在下個5S過期,他等待socket事件的時間,將不會比5S長,在無網絡事件出現時,應用程序等待超過5S是沒有必要的,當然每個平台的定時器的精度是不一樣的。
1.3 線程安全和線程的兼容性
1.3.1 線程安全
線程安全的討論是比較麻煩的事情,但是,幸運的是,下面的設計原則,在整個協議中的應用都保持了一致性。
對象一定是線程安全的,但是數據結構一定不是線程安全的。
看到這個主題,很自然的想到,對象和簡單數據結構的區別不是那么清晰,但是有一些例子可以提供,可能更明白寫。
數據結構的例子:
PJLIB的數據結構,比如:
l 鏈表、數組、哈希表、字符串、內存池。
l SIP消息的元素,URLs、header fields和SIP消息
這些數據結構並不是線程安全的,數據結構的線程安全由包含他們的對象保證。如果數據結構是線程安全的,將會嚴重的影響協議棧的性能並且消耗操作系統的資源。
相比之下,PJSIP的對象一定是線程安全的,比如:
l PJLIB 對象,比如ioqueue
l PJSIP 對象,比如: endpoint、transactions、dialogs等。
1.3.2 復雜性
更糟糕的是,一些對象在頭文件中暴露了他們的聲明(pjsip_transaction 和 pjsip_dialog)。
雖然API暴露可以保證線程的安全性,應用在訪問數據結構前,必須通過pj_mutex_lock獲取到對象的互斥鎖。
更糟糕的是,dialog暴露的不同的API鎖定dialog,應用程序應該調用pjsip_dlg_inc_lock和pjsip_dlg_dec_lock() 替代pj_mutex_lock和pj_mutex_unlock。兩種處理方式區別是:
Dialog的inc/dec保證了dialog將不會被銷毀在函數調用時。而pj_mutex_unlock會因為dialog銷毀了而導致層序crash。
考慮下面的例子:
pj_mutext_lock(dlg->mutex)
pjsip_dlg_end_session(dlg,…)
pj_mutex_unlock(dlg->mutex)
在上面的例子中,應用可能會crash,因為pjsip_dlg_end_session可能會銷毀dialog在某些情況下。例如:INVITE事務沒有被應答,事務會被馬上銷毀,導致dialog被立刻銷毀。Dialog
Inc/dec可以通過增加dialog的引用計數,防止這類情況的發生。再end_session時,dialog 不會被銷毀,dialog 可能會在dec_lock函數中被銷毀。所以正確的調用順序應該如下:
Pjsip_dlg_inc_lock(dlg)
Pjsip_dlg_end_session(dlg)
Pjsip_dlg_dec_lock
最最糟糕的是,鎖的調用順序一定是正確的,否則引起死鎖。比如:應用想去鎖dialog和dialog 的 transaction,應用必須獲得dialog mutex在獲取transaction mutex之前,否則當其他的線程以相反的順序拿鎖時,將會引起死鎖。
1.3.3 解決辦法
幸運的是,應用程序很少會直接獲取對象的鎖,所以上面的問題很少會發生。應用應該使用對象的API訪問對象,API會對象檢查,保證了lock的正確性,避免了死鎖和crash的產生。
應用程序的回調被對象調用,這些回調被調用時,對象的鎖已經獲取到了。所以應用能夠安全的獲取對象的數據結構,不必要獲取對象的鎖。