oSIP協議棧(及eXoSIP、Ortp等)使用入門

http://ghj19850926.blog.163.com/blog/static/18591560201241410033429/

 一直沒空仔細研究下oSIP,最近看到其版本已經到了3.x版本,看到網上的許多幫助說明手冊都過於陳舊,且很多文檔內容有點誤人子弟的嫌疑~~ 
  Linux下oSIP的編譯使用應該是很簡單的,其Install說明文檔裏也介紹的比較清楚,本文主要就oSIP在Windows平臺下VC6.0開發環境下的使用作出描述。 
  雖然oSIP的開發人員也說明了,oSIP只使用了標準C開發庫,但許多人在Windows下使用oSIP時,第一步就被卡住了,得不到oSIP的LIB庫和DLL庫,也就沒有辦法將oSIP使用到自己的程序中去,所以第一步,我們將學習如何得到oSIP的靜態和動態鏈接庫,以便我們自己的程序能夠使用它們來成功編譯和執行我們的程序。


第一階段: 
------------------------------------------------------ 
  先創建新工程,網上許多文檔都介紹創建一個Win32動態鏈接庫工程,我們這裏也一樣,創建一個空白的工程保存。 
  同樣,將oSIP2版本3.0.1 src目錄下的Osipparser2目錄下的所有文件都拷到我們剛創建的工程的根目錄下,在VC6上操作: 
Project-Add To Project-Files 
  將所有的源程序和頭文件都加入到工程內,保存工程。 
  這時,我們可以嘗試編譯一下工程,你會得到許多錯誤提示信息,其內容無非是找不到osipparser2/xxxxx.h頭文件之類。 
  處理:在Linux下,我們一般是將頭文件,lib庫都拷到/usr/inclue;/usr/lib之類的目錄下,c源程序裏直接寫#include <xxx.h>時,能直接去找到它們,在VC裏,同樣的,最簡單的方法就是將oSIP2源碼包中的Include目錄下的osipparser2目錄直接拷到我們的Windows下默認包含目錄即可,這個目錄在VC6的Tool-Options-Directories裏設置,(當然,如果你知道這一步,也可以不用拷貝文件,直接在這裏把oSIP源碼包所在目錄加進來就可以了),默認如果裝在C盤,目錄則爲C:/Program Files/Microsoft Visual Studio/VC98/Include。 
  這時,我們再次編譯我們的工程,順利編譯,生成osipparser2.dll,這時,網上很多文檔裏可能直接就說,這一步也會生成libs目錄,裏面裏osipparser2.lib文件,但我們這裏沒有生成:) 
  最簡單的方法,不用深究,直接再創建一個工程,同上述創建動態鏈接庫方法,創建一個Win32靜態鏈接庫工程,直接編譯,即可得到osipparser2.lib。 
------------------------------------------------------ 
  上面,我們得到了Osip的解析器開發庫,下面再編譯完整的Osip協議棧開發庫,同樣照上述方法,分別創建動態鏈接庫工程和靜態鏈接庫工程,只是要拷的文件換成src下的osip目錄下文件和include下的osip目錄,得到osip2.dll和osip2.lib。 
  在編譯osip2.dll這一步可能會再次得到錯誤,內容含義是找不到鏈接庫,所以,我們要把前面編譯得到的osipparser2.lib也拷到osip工程目錄下,並在VC6中操作: 
  Project-Setting-Link中的Object/Library Modules: 
kernel32.lib user32.lib ... xxx.lib之類的內容最後增加: osipparser2.lib 
  保存工程後再次編譯,即可成功編譯osip2.dll。 
------------------------------------------------------ 
  至此,我們得到了完整的oSIP開發庫,使用時,只需在我們的程序裏包含oSIP的頭文件,工程的鏈接參數裏增加osipparser2.lib和osip2.lib即可。 
------------------------------------------------------ 
  下面我們驗證一下我們得到的開發庫,並大概瞭解一下OSIP的語法規範。 
  在VC裏創建win32控制檯程序工程,將libosip源碼包的SRC目錄下的Test目錄內的C源程序隨便拷一個到工程時,直接編譯(工程設置裏照前文方法在link選項裏增加osip2.lib,osipparser2.lib引用我們之前成功編譯得到的靜態庫文件)就可以運行(帶參數運行,參數一般爲一個文本文件,同樣從Test目錄的res目錄裏拷一個與源文件同名的純文本文件到工程目錄下即可)。 
  該目錄下的若干文件基本上是測試了Osip的一些基本功能函數,例如URI解析之類,可以大概瞭解一下oSIP的語法規範和調用方法,同時也能校驗一下之前編譯的OSIP開發庫能否正常使用,成功完成本項工作後,可以進入下一步具體的oSIP的使用學習了。 
------------------------------------------------------ 
  由於oSIP是比較底層的SIP協議棧實現,新手較難上手,而官方的示例大都是一些僞代碼,需要有實際的例子程序參考學習,而最好的例子就是同樣官方發佈的oSIP的擴展開發庫exosip2,使用exoSIP可以很方便地快速創建一個完整的SIP程序(只針對性地適用於SIP終端開發用,所以我們這裏只是用它快速開發一個SIP終端,用來更方便地學習oSIP,要想真正掌握SIP的開發,需要掌握oSIP並熟讀RFC文檔才行,exoSIP不是我們的最終學習目的),通過成功編譯運行一個自己動手開發出的程序,再由淺入深應該是初學都最好的學習方法通過對使用exosip開發庫的使用創建自己的SIP程序,熟悉後再一個函數一個函數地深入學習exosip提供的接口函數,就可以深入理解osip 了,達到間接學習oSIP的目的,同時也能從eXoSIP中學習到正確使用oSIP的良好的編程風格和語法格式。 
  而要成功編譯ExoSIP,似乎許多人被難住了,直接在XP-sp2上,用VC6,雖然你使用了eXoSIP推薦的winsock2.h,但是會得到一個sockaddr_storage結構不能識別的錯誤,因爲vc6自帶的開發庫太古董了,需要升級系統的Platform SDK,下載地址如下: 
