PJSIP开发手册之用户代理(十)

第十章基本用户代理层(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框架。

PJSIP开发手册之用户代理(十)_第1张图片

这个图展示了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())。在这种情况下,从这个特定的call leg接收到的后续的响应,将进入这个新的Dialog中。

接收到一个交叉的2XX响应,应用可以:

  • 终止这个特定的call leg。在这种情况下,应用将根据响应构建ACK请求,发送ACK,接着构建一个BYE transaction并发送它到call leg。应用在将ACK和BYE请求发给transaction/transport层之前,必须根据响应中的Record-Route头部域来构建ACK和BYE请求的Route头部域。
  • 为这个特定的call leg创建一个Dialog(通过调用pjsip_dlg_fork())。应用接着将构建和发送ACK请求到call leg,为了建立这个Dialog。在Dialog建立之后,应用可能通过发送BYE请求来终止这个Dialog。

创建交叉Dialog

应用通过调用pjsip_dlg_fork()函数来创建一个交叉Dialog。这个函数创建一个Dialog和执行以下步骤:

  • 复制原始的Dialog真的所有属性(包括认证客户端会话)到新的Dialog
  • 根据响应中To头部域的标签,分配不同的remote标签
  • 注册这个新的Dialog到用户代理的Dialog集
  • 如果原始的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

PJSIP开发手册之用户代理(十)_第2张图片

Dialog结构

Dialog结构和它的API声明在。下面的代码展示了pjsip_diaog的声明:

PJSIP开发手册之用户代理(十)_第3张图片

Dialog创建API

一个dialog可以通过调用以下任一个API来创建。

PJSIP开发手册之用户代理(十)_第4张图片

创建一个新的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。这些展示如何:

  • 创建和初始化到来的Dialog
  • 创建UAS transaction来处理到来的INVITE请求和传输1xx响应
  • 可靠地传输2xx响应到INVITE
  • 处理到来的ACK

通常,大多数错误处理为了简洁被忽略。真实世界的应用应该准备处理过程中出现的所有错误情况。

 

创建初始INVITE Dialog

在这个例子中,我们将了解如何为一个到来的INVITE请求创建一个Dialog和以180/Ringing临时响应响应这个Dialog。

PJSIP开发手册之用户代理(十)_第5张图片

应答Dialog

在这个例子中我们将了解如何发送200/OK响应来建立这个Dialog。

PJSIP开发手册之用户代理(十)_第6张图片

处理CANCEL请求

在这个例子中我们将了解如何处理到来的CANCEL请求。

PJSIP开发手册之用户代理(十)_第7张图片

PJSIP开发手册之用户代理(十)_第8张图片

处理ACK请求

在这个例子中我们了解如何处理到来的ACK请求。

PJSIP开发手册之用户代理(十)_第9张图片

外出的INVITE Dialog

下面的例子们说明外出的INVITE Dialog如何处理。

创建初始Dialog

PJSIP开发手册之用户代理(十)_第10张图片 PJSIP开发手册之用户代理(十)_第11张图片

接收响应

PJSIP开发手册之用户代理(十)_第12张图片

发送ACK

PJSIP开发手册之用户代理(十)_第13张图片

PJSIP开发手册之用户代理(十)_第14张图片

终止Dialog

下面的例子说明终止INVITE Dialog的一种方法,如,通过发送BYE。

PJSIP开发手册之用户代理(十)_第15张图片





你可能感兴趣的:(PJSIP学习)