Sip協議棧學習(一)---———阿冬專欄!!
對於Doubango中得sip協議棧,是通過SipStack類粘合上層程式碼與底層程式碼的,該類定義在SipStack.h中,實現在SipStack.cxx中。當構造好一個SipStack例項之後,對於底層而言,實際上是建立了一個tsip_stack_t 的例項,這個例項在SipStack類中通過tsip_stack_handle_t *handle欄位指向。此時,handle對於上層而言是不透明的,從tsip_stack_handle_t的定義也可以看出:
typedefvoid tsip_stack_handle_t
因此,handle實際上是一個void指標。指向具體的tsip_stack_t例項。實際上,stack的底層例項已經建立好,並且已經設定好了很多欄位資訊。但是stack仍舊是沒有啟動的,若要啟動協議棧,對於上層程式碼而言,可以通過SipStack->start()達到此目的。
SipStack->start(),正是提供給上層啟動底層協議棧的粘合介面。而該介面僅僅是對底層協議棧啟動的真正函式tsip_stack_start(tsip_stack_t *stack)函式的一個封裝而已。
底層協議棧的啟動--tsip_stack_start(…)
tsip_stack_start(tsip_stack_t *stack)函式主要完成三件事情:
1. 對協議棧執行模式的處理
判斷工作模式為伺服器模式或者是客戶端模式,在預設情況下,Doubango是作為一個客戶端軟電話的底層運作機制配置的,因此在這種情況下,我們得到的是客戶端執行模式。
另外,還要判斷網路層協議族和傳輸層(tcp,udp)型別,預設情況下為IPV4以及UDP。選擇一個最合適的本地ip地址(根據目前的網口選擇)網口的IP地址最終由getaddrinfo(…)獲得。
2. 啟動協議棧的sipevent 事件處理執行緒run(…)
在stack的結構中,有一個TSK_DECLARE_RUNNABLE的巨集宣告,這個巨集實際上在stack的頂端嵌入了一個tsk_runnable_t__runnable__的結構例項,因此,stack可以被安全的強制轉化為一個tsk_runnable_t的型別例項,而這就是TSK_RUNNABLE巨集的作用。
TSK_RUNNABLE(stack)->objects是一個佇列頭,傳入其中的元素是通用的tsk_list_item_t,
因此,TSK_RUNNABLE(stack)->objects實際上是一個佇列,該佇列中掛載的結構例項便是tsip_event_t結構的例項。
Stack的run(…)執行緒在stack的生存週期內一直在執行,它的作用是從TSK_RUNNABLE(stack)->objects中取出一個tsip_event_t例項,然後通過註冊到stack的回撥函式,將該sip事件傳遞到高層的使用者程式碼中。實際上,這裡的傳遞需要穿越粘合層,因為stack中回撥函式即stack->callback(…),便是由SipStack類註冊的stack_callback(…)函式,該函式的原型如下:
int stack_callback(tsip_event_t*sipevent);
3. 啟動協議棧的傳輸層——tsip_transport_layer
首先,生成一個預設的tsip_transport_t例項,連結如stack的tsip_transport_layer的transports佇列。
其次,啟動tsip_transport_layer,這裡便完成了套接字的生成,埠繫結,若是tcp協議還要完成connect的相關事宜。
到了這裡,粘合層的任務便已經完成了,底層的套介面已經啟動,可以接受來自對端的sip訊息和處理該sip訊息了。
協議棧的預設傳輸結構對於一個剛啟動的協議棧來說,它需要有一個傳輸層,支援若干的傳輸結點。每一個傳輸結點對應於一個埠,若採用TCP連線,一個傳輸結點就針對於一個點到點的連線,這個連線負責sip信令的可靠交換;若採用UDP進行sip信令的交換,則需要由應用程式維護一個定時器,以防資料包丟失的時候用於重傳。
Doubango裡一個協議棧對應於一個tsip_stack_t例項,而這樣一個例項又擁有有三個層次,從上到下依次為:
事物層,對話層,傳輸層。
各種關係如下圖所示:
在實際中,在協議棧啟動的時候,會首先生成一個預設的傳輸例項,並掛接到傳輸層的transports佇列中,完成該工作的函式是:
- tsip_transport_layer_add(tsip_transport_layer_t* self,
- const char* local_host,
- tnet_port_t local_port,
- tnet_socket_type_ttype,
- const char* description);
在建立預設傳輸例項時,各個形參對應的實參為:
- l tsip_transport_layer_t *self------傳輸層例項:stack->layer_transport
- l constchar* local_host--------------伺服器主機IP: stack->network.local_ip,
- l tnet_port_t local_port------本機繫結埠,若不指定一般為0,並由stack->network.local_port返回記錄
- l tnet_socket_type_t type---------套介面型別,指定為IPV4的UDP或TCP,或者IPSEC
- l description ----------------------------指定為“siptransport”,記錄到傳輸例項中。
Sip訊息的傳輸載體—tsip_transport_t結構
- typedefstruct tsip_transport_s
- {
- TSK_DECLARE_OBJECT;
- tsk_bool_t initialized;//指定是否已經初始化
- const tsip_stack_t*stack;//記錄所屬sip協議棧
- tnet_socket_type_t type;//套介面型別,tcp,udp
- struct sockaddr_storagepcscf_addr;//通用套接字地址結構,用於記錄伺服器端地址
- tnet_fd_t connectedFD;//套介面描述字
- tnet_transport_handle_t *net_transport;//對應一個網路的傳輸例項
- constchar *scheme;
- constchar *protocol;
- constchar *via_protocol;
- constchar *service; /**< NAPTRservice name */
- tsk_buffer_t *buff_stream;
- }
- tsip_transport_t;
在建立一個tsip_transport_t例項的時候,會隨便建立一個tnet_transport_t例項,tsip_transpor_t與tnet_transport_t是一一對應的關係。
而tnet_transport_t對應了兩個執行緒,一個執行緒成為mainthread執行緒,一個稱為run執行緒。他們的作用描述如下:
Mainthread執行緒:在其主迴圈內用於從套介面緩衝區讀取資料,並生成tnet_transport_event_t例項,這個例項代表到達的一個網路層訊息。生成以後把它連線入tnet_transport_t的一個佇列,該佇列負責管理各個tnet_transport_event_t例項。
Run執行緒:把tnet_transport_event_t例項從上所述佇列中出佇列,通過回撥傳入tsip_transpor_t的處理函式,對於UDP和TCP對應的回撥函式分別是,
tsip_transport_layer_dgram_cb(…)和tsip_transport_layer_stream_cb(…)
這兩個函式是在啟動第一個預設傳輸例項tsip_transport_t例項時記錄到tnet_transport_t的callback欄位的,callback是一個函式指標,tnet_transport_t用它來把訊息回傳給tsip_transport_t進行處理。
SIP協議的INVITE訊息發起流程當通過sip協議發起一個會話時,需要通過invite訊息實現該流程。而SIP協議是一個基於事務的協議,每一個sip會話的都是通過sip部件間的一系列訊息來完成的。首先需要明確的重要概念就是事務。
事務
在SIP協議中,一個事務是指完成一次訊息互動的整個流程。以INVITE訊息為例,一個基於代理伺服器交換信令和語音視訊資料包的事務模型如下圖所示:
如圖所示,對於1003來說,虛線框內的從INVITE訊息到代理伺服器回送的200OK整個訊息互動流程稱為一個客戶端事務,而對於1005來說,虛線框內的從INVITE訊息到代理伺服器回送的200OK整個訊息互動流程稱為一個伺服器事務。
不管是客戶端事務還是伺服器事務,都必須維護一個有限狀態機,記錄當前事務的進展情況,事務和其狀態機的維護,構成了一款SIP終端軟體最重要的一部分。
客戶端INVITE事務的狀態轉換如上圖所示。
當用兩個終端發起INVITE呼叫時,用wireshark抓包得到的結果如下圖所示:
其中ip:192.168.1.33是代理伺服器的地址,ip:192.168.1.104是客戶端sip終端的地址。這裡,代理伺服器的作用相當於一個UAS。這裡總共有兩個事務流程,整個過程如下:
a) Sip終端在地址192.168.1.104向伺服器發起一個會議3000的INVITE訊息,客戶端進入calling狀態,啟動A和B的定時器,用於INVITE訊息的超時重傳。
b) 伺服器傳送100/trying,客戶端收到後進入proceeding狀態,取消A和B定時器。
c) 伺服器傳送407要求認證,客戶端進入Completed狀態。
d) 客戶端通過ACK傳送認證資訊。
e) 進入Terminated狀態後銷燬該事務。
到此為止,一個客戶端INVITE事務結束。
a) Sip終端在地址192.168.1.104再次向伺服器發起一個會議3000的INVITE訊息,啟動A和B的定時器,用於INVITE訊息的超時重傳
b) 伺服器傳送100/trying,客戶端收到後進入proceeding狀態,取消A和B定時器。
c) 伺服器傳送200OK,客戶端進入Accepted狀態。
d) 客戶端向伺服器傳送ACK應答。
注:其中在進入calling狀態之前,也就是在傳送INVITE訊息時,客戶端必須將其中的SDP訊息包含著INVITE訊息的content中傳送到。
在客戶端接收到Accepted訊息(200Ok)後,或根據得到的SDP做解析,啟動正確的音視訊編解碼器,生成RTP埠,在最後的ACK中傳送給伺服器,這是,通話開始進行。
在Doubango協議棧中,最後一步的處理由tsip_dialog_invite.client.c檔案的int c0000_Outgoing_2_Connected_X_i2xxINVITE(va_list *app)函式處理。
相關資料結構1. tsip_dialog_invite_t
描述:
一個invite_dialog代表了一個invite期間的所有的信令流程,因此,它首先是一個普遍的dialog的特殊化結構,在該結構的起始部分,有一個TSIP_DECLARE_DIALOG宣告,該宣告展開後是一個tsip_dilog_t __dialog欄位的定義,這是一種在C中一個具化物件對通用物件的繼承機制,tsip_dialog_t物件代表了更通用的例項,而tsip_dialog_invite_t物件則是tsip_dialog_t物件的具化和拓展。
一個dialog本身有自己的有限狀態處理機,有自己的當前狀態和當前狀態應執行的動作,還有一個狀態變化時的回撥函式。它還需要有一個欄位指向它所屬的session,這些欄位,反應在資料型別上上,分別是:
tsk_fsm_t(有限狀態機),tsip_action_t(狀態執行動作),tsip_dialog_state_t(當前狀態),
tsip_dialog_event_callback_f(回撥函式),tsip_ssession_t(所屬session)。
建立時機:
tsip_action_INVITE()函式是上層應用與底層協議棧的介面,當上層應用發起一個INVITE時,便會分層呼叫到該介面。
tsip_dialog_invite_t型別的例項便是在該介面中建立的,對於建立時,還需要在協議棧的全域性dialog佇列中尋找一遍,看是否相關的dialog已經在之前被建立,若是,則沿用老的例項,若否,則用tsip_dialog_layer_new()函式建立一個新的例項,並鏈入到協議棧dialog_layer層的全域性佇列中。
關鍵點:
2. tmedia_session_mgr_t
描述:
一個tmedia_session_mgr_t例項是由上述的tsip_dialog_invite_t例項的session_mgr欄位記錄的,代表了在一個對話期間各種媒體設定的管理者。
當發起一個INVITE時,首先必須存在一個代表信令流程的物件即dialog物件,其次,還要有一個負責管理媒體資訊的物件,及該結構的例項。
建立時機:
該結構的首次建立是在invite_dialog的狀態轉換過程中,c0000_Started_2_Outgoing_X_oINVITE()函式是一個狀態轉化函式,它代表著當前dialog由初始狀態向發起INVITE後狀態轉化需要執行的操作,在該函式內部,通過tmedia_session_mgr_create()建立tmedia_session_mgr_t例項,然後填寫本地媒體資訊(即sdp中得各個欄位),申請rtp埠號,生成代表rtp的網路傳輸例項(tnet_transport_t物件),最後在該狀態轉換函式中,通過協議棧的代表信令的網路傳輸例項將INVITE訊息傳遞出去。
關鍵點:
在tmedia_session_mgr_t中,有一個tmedia_sessions_L_t*sessions的欄位,這個欄位其實是一個佇列,掛載的表示代表了各種媒體的資訊(音訊,視訊),每一個佇列節點是一個tmedia_session_t的例項。
關鍵函式:
_tmedia_session_mgr_load_sessions(tmedia_session_mgr_t *mgr)函式
原型:_tmedia_session_mgr_load_sessions(tmedia_session_mgr_t *mgr)
作用:用於生成和載入各種型別的tmedia_session_t例項。
說明:_tmedia_session_mgr_load_sessions()函式在tmedia_session_mgr_create()函式中被呼叫,在這個函式內,會生成各個tmedia_session_t例項,掛入tmedia_session_mgr_t例項的sessions佇列。其中在_tmedia_session_mgr_load_sessions()函式內的處理片段為:
… } |
__tmedia_session_plugins是一個tmedia_session_plugin_def_t型別的全域性指標陣列,用於存放各種媒體型別的定義。
tmedia_session_create()函式用於更具媒體的型別建立一個tmedia_session_t型別的例項,媒體型別由上層應用指定,記錄在tmedia_session_mgr_t例項的type欄位,常用的有audio,vedio,audiovideo三個型別。
3. tmedia_session_t
描述:
這其實也是一個通用結構,被更具體的結構包含著。
而tdav_session_audio_t,tdav_session_video_t等結構便是更具體的結構,在這些結構的其實,包含了該通用的結構。每一個更通用的結構都記錄一個trtp_manager_s的例項,這個結構便是代表語音流和視訊流的管理者。
在建立tmedia_session_t例項時,是根據plugin->objdef來呼叫tsk_object_new()的,呼叫語句為tsk_object_new(plugin->objdef),plugin是上一節所述的__tmedia_session_plugins 所記錄的tmedia_session_plugin_def_t 例項,故當plugin->type為audio型別時,建立的真正結構為tdav_session_audio_t,真正的建構函式為tdav_session_audio_ctor(),定義在tdav_session_audio.c中。
這個過程的呼叫堆疊為:
|
建立時機:
見上一條tmedia_session_mgr_t的關鍵點。
關鍵點:
在一個tmedia_session_t結構的例項中,承載了這個tmedia_session_t實際型別的編解碼外掛,例如tmedia_session_t的type為video時,在tmedia_session_init(tmedia_session_t*self)函式中會初始化一個tmedia_session_t的例項,而根據多型的原理,這個tmedia_session_t例項的真正型別為tdav_session_video_t例項,因此,在初始化時,會從__tmedia_codec_plugins陣列中載入所有的type為video的解碼模組,__tmedia_codec_plugins陣列會在啟動協議棧時初始化,具體的初始化函式是tdav_init(),這是也會根據能夠載入的編解碼模組,將各個編解碼外掛記錄到該__tmedia_codec_plugins全域性指標陣列中,陣列的每一個項都指向了一個plugin,一個plugin代表了一個實際的編解碼模組。
關鍵函式描述:
tmedia_session_init(…)函式:
原型: tmedia_session_init(tmedia_session_t*self,tmedia_type_ttype)
作用: 初始化一個新生成的tmedia_session_t例項,載入各個編解碼模組
_tmedia_session_load_codecs(tmedia_session_t *self)函式;
描述:該函式被tmedia_session_init(…)函式呼叫,用於載入編解碼模組
載入編解碼模組的程式碼片段為:
|
4. tdav_session_audio_t
5. tdav_session_video_t
6.tsip_request_t
描述:
在一個tsip_dialog_invite_t例項被建立,進入它的狀態機執行時,第一個狀態的轉化為:
Started -> (oINVITE)-> Outgoing
此時由函式int c0000_Started_2_Outgoing_X_oINVITE(va_list*app)進行該狀態的處理,在此函式內部,會為tsip_dialog_invite_t例項建立tmedia_session_mgr_t例項並記錄在tsip_dialog_invite_t例項的msession_mgr欄位,建立msession_mgr的同時,會建立msession_mgr的sessions佇列和載入各種編解碼模組。完成這一切後,會呼叫send_INVITEorUPDATE(…)函式,完成一個sip的invite request。
該函式的原型為:
int send_INVITEorUPDATE(tsip_dialog_invite_t*self,tsk_bool_tis_INVITE,tsk_bool_tforce_sdp)
從名字可以看出,函式除了完成INVITE外,還可以完成一個sip會話的更新操作,例如可能編解碼變更,而不想重新發起一個INVITE請求時,便在原有請求的基礎上進行UPDATE。
具體是一個新的INVITE還是一個UPDATE,由引數is_INVITE指定。
在一個新的INVITE請求發起時,最重要的結構便是tsip_request_t結構,這個結構代表了一個真正的sip訊息。
關鍵點:
在一個INVITE請求發起時,生成一個tsip_request_t例項的過程有以下幾步:
a) 用tsip_dialog_request_new(…)函式生成tsip_request_t結構的例項。並做相應的初始化。
b) 當這個dialog的狀態為tsip_initial時,需要為SIP_INVITE訊息的訊息體生成SDP包,此時分解為以下幾個小步驟(假設程式碼中的self為tsip_dialog_invite_t例項):
1) 呼叫用函式tmedia_session_mgr_get_lo(self->msession_mgr),通過msession_mgr中sessions佇列記錄的tmedia_session_t例項和每個tmedia_session_t例項codecs佇列中記錄的編解碼模組生成一個tmedia_sdp_t的例項。該例項便攜帶了媒體協商的所有資料。在這個過程中,還要完成RTP埠的生成,為以後的音視訊通話搭建真正的通道,這個過程將在後面解析。
2) 呼叫tsdp_message_tostring(…)函式將tmedia_sdp_t的例項轉化為ASCII字元。假設這一步中得到的字串為sdp。
3) 呼叫tsip_message_add_content(…)函式將sdp拷貝到tsip_request_t例項的的message_body中。釋放sdp。
RTP埠的生成過程
tmedia_session_mgr_get_lo(…)函式
原型:const tsdp_message_t*tmedia_session_mgr_get_lo(tmedia_session_mgr_t*self)
作用:準備SDP資訊,給新的通話開一個RTP埠