http://www.microsoft.com/msdownl ... PSP2FULLInstall.htm(VC6的支持已經停止,這是VC6能使用的最新SDK) 
  成功安裝後編譯前需加OSIP_MT宏,以啓用線程庫,否則在程序中使用eXoSIP庫時會出錯,而編譯時也會得到許多函數未定義的Warning提示,編譯得到exosip2.lib供我們使用,當然,在此之前需要成功編譯了osip2和osipparser2,而在之後的實際使用時,發現oSIP也需要增加OSIP_MT宏,否則OSIP_MT調用oSIP的線程庫時會出錯,所以我們需要重新編譯oSIP了:),因爲eXosip是基於oSIP的(同上方式創建靜態和動態鏈接庫工程,並需在Link中手工添加oSIP和oSIPparser的lib庫)。 
------------------------------------------------------ 
  創建新工程,可以是任意工程,我們從最簡單的Win32控制檯程序開始,爲了成功使用oSIP,我們需要引用相關庫,調用相關頭文件,經過多次試驗,發現需要引用如下的庫: 
exosip2.lib osip2.lib osipparser2.lib WSock32.Lib IPHlpApi.Lib WS2_32.Lib Dnsapi.lib 
  其中,除了我們上面編譯得到的三個oSIP庫外,其它庫都是系統庫,其中有一些是新安裝的Platform SDK所新提供的。 
  至此,我們有了一個簡單的開發環境了,可以充分利用網上大量的以oSIP爲基礎的代碼片段和官方說明文檔開始具體函數功能的測試和使用了:) 
------------------------------------------------------ 
  我們先進行一個簡單的純SIP信令(不帶語音連接建立)的UAC的SIP終端的程序開發的試驗(即一個只能作爲主叫不能作爲被叫的的SIP軟電話模型),我們創建一個MFC應用程序,對話框模式,照上面的說明,設置工程包含我們上面得到的oSIP的相關開發庫及SDK的一些開發庫,並且由於默認LIBC的衝突,需要排除MSVCRT[D]開發庫(其中D代表Debug模式下,沒有D表示Release模式下),直接使用eXosip的幾個主要函數就可以創建一個基本的SIP軟電話模型。

  其主要流程爲: 
  初始化eXosip庫-啓動事件監聽線程-向SIP Proxy註冊-向某SIP終端(電話號碼)發起呼叫-建立連接-結束連接

  初始化代碼: 
int ret = 0; 
ret = eXosip_init (); 
eXosip_set_user_agent("##YouToo0.1"); 
if(0 != ret) 

AfxMessageBox("Couldn't initialize eXosip!/n"); 
return false; 

ret = eXosip_listen_addr (IPPROTO_UDP, NULL, 0, AF_INET, 0); 
if(0 != ret) 

eXosip_quit (); 
AfxMessageBox("Couldn't initialize transport layer!/n"); 
return false; 
}

  啓動事件監聽線程: 
AfxBeginThread(sip_uac,(void *)this);

  向SIP Proxy註冊: 
eXosip_clear_authentication_info(); 
eXosip_add_authentication_info(uname, uname, upwd, "md5", NULL); 
real_send_register(30);  /* 自定義函數代碼請見源碼 */

  發起呼叫(構建假的SDP描述,實際軟電話使用它構建RTP媒體連接): 
osip_message_t *invite = NULL; /* 呼叫發起消息體 */ 
int i = eXosip_call_build_initial_invite (&invite, dest_call, source_call, NULL, "## YouToo test demo!"); 
if (i != 0) 

AfxMessageBox("Intial INVITE failed!/n"); 

char localip[128]; 
eXosip_guess_localip (AF_INET, localip, 128); 
snprintf (tmp, 4096, 
"v=0/r/n" 
"o=josua 0 0 IN IP4 %s/r/n" 
"s=conversation/r/n" 
"c=IN IP4 %s/r/n" 
"t=0 0/r/n" 
"m=audio %s RTP/AVP 0 8 101/r/n" 
"a=rtpmap:0 PCMU/8000/r/n" 
"a=rtpmap:8 PCMA/8000/r/n" 
"a=rtpmap:101 telephone-event/8000/r/n" 
"a=fmtp:101 0-11/r/n", localip, localip, "9900"); 
osip_message_set_body (invite, tmp, strlen(tmp)); 
osip_message_set_content_type (invite, "application/sdp"); 
eXosip_lock (); 
i = eXosip_call_send_initial_invite (invite); 
eXosip_unlock ();

  掛斷或取消通話: 
int ret; 
ret = eXosip_call_terminate(call_id, dialog_id); 
if(0 != ret) 

AfxMessageBox("hangup/terminate Failed!"); 
}

  可以看到非常簡單,再藉助於oRTP和Mediastreamer開發庫,來快速爲我們的SIP軟電話增加RTP和與系統語音API接口交互及語音編碼功能,即可以快速開發出一個可用的SIP軟電話,關於oRTP和Mediastreamer的相關介紹不是本文重點,將在有空的時候考慮增加相應使用教程,文章前提到的地方可以下載基本可用的完整SIP軟電話的VC源碼工程文件供參考使用,完全CopyLeft,歡迎轉載,但請在轉載時註明作者信息,謝謝!

