第十章基本用户代理层(UA)
基本Dialog概念
基本的UA dialog提供管理SIP dialogs的基础设备和dialog usages,像dialog的状态,会话计数器,Call-ID,From,To,和Contact头部域,transactions中CSeq的排序,和路由集。
这个基本的UA dialog是不知道它正在使用哪种类型的会话(例如,INVITE会话,SUBSCRIBE/NOTIFY会话,REFER/NOTIFY会话,等等),并且它可以被用来同时在同一个dialog内建立多个不同类型的会话。
一个PJSIP dialog可以被认为只是一个存储常用dialog属性的被动数据结构。你不能讲dialog和INVITE会话混淆。一个INVITE会话是一个会话(也通常叫做dialog usage)在一个dialog内。同一个dialog内也可以有其他的会话、usages;它们共享共同的dialog属性(虽然每个dialog中可能只有一个INVITE会话)。
PJSIP dialog不知道它的会话的状态。它不知道是否一个INVITE会话已经被建立或者失去连接。事实上,PJSIP dialog甚至不知道在dialog中的是哪种类型的会话。它关心的只是这个dialog中有多少个活动的会话。Dialog初始的时候有一个活动的会话,并且当这个会话计数器到达0并且最后的transaction也终止了,这个dialog将被销毁。
增加和减小会话计数器是每个dialog usages的责任。
Dialog会话
Dialog会话在PJSIP的dialog框架中只是用一个引用计数器来表示。每当Dialog usages在一个特定的Dialog中创建或销毁一个会话时,它们将增加或减小这个引用计数器。
Dialog会话是Dialog usages创建的。在一个特定的Dialog中,一个Dialog usages可以创建多个会话(除了INVITE usage,它只能在一个Dialog中创建一个INVITE会话)。
关于“会话”,准确的表示将在Dialog usage模块中定义。基本的Dialog只关心Dialog中活动的会话数目。
Dialog Usages
Dialog usages是PJSIP的模块,注册到Dialog来接收Dialog的事件。多个模块可以注册到一个Dialog,因此Dialog可以有多个usages。每个Dialog usages模块负责处理一个指定的会话。例如,每当接收到一个新的SUBSCRIBE请求(和增加Dialog的会话计数器)时,subscribe usage模块将创建一个新的subscribe会话。当这个subscribe会话终止时减少这个会话计数器。
Dialog处理Dialog usages的过程和endpoint处理模块的过程类似;每当on_rx_request()和on_rx_response()事件发生,Dialog将按照Dialog usages的优先级从高到低,将事件传到每个usages,直到有一个模块返回true(例如,非0),在这种情况下,Dialog将停止分发事件。On_tsx_state()通知将分发到所有的Dialog usages。每个Dialog usage应该滤掉不属于它的transaction事件。
在它更底层的使用方面,应用可以直接管理Dialog,并且它是Dialog的“usage”(或用户)。在这种情况下,应用负责管理一个Dialog内的会话,即处理所有的请求和响应和建立/销毁会话。
在最后一章中,我们将学习管理会话的高层APIs。这些高层APIs是PJSIP模块并作为Dialog usages注册到Dialog。它们将处理各种类型会话中指定的各种类型的SIP消息(例如,一个INVITE usage模块将处理INVITE,PRACK,CANCEL,ACK,BYE,UPDATE和INFO,等等)。这些高层APIs根据会话的说明提供高层的回调函数。
在这章中,我们只学习基本,底层的Dialog usages。
Dialog集
每个Dialog包含在一个Dialog集中。每个Dialog集用一个共同的本地标签(如,From标签)标识。通常一个Dialog集只有一个Dialog。一个Dialog集有多个Dialog的情况,只有当外出的INVITE交叉时,在这种情况下,每个接收到含有不同的To便签的响应消息将在同一个Dialog集中创建一个新的Dialog。
一个Dialog集在PJSIP中定义为一个不透明的类型(即void*)。一个Dialog结构(pjsip_dialog)有一个成员dlg_set用来标识它所属的Dialog集。应用可以使用linked list API来保存一个Dialog的所有兄弟姐妹(在同一个会话中)。
客户端认证
一个Dialog维持一个客户端认证会话(pjsip_auth_clt_sess),用来认证这个Dialog中发给下流服务器的请求。这个基本Dialog使用适当的认证头部域初始化每个外出的请求。然而,认证挑战必须由Dialog usages来处理;例如,这个基本Dialog当收到一个请求的401/407响应时不会自动重新尝试这个请求。
类图
下面的图说明了用户代理层和基本Dialog框架。
这个图展示了Dialog和它的usages之间的关系。在最基础/底层的情况下,应用模块是Dialog的唯一usage。在更高层的情况下,一些高层的模块(例如,pjsip_invite_usage和pjsip_subscribe_usage)可以作为dialog usages注册到一个dialog上,并且应用将从这些usages接收事件,而不是直接从dialog接收。
这个图也展示了PJSIP用户代理模块(pjsip_user_agent)。用户代理模块是所有dialogs的所有者;用户代理模块维持一个存放目前所有活动的dialog集的哈希表。
交叉
处理交叉情况
当用户代理检测到下流代理发来的响应的时候,交叉响应用户代理模块提供可以被应用注册的回调函数。一个交叉的响应(可以是临时的或者2xx响应)的dialog的To标签和其他已存在的dialog的To标签不同。当收到这样的响应时,用户代理将调用on_dlg_forked()回调函数接收到的响应和原来的dialog(应用初始创建的dialog)作为参数传入。
处理交叉情况完全是应用的责任。
接收到一个临时的交叉响应,应用可以:
接收到一个交叉的2XX响应,应用可以:
创建交叉Dialog
应用通过调用pjsip_dlg_fork()函数来创建一个交叉Dialog。这个函数创建一个Dialog和执行以下步骤:
注意:这个函数不会从原始Dialog复制Dialog usages(如,模块)。
在一个新的Dialog被创建之后,应用必须重新注册每个Dialog usage到这个新Dialog,通过调用pjsip_dlg_add_usage()。
这个新Dialog必须作为回调函数的返回值被返回。这将导致用户代理分发这个消息到这个新的Dialog,导致Dialog usages(如,应用)接收到on_rx_response()的通知关于这个新的Dialog的行为。
使用定时器去处理失败的交叉Dialog
应用可以给Dialog安排指定的应用定时器通过调用pjsip_dlg_start_app_timer()函数。对于和一个Dialog相关的定时器,这个定时器比通用的定时器更完美,因为当这个Dialog被销毁时这个定时器将自动删除。
定时器对于处理失败的交叉Dialog很重要。一个交叉的早期的Dialog可能没有通过一个最终的响应来完成,因为交叉的代理在接收到2xx响应后,将不会转发300-699响应。因此终止这些早期Dialog的唯一方法就是为这些Dialog设置一个定时器。
最好的使用Dialog的应用定时器来处理失败的交叉早期Dialog的方法是,开始这个定时器在Dialog集中其他的交叉Dialog第一次接收到2xx响应时。当这个定时器超时并且没有接收到2xx响应时,这个Dialog应该被终止。
Cseq排序
当这个请求被发送时(与之相对应的,当这个请求被创建时),这个Dialog的本地cseq被更新。当CSeq的头部域存在在这个请求中时,这个值可能被更新为这个Dialog内这个被发送的请求。
当远端接收到这个请求时,远端的Dialog中的cseq被更新。如果Dialog的远端cseq为空,接收第一个请求将设置这个Dialog的远端cseq。对于后续的请求,当Dialog接收cseq小于Dialog记录的cseq的请求时,这个请求将被这个Dialog自动地无状态地响应一个500响应(内部服务器错误)。当这个请求的cseq大于这个Dialog的记录cseq,这个Dialog将自动更新远端的cseq(包括一个请求的cseq大于记录的cseq超过1)。RFC3261 Section 12.2.2
Transactions
Dialog通常表现为有状态的。当有请求到来,它将自动创建UAStransaction,和当它被要求发送请求时,创建UAC transaction。
Dialog表现为无状态的唯一情况是当它接收到的请求的cseq小于当前的cseq时,这将导致以500(内部服务器错误)来响应这个请求。
当代表一个Dialog的一个transaction被创建(通过Dialog API,对UAC和UAS transaction),这个transaction的用户被设置为用户代理实例,并且这个Dialog实例将放入这个transaction的mod_data中合适的索引下。这个索引是这个用户代理的模块ID。当事件或消息到达,这个transaction将向用户代理模块报道这个事件,这个用户代理模块将查找这个Dialog和将事件传给这个Dialog。
基础UA API指南
用户代理模块API
Dialog结构
Dialog结构和它的API声明在
Dialog创建API
一个dialog可以通过调用以下任一个API来创建。
创建一个新的dialog并返回这个实例在p_dlg参数中。在创建这个dialog之后,应用可以通过调用pjsip_dlg_add_usage()增加模块作为dialog usages。
注意最初,这个dialog中的会话指针将初始化为0。
根据到来的创建一个dialog的请求(如,INVITE,REFER,或SUBSCRIBE)来初始化UAS dialog,并设置contact参数到本地的Contact中去。如果contact没有被指定,这个本地的contact将被初始化为请求的To头部域的URI。
如果这个请求有To标签参数,dialog的本地标签将被初始化为这个值。否则将生成一个全局唯一的本地标签。
如果请求中存在Record-Route头部域,这个函数也根据这个头部域将初始化dialog的路由集。
注意最初,这个dialog的会话计数必须初始化为0。
基于rdata中接收到的响应,创建一个新的(交叉的)dialog。这个函数拷贝original_dlg(包括认证会话)到一个新的dialog,但是这个新的dialog将有一个从响应的To头部域中拷贝来的新的远端标签。返回时,new_dlg将被注册到用户代理。应用只需要增加模块作为dialog的usages。
注意最初,这个dialog的会话计数必须初始化为0。
Dialog终止
一旦会话计数器到达0并且所有悬挂的会话已经被销毁,Dialog通常是自动被销毁。然而,这里有些情况,如果dialog usages需要早点销毁dialog,例如,当这个初始化失败时。
pjsip_dlg_terminate()函数被用来早点销毁dialog。这个函数通常被dialog usage调用。应用与公司使用合适的高层的API像pjsip_inv_terminate()来销毁这个会话和dialog。
销毁dialog并把它从UA模块的哈希表中注销。这个函数只有在会话计数器为0时被调用。
Dialog会话管理API
下面的函数被用来管理dialog的会话计数器。
增加dialog中会话的数量。注意最初(在创建之后)dialog已经将会话计数器设置为0。
减少dialog中会话的数量。一旦会话计数器到达0并且不存在悬挂的transaction,这个dialog将被销毁。注意,当这个函数被调用时,如果这里没有悬挂的transaction,这个函数可能立刻销毁这个dialog。
Dialog usages API
下面的函数被用来管理一个dialog中的dialog usages。
增加一个模块作为dialog usage,并选择性设置这个模块指定的数据。
绑定模块指特定数据到这个dialog上。
得到之前绑定到这个dialog的模块特定数据。应用通过访问dlg->mod_data[module_id]可以直接得到值。
Dialog请求和响应API
创建一个基本/通用的请求,使用指定的method和选择性指定的cseq。cseq使用值
-1让dialog自动为这个请求放入下一个Cseq数。否则对于一些请求,如CANCEL和ACK,应用必须把原始的INVITE请求中的Cseq作为参数放入。这个函数也将放入Contact头部域。
发送请求消息到远端。如果请求不是一个ACK请求,这个dialog将有状态地发送这个请求,通过创建一个UAC transaction和使用这个transaction发送这个请求。当请求不是ACK或CANCEL,这个dialog将增加它的本地Cseq数并更新请求中的Cseq。
注意在这个函数返回之前dialog usages的on_tsx_state的回调函数可能被调用。
如果p_tsx不是null,这个参数将被设置为发送这个请求的transaction的实例。
无论这个操作的状态如何,这个函数减少这个传输数据的引用指针。
为rdata中到来的请求创建一个响应消息,使用状态码st_code和状态字段st_text。这个函数和Endpoint的API pjsip_endpt_create_response()不同,它可以在适当的时候在响应中加入Contact和Record-Route头部域。
使用其他状态码修改之前发送的响应。适当的时候会加入Contact头部域。
有状态地发送响应消息。这个transaction实例必须是on_rx_request()回调函数中报道的transaction。
无论这个操作的状态如何,这个函数减少这个传输数据的引用指针。
Dialog辅助API
设置dialog的初始路由集为route_set。这个只能在任何请求发送之前,对于UAC dialog调用。在这个dialog被建立之后,这个路由集将被改变。
对于UAS dialog,这个路由集将被pjsip_dlg_create_uas()从Record-Route头部域初始化。
这个route_set参数是标准的Route头部域列表(即,带有标记)。
使用应用指定的app_id和回调函数cb,和这个dialog开始应用定时器。应用可以只设置一个dialog一个应用定时器。这个定时器比dialog指定定时器更有用,因为它将自动被销毁一旦这个dialog被销毁。
注意这个定时器也将被拷贝到这个交叉的dialog。
停止应用指定定时器
得到到来rdata中的dialog实例。如果一个到来的消息和一个已存在的dialog匹配,这个用户代理必须已经将这个匹配的dialog实例放入rdata中,或者否则这个函数将返回NULL,如果没有找到可以匹配的dialog。
得到指定transaction中的dialog实例。
例子
INVITE UAS Dialog
下面的例子使用基本/底层的Dialog API来处理一个到来的Dialog。这些展示如何:
通常,大多数错误处理为了简洁被忽略。真实世界的应用应该准备处理过程中出现的所有错误情况。
创建初始INVITE Dialog
在这个例子中,我们将了解如何为一个到来的INVITE请求创建一个Dialog和以180/Ringing临时响应响应这个Dialog。
应答Dialog
在这个例子中我们将了解如何发送200/OK响应来建立这个Dialog。
处理CANCEL请求
在这个例子中我们将了解如何处理到来的CANCEL请求。
处理ACK请求
在这个例子中我们了解如何处理到来的ACK请求。
外出的INVITE Dialog
下面的例子们说明外出的INVITE Dialog如何处理。
创建初始Dialog
接收响应
发送ACK
终止Dialog
下面的例子说明终止INVITE Dialog的一种方法,如,通过发送BYE。