RFC3261(17 事务)
SIP是一个基于事务处理的协议:部件之间的交互是通过一系列相互独立的消息交换来完成的。特别是,一个SIP 事务由一个单个请求和这个请求的所有应答组成,这些应答包括了零个或者多个临时应答以及一个或者多个终结应答。在事务中,当请求是一个INVITE(叫做INVITE事务),当终结应答不是一个2xx应答的时候,事务还包括一个ACK。如果应答是一个2xx应答,那么ACK并不认为是事务的一部分。
这个分开的原因是基于传递全部200(OK)应答到UAC的INVITE请求的重要性所决定的。要把所有的200应答全部发给UAC,UAS独自负责这些应答的重新传送(参见13.3.1.4),UAC独自负责这些应答的ACK确认(参见13.2.2.4)。由于ACK的重传只由UAC发起,所以在自己的事务中进行重传会比较有效。
事务分为客户端和服务端两方。客户端的事务是客户端事务,服务器端的事务就是服务端事务。客户端事务发出请求,并且服务端事务送回应答。客户端和服务端事务都是逻辑上的概念,他们可以被无数部件所包含。特别是,它们在UA中和有状态的proxy服务器中存在。以第4节的例子来说,在这个例子中,UAC执行客户端事务,它的外发proxy执行服务端事务。外发proxy同时也执行客户端事务,把请求发送到一个另一端proxy的服务端事务。这个proxy也同时执行一个客户端事务,把请求发到一个UAS的服务端事务上去。如图4所示:
图4: 事务关系
无状态的proxy并没有客户端或者服务端的事务。事务是一边基于UA或者有状态的proxy,另外一边也基于UA或者有状态的proxy。就SIP事务而言,无状态的proxy实际上实现透明传输。客户端事务用于从一个元素中接收一个请求,这个客户端是内嵌的(这个元素就是“事务用户”或者TU;它可以是一个UA或者有状态的proxy),并且可靠的把这个请求传送到一个服务端事务。
客户端事务也负责接收应答并且把应答转交TU处理,过滤掉重发的应答或者不允许的应答(比如给ACK的应答)。另外,在INVITE请求的情况下,客户端事务也负责产生给2xx应答的ACK请求。
类似的,服务端事务也负责从通讯层接收请求并且转发这个请求到TU。服务端事务过滤重发的请求。并且服务端事务从TU接收应答并且转发到通讯层来发送。在INVITE事务的情况下,它需要接收给非2xx应答的终结应答的ACK请求。
2xx应答和它的ACK请求通过特定的方式来接收和处理。这个应答只会被UAS重发,并且它的ACK只由UAC产生。由于呼叫者知道整个已经接收呼叫的用户集合,所以需要这种端到端的处理。由于这样的特别处理,2xx应答的重发是基于UA核心的,并非基于通讯层。类似的,给2xx应答的ACK处理也是由UA核心处理的,每个路径上的proxy仅仅转发这些INVITE的2xx应答以及他们的ACK。
17.1 客户端事务
客户端事务是通过维持一个状态机来提供服务的。
TU和客户端事务通过一个简单的接口进行通讯。当TU希望初始化一个新的事务,它创建一个客户端事务并且通过设置ip地址、端口和transport来把一个SIP请求交给它传送。然后客户端事务开始执行它自己的状态机。合乎规格的应答会从客户端事务传送给TU。
总共有两种类型的客户端事务状态机,根据TU传递的请求的方法不同来区分。一个用于处理INVITE请求。这种状态机对应的是一个INVITE客户事务。另外一个是用来处理其他所有的非INVITE请求的。它对应的是非INVITE客户事务。对于ACK来说,是不存在客户事务的。如果TU希望送一个ACK请求,它直接交给通讯层进行通讯处理。
INVITE事务和其他事务是不同的,因为它的时间周期很长。通常,对于INVITE请求的应答来说,都需要人的参与,这样会导致在应答INVITE请求之前会有很长的延时。在三方握手(人,两方机器)的时候也会有很长的延时。在另一方面,其他请求的响应都是很快就完成的。因为其他非INVITE请求事务是双方的握手,TU能够立刻对非INVITE请求作出应答。
17.1.1 INVITE客户事务
17.1.1.1 INVITE事务概述
INVITE请求包含了一个三次握手。客户端事务发送一个INVITE,服务端事务回送一个应答,客户端事务发送一个ACK。对于非可靠传输(比如UDP),客户端事务每隔T1重发请求,每次重发后间隔时间加倍。T1是一个估计的循环时间(RTT),缺省设置成为500ms。几乎所有的事务定时器都以T1为单位,并且调整T1的值也就调整了那些定时器的值。请求不会在可靠的通讯协议上重新发送。在接收到1xx应答以后,重发机制完全停止,并且客户端等待更进一步的应答。服务端事务可以发送附加的1xx应答,这个应答并非由服务端事务可靠传输。最后,服务端事务会发送一个终结应答。对于非可靠的传输协议,应答会间隔时间来重发,对于可靠的传输协议,它只发送1次。对于客户端事务所接收的每一个终结应答,客户端事务都发送一个ACK,用于终止应答的重发送。
17.1.1.2 正式的描述
INVITE客户端事务的状态机在图5中展示。初始状态“calling”,必须保证TU是用INVITE请求来初始化一个新的客户端事务。客户端事务必须把请求发送到通讯层来进行发送(18节)。如果使用的是非可靠传输的通讯层,客户端事务必须启动一个定时器A并且由缺省值T1组成。如果是一个可靠的通讯协议,那么客户端事务不应当启动定时器A(定时器A控制请求的重发送)。对于任何通讯协议来说,客户端事务必须启动一个定时器B并且有着64×T1秒的缺省值(定时器B控制事务的超时)。
当定时器A触发了,客户端事务必须重发这个请求,把请求交给通讯层进行发送,并且重新设置定时器为2*T1。在传输层中重传的定义是指把刚才通过传输层发送的消息,再次交给传输层重新发送一次。
当定时器A在2×T1后触发了,请求必须再次重传(如果客户端事务依旧还是在这个状态的话)。这个处理必须持续下去,这样请求才能每重发一次以后定时器延时1倍。重发机制只有当客户端事务在”calling”状态的时候才能进行。
缺省的T1是500ms。T1是一个客户端和服务端间事务处理的估计时间TIT。节点可以(不推荐)使用更小的T1值,比如通常不接入Internet的私有网络。T1也可以设置成为大一点的值,并且我们建议如果当我们知道RTT值比较大的时候(比如高延时的网络)应当设置T1成为大一点的值。不管T1如何取值,本节要求的重传机制指数延时是必须使用的。
当定时器B触发的时候,如果客户端事务依旧在”calling”状态,那么客户端事务应当通知TU发生了超时。客户端事务必须不能产生ACK。64×T1是和在不可靠通讯链路上传输7个请求的时间相同。
如果客户端事务在”calling”状态接收到一个临时应答,那么就把状态切换到“proceeding”状态,客户端事务不应当再次重新发送请求了。进一步说,临时应答必须传送给TU。在“proceeding”状态的任何临时应答都必须传送给TU。
在“calling”或者“proceeding”状态的时候,如果接收到一个应答码是300-699的应答,那么就必须把状态切换到”Completed”。客户端事务必须把收到的应答转给TU,并且客户端事务必须产生ACK请求,即使通讯层是可靠传输的(在17.1.1.3节中有描述怎样根据应答创建一个ACK请求)并且把ACK交给传输层进行传送。ACK必须和原始请求发送到相同的地址、端口和transport。当客户端事务进入“Completed”状态的时候,应当开始一个定时器D,缺省值是在非可靠通讯上是至少32秒,在可靠通讯上是0秒。定时器D反应了服务端事务在非可靠传输的情况下,在“completed”状态维持的时间。这个是和INVITE请求服务端事务定时器H相同的,定时器H的缺省值是64*T1。不过,客户端事务不知道服务端事务使用的T1值,所以我们用绝对值32秒来代替T1用作定时器D的缺省值。
在“completed”状态下,收到的任何终结应答的重传都应当产生一个ACK应答到通讯层来重新发送,但是新近收到的应答却不能传送给TU。一个应答是否是重传的定义是根据这个应答是否和客户端事务按照17.1.3定义的规则匹配。
图5: INVITE客户端事务
如果在客户端事务状态是“Completed”的时候,定时器D触发,那么客户端事务必须转到终结状态。当客户端状态是”calling”或者“proceeding”状态的时候,接收到一个2xx应答必须导致客户端事务进入“terminated”状态,并且应答必须交给TU处理。处理这个应答的方法依赖于TU是否是一个proxy核心还是是UAC核心。UAC核心会给应答产生ACK,proxy核心会转发一个200(OK)应答到上行队列。这个在proxy和UAC之间,对200(OK)的不同的处理是导致对应答的处理不在事务层进行的原因。
当客户端事务进入“terminate”状态以后,客户端事务必须立刻销毁。这样才能保证正确操作。原因是当给一个INVITE请求的2xx应答的不同处理;对于proxy转发的时候和对UAC处理ACK的时候是不一样的。因此,每一个2xx都需要交给proxy 核心(这样才能被转发),或者交给UAC核心(这样才能被ACK确认)。这期间没有事务层的处理。无论应答是否由通讯层收到,如果通讯层找不到匹配的客户端事务(用17.1.3的方式),那么应答就应当交给核心处理。这是由于与之匹配的客户端事务已经被第一个2xx应答所销毁,后续的2xx应当就匹配不成功了,于是就交给核心来处理。
17.1.1.3 构造ACK请求
本节定义了在客户端事务中构造ACK请求的方法。UAC核心为2xx应答产生ACK请求必须使用13节描述的方法,而不是用下边的方法。
在客户端事务中构造的ACK请求必须包括与原始请求相同的Call-ID、From、Request-URI头域值(就是说和在客户端事务发到通讯层的请求中的这些头域值相同)。在ACK请求中的To头域必须和被确认的应答的To头域值相同,因此通常和原始请求有所不同,不同点在增加了附加的tag参数。ACK必须包含一个单个的Via头域,并且必须和原始请求的最上边一个Via头域值相等。ACK的Cseq头域必须包含和原始请求的Cseq的序列号相同,但是方法参数应当是”ACK”。
如果INVITE请求的应答是有Route头域的,这些Route头域必须也在ACK中。这是确保ACK能够正确路由通过下行队列的无状态的proxy。
虽然请求可以包含一个包体,但是ACK的包体却比较特别,因为请求不能因为不能理解包体而拒绝这个请求。因此,我们不建议在给非2xx应答的ACK请求中放置包体,但是如果放置了,并且假设给INVITE的应答不是415应答,那么包体的类型应当严格和INVITE请求中定义的那样。如果是415应答,那么ACK的包体应当和415应答中的Accept列出的类型一致。
例如:有如下请求
INVITE sip:bob@biloxi.com SIP/2.0
Via: SIP/2.0/UDP pc33.atlanta.com;branch=z9hG4bKkjshdyff
To: Bob <sip:bob@biloxi.com>
From: Alice <sip:alice@atlanta.com>;tag=88sja8x
Max-Forwards: 70
Call-ID: 987asjd97y7atg
Cseq: 986759 INVITE
给非2xx终结应答的ACK请求应当是:
ACK sip:bob@biloxi.com SIP/2.0
Via: SIP/2.0/UDP pc33.atlanta.com;branch=z9hG4bKkjshdyff
To: Bob <sip:bob@biloxi.com>;tag=99sa0xk
From: Alice <sip:alice@atlanta.com>;tag=88sja8x
Max-Forwards: 70
Call-ID: 987asjd97y7atg
Cseq: 986759 ACK
17.1.2 非INVITE客户端事务
17.1.2.1 非INVITE事务概览
非INVITE事务并不使用ACK。他们只是简单的请求-应答的交互。对于非可靠的通讯来说,请求是间隔倍增T1的时间重新传输(直到间隔时间达到T2)。如果收到了一个临时应答,在非可靠通讯上,重传继续直到达到T2。只有当重传的请求收到的时候,服务端事务会重传其发出的最后一个应答,既可以是临时的应答也可以是终结应答。这就是为什么请求在收到一个临时应答之后还需要一直重传的原因;他们能够确保收到一个终结应答。
不像INVITE事务,非INVITE事务不需要对2xx应答做特别处理。UAC对一个非INVITE请求来说,只会产生一个单个的2xx应答。
17.1.2.2 正式的描述
在图6中讲述了非INVITE客户端事务的状态机。这个状态机和INVITE客户端事务的状态机非常像。
当TU用请求来初始化一个新的客户端事务的时候,首先进入的是“trying”状态。当进入这个状态的时候,客户端事务应当初始化一个定时器F,这个定时器F应当有一个初始值64×T1秒。这个请求必须交给通讯层来发送。如果使用的是非可靠传输的通讯协议,客户端事务必须还设置定时器E,初始值是T1。如果定时器E触发了,并且还是在“trying”状态,那么定时器需要设置成为MIN(2*T1,T2),并且重新发送;如果再次触发了,那么就再设置成为MIN(4*T1,T2),每次都是倍增,直到T2。这个过程会一直继续,直到重发的间距是T2为止。缺省的T2是4秒,并且它大概是一个在没有立刻响应的情况下,非INVITE服务端事务处理一个请求的时间。根据缺省的T1和T2,那么间隔就会是:500ms,1s,2s,4s,4s,4s以次类推。
如果定时器F触发了,并且客户端事务依旧是在“trying”状态,那么客户端事务应当通知TU这个超时,并且转入“terminate”状态。如果在“trying”状态的时候收到了一个临时应答,那么这个应答必须转给TU处理,并且客户端事务转到“proceeding”状态。如果在“trying”状态收到了一个终结应答(200-699的应答码),那么应答必须交给TU,并且客户端事务必须转到“Completed”状态。
如果定时器E在”Proceeding”状态触发了,那么请求必须交给通讯层进行传输,并且定时器E必须重新设置成为T2秒。如果定时器F在”Proceeding”状态触发了,那么必须通知TU超时了,并且客户端事务必须转到终结状态。如果在”Proceeding”状态的时候收到了一个终结应答(状态码200-699),这个应答必须发送给TU,并且客户端事务必须转到”Completed”状态。
一旦客户端事务进入“Completed”状态,对于非可靠传输的情况,客户端事务必须设置一个定时器K=T4秒,对于可靠传输的情况,设置定时器K=0秒。这个“Completed”状态维持的目的是为了缓冲可能会收到的其他重发的应答(这是为什么客户端事务在这里为非可靠传输维持一段时间的原因)。T4代表了网络在客户端和服务端事务中传输信息可能的时间。缺省的值T4=5秒。当应答具有相同的事务匹配的时候,根据17.1.3的判定,这个应答就是重发的应答。如果定时器K在这个状态被触发,客户端事务必须转到“Terminate”状态。
当事务进入终结状态,就必须立刻终止了。
图6: 非INVITE客户端事务
17.1.3 客户端事务匹配应答
当客户端事务的通讯层收到一个应答,他必须决定是否由客户端事务来处理这个应答,这样17.1.1和17.1.2才能够正确执行。在Via头域的最上边的branch参数就是用来做这个的。一个应答和一个客户端事务匹配的话,就有两个条件:
1、 如果应答Via最上边的branch参数和创建这个客户端事务的请求的Via最上边的branch参数相同。
2、 如果Cseq头域的方法参数和创建事务的请求的方法相同。这是因为CANCEL方法的事务和源请求的事务不同,但是却有相同的branch参数所决定的。
如果一个请求是广播发送的,他可能从不同的服务器上得到不同的应答。这些应答的最上边的Via都有相同的branch参数,但是在To tag中是不同的。当收到了第一个应答,基于上边的规则,将会判定是这个客户端事务的应答,其他的应答将会视同为重发。这并不是错误的情况;多点传送SIP只是提供了一个初步的“寻找最接近的单点”服务的方法,这样就限定了只需要处理单个应答。详情参见18.1.1。
17.1.4 处理通讯错误。
当客户端事务发送一个请求到通讯层发送的时候,如果通讯层报告发送失败,那么需要执行下列步骤。
客户端事务应当通知TU这个通讯失败,并且客户端事务应当直接转到“Terminate”状态。TU处理通讯失败的机制在附件[4]中描述。
17.2 服务端事务
服务端事务是用来传输请求到TU并且可靠的传输应答的。它是通过状态机来实现的。服务端事务是当请求到达的时候由核心创建的,事务的处理也是主要围绕着对应请求的(也就是说并非全部都是和对应请求相关)。
和客户端事务对应的,状态机依赖于是否接收的请求是INVITE请求。
17.2.1 INVITE服务端事务
INVITE服务端事务的状态图在图7表达。
当为一个请求创建了服务端事务的时候,服务端事务进入“proceeding”状态。除非服务端事务知道TU在200ms之内会生成临时或者终结应答(在这种情况下,TU可能会产生100Trying应答),他必须生成100(Trying)应答。这个临时应答是用来停止客户端重发请求的,这个可以避免网络风暴。这个100(Trying)应答是根据8.2.6节描述的步骤构造的,除此之外: 如果接收的请求头中的To头域没有tag标志,那么原来描述的可以增加tag标记,更改成为不应该增加tag标志。这个请求必须交给TU处理。
TU可以给服务端事务任意数量个临时应答。只要服务端事务在“proceeding”状态,每个临时应答都应当交给通讯层发送。这些临时应答并非被通讯层可靠的发送(它们并不重新发送临时应答)并且临时应答并不改变服务端事务的状态。如果在“proceeding”状态,收到一个请求的重发请求,那么就需要把从TU最近收到的那个临时应答重新交给通讯层发送一次。请求是否是重发的请求,是基于17.2.3来判定的匹配相同服务端事务的请求。
如果,在“proceeding”状态,TU发送了一个2xx应答给服务端事务,服务端事务必须把这个应答交给通讯层进行发送。这个并非由服务端事务进行重发;对于2xx应答的重发是由TU处理的。服务端事务必须转到“Terminated”状态。
当在“Proceeding”状态的时候,如果TU交给服务端事务一个300到699的应答,那么应答必须交给通讯层进行发送,并且状态机必须进入“Completed”状态。对于非可靠传输的情况,必须设置定时器G=T1秒,对于可靠传输的情况,不设置定时器G(=0的情况就是不设置)
这个是和RFC2543所不同的,2543要求应答都要重发,甚至在可靠传输的情况下。
当进入了“Completed”状态,必须为所有的传输设置一个定时器H=64×T1秒。定时器H决定何时服务端事务取消重发应答。这个值和定时器B的取值一样,是等同于客户端事务会重试发送请求的时间。如果定时器G触发了,那么应答会交给通讯层再次发送,并且定时器设置成为MIN(2*T1,T2)秒。依此类推,当定时器G再次触发,那么定时器G的值会翻倍,直到T2。这个和非INVITE客户端事务的”trying”请求的重发机制是一样的。进一步说,当在“Completed”状态的时候,如果接收到重发的请求,服务端事务应当把应答交给通讯层再次发送。
当服务端事务在“Completed”状态的时候,如果收到了一个ACK请求,服务端事务必须转到“Confirmed”状态。因为定时器G会在这个状态被忽略,所有的应答重发都会被终止。
如果在“completed”状态的时候定时器H触发了,就意味着没有收到ACK请求。在这个情况下,服务端事务必须转到“Terminated”状态,并且必须通知TU事务失败。
图7:INVITE服务端事务
设定“Confirmed”状态的目的是为了处理任何附加的ACK消息,这是由重发的终结应答所触发的。当进入这个状态,如果是在不可靠传输协议,那么就要设定一个定时器I=T4秒,如果是可靠传输协议,那么就设定I=0。当定时器I触发了,服务端事务必须转到“Terminated”状态。
当服务端事务状态处于“Terminated”状态,这个事务必须立刻销毁。和客户端事务一样,这是为了保证给INVITE的2xx应答的可靠性。
17.2.2 非INVITE服务端事务
对非INVITE服务端事务的状态机是在图8中表示。
当收到一个不是INVITE或者ACK的请求的时候,状态机会初始化成为“trying”状态。并且这个请求会交给TU处理。当在“trying”状态,任何重发的请求会被忽略。一个请求在通过17.2.3节的步骤,匹配现有的服务端事务,将被认为是重发的请求。
当处于“trying”状态,如果TU交给服务端事务一个临时应答,服务端事务应当进入“Proceeding”状态。这个应答必须交给通讯层进行发送。在“Proceeding”状态下从TU收到的任何应答都必须交给通讯层进行发送。如果一个重发的请求在“proceeding”状态下收到了,那么最近发出的一个临时应答应当再次交给通讯层进行重发。如果在“Proceeding”状态下,TU交给服务端事务一个终结应答(应答码是200-699),那么服务端事务必须进入“Completed”状态,并且应答必须交给通讯层进行发送。
当服务端事务进入了“Completed”状态,对于不可靠传输协议来说,必须设定一个定时器J=64×T1秒,对于可靠传输来说,设定为0秒(就是不设定定时器)。在“Completed”状态下,当服务端事务收到了一个重发请求的时候,服务端事务必须交给通讯层终结应答来重新发送。在“Completed”状态下,任何其他TU传递下来给服务端事务的终结应答都必须被抛弃。服务端事务保持这个状态直到定时器J触发,当定时器J触发了以后,服务端事务必须进入“Terminated”状态。
图8: 非INVITE 服务端事务
17.2.3 为服务端事务匹配请求。
当服务端从网络上收到一个请求以后,他必须和现有的事务进行判定。这个是根据下边的规则来判定的。
首先要检查请求中的Via头域的最上一个branch参数。如果他以”z9hG4bk”开头,那么这个请求一定是由客户端事务根据本规范产生的。因此,branch参数在该客户端发出的所有事务中都是唯一的。根据下列规则我们可以判定请求是否和事务匹配:
1、 请求中的最上的Via头域的branch参数和创建本事务的请求的最上的Via头域的branch参数一样,并且:
2、 请求的最上的Via头域的sent-by参数和创建本事务的请求的最上的Via头域的send-by参数一样,并且:
3、 请求的方法和创建本事务的方法一样。这有一个例外,就是ACK,ACK对应的创建本事务的请求方法是INVITE。
这个匹配规则用于INVITE和非INVITE事务。
send-by参数被用于匹配过程,这是因为有可能存在无意/恶意的相同的不同客户端传来的branch参数。
如果最上的Via头域的branch参数不存在,或者没有包含那个”z9hG4bk”,那么就用下列步骤进行判定。这是为了和RFC2543进行兼容的。
如果是INVITE请求,并且这个INVITE请求的Request-URI、To tag、From tag、Call-ID、Cseq和最上的Via头域都和创建事务的INVITE请求的这些字段匹配,那么这个INVITE请求就是匹配这个事务的INVITE请求。在这个情况下,INVITE就是创建这个事务的INVITE请求的一个重发。ACK请求在匹配创建事务的INVITE请求的Request-URI、From tag、 Call-ID、Cseq序列号(非方法字段)、 最上的Via头域、并且To tag和服务端事务发出的应答的To tag相同,这个ACK就是这个事务的ACK。当这些头域比较完成,那么这个匹配也就完成了。在ACK比较中包含To tag的比较是为了在proxy上能够区别给2xx的ACK和给其他应答的ACK,这个proxy可能会转发全部的应答(这个会在某种罕见的情况下发生。特别是,当一个proxy分支一个请求,接着宕机了,应答会转发到别的proxy,这个proxy可能会终止转发多重应答到上行队列)。一个匹配INVITE请求事务的ACK请求,如果这个INVITE请求已经被前一个ACK请求所匹配,那么这个ACK请求就是上一个ACK请求的重发。
对于所有的其他请求方法,如果请求的Request-URI、To tag、From tag、Call-ID、 Cseq(包括Cseq中的方法字段),以及Via头域的最上值,都和创建服务端事务的请求想匹配,那么这个请求就是这个事务的匹配请求。匹配是基于针对每一个头域值的判定进行的。当非INVITE请求和现有事务匹配了,那么它就是创建这个事务的请求的一个重发。
由于匹配规则中包含了Request-URI,服务器不能匹配应答对应到事务。所以当TU传送了一个应答到服务端事务,它必须为这个应答指定传送到那个服务端事务。
17.2.4 处理通讯错误
当服务端事务发送一个应答到通讯层要发送的时候,如果通讯层报告发送失败,那么就需要执行下列的步骤:
首先,附件[4]的步骤需要执行,这就是说需要把应答发送一个备份的地点。如果这个也失败了,基于[4]中对失败的定义,服务端事务应当通知TU发送失败,并且把状态切换到终止状态。