第二階段: 
--------------------------------------------------- 
  得到了一個SIP軟電話模型後,我們可以根據軟電話的實際運行表現(結合用Ethereal抓包分析)來進行代碼的分析,以達到利用eXoSIP來輔助我們學習oSIP的最終目的(如要快速開發一個可用的SIP軟電話,請至前面提到的論壇去下載使用oRTP和Mediastreamer快速搭建的一個基本完整可用的SIP軟電話##YouToo 0.1版本的VC源碼工程文件作參考)。

  現在從eXosip的初始化函數開始入手,來分析oSIP的使用,這是第二階段,第三階段就是深入學習oSIP的源碼了,但大多數情況下應該沒有必要了,因爲在第二階段就有部分涉及到第三階段的工作了,而且oSIP的源碼也就大多是一些SIP數據的語法解析和狀態機的實現,能深入理解了SIP協議後,這些只是一種實現方式,沒必要完全去接受,而是可以用自己的方式和風格來實現一套,比如,更輕量化更有適用目的性的方式,oSIP則只起參考作用了。

  eXosip_init()是eXosip的初始化函數,我們來看看它的內部實現: 
  首行是定義的 osip_t *osip,這在oSIP的官方手冊裏我們看到,所有使用oSIP的程序都要在最開始處聲明一個osip_t的指針,並使用osip_init(&osip)來初始化這個指針,銷燬這個資源使用osip_release(osip)即可。 
  我們可以在代碼中看到很多OSIP_TRACE,這是調試輸出宏調用了函數osip_trace,可以用ENABLE_TRACE宏來打開調試以方便我們開發調試。 
  其它就是很多的eXosip_t的全局變量eXosip的一些初始化操作,包括最上面的memset (&eXosip, 0, sizeof (eXosip))完全清空和下面的類似eXosip.user_agent = osip_strdup ("eXosip/" EXOSIP_VERSION)的exosip變量的一些初始值設置,其中有一個eXosip.j_stop_ua = 0應該是一個狀態機開關,後面可以看到很多代碼檢測這個變量來決定是否繼續流程處理,默認置成了0表示現在exosip的處理流程是就緒的,即ua是not stop的。 
   
  osip_set_application_context (osip, &eXosip)是比較有意思的,它讓下面的eXosip_set_callbacks (osip)給osip設置大量的回調函數時,能讓osip能訪問到eXosip這個全局變量中設置的大量程序運行時交互的信息,相當於我們在VC下開啓一個線程時,給線程傳入的一個void指針指向我們的MFC應用程序的當前dialog對象實例,可以用void *osip_get_application_context (osip_t * osip)這個函數來取出指針來使用,不過好象exosip中並沒有用到它,可能是留給個人自已擴展的吧:) 
   
  還能看到初始化代碼前面有一段WIN32平臺下的SOCK的初始化代碼,可以知道eXosip是用的原生的winsock api函數,也就是我們可能以前學過的用VC和WINAPI寫sock程序時(不是MFC),用到的那段SOCK初始代碼,還有一段有意思的代碼,就是jpipe()函數,它們返回的是一個管道,一個有2個整型數值的數組(一個進一個出),查看其代碼發現,非WIN32平臺是直接使用的pipe系統函數,而WIN32下則是用一對TCP的本地SOCK連接來模擬的管道,一個SOCK寫一個SOCK讀,這段代碼是比較有參考價值的:) 
j = 50; 
while (aport++ && j-- > 0) 

  raddr.sin_port = htons ((short) aport); 
  if (bind (s, (struct sockaddr *) &raddr, sizeof (raddr)) < 0) 
  { 
    OSIP_TRACE (osip_trace (__FILE__, __LINE__, OSIP_WARNING, NULL, 
    "Failed to bind one local socket %i!/n", aport)); 
  } else 
  break; 

含義即,依次檢測50個端口,從static int aport = 10500;即10500~10550端口找出一個可用的本地端口來綁定listen模擬pipe的一對sock。 
  eXosip_set_callbacks (osip)沒有什麼好看的,無非是和oSIP官方文檔介紹的一樣,設置一大堆的回調函數,關鍵是回調函數的實現,這也是許多初學者使用oSIP被卡殼的主要原因,不知道oSIP構建的程序是怎樣跑起來的,隨便選幾個回調函數看一下eXosip是怎樣實現的,有許多是形如下文的函數: 
