nua 模塊包含UA庫的實現代碼,它關心SIP UA的基本功能。它的功能包括呼叫話務管理、消息,及事件檢索。
概述
NUA API爲高層應用程序提供了一個透明的,完全控制的SIP協議引擎。NUA在現有的事務語義之(nta模塊的實現)上提供呼叫語義。通過NUA可以搭建不同的SIP UA,比如說終端、網關,或MCU。
nua 引擎爲應用程序員隱藏了許多底層信令和媒體管理細節。它可以使用不同的媒體接口,甚至可以是遠程的,完全透明的方式處理媒體。
UA庫內的協議引擎和應用程序可以運行在不同的線程中。協議引擎與應用層線程通過事件(events)機制通信,通過回調函數傳遞給應用層。回調函數由應用層的上下文相關線程調用,通過一個su_root_t 對象體現。
NUA用戶的Sofia 概念
引言
Sofia 軟件套件是基於目前的基礎觀點和概念建立的,這些概念在Sofia軟件的所有層級中通用。裏面的許多內容都由Sofia工具庫(su)實現,並對多數主流OS提供了統一的接口封裝。
這面章節介紹這些概念,NUA用戶必須理解它們才能讓Sofia程序正常工作。其它工具(SU庫提供的,或Sofia 軟件套件的其它庫提供的)可能對應用程序開發者也是有幫助的,但是使用時務必小心,因爲它們有可能改變Sofia軟件套件的行爲,從而導致NUA庫工作異常。更多細節可以參考[su]模塊的相關文檔。
事件循環Event loop 與根對象 root object
NUA以事件反應器模式(也稱爲分發及通知模式)驅動事件系統(請參考[Using Design Patterns to Develop Reusable Object-oriented Communication Software, D.C. Schmidt, CACM October '95, 38(10): 65-74]一書)。Sofia以任務作爲編程模型的基本執行單元。根據編程模型,程序可以請求事件循環在特定事件觸發時調用回調函數。具體事件包括I/O激活,定時器或其它任務傳遞的異步消息。
root 對象是應用軟件中描述一個任務的句柄。透視事件的另一種方式是:root對象描述任務的主事件循環。通過root對象,任務代碼可以訪問它的上下文信息(magic)和線程同步,比如說等待對象、定時器,消息。
使用NUA服務的應用必須創建一個root對象,並設置處理NUA事件的回調函數。調用 su_root_create() 創建root對象,調用nua_create() 函數註冊回調函數。root對象的數據類型是su_root_t。
Magic
magic 是一種描述上下文指針的術語,應用程序可以把它綁定到Sofia棧的各種對象上(比如說root對象和操作句柄)。當主事件循環調用註冊的回調函數時,上下文指針會回傳給應用層代碼。Sofia棧保留回調函數調用之間的上下文信息。應用層可以利用上下文信息存儲處理事件所需要的任何信息。
內存處理
給定的任務需要分配許多小塊內存時,使用home-based內存管理機制會很便利。內存通過home對象分配,它維護所有內存塊的引用信息。當home對象釋放時,所有相關內存一併釋放。這個機制簡化了應用邏輯,因爲應用層代碼不需要跟蹤內存塊的分配記錄,也不需要獨立釋放每一個內存塊。
使用NUA服務的應用程序可以使用SU庫提供的內存管理服務,但這不是強制要求的。
Tags
Tagging 是Sofia軟件所使用的一種機制,它用於向函數打包參數。它可以傳遞非固定類型的可變長度的參數。對應用層程序員來說,tagging是一個可見的宏,可用它封裝傳遞的參數。評估時,tagging宏創建一個包含tag的結構(記錄參數的類型),和一個值(指向不透明數據)。通過檢查tag,Sofia軟件的各層可以檢查它們是否能夠處理參數,或者簡單地把它傳遞給下一層處理。
有一些具有特殊含義的tag:
- TAG_NULL() (與TAG_END()同義) ,tag列表的結束
- TAG_SKIP() 空tag 項
- TAG_NEXT() 指向另一個tag列表的tag項,結束當前tag列表
- TAG_ANY() 接受任何tag的的篩選tag
- TAG_IF() tag 項的條件包含
如果NUA函數的參數表末尾是以下這樣的,那麼可以通過tag列表調用:
tag_type_t tag,
tag_value_t value,
...);
參數表的最後一個tag值必須爲TAG_NULL() (或 TAG_END(), 它等價於 TAG_NULL()).
每個tag都有兩個版本:
NUTAG_<tagname>
它帶有一個值參數;
NUTAG_<tagname>_REF
它帶有一個引用參數。後者使用tl_gets()函數從tag列表中檢查tag值。
對於SIP頭域來說,還有另一種tag的版本:
SIPTAG_<tagname>_STR
這個版本以一個C語言字符串作爲參數。不帶_STR 後綴的相應tag將解析的值結構作爲參數。
下面是一個調用實例:
nua_unregister(op->op_handle,
TAG_IF(use_registrar, NUTAG_REGISTRAR(registrar)),
SIPTAG_CONTACT_STR("*"),
SIPTAG_EXPIRES_STR("0"),
TAG_NULL());
使用NUA服務的應用程序必須使用tag參數爲函數傳參。nua_invite() 表現了從tag構建SIP消息的過程。
調試與日誌
Sofia 棧的模塊包含可配置的調試及日誌功能,它們的服務接口在<sofia-sip/su_log.h>裏定義。調試及日誌細節(比如說日誌等級,輸出文件名)可以通過環境變量配置,或者通過配置文件指定,還有代碼層的指定。
定義的環境變量及含義:
- SOFIA_DEBUG 默認的調試等級 (0..9)
- NUA_DEBUG NUA 調試等級 (0..9)
- NTA_DEBUG 事務引擎調試等級 (0..9)
- TPORT_DEBUG 傳輸層事件調試等級 (0..9)
- TPORT_LOG 如果設置,在傳輸層打印所有解析出來的SIP消息
- TPORT_DUMP 指定一個文件名,用於轉儲傳輸層未解析的消息
預定義的調試輸出等級:
- 0 致命性錯誤,崩潰級的
- 1 嚴重錯誤,至少是子系統層的問題
- 2 非嚴重的錯誤信息
- 3 告警,進展信息
- 5 信令協議動作 (收到包, ...)
- 7 媒體協議動作 (收到包, ...)
- 9 進入/退出函數,非常細緻的進展信息
使用NUA服務的應用程序可以使用Sofia棧提供的調試和日誌服務,但不強求。
NUA Concepts
NUA 棧對象
棧對象描述SIP棧和媒體引擎的實例。它包含指向棧root對象的引用,UA指定的設置,還有SIP事務引擎的引用。
NUA棧對象調用nua_create() 創建,調用nua_destroy()銷燬。nua 引擎可以用nua_shutdown() 函數優雅地釋放會話。
NUA棧對象的類型爲nua_t。
NUA 操作句柄
操作句柄描述一個抽象的SIP call/session。它包含SIP dialog的信息和媒體session,呼叫的狀態機,高層SDPoffer-answer協議,註冊,訂閱,發佈和簡單的SIP事務。操作句柄還可能包含用於NUA構建SIP消息的tag列表,比如說From和To頭域。
應用程序用NUA顯式創建操作句柄來發送消息(nua_handle()函數),或者棧處理收入呼時隱式創建(INVITE 或MESSAGE)。句柄通過NUA調用銷燬(nua_handle_destroy())。
指示及響應事件與操作句柄關聯。
NUA操作句柄的類型是nua_handle_t。
棧線程及消息傳遞概念
棧線程與應用程序分離,它提供實時的協議棧操作,因此應用程序線程可以自己堵塞處理,比如重繪UI。
棧線程與應用線程之間的通信是異步的。很多NUA API函數都會發消息給棧線程處理,類似的,某些事件發生後,棧線程也會嚮應用線程發消息。當應用程序調用su_root_run() 或 su_root_step()函數時,嚮應用線程發的消息作爲它的回調進行傳遞。
SIP 消息和頭域處理
SIP消息通過類型安全的SIPTAG_ tag處理。每個SIPtag有三種版本:
- SIPTAG_<tagname>() 以解析後的值作爲參數。
- SIPTAG_<tagname>_STR() 以未解析的字符串爲參數
- SIPTAG_<tagname>_REF() 以引用爲參數,與tl_gets() 函數一同使用,以從tag列表中檢索tag值。
- SIPTAG_<tagname>__STR_REF() 以引用爲參數,與tl_gets() 函數一同使用,以從tag列表中檢索字符串tag值。
For example a header named "Example" would have tags names SIPTAG_EXAMPLE(), SIPTAG_EXAMPLE_STR(), and SIPTAG_EXAMPLE_REF().
比如說,有個頭域名爲"Example",它將擁有這幾個tag:SIPTAG_EXAMPLE(), SIPTAG_EXAMPLE_STR(), 和SIPTAG_EXAMPLE_REF()。
當在NUA呼叫中使用tag時,會將相應的頭域添加到消息中。如果頭域只能在消息中出現一次,並且消息中已經存在一個相應的頭域,那麼tag提供的值取代現有的頭域值。傳遞tag值NULL對頭域沒有影響。傳遞tag值 (void *)-1將相應的頭域從消息中移除。
例如:
- 發送一條帶有一個Event:頭域和兩個 Accept:頭域的SUBSCRIBE 消息:
nua_subscribe(nh,
SIPTAG_EVENT_STR("presence"),
SIPTAG_ACCEPT(accept1),
SIPTAG_ACCEPT(accept2),
TAG_END());
- 處理nua_r_subscribe事件時提取tag值:
sip_accept_t *ac = NULL;
sip_event_t *o = NULL;
tl_gets(tl,
SIPTAG_EVENT_REF(o), /* _REF takes a reference! */
SIPTAG_ACCEPT_REF(ac),
TAG_END());
SIP/NUA 教程
本節使用消息序列圖描述NUA/Sofia棧的基本使用場景。
外呼
入呼
基本外發操作
基本消息接收操作
帶鑑權的外發操作
簡單應用實例
接下來,將以代碼形式給出一個實例,展示如何利用NUA服務構建應用。實例代碼並不完整,但能夠體現NUA使用的所有相關細節。
在sourceforge上有一個可用的實例程序 sofisip_cli.c ,可以作爲更完整的示例研究。
數據結構定義
使用NUA提供服務的程序通常定義一些用於存儲上下文信息(比如說"magic")的數據域。指向這些上下文信息數據域的指針通過類型定義傳遞給NUA。
/* type for application context data */
typedef struct application application;
#define NUA_MAGIC_T application
/* type for operation context data */
typedef union oper_ctx_u oper_ctx_t;
#define NUA_HMAGIC_T oper_ctx_t
信息域內容本身可以定義爲C結構體或聯合體:
/* example of application context information structure */
typedef struct application
{
su_home_t home[1]; /* memory home */
su_root_t *root; /* root object */
nua_t *nua; /* NUA stack object */
/* other data as needed ... */
} application;
/* Example of operation handle context information structure */
typedef union operation
{
nua_handle_t *handle; /* operation handle /
struct
{
nua_handle_t *handle; /* operation handle /
... /* call-related information */
} call;
struct
{
nua_handle_t *handle; /* operation handle /
... /* subscription-related information */
} subscription;
/* other data as needed ... */
} operation;
NUA棧對象和句柄對應用層程序員來說是不透明的。同樣,應用層的上下文對NUA棧模塊也是完全不透明的。NUA函數傳入一個指針,然後這個指針在回調函數的參數中回傳給應用層。在這種場景下,應用層上下文信息結構還可以存儲root對象和home內存基址。此外,應用層上下文信息中還包含NUA棧對象信息。
初始化與清除
以下代碼展示應用層初始化系統,進入主循環處理消息,消息 處理結束,系統清除的過程。
如果應用程序不僅響應收到的SIP消息,還必須有向NUA發送消息的方法。這可以通過單獨的線程處理,調用NUA函數來發消息,或建立socket連接向其它應用發命令(參考su_wait_create()和su_root_register())。
/* Application context structure */
application appl[1] = {{{{(sizeof appl)}}}};
/* initialize system utilities */
su_init();
/* initialize memory handling */
su_home_init(appl->home);
/* initialize root object */
appl->root = su_root_create(appl);
if (appl->root != NULL) {
/* create NUA stack */
appl->nua = nua_create(appl->root,
app_callback,
appl,
/* tags as necessary ...*/
TAG_NULL());
if (appl->nua != NULL) {
/* set necessary parameters */
nua_set_params(appl->nua,
/* tags as necessary ... */
TAG_NULL());
/* enter main loop for processing of messages */
su_root_run(appl->root);
/* destroy NUA stack */
nua_destroy(appl->nua);
}
/* deinit root object */
su_root_destroy(appl->root);
appl->root = NULL;
}
/* deinitialize memory handling */
su_home_deinit(appl->home);
/* deinitialize system utilities */
su_deinit();
事件處理
在應用初始化時,通過nua_create()函數向NUA棧註冊事件的相關回調函數,NUA棧捕獲事件時,就會調用回調函數處理事件。回調函數的內容是一組最簡單的 switch/case語句,把捕獲的事件分發給具體的處理函數。
void app_callback(nua_event_t event,
int status,
char const *phrase,
nua_t *nua,
nua_magic_t *magic,
nua_handle_t *nh,
nua_hmagic_t *hmagic,
sip_t const *sip,
tagi_t tags[])
{
switch (event) {
case nua_i_invite:
app_i_invite(status, phrase, nua, magic, nh, hmagic, sip, tags);
break;
case nua_r_invite:
app_r_invite(status, phrase, nua, magic, nh, hmagic, sip, tags);
break;
/* and so on ... */
default:
/* unknown event -> print out error message */
if (status > 100) {
printf("unknown event %d: %03d %s\n",
event,
status,
phrase);
}
else {
printf("unknown event %d\n", event);
}
tl_print(stdout, "", tags);
break;
}
} /* app_callback */
發起呼叫
以下三個函數展示基本的SIP呼叫是怎樣發起的。
place_a_call()函數創建一個句柄並調用SIP INVITE方法:
operation *place_a_call(char const *name, url_t const *url)
{
operation *op;
sip_to_t *to;
/* create operation context information */
op = su_zalloc(appl->home, (sizeof *op));
if (!op)
return NULL;
/* Destination address */
to = sip_to_create(NULL, url);
if (!to)
return NULL;
to->a_display = name;
/* create operation handle */
op->handle = nua_handle(appl->nua, op, SIPTAG_TO(to), TAG_END());
if (op->handle == NULL) {
printf("cannot create operation handle\n");
return NULL;
}
nua_invite(op->handle,
/* other tags as needed ... */
TAG_END());
} /* place_a_call */
app_r_invite() 函數在收到INVITE消息的應答時,由協議棧的回調函數調用。這裏假設沒有激活automatic acknowledge,因此必須顯式地發送ACK消息。
void app_r_invite(int status,
char const *phrase,
nua_t *nua,
nua_magic_t *magic,
nua_handle_t *nh,
nua_hmagic_t *hmagic,
sip_t const *sip,
tagi_t tags[])
{
if (status == 200) {
nua_ack(nh, TAG_END());
}
else {
printf("response to INVITE: %03d %s\n", status, phrase);
}
} /* app_r_invite */
在呼叫狀態變化時,發出nua_i_state event事件,回調函數調用app_i_state()函數:
void app_i_state(int status,
char const *phrase,
nua_t *nua,
nua_magic_t *magic,
nua_handle_t *nh,
nua_hmagic_t *hmagic,
sip_t const *sip,
tagi_t tags[])
{
nua_callstate_t state = nua_callstate_init;
tl_gets(tags,
NUTAG_CALLSTATE_REF(state),
NUTAG__REF(state),
state = (nua_callstate_t)t->t_value;
printf("call %s\n", nua_callstate_name(state));
} /* app_i_state */
接收來電
收到INVITE消息時,回調函數調用app_i_invite()函數。這裏假設沒有開啓自動應答,因此必須顯示發送應答消息:
void app_i_invite(int status,
char const *phrase,
nua_t *nua,
nua_magic_t *magic,
nua_handle_t *nh,
nua_hmagic_t *hmagic,
sip_t const *sip,
tagi_t tags[])
{
printf("incoming call\n");
nua_respond(nh, 200, "OK", SOA_USER_SDP(magic->sdp), TAG_END());
} /* app_i_invite */
呼叫成功建立,媒體激活時,回調函數調用app_i_state() 函數:
void app_i_active(int status,
char const *phrase,
nua_t *nua,
nua_magic_t *magic,
nua_handle_t *nh,
nua_hmagic_t *hmagic,
sip_t const *sip,
tagi_t tags[])
{
printf("call active\n");
} /* app_i_active */
拆線
以下三個函數展示一通SIP呼叫是如何終止的。
erminate_call() 函數發出SIP BYE消息。
void terminate_call(void)
{
nua_bye(op->handle, TAG_END());
} /* terminate call */
在收到BYE消息的應答消息時,回調函數調用app_r_bye()函數。它銷燬呼叫句柄並釋放存儲操作上下文信息的相關內存。
void app_r_bye(int status,
char const *phrase,
nua_t *nua,
nua_magic_t *magic,
nua_handle_t *nh,
nua_hmagic_t *hmagic,
sip_t const *sip,
tagi_t tags[])
{
if (status < 200)
return;
printf("call released\n");
/* release operation handle */
nua_handle_destroy(hmagic->handle);
op->handle = NULL;
/* release operation context information */
su_free(appl->home, hmagic);
} /* app_r_bye */
收到BYE消息時,回調函數調用app_i_bye()函數。它銷燬呼叫句柄並釋放存儲操作上下文信息的相關內存。
void app_i_bye(int status,
char const *phrase,
nua_t *nua,
nua_magic_t *magic,
nua_handle_t *nh,
nua_hmagic_t *hmagic,
sip_t const *sip,
tagi_t tags[])
{
printf("call released\n");
/* release operation handle */
nua_handle_destroy(hmagic->handle);
op->handle = NULL;
/* release operation context information */
su_free(appl->home, hmagic);
} /* app_i_bye */
發一條消息
以下代碼展示如何發一條SIP MESSAGE 消息。
send_message() 函數發出SIP MESSAGE。
void send_message(void)
{
op_t *op;
/* create operation context information */
op = su_zalloc(appl->home, sizeof(op_t));
if (op = NULL) {
printf("cannot create operation context information\n");
return;
}
/* how we create destination_address? */
/* create operation handle */
op->handle = nua_handle(appl->nua,
op,
NUTAG_URL(destination_address),
TAG_END());
if (op->handle == NULL) {
printf("cannot create operation handle\n");
return;
}
/* send MESSAGE */
nua_message(op->handle,
SIPTAG_CONTENT_TYPE_STR("text/plain"),
SIPTAG_PAYLOAD_STR("Hello, world!"),
/* other tags as needed ... */
TAG_END());
} /* send_message */
收到MESSAGE消息的應答時,回調函數調用app_r_message()函數:
void app_r_message(int status,
char const *phrase,
nua_t *nua,
nua_magic_t *magic,
nua_handle_t *nh,
nua_hmagic_t *hmagic,
sip_t const *sip,
tagi_t tags[])
{
printf("response to MESSAGE: %03d %s\n", status, phrase);
} /* app_r_message */
接收消息
以下代碼展示收到SIP MESSAGE時是怎樣處理的。
收到SIP MESSAGE時,回調函數調用 app_i_message():
void app_i_message(int status,
char const *phrase,
nua_t *nua,
nua_magic_t *magic,
nua_handle_t *nh,
nua_hmagic_t *hmagic,
sip_t const *sip,
tagi_t tags[])
{
printf("received MESSAGE: %03d %s\n", status, phrase);
printf("From: %s%s" URL_PRINT_FORMAT "\n",
sip->sip_from->a_display ? sip->sip_from->a_display : "",
sip->sip_from->a_display ? " " : "",
URL_PRINT_ARGS(sip->sip_from->a_url));
if (sip->sip_subject) {
printf("Subject: %s\n", sip->sip_subject->g_value);
}
if (sip->sip_payload) {
fwrite(sip->sip_payload->pl_data, sip->sip_payload->pl_len, 1, stdout);
fputs("\n", stdout);
}
} /* app_i_message */
建立Presence Server
...
application_t *app;
operation_t *oper;
...
oper->app = app;
app->nua = nua_create(ssip->s_root,
app_callback,
app,
TAG_NULL());
...
oper->handle = nua_handle(app->nua, app,
NUTAG_URL(to->a_url),
SIPTAG_TO(to),
ta_tags(ta));
...
nua_notifier(oper->handle,
SIPTAG_EXPIRES_STR("3600"),
SIPTAG_EVENT_STR("presence"),
SIPTAG_CONTENT_TYPE_STR("application/pidf-partial+xml"),
NUTAG_SUBSTATE(nua_substate_pending),
TAG_END());
在 nua_notifier對象,即presence server建立之後,返回一個nua_r_notifier。app_callback函數的狀態及phrase值顯示創建成功。
收到訂閱消息時的鑑權可以由回調函數處理:
void app_callback(nua_event_t event,
int status, char const *phrase,
nua_t *nua, application_t *app,
nua_handle_t *nh, oper_t *op,
sip_t const *sip, tagi_t tags[])
{
nea_sub_t *subscriber = NULL;
switch (event) {
case nua_i_subscription:
tl_gets(tags,
NEATAG_SUB_REF(subscriber),
TAG_END());
nua_authorize(nua_substate_active);
default:
break;
}
關停
以下函數展示應用程序是如何終止NUA棧的工作的。
shutdown()函數開始關停操作:
void shutdown(void)
{
nua_shutdown(appl->nua);
} /* shutdown */
無論NUA棧的停止是成功或失敗,回調函數調用app_r_shutdown()函數。
void app_r_shutdown(int status,
char const *phrase,
nua_t *nua,
nua_magic_t *magic,
nua_handle_t *nh,
nua_hmagic_t *hmagic,
sip_t const *sip,
tagi_t tags[])
{
printf("shutdown: %d %s\n", status, phrase);
if (status < 200) {
/* shutdown in progress -> return */
return;
}
/* end the event loop. su_root_run() will return */
su_root_break(magic->root);
} /* app_r_shutdown */