OSIP協議棧使用入門(續一:純協議棧邏輯分析)(轉)

 很長時間之前,簡單粗略地看了下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)處理邏輯中,增加對新增事件和狀態類型的判斷和調用回調函數的邏輯即可。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章