static void 
cb_sndbye (int type, osip_transaction_t * tr, osip_message_t * sip) 

  OSIP_TRACE (osip_trace 
  (__FILE__, __LINE__, OSIP_INFO3, NULL, "cb_sndbye (id=%i)/r/n", 
  tr->transactionid)); 

  即,只是打印一下調試,並沒有完整實現什麼功能,我們學習時,完全可以用相同的方法,定義一大堆回調函數,並不忙想怎麼完全實現,先都是隻打印一下調試信息,看具體的應用邏輯根據抓包測試分析和看調試看程序走到了哪一步,調用了哪一個回調,來明白具體回調函數要實現什麼用途,再來實現代碼就方便多了,當然,如果看透了RFC文檔,應該從字面就能知道各個回調函數的用途了,這是後話,不是誰都能快速完全看懂RFC的,所以我們要參考eXosip:) 
   
  我們對其中的重要的回調函數進行逐個的分析: 
  --------------------------- 
  osip_set_cb_send_message (osip, &cb_snd_message) SIP消息發送回調函數 
  這個函數可能是最重要的回調函數之一,消息發送,包括請求消息和迴應消息,一般情況下,狀態機的狀態就是由它控制的,發起一個消息初始化一個狀態機,迴應一個消息對狀態機修改,終結消息發送結束狀態機…… 
  看cb_snd_message的函數實現,要以發現,其主要代碼是對參數中的要發送的消息osip_message_t * sip進行分析,找出消息要發送的真實char *host,int port的值(這些參數可以省略,但要發送消息肯定需要host和port,所以要從sip中解析),最後根據sip中解析出的傳輸方式是TCP還是UDP選擇最終進行消息發送處理的函數cb_udp_snd_message,cb_tcp_snd_message處理(它們的參數一致,即本函數只是補全一些省略的參數並對消息進行合法性檢查)。 
  **畢竟eXosip是一個通用的開發庫,它考慮了要支持TCP,UDP,TCPs,IPV4,IPV6,WIN32,*nix,WINCE等等多樣化的複雜環境,所以,我們可以略過我們暫時不需要的部分,比如,IPV6相關的代碼實現等。 
   
  由於我們大多數情況下SIP是用的UDP,所以先來看一下cb_udp_snd_message的實現,它從全局變量exosip中獲取可用的sock,並盡最大能力解析出host和port(??難道前面的函數還不夠解析徹底??如最終仍無port信息則默認設置爲5060),使用osip_message_to_str (sip, &message, &length)函數將要發送的格式化的SIP消息轉換成能用SOCK傳輸的簡單數據併發送即完成消息發送,代碼中有許多複雜的環境探測和錯誤控制等等等等,我們可以暫時不用過多關注,可以繼續向下,結尾處有一個keeplive相關代碼,從代碼字面分析,可能是SIP的Register消息的自動重發相關代碼,可以在後面再細化分析。 
  cb_tcp_snd_essage的函數實現要比上文的udp的實現簡單很多,主要是環境探測錯誤控制方面,因爲畢竟tcp是穩定連接的,對比一下代碼,可以看到主要流程還是將SIP消息轉換後,發送到從SIP消息中解析出的host和port對應的目標。 
   
  看完兩個函數,可以知道,eXosip需要有兩個sock,是一個數組,0是給UDP用的,1是給TCP用的,要用SOCK當然要初始化,就是下文要介紹的eXosip的網絡相關的初始化了,上面的exosip_init可以看成是這個開發庫的系統初始化吧:)  
  至些,我們應該知道了oSIP開發的SIP應用程序的消息是從哪裏發出的吧,對了,就是從這個回調函數裏,所謂萬事開頭難,就象開發WIN32應用程序時,找到了WIN32程序的main函數入口下面的工作就好辦了,下面就都是爲一些事件消息開發對應的處理函數而已了:)

  osip_set_kill_transaction_callback 事務終結回調函數 
  對應ICT,IST,NICT,NIST客戶/服務器註冊/非註冊事務狀態機的終結,主要是使用osip_remove_transaction (eXosip.j_osip, tr)將當前tr事務刪除,再加上一系列的清理工作,其中,NICT即客戶端的非Invite事務的清理比較複雜一些,要處理的內容也比較多,可以根據實際應用的情況進行有必要的清理工作:)

  cb_transport_error 傳輸失敗處理回調 
  對應於上面說到的四種事務狀態機,如果它們在處理時失敗,則在這時進行統一處理。 
  從代碼可知,只是在NOTIFY,SUBSCRIBE,OPTION操作失敗才進行處理,其它錯誤可直接忽略。

  osip_set_message_callback 消息發送處理回調 
  根據type不同,表示不同的消息發送狀態 
  OSIP_XXX_AGAIN 重發相關消息 
  OSIP_ICT_INVITE_SENT 發起呼叫 
  OSIP_ICT_ACK_SENT ACK迴應 
  OSIP_NICT_REGISTER_SENT 發起註冊 
  OSIP_NICT_BYE_SENT BYE發出 
  OSIP_NICT_CANCEL_SENT Cancel發出 
  OSIP_NICT_INFO_SENT,OSIP_NICT_OPTIONS_SENT,OSIP_NICT_SUBSCRIBE_SENT,OSIP_NICT_NOTIFY_SENT,OSIP_NICT_UNKNOWN_REQUEST_SENT 
  我們可以看到,eXosip沒有對它們作任何處理,我們可以根據自己需要,比如,重發2xx消息前記錄一下日誌之類的,擴展一下retransmission的處理方式,發起Invite前記錄一下通話日誌等等。

  OSIP_ICT_STATUS_1XX_RECEIVED uac收到1xx消息,一般是表示對端正在處理中,這時,主要是設置一下事務狀態機的狀態值,並對會話中的osip的一些參數根據返回值進行相應設置,裏面有許多條件判斷,但我們常用的一般是100,180,183的判斷而已,暫時可以忽略裏面複雜的判斷代碼。 
  OSIP_ICT_STATUS_2XX_RECEIVED uac收到2xx消息,這裏主要跟蹤一下Register情況下的2xx,表示註冊成功,這時會更新一下exosip的註冊字段值,以便讓eXosip能自動維護uac的註冊,BYE的2xx迴應是終結消息,Invite的2xx迴應,則主要是初始化一下會話相關的數據,表示已成功建立連接。 
  其它4xx,5xx,6xx則分別是對應的處理,根據實現情況進行概要的查看即可。 
  report_event (je, sip)是代碼中用來進行事件處理的一個函數,跟蹤後發現,其最終是使用了我們上文提到的jpipe管道,以便在狀態機外實時觀測狀態機內的處理信息。 
   
  OSIP_NIST_STATUS_XXX_SENT即對應於上面的uac的處理,這裏是uas的對應的消息處理,相比較於uac簡單一點。

  前面簡單介紹了一下大量的回調函數及它們的概要處理邏輯,可能會比較混亂,暫時不用管它,只需要記得一個大概的形象,知道一個SIP處理程序是通過osip_set_cb_send_message回調函數來實現真實地發送各種SIP消息,並且SIP的標準事務模型是由oSIP實現好了,我們只需要給不同的事務狀態設置不同的回調處理函數來處理事務,具體的狀態變化和內部邏輯不用管就可以了。

  下面來說一下消息處理回調函數用到的SOCK的初始化函數,即我們上面說的除了系統初始化外的網絡初始化函數eXosip_listen_addr: 
  從上文知道了,系統將初始化兩個SOCK,一個UDP一個TCP,但查看代碼發現還有第三個,TCPs的,但好象還不能實用,現在不管它,代碼首先是根據傳輸是UDP還是TCP來設置對應的數組值,並且如果沒有提供IP地址和端口號,系統會自動取出本機網絡接口並創建可用的SOCK(http_port的方式暫不用考慮)。 
  SOCK初始化後,如何開始SIP事務的呢?看到這個調用eXosip.j_thread = (void *) osip_thread_create (20000, _eXosip_thread, NULL),對的,這裏啓用了一個線程,即,eXosip是調用oSIP的線程函數(沒用系統提供的線程函數,是爲了跨平臺)進行事務處理的狀態機邏輯是在一個線程中處理的,這樣就明白了爲什麼一直沒能看到順序執行下來的程序啓動代碼了,接下去看,線程實際處理函數是_eXosip_thread,這裏面的代碼中,我們看到了上文提到的狀態機控制開關變量while (eXosip.j_stop_ua == 0),即,當j_stop_ua設置爲1時,osip_thread_exit ()結束事務處理即程序終結,再接下去看,_eXosip_execute是最終的處理函數了,而且它在程序未終結情況下是一直邏輯在執行,注意,要啓用oSIP的多線程宏OSIP_MT。 
   
  看到_eXosip_execute的代碼中有很多時間函數和變量,仔細看,除去一些控制代碼,主要處理函數是eXosip_read_message (1, lower_tv.tv_sec, lower_tv.tv_usec),即取出消息,1表示只取出一條消息,其代碼量非常的大,但同樣的,其中也許多的控制代碼和錯誤檢測代碼,我們在查看時可以暫時忽略掉它們。 
  eXosip_read_message讀取消息時,即沒有采用sock的block也沒有用非block方式,而是採用了select方式,具體應用可查詢fd_set相關文檔。 
  根據jpipe_read (eXosip.j_socketctl, buf2, 499),我們可以估計,buf2中應該是保存的我們的控制管道的數據,具體作用至些還沒有表現出來,應該是用來反映一些狀態機內部的警示之類的信息,實際的SIP的處理的狀態機的數據是存放在buf中,使用_eXosip_recvfrom獲取的,獲取後sipevent = osip_parse (buf, i)解析,使用osip_find_transaction_and_add_event (eXosip.j_osip, sipevent)來查詢事件對應的事務狀態機,找到後就如同其註解所說明的,/* handled by oSIP ! */,即我們上文設置的那一大堆回調函數,至此,我們知道了整個SIP應用所處理的大概流程了。 
  如果沒有找到事務狀態機呢?直接丟棄嗎?不是的,如果這是一個迴應消息,但沒有事務狀態機處理它,那它是一個錯誤的,要進行清理後才能丟棄,而如果是一個請求,那更不能丟棄了,因爲UAS事務狀態機要由它來啓動創建的(迴應消息表示本地發出了請求消息,即UAC行爲,事務狀態機應是由啓動UAC的代碼初始化啓動的),整個邏輯應該是很簡單的,但eXosip的實現代碼卻非常多,可見其花了非常多的精力在保證會話的穩定性和應付網絡複雜情況上,我們可以對其進行大量的精簡來構建滿足我們需求的代碼實現。 
  先來看錯誤的迴應消息的處理函數eXosip_process_response_out_of_transaction,可以看到其代碼就是一大堆的賦值語句,XXX= NULL,即將一大堆的運行時變量清空,再調用osip_event_free清空事件,或者就是一些複雜的情況下,需要通過解析現在的運行時數據,從中分析出"可能"的正在等待迴應的對端,併發送相關終結通知消息等等,可以根據實際需要進行簡化。 
  請求事件的處理eXosip_process_newrequest,首先是對事件進行探測,MSG_IS_INVITE、MSG_IS_ACK、MSG_IS_REQUEST……,對事件進行所屬狀態機分類,隨後使用_eXosip_transaction_init (&transaction,(osip_fsm_type_t) tx_type,eXosip.j_osip, evt->sip)根據探測結果進行狀態機初始化,實際調用的是osip_transaction_init,初始化後即將事件入狀態機osip_transaction_add_event (transaction, evt),由狀態機自動處理後調用相應回調函數處理邏輯了。當然,eXosip爲方便快速開發SIP終端應用,在下面又添加了許多自動化的處理代碼,來和我們在回調函數中設置的處理代碼相區分。

  線程調用的事件處理函數代碼最後是 
