1. 程式人生 > >oSIP協議棧(及eXoSIP,Ortp等)使用入門(轉)

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

(CopyLeft by Meineson | www.mbstudio.cn,原創文章,歡迎轉載,但請保留出處說明!)

本文件最新版本及文中提到的相關原始碼及VC6工程檔案請在本站找,嘿嘿~~
(首頁的SkyDriver公開資料夾中,可能需要用代理才能正常訪問該空間——空間絕對穩定,不會丟失檔案!)

(最近工作重心不在SIP開發,SO本文件也沒有機會更新,有技術問題也請儘量諮詢他人,本人不一定能及時回覆。)

  一直沒空仔細研究下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學習應用了。 

------------------------------------------------------
[下回預告:完全基於oSIP的軟電話實現及oSIP進一步學習]

(CopyLeft by Meineson | www.mbstudio.cn,原創文章,歡迎轉載,但請保留出處說明!)