if (eXosip.keep_alive > 0) 

  _eXosip_keep_alive (); 

  這段代碼印證了上文提到了,keep_alive是用來設置是否自動重新註冊,由_eXosip_keep_alive函數來實現自動將eXosip全局變量中保存的註冊消息解析後自動根據需要重新向SIP服務器發起Register註冊。 
  同樣,因爲註冊消息發起是UAC的行爲,將它放在這裏,可以看出來所有事件消息的事務狀態機處理都是在這裏,只不過這裏只創建UAS的事務狀態機,UAC的事務狀態機的創建則要繼續到下面找了,從我們的YouToo軟電話代碼中可知,發起呼叫和發起註冊分別調用了eXosip_call_send_initial_invite,eXosip_register_send_register這兩個函數(另外用到的兩個build函數則是分別構建這兩個send函數要發送的SIP消息),查看這兩個函數可知,UAC的事務處理狀態機是在這裏進行初始化的。 
  eXosip_register_send_register中可以看到是_eXosip_transaction_init (&transaction, NICT, eXosip.j_osip, reg)初始化UAC狀態機,實際也同UAS是調用的osip_transaction_init函數,同樣使用osip_transaction_add_event (transaction, sipevent)將事件入狀態機,狀態機隨後將自動處理調用相應回調函數處理邏輯了。 
  另有osip_new_outgoing_sipmessage(reg),表示發送消息,到這裏,我們應該可以理解,真實的發送操作,是要到由狀態機處理後,調用了消息發送回調函數才真正地將註冊消息發送出去的。 
  同註冊消息發送,它是NICT狀態機,呼叫消息的發送是ICT,由eXosip_call_send_initial_invite處理,_eXosip_transaction_init (&transaction, ICT, eXosip.j_osip, invite)初始化了狀態機,之前還有一個eXosip_call_init是用來初始化eXosip的一些參數的,暫時不管它,同樣osip_new_outgoing_sipmessage (invite)發送呼叫消息,但實際還是要狀態機處理後調用消息發送回調函數真實發送呼叫請求函數的,osip_transaction_add_event (transaction, sipevent)則標準地,將事件入狀態機,狀態機將能處理隨後的應用邏輯調用相應的回調函數了。

  好了,作了這麼多的分析,我們瞭解了eXosip是怎樣調用oSIP來形成被我能方便地再次調用的了,可以看到,爲了實現最大限度的跨平臺和兼容性,代碼中有大量的測試代碼,宏定義和錯誤再處理代碼,看起來非常吃力,但瞭解了其主要的調用框架: 
  初始化,回調函數設置,UAC和UAS事務處理狀態機的啓動,事件處理流程等,就可以基本明白了oSIP各個函數的主要作用和正確的用法了,下一步,可以參考eXosip來針對某個應用,去除掉大量暫時用不到的代碼,來構建一個簡單的SIP軟電話和SIP服務器,來進一步深入oSIP學習應用了。 

續一:純協議棧邏輯分析(轉)

轉自http://mbstudio.spaces.live.com/blog/cns!C898C3C40396DC11!2860.entry

很長時間之前,簡單粗略地看了下Osip,eXosip,ortp等並快速"封裝"了一個Windows下的基於VC6的MFC的SIP軟電話(全部源代碼VC6工程文件及Lib庫可在本Blog共享文件夾找到),由於時間限制,只能是一知半解地純"應用"式地分析了一下osip,eXosip等開發庫的代碼,作爲興趣愛好者參考瞭解下SIP電話工作原理還可以,但作爲商用產品開發參考則還是太淺顯了些:) 
  最近擴展嵌入式Linux平臺上的SIP功能模塊(基於OSIP),由於使用的Osip不包括Call Transfer相關字段(Refer,Notify等)的解析和狀態機控制(最近的Osip版本是否有擴展未查看)不能支持呼叫轉接,需要手工擴展,有機會對Osip的主要事務狀態機、解析庫等部分稍有了些較深入的瞭解,結合SIP RFC總結分享如下。 
  (注:下文假設閱讀者已經大概瞭解SIP協議的簡單呼叫流程,會使用Ethereal等抓包工具分析SIP消息結構,對C語言的指針、鏈表、內存控制及狀態機等概念有足夠的認識。)

  要應用Osip到我們的程序中去,首先要看官方文檔,文檔中對Osip協議棧提供的各個功能部件如何使用都有比較詳細的描述,但未進行整體性的分析,某些中文的指導文檔也都停留在對其簡單的翻譯,不能爲不熟悉該協議棧使用的用戶快速參考使用,本文檔不按照Osip的代碼進行按功能分塊說明,而是根據實際使用時的代碼使用順序來對主要邏輯流程進行分析,並適當對流程中使用到的功能部件進行說明,具體更詳細的功能說明或疑問可直接查看官方文檔對應部分的解釋或直接查看功能函數源代碼即可解決。 
準備工作 
先認識幾個結構體:osip_t,osip_message_t,osip_dialog_t,osip_transaction_t; 
  osip_t是一個全局變量,所有要使用Osip協議棧的事務處理能力的程序都要第一步就初始化它(相對應於只使用osipparser庫進行SIP消息字段解析的應用來說,如果只使用parser庫到自己的程序中,想必對SIP協議棧已經很熟悉了,不需再往下看了^_^),它內部主要是定義了Osip協議棧的四個主要事務鏈表、消息實際發送函數及狀態機各狀態事件下的回調函數等; 
  osip_message_t是SIP消息的C語言結構體存儲空間,收到SIP消息解析後存在該結構中方便程序使用接收到的消息中的指定的字段,發送消息前爲方便設置要發送的字段值,將要發送的內容存在該結構中等發送時轉爲字符串; 
  osip_dialog_t則是SIP RFC中的dialog或叫call leg的定義,它標識了uac和uas的一對關係,並一直保持到會話(session)結束,一個完整的dialog主要包括from,to,callid,fromtag,totag,state等(可查看源碼),其中fromtag,totag,callid在一個dialog成功建立後才完整,體現在SIP消息中,就是From、To的tag,Call-id字段的值相同時,這些消息是屬於它們對應的一個Dialog的,例如將要發起invite時,只有fromtag,callid填充有值,在收到to遠端的響應時,收到totag填充到dialog中,建立成功一個dialog,後繼的邏輯均是使用這個dialog進行處理(如transaction事務處理),state表示本dialog的狀態,與transaction的state有很大的關聯,共用由Enum結構state_t定義; 
osip_transaction_t則是RFC中的事務的定義,它表示的是一個會話的某個Dialog之間的某一次消息發送及其完整的響應,例如invite-100-180-200-ack這是一個完整的事務,bye-200這也是一個完整的事務,體現在SIP消息中,就是Via中的branch的值相同表示屬於一個事務的消息(當然,事務是在Dialog中的,所以From、To的tag,Call-id值也是相同的),事務對於UAC,UAS的終端類型不同及消息的不同,分爲四類,前面說的invite的事務,主叫uac中會關聯一個ict事務,被叫uas會關聯一個ist事務,而除了invite之外,都歸類定義主叫nict,被叫nist,在Osip中,它是靠有限狀態機來實現的上述四種事務(osip_fsm_type_t中定義)的,它的主要屬性值有callid,transactionid,分別來標識dialog和transaction,其中還有一個時間戳birth_time標識事務創建時間,可由超時處理函數用來判斷和決定超時情況下的事務的進行和銷燬,而它的state屬性是非常重要的,根據上述的事務類型不同,其值也不同,它是前面提到的狀態機的"狀態",在實際狀態機的邏輯執行中是一個關鍵值; 
Osip初始化 
  提到osip的初始化,可能大家都看過官方文檔裏第一頁的代碼,首先就是osip_init(&osip)初始化了全局的osip_t結構體,然後對它的回調函數進行設置,很多人估計就是一看到這密密麻麻的一頁多的call_back設置被嚇到了,但結合前面分析的三個結構體的含義,這裏的含義就很清晰了: 
  osip_t中有一個cb_send_message函數指針,它是Osip最終與外界網絡交互的接口,它的參數有( osip_transaction_t * trn, /*本消息所屬的事務*/ 
osip_message_t * sipmsg, /*待發送的消息結構體*/ 
char *dest_socket_str, /*目標地址*/ 
int32_t dest_port, /*目標端口*/ 
int32_t send_sock)    /*用來發送消息的socket*/ 
  其中trn傳入主要是爲了方便獲取事務的上下文數據,它有一個void指針your_instance,可以用來傳入更多數據方便發送消息時參考,例如將該事務所屬的dialog指針傳入; 
  而sipmsg則是我們要發送的SIP消息的C結構體,使用osip_message_to_str將其按RFC文檔格式轉換爲一個字符串(osip中的parser模塊的主要功能),再通過任意你自己的網絡數據發送函數使用send_sock發送給dest_socket_str和dest_port指定的目標,當然,要記得使用osip_free釋放剛纔發送出去的字符串佔用的內存,Osip中很多osipparser提供的消息解析處理函數都是動態內存分配的,使用完畢後需要及時釋放; 
  使用osip_set_cb_send_message成功設置回調函數,我們的SIP消息就有了出口了,下面繼續分析(當然,瞭解到了上面的流程,也可以手工指定了)。

  下面的回調函數分爲三類,分別是普通事務消息(osip_message_callback_type_t中定義)的處理回調函數、事務銷燬事件(osip_kill_callback_type_t中定義)的清理回調函數以及事務執行過程中的錯誤事件(osip_transport_error_callback_type_t中定義)處理回調函數: 
  先說簡單的,事務銷燬事件,事務正常結束(成功完成狀態機流程)或由超時處理函數強制終結等情況下均調用了這些回調函數,一般就是釋放事務結構體,爲ICT,NICT,IST,NIST各設置或共用一個回調函數均可,只要正確釋放不再使用的內存即可; 
  錯誤處理函數則是在整個狀態機執行過程中發生的任何錯誤的出口,一般用來安插log函數方便調試,也可以直接設爲空函數; 
  而最關鍵的就是正常消息的處理回調函數了,其量是非常大的,但仔細分下類,也和上面的回調函數一樣,也是分爲四類,我們可有根據實際程序的需要來進行設置,例如,SIP電話機就不需要處理OSIP_NIST_REGISTER_RECEIVED這個SIP註冊服務器才需要處理的Register消息事件了,精簡一下,如果只是要做一個只需要實現主叫功能且不考慮錯誤情況的UAC的Demo軟電話程序,則只需要設置如下幾個事件的回調函數: 
  OSIP_ICT_INVITE_SENT 發出Invite開始呼叫 
  OSIP_ICT_STATUS_1XX_RECEIVED 收到180 
  OSIP_ICT_STATUS_2XX_RECEIVED 收到200 
  OSIP_ICT_ACK_SENT  發出ack確定呼叫 
  OSIP_NICT_BYE_SENT  發出bye結束呼叫 
  OSIP_NICT_STATUS_2XX_RECEIVED 收到200確認結束呼叫 
  OSIP_NIST_BYE_RECEIVED 收到bye結束呼叫 
  OSIP_NIST_STATUS_2XX_SENT 發出 200確定結束呼叫 
而要增加接受呼叫的被叫UAS功能,則只需要增加如下事件: 
  OSIP_IST_INVITE_RECEIVED 收到invite開始呼叫 
  OSIP_IST_STATUS_1XX_SENT 發出180 
  OSIP_IST_STATUS_2XX_SENT 發出200 
  OSIP_IST_ACK_RECEIVED  收到ack確認呼叫 
具體的函數定義,則直接參考osip_message_cb_t,osip_kill_transaction_cb_t,osip_transport_error_cb_t即可,回調函數的設置同上可以手工設置,也可以使用Osip提供的對應的osip_set_xxx_callback函數; 
發出SIP消息 
  要發送SIP消息,從上面的分析可知有幾個必要的條件,osip_messag_t結構的待發送消息,osip_dialog_t結構體的dialog以及osip_transaction_t的事務; 
  首先osip_malloc新分配一個dialog,使用osip_to_init,osip_to_parse,osip_to_free這類parser函數功能函數按RFC設置call-id,from,to,local_cseq等必要字段(原則是:後面生成實際SIP消息結構體要用到的字段就需要設置),使用osip_message_init初始化一個sipmsg,根據dialog來填充該結構體(不同的消息填充的數據是不同的,沒有捷徑可走,只能看RFC根據需要填充字段),如果要給SIP消息添加Body例如SDP段,需要使用osip_message_set_body,osip_message_set_content_type函數,設置的值是純文本,如果是SDP,Osip有提供簡單的解析和生成便捷函數例如sdp_message_to_str,sdp_message_a_attribute_add,但只是簡單的字符操作,要填充合法的字段需要自己參考SDP的RFC文檔,同樣沒捷徑可走。 
  現在我們有了兩個必要條件了,還有最後一個也是最關鍵的部件,就是事務的創建和觸發, 
int osip_transaction_init( 
osip_transaction_t ** transaction, /*返回的事務結構體指針*/ 
osip_fsm_type_t ctx_type, /*事務類型ICT/NICT/IST/NIST*/ 
osip_t * osip,  /*前文說的全局變量*/ 
osip_message_t * request) /*前面生成的sipmsg*/ 
  創建了一個新的事務,並自動根據事務類型、dialog和sipmsg進行了初始化,最重要的是它使用了__osip_add_ict等函數,將本事務插入到全局的osip_t結構體的全局FIFO鏈表中去了,不同的事務類型對應不同的FIFO,由前文可知,本類函數有四個,FIFO也有四個,對應ICT,NICT,IST,NIST,注意這個這裏使用osip_transaction_set_out_socket把發送sip消息的socket接口配給該事務,方便自動調用前面設置的發送消息回調函數使用它自動發送消息; 
  前文提到了transaction裏的state作爲狀態機的"狀態",要執行狀態機,就需要有"事件"來觸發,事件結構體osip_event_t需要使用osip_new_outgoing_sipmessage來對sipmsg進行探測生成,設置正確的事件值,省卻了我們手工設置的工作,它調用evt_set_type_outgoing_sipmessage來設置"事件"type_t,並將sipmsg掛到事件結構體的sip屬性值上,有了根據消息分析出的事件後,使用osip_fifo_add(trn->transactionff, ev)將事件插入到事務的事件FIFO中,即transactionff屬性; 
   
  有了上面的發送消息的必要條件了,消息是如何實際出發的呢?上面提到了,SIP消息的發送和響應是一個事務,不能隔離開來,即消息的發送需要事務狀態機來控制,我們上面設置了狀態機的狀態和事件,要觸發它,就是要執行狀態機了: 
  osip_ict_execute 
  osip_nict_execute 
  osip_ist_execute 
  osip_nist_execute 
  分別用來遍歷前面提到的四個事務FIFO,取出事務,再依次取出事務內的事件FIFO上的事件,使用osip_transaction_execute依次執行(有興趣的可以更深一步去查看,可以看到它最終就是調用了我們前面設置的消息回調函數,至於具體調用哪個,這就是OSIP協議棧內部幫我們做的大量的工作了^_^); 
  如果某個事務不能正常終結怎麼辦呢?例如發出了Invite沒有收到任何響應,按RFC定義,不同的事務有不同的超時時間,osip_timers_ict[nict|ist|nist]_execute這些函數就是來根據取出的事務的時間戳與當前時間取差後與規定的超時時間比對,如果超時,就自動設置了超時"事件"並將事務"狀態"設爲終結,使用前面設定的消息超時事件回調函數處理即可(如果設置了); 
  如果網絡質量不穩定,經常丟失消息,需要使用osip_retransmissions_execute函數來自動重發消息而不是等待超時; 
  爲了即時響應SIP消息的處理推動狀態機,上述的九個函數需要不停執行,可以將它放入單獨線程中。 
收到SIP消息 
  有了前面的發送SIP消息的理解,接收消息的處理就方便理解了,收到SIP消息,使用osip_parse進行解析,得到一個osip_message_t的sipmsg,使用evt_set_type_incoming_sipmessage得到事務的"事件",並同上將sipmsg掛到事件結構體的sip字段,隨後立即使用osip_find_transaction_and_add_event來根據"事件"查找事務(有興趣可以深入看一下,事務的查找是通過SIP消息Via中的branch來匹配的),否則新建事務,然後推動狀態機執行。 
狀態機內部邏輯 
  弄清了上面的狀態機的大概邏輯,設置正確完備的回調函數,就可以正確使用Osip來進行工作了,如果要進一步深入Osip,比如要擴展Osip的狀態機處理自定義的消息字段和實現新的事務邏輯來生成新業務時,就需要對狀態機的內部邏輯有一定的瞭解; 
  前面一再強調,Osip內部的幾個重要的數據結構osip_message_t,osip_dialog_t,osip_transaction_t,其中面向用戶的主要是前後兩個,而中間的dialog則很多時候是在狀態機內部使用的,例如:收到消息,解析到sipmsg中,查找transaction並進行驅動,隨後找到它關聯的dialog(或者新生成)解析填充要發送的消息結構體sipmsg,再次根據dialog和sipmsg查找或生成transaction。 
  如果要擴展Osip,要做工作主要有: 
  擴展osip_message_t,增加要解析的字段或消息頭,並參考原Osip函數生成對應的SIP字符串生成和解析函數; 
  擴展osip_dialog_t,增加新的屬性,對應osip_message_t的新增內容; 
  擴展狀態機的事件和狀態類型,設置對應的回調函數,並關聯新增事件和狀態類型到osip_message_t的解析函數或osip_dialog_t的初始化函數中,而osip_transaction_t大多數時候不需要擴展,只要在對應的事務類型(大多數時候是NICT、NIST)處理邏輯中,增加對新增事件和狀態類型的判斷和調用回調函數的邏輯即可。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章