SIP协议详解(中文)-4

From: Alice <sip:alice@atlanta.com>;tag=1928301774
Call-ID: a84b4c76e66710
Cseq : 63104 OPTIONS
Contact: <sip:alice@pc33.atlanta.com>
Accept: application/sdp
Content-Length: 0
11.2 处理OPTIONS请求
给OPTIONS的应答的构造遵循标准的构造规则(8.2.6节描述)。应答码的选择必须和处理INVITE请求一样的应答码(就像处理INVITE请求一样的返回)。这就是说,200(OK)应当在UAS能够接收请求的时候返回,486(忙)应当在UAS如果忙的时候返回。这样OPTIONS可以用来检测UAS的基本状态,这就是说,我们可以用OPTIONS来知道UAS能否接收INVITE请求。
在一个对话中的OPTIONS请求会产生一个200(OK)的应答,这是和在对话外创建的并且对对话没有任何影响的请求相同。
这个OPTIONS的使用有一定的限制,因为对于proxy处理OPTIONS和INVITE请求的不同。一个分支的INVITE可以有多个200(OK)的应答返回,但是一个分支的OPTIONS只能有单个200(OK)应答返回。因为这是由于proxy处理OPTIONS请求是当作非INVITE的处理。参见16.7节有详细的说明。
如果OPTIONS请求的应答是由proxy服务器给出的,proxy返回一个200(OK)的应答,列出这个服务器的各种选项和能力。
应答没有消息体
Allow,Accept,Accept-Encoding,Accept-Language,和Supported头域应当在200(OK)应答中出现。如果这个是由proxy产生的应答,那么Allow头域应当忽略,因为proxy是方法无关的(也就是说不知道该如何处理方法的)。Contact头域可以在200(OK)的应答中出现,并且与3xx应答有相同的语义。这就是说,他们可以列出指向客户的一串名字和方法的集合(用以转发请求)。一个Warning头域是可以存在的。消息体也可以存在,消息体的类型是由OPTIONS请求的Accept头域指明的(application/sdp是缺省的,如果Accpet头域不存在的话)。如果Accept头域中包含了一个类型能描述媒体接收能力,UAS应当在应答中包含一个消息体用于这个用途。详细的application/sdp包体说明在[13]中描述。
UAS生成的OPTIONS应答例子。(对应11.1节中的请求例子)
SIP/2.0 200 OK
Via: SIP/2.0/UDP pc33.atlanta.com;branch=z9hG4bKhjhs8ass877
; received = 192.0.2.4
To: <sip:carol@chicago.com>;tag=93810874
From: Alice <sip:alice@atlanta.com>;tag=1928301774
Call-ID: a84b4c76e66710
Cseq: 63104 OPTIONS
Contact: <sip:carol@chicago.com>
Contact: mailto:carol@chicago.com
Allow: INVITE,ACK,CANCEL,OPTIONS,BYE
Accept: application/sdp
Accept-Encoding: gzip
Accept-Language: en
Supported: foo
Content-Type: application/sdp
Content-Length:274
(SDP not shown)
12 对话(Dialog)
一个UA的核心概念就是对话。对话是表现为两个用户代理(UA)之间的持续一段时间的点对点的SIP关系。对话(Dialog)使得用户代理之间的消息顺序传递和两个用户代理之间的请求正确路由更加容易。对话(Dialog)可以认为是对SIP消息解释的上下文关系。第8节讲述了方法无关的UA处理和响应对话(Dialog)外的请求。本节将讨论如何通过请求和应答来创建一个对话(Dialog),并且在对话(Dialog)中如何发起和响应后续的请求。
一个对话在参与对话的UA中都有一个dialog ID作为标记,这个ID由Call-ID,和一个本地tag和远程tag组成。各个UA的dialog ID在对话中是不一样的。特别是,在一边UA的本地tag,在另外一方就是远程tag。这些tag都是互相不透明的,并且使得整个dialog ID是唯一的。
dialog ID同样是和所有的To头域中包含了tag参数的请求及应答相关。
填写一个消息中的dialog ID的规则依赖于SIP元素是UAC还是UAS。对于UAC来说,dialog ID中的Call-ID的值会填写到消息中的Call-ID域中,远程tag放在消息中的To的tag参数中,本地tag放在From的tag参数中。(这些规则对请求和应答都适用)。对于UAS来说,dialog ID的Call-ID值放在消息的Call-ID头域中,远程tag放在From头域的tag中,本地tag放在To头域的tag参数中。
一个对话包含一些特定的状态用于以后的对话中的消息传送。这个状态由dialog ID,本地序列号(用来排序UA到对方的请求的序列),远程序列号(用来排序请求从远端到本UA),本地URI,远端URI,remote target,一个布尔类型的标记”secure”,路由集合(一组有序的URI)组成。
路由集合是由发送请求到对方需要途径的一组服务器列表组成。一个对话可以处于”early”状态,这是由于当这个对话收到了临时应答而创建,并且当收到了2xx终结应答的时候转换到”confirmed”状态。对于其他应答,或者没有应答,”early”对话将会终结。

12.1 创建一个对话
对话是由对一组特定请求的没有失败的应答来创建的。在本规范中,只有包含To tag的2xx和101-199应答,并且请求是INVITE的,会建立一个对话。当收到一个非终结应答的时候,对话会建立成”early”状态,并且成为early dailog。创建对话的时候可以使用Extension来定义扩展。13节描述了INVITE请求的更多细节。在这里,我们描述与方法无关的对创建对话状态的处理。
UA必须按照下边描述的方法对dialog ID进行赋值。
12.1.1 UAS行为
当UAS响应一个请求给出一个应答,并且这个应答会建立一个对话的时候(比如对INVITE的2xx应答),UAS必须拷贝所有的请求中的Record-Route头域到应答中去(包括URI,URI参数,和其他任何Record-Route头域的参数,无论UAS是不是认识的参数都需要原样拷贝),并且必须维持这些参数的顺序。UAS必须增加一个Contact头域给应答。这个Contact头域包含一个UAS在后续对话请求中接收请求的地址(这个包含了给INVITE请求的2xx应答的ACK请求处理的地址)。通常情况下,UAS会用IP地址或者FQDN形式来发布自己的这个Contact地址。这个在Contact头域中的URI必须是一个SIP或者SIPS URI。如果创建对话的请求在Request-URI中包含的是SIPS URI,或者在Record-Route头域的最上的一个值是SIPS URI,或者如果请求中没有Record-Route头域但是请求中的Contact头域是SIPS URI,那么给出的应答中的Contact头域必须是一个SIPS URI。 这个URI应该是全局有效的(就是说,这个URI可以用于对话外的消息)。同样的,在请求INVITE中的Contact头域的URI也不应当仅限于这个对话中使用。因此它可以用于对话外的消息中。
UAS接着创建这个对话的状态。对话状态必须维持直到对话结束。
如果请求是通过TLS过来的,并且Request-URI包含一个SIPS URI,”secure”标志将被赋值成为TRUE。
路由集合必须设置成为请求中的Record-Route的URI列表,保留所有的URI参数和顺序。如果请求中没有Record-Route头域,那么路由集合必须设置成为空。这个路由集合,即便是空的,为了以后的对话中的请求,也要覆盖任何预先存在(pre-existing)的路由集合。remote taget必须设置成为请求的Contact头域中的URI。
远程序列号必须设置成为请求中的Cseq头域的序列号。本地序列号必须设置成为空。dialog ID中的呼叫标志应该设置成为请求的Call-ID头域的值。dialog ID的本地tag必须设置成为对请求的应答包中的To头域的tag,并且dialog ID的远程tag必须设置成为请求中的From 头域中的tag。UAS必须能够处理接收到的请求中的From头域没有tag标志,在这种情况下,这个tag就是空值。这是为了兼容RFC2543协议,它并没有定义From tag。
远程URI(remote URI)必须设置成为From头域中的URI,并且本地URI必须设置成为TO头域中的URI。
12.1.2 UAC行为
当一个UAC发出一个请求,这个请求能够建立一个对话(比如这个请求是INVITE),它必须在Contact头域中提供一个基于全局的SIP或者SIPS URI(例如,可以在对话外使用的SIP URI)。如果请求包含一个Request-URI或者最上的Route头域是SIPS URI,Contact头域也必须包含的是SIPS URI。
当一个UAC接收到应答,并且这个应答建立对话的时候,它也同样构造这个对话的状态。这个状态必须维持到对话的结束。
如果这个请求是基于TLS发送的,并且Request-URI包含一个SIPS URI,那么”secure”标志被设置成为TRUE。
路由集合必须设置成为应答中的Record-Route头域的URI列表,保留所有的URI参数和顺序。如果在应答中没有Record-Route头域,那么这个路由集合必须设置成为空集合。这个路由集合即便是空的,为了以后的对话中的请求,也要覆盖任何预先存在(pre-existing)的路由集合。remote taget必须设置成为应答中的Contact头域的URI。
本地序列号必须设置成为请求中的Cseq头域的序列号。远程序列号必须设置成为空(他会由远端的UA在对话中发送请求而建立)。dialog ID中的呼叫标志必须设置成为请求的Call-ID头域的值。dialog ID的本地tag必须设置成为请求中的From头域的tag,dialog ID的远程tag必须设置成为应答中的To头域的tag。UAC必须能够处理接收到的应答的To头域中没有tag的情况,在这个情况下,tag值取值成为空。这是为了能够向下兼容RFC2543,它没有规定To的tag。
remote URI必须设置成为To头域的URI,local URI必须设置成为From头域的URI。

12.2 对话中的请求
当两个UA之间的对话建立以后,他们都可以在对话中初始化一个新的事务(transaction)。如果UA发送请求,将遵循UAC的事务规则。UA接收请求将遵循UAS的规则。在建立对话的事务过程中,UA扮演的角色可能是不一样的。
在对话中的请求可以包含Record-Route和Contact头域。不过,虽然他们会修改remote target的URI,但是这些请求也不会导致对话的路由集被改变。明确说,如果请求不是刷新target的请求,那么这个请求不会更改对话的remote target URI,如果请求是刷新target的请求,那么这个请求才会更改对话的remote target URI。对于用INVITE建立的对话来说,唯一的能够刷新target的请求就是re-INVITE(见14节说明)。可能会有其他扩展定义通过其他方法来刷新target的请求。
注意ACK不是一个刷新target的请求。
刷新target请求只会更改对话的remote target URI,并且更改由Record-Route指定的路由集合。如果更新路由集合会带来严重的和RFC2543向后兼容问题。

12.2.1 UAC行为
12.1.1.1 产生请求
在对话中的请求是通过用许多对话的状态部分来构造的。在TO头域中的URI部分必须设置成为对话状态中的remote URI。To头域的tag参数必须设置成为dialog ID中的remote tag部分。请求的From URI必须设置成为对话状态中的local URI。From头域的tag参数必须设置成为dialog ID的local tag部分。如果remote或者local tag是空值,那么tag参数必须分别从From或者To头域中去除。
在请求序列中的原始请求的To和From头域的URI的使用方法是为了向下兼容RFC2543协议的,在RFC2543协议中,使用URI作为对话的标志。在这个规范中,只有tags用于区分对话。有可能在本协议的后续版本中,在对话中的请求必须强制反应原始请求的To和From头域的URI将会去除。
请求的Call-ID必须设置成为对话的Call-ID。在对话中的请求必须严格遵循单个递增的Cseq序列号(每次增加1)(当然要除了ACK和CANCEL,这两个请求中的Cseq必须和原始的请求或者确认请求一样)。因此,如果本地序列号(local sequence number)不为空,那么本地序列号码必须依次增加1,并且这个数值要存放到Cseq头域中。如果本地序列号码是空的,那么在8.1.1.5节约定的初始值必须填写进去。在Cseq头域中的method字段必须和请求的方法(method)一致。
通过使用32位的长整数,使得即使每秒种产生1笔请求,也会要136年才会用完这个整数出现重复。这个序列号的初始值的选取是为了让对话中后续的请求序列号不会重复。非0的初始值可以考虑采用时间来作为初始的序列号。一个客户端可以用31位有符号整数或者32位无符号整数来存放时间作为初始化的序列号。
UAC使用remote target和路由集合来构造请求中的Request-URI和Route头域。如果路由集合是空的,那么UAC必须把remote target URI放到Request-URI中,并且UAC不能添加Route头域到请求中。
如果路由集合不为空,并且路由集合的第一个URI包含lr参数(见19.1.1),那么UAC必须填写remote target URI到Request-URI,并且必须包含Route头域,这个Route头域按照顺序填写路由集合和路由集合的参数。
如果路由集合不为空,并且路由集合的第一个URI没有包含lr参数,那么UAC必须把第一个URI放在Request-URI中,并且拆去所有不被Request-URI允许的参数。UAC必须增加一个Route头域顺序包含所有剩下的路由集合元素,及其参数。UAC接着必须把remote target URI放在Route头域的最后一项。
例如,如果remote targe是: sip:user@remoteua 并且路由集合包括:
<sip:proxy1>,<sip:proxy2>,<sip:proxy3;lr>,<sip:proxy4>
那么请求应该有下列的Request-URI和Route头域
METHOD sip:proxy1
Route: <sip:proxy2>,<sip:proxy3;lr>,<sip:proxy4>,<sip:user@remoteua>
如果路由集合的第一个URI不包含lr参数,那么对应的说明proxy并不能支持本文档所约定的路由机制,而是支持RFC2543文档所约定的路由机制,那么在发送信息的时候需要通过替换Request-URI为接收到的第一个Route头域的值。将Request-URI的值放在Route头域的目的是为了保护Request-URI,使得它经过严格路由的时候不丢失(当请求遇到一个松散路由的时候会返回到Request-URI中?????。)
在对话内的任何一个刷新target的请求中,都应当包含一个Contact头域,并且这个URI除非有必要,否则都应当是和对话内上次请求的URI值一样。如果”secure”标志设置成为TRUE,那么URI也应当是SIPS URI。
如果在12.2.2节讨论的那样,在刷新target请求中的Contact头域会更新remote target URI。这个允许UA提供一个新的联系地址(Contact address),表明它在对话中改变了自己的地址。不过,如果请求不是刷新target的请求,那么不会影响对话中的remote target URI。请求中的剩下的部分请按照8.1.1节描述的填写。一旦请求被创建了,请求将按照对话外请求发送标准步骤(8.1.2节)来解析服务器的地址并且发送请求。
8.1.2节中的步骤一般把请求发送到Route头域的最上一个地址,或者如果没有Route头域,那么就发送到Request-URI地址。由于受到特定的限制,这些步骤也允许把请求发送到另外一个地址(比如在route set中没有的缺省的外发proxy)
12.2.1.2 处理应答
UAC将会从transaction层收到请求的应答。如果客户端的事务层返回一个超时,这会等同于一个408(请求超时)的应答。UAC处理3xx应答的时候,在这个应答是在对话内的请求的应答的处理方法和在对话外的处理方法是一样的。这个方法在8.1.3.4节中描述。需要注意的是,虽然UAC会尝试新的地址(处理3xx应答的时候),但是它依旧使用对话内的路由集合来构造请求的Route头域。
当UAC收到一个刷新target请求的2xx应答的时候,如果对话的remote target URI存在,那么它必须用这个应答的Contact头域的值来替换对话的remote target URI。
如果对话那的请求的应答是481应答(呼叫/事务不存在Call/Transaction Does Not Exits)或者一个408(请求超时),那么UAC应当终止这个对话。并且UAC应当在请求完全没有应答的时候(客户端transaciton将会通知TU这个超时)客户端transaction终止这个对话。
对于INVITE初始化的对话,终止对话需要发送一个BYE。
12.2.2 UAS行为
在对话中发送的请求,就像其他请求一样,是原子请求。如果UAS收到某个请求,所有的相关状态要么一起改变,要么就一起不变。在某些请求中,请求会影响好几个状态(比如INVITE请求)。
UAS从transaction层收到请求。如果请求的To头域有tag字段,UAS的处理核心需要校验对话的ID,拿请求中的tag和现存的对话相比较。如果匹配成功,那么就是一个在对话中的请求。在这种情况下,UAS首先使用8.2节中的对话外请求处理的步骤。如果请求To头域包括了一个tag字段,但是对话的ID并不匹配现存的对话,UAS可能是因为崩溃而重新启动,或者收到了一个另外(可能是错误的)UAS(UAS可以构造To的tags,这样UAS在灾备恢复下,可以把这个tag看成它自己的)。还有一种简单的可能是请求发送错误了。在这个基础上,UAS可以选择接受或者拒绝请求。在允许的情况下,尽量处理这些请求会提供灾难恢复的机制。UAS如果希望支持这样的特性就必须遵循一些原则,比如用始终使用单调递增的Cseq序列号,甚至是在重新启动之后也这样,在重启动后重建路由集合,处理越界的RTP时间戳和序列号等等。
如果UAS由于不希望重构对话而拒绝这个请求,它必须应答对方一个481(呼叫/事务不存在。Call / Transaction不存在)应答。
对于在对话中接收到的,那些不会用任何形式更改对话状态的请求,比如OPTIONS请求,他们等同于在对话外的处理请求。
如果远端的序列号(remote sequence number)是空的,它必须设置成为请求中的Cseq头域的序列号(sequence number)。如果remote sequence number不是空的,但是请求中的sequence number小于这个remote sequence number,请求就是非顺序的,并且必须通过应答500(服务器内部错误)打回去。如果remote sequence number不是空的,并且请求中的序列号大于这个remote sequence number,请求就是按照顺序的。这个请求中的Cseq的序列号可以比remote sequence number大不止1。在这种情况下,并非是错误的,并且UAS应当准备接收和处理比上次处理的请求Cseq值大于1 的请求。UAS必须设置remote sequence number成为请求中的Cseq头域中的序列号。
如果一个proxy废弃掉一个UAC产生的请求,并且UAC重新递交这个请求的时候。这个请求是会具有一个全新的Cseq序列号。UAS是不会收到第一个请求的,这样,Cseq序列号就会出现间隔,这样的间隔并非是一种错误的情况。
当UAS接收到一个target刷新请求的时候,如果请求中存在Contact头域,它必须用Contact头域中的URI来替换对话的remote target URI。
12.3 终止对话
在建立对话中的终结对话,跟请求方法无关,如果对话外的请求产生了一个非2xx终结应答,任何前边请求创建的”早期对话”(early dialogs)将会终止。在已经建立的对话中,终结对话就是请求方法相关的。在这个定义中,BYE方法将会终结一个对话。15节有细致的讨论。
13 初始化一个会话
13.1 概览
当UAC希望初始化一个会话(比如,audio,video或者游戏),它首先构造一个INVITE请求。这个INVITE请求一个服务器来建立一个会话。这个请求可能会由proxy层层转发,最后到达一个或者多个可能能够处理这个邀请的UAS。这些UAS需要看看是否用户接收这个邀请。然后UAS可以接收这个请求(也就是会话建立了),通过发送2xx应答。如果邀请被拒绝,根据拒绝的原因,3xx,4xx,5xx或者6xx应答将会发送。在发送终结应答之前,UAS可以发送一些临时应答(1xx)应答给UAC,以便UAC能够掌握建立会话的进度。
当收到了一个或者多个临时应答,UAC可能收到一个或者多个2xx应答或者一个非2xx终结应答。由于在INVITE终结应答之前,可能有不少时间,在INVITE事务的可靠性机制和其他的请求不同(比如OPTIONS)。当UAC收到了终结应答,UAC需要给每一个INVITE的终结应答,发送一个ACK请求。发送ACK请求的步骤依赖于应答的类别。对于在300到699的终结应答,ACK是在transaction层处理的,并且遵循一系列规则(17节)。对于2xx应答,ACK是由UAC处理核心产生的。
INVITE的一个2xx应答会建立一个会话,同时也建立了一个基于发送INVITE请求的UA和产生2xx应答的UA之间的对话。因此,当从多个远程UA收到了多个2xx应答(可能由于INVITE的分支),每一个2xx建立一个不同的对话(dialog)。所有这些对话都是同一个呼叫的组成部分。
本节介绍了INVITE请求建立会话的详细过程。支持INVITE的UA也一定同时支持ACK,CANCEL和BYE。
13.2 UAC处理
13.2.1 创建一个初始化的INVITE
由于初始化的INVITE请求是一个对话外的请求,它遵循8.1.1节的步骤创建。除此之外还有专门针对INVITE的附加处理步骤。
在INVITE中应当包括一个Allow头域(20.5节)。它用来标志在这个INVITE建立的这个对话(dialog)中什么样的方法可以接受。比如,一个UA可以在对话中接收和处理INFO请求[34],那么在INVITE请求的Allow头域中应当列出这个INFO方法。在INVITE请求中,Supported头域应当包含。这个头域包含了所有这个UAC支持的扩展部分。
在INVITE中可以包含一个Accept头域(20.1节)。这个标志了UA在后续建立的对话中,能兼容的接收和发送的Content-Type。Accept头域支持不同会话描述格式(session descrioption format)的时候特别有用。
UAC可以通过包含一个Expire头域(20.19节)来限制邀请的有效期限。如果Expire头域的时间到了还没有接收到INVITE的终结应答,UAC处理核心应当像9节描述的那样产生一个对INVITE请求的CANCEL请求,
UAC还可以根据需要增加Subject(20.36节),Organization(20.25节)和User-Agent(20.41节)头域。这些头域都包含了INVITE的相关资料。UAC可以给INVITE增加一个消息体。8.1.1.10节讲述了如何构造Content-Type头域来描述消息体。
对于消息体,有一些特别的规定――他们是基于某种磋商机制的,他们对应的Content-Disposition 是”session”(会话的)。SIP使用一个请求/应答模型,UA发出一个会话描述,称作是请求,里边包含了会话的描述。这个请求标志了特定的联系内涵(比如audio,vidio,game),这些内涵的参数(比如解码器等等),并且从应答方接收媒体信息的地址。对方UA会回应另外一个会话的描述,称之为应答,标志了能接受的联系内涵,这些内涵的参数。这个请求/应答的交换实在对话的上下文进行中的,所以如果一个SIP INVITE请求导致了多个对话,每一个对话都包含自己独立的请求/应答的交换。请求/应答模型定义了对于请求和应答的限制。(比如在上一个请求尚未处理完成情况下不能发起下一个请求)。这也导致了请求/应答在SIP消息中出现的位置限制。在这个规范中,请求和应答只能出现在INVITE、ACK请求和其应答中。请求和应答的使用中更进一步被限制。在初始化一个INVITE事务中,规则如下:
o 初始化请求必须在INVITE中,如果不在INVITE请求中,就必须在UAS回送给UAC的第一个非失败的可靠消息中。在这个规范中,这个应答就是2xx应答。
o 如果初始的请求是一个INVITE,那么应答必须是由UAS发送回给对应发出INVITE请求的UAC的可靠的非失败的消息。在本规范中,只有2xx应答对应这个INVITE请求。同样相同的应答可能在之前发送的零食应答中存在。UAC必须把它接收到的第一个会话描述当作是应答,并且必须忽略任何在初始INVITE请求中后续的会话描述应答描述。
o 如果初始请求是在第一个可靠的非失败的UAS回送给UAC的消息中,那么应答必须在这个消息的确认消息中(在本规范中,就是给2xx应答的ACK确认消息)
o 在发送或者接收到第一个请求的应答之后,UAC可以同样依据这样的问答方法产生后续的请求。但是只能在收到每一个请求的应答之后才能发起下一个请求。不能在上一个请求尚未收到应答的时候发起下一个请求。
o 当UAS发送或者接收到初始化的请求的时候,禁止在它给初始的INVITE请求的应答中产生后续的请求(协商会话描述请求)。这就意味着基于本规范的UAS在完成初始化的事务之前,不会产生任何会话描述请求。

具体来说,根据本规范,上边的规则分别定义了两种UA之间交换信息的方法。请求实在INVITE中,应答是在2xx(可能在1xx中也存在,具备相同的值)中,或者请求在2xx中,应答在ACK中。(这个意思是说,两个UA之间建立连接的时候,首先需要协商一下两个UA能够支持的消息体正文,那么这个协商关系也是通过问答形式的,也就是通过请求/应答的,这个媒体磋商的请求既可以在UAC发起的INVITE请求中,也可以在UAS回应的2xx应答中。同样的,媒体磋商的应答既可以在UAS的2xx应答或者1xx应答中,也可以在ACK确认请求中)。所有的支持INVITE请求的UA都必须支持
两种交换方式。会话描述协议(session description protocol sdp)(RFC 2327[1])在所有的UA中都必须得到支持,并且它的用法和请求/应答的构造必须遵循[13]中定义的步骤。
在上边讲述的请求/应答模型中,只能适用于在包头域Content-Disposition中的值是”session”的包体情况。因此,有可能会INVITE和ACK请求中都包含一个包体信息(比如,INVITE包含一个相片(Content-Disposition:render)并且ACK包含一个会话描述(Content-Disposition:session))。
如果Content-Disposition头域不存在,Content-Type 是application/sdp的包体实现就等同于Content-Disposition”session”,其他Content-Type的情况就是实现”render”。
当INVITE请求创建以后,UAC遵循对话外请求发送的步骤进行发送(8节)。这也就是创建一个客户事务并且由这个客户事务发送请求并且处理应答。
13.2.2 处理INVITE应答
当INVITE请求被传送给INVITE的客户事务层进行处理,UAS等待INVITE的应答。如果INVITE客户事务层返回一个超时而不是收到一个应答,那么这个TU就应当像收到一个408(请求超时)应答(8.1.3节)那样进行处理。
13.2.2.1 1xx应答
有可能在收到一个或者多个终结应答之前,UAC会收到0个或者1个或者多个临时应答。INVITE的临时应答会建立”early dialogs”(早期对话)。如果一个临时应答在To头域中有一个tag子顿,并且应答的dialog ID并不是已经存在的对话的ID,那么就应当遵循12.1.2节定义的步骤创建一个对话(早期对话)。
early dialog只会在下边这个情况中需要:如果一个UAC需要在完成初始的INVITE事务之前,给对方发送一个对话内的请求的时候,就需要early dialog。在临时应答中的头域可以在当对话是early state的时候都有效(也就是说,比如一个临时应答的Allow 头域包含的方法,在对话状态是early state的时候都是有效的。[由于Allow是允许的方法集合,所以,当对话状态是早期对话的时候,这个Allow的集合是不会改变的,但是当创建正式的dialog之后,Allow的集合可能会改变哦]。)
13.2.2.2 3xx应答
一个3xx应答可能包含一个或者多个Contact头域值,这个头域值提供了被叫方可能存在的地点。UAC可以根据3xx应答的状态码(21.3节)来决定是否尝试这些新的地址。
13.2.2.3 4xx,5xx,6xx应答
在INVITE请求中,可能会收到单个非2xx终结应答。4xx,5xx,6xx应答如果包含了Contact头域,那么这个头域值指示了错误的详细信息的解释地点。后续的终结应答(只有可能在发生错误的情况下),必须被忽略掉。
所有的早期对话都会由于接收到非2xx终结应答而结束。
一旦接收到了非2xx终结应答,UAC处理核心就认为INVITE事务结束了。INVITE客户事务处理生成对这个应答的ACK(参见17节)。
13.2.2.4 2xx 应答
单个INVITE请求可能会导致多个2xx应答返回给UAC,这是因为proxy可以分支。每一个应答都是由To中的tag参数来进行区分的,并且每一个应答都代表了一个独立的对话,具备单独的对话ID。
如果在2xx应答中的对话ID和一个现存的对话匹配,那么这个对话必须切换到”confirmed”状态,并且对话的路由集合必须基于2xx的应答进行重新计算(参见12.2.1.2)。如果不匹配,那么必须创建一个新的对话,这个对话具备”confirmed”状态,参见12.1.2的步骤进行创建。
注意在对话状态中,只有路由集合不需要重新计算。其他部分比如对话内的最大的序列号(远程的和本地的)等都不需要重新计算。路由集合只是由于需要向后兼容而需要重新计算。RFC 2543并没有要求在1xx应答中反射Record-Route头域回来,只在2xx请求中要求了。我们不能更新对话状态的全部部分,因为在早期对话(early dialog)中可能会存在对话中的请求,比如更改序列号等等。UAC核心必须为每一个2xx应答,产生一个ACK请求。除了在Cseq和身份认证相关的头域之外,ACK请求的头域的创建和在对话中的请求创建的方法一样(12节)。Cseq头域的序列号部分必须和需要确认的INVITE请求一样,但是Cseq的方法部分必须是ACK。ACK必须包含和INVITE请求相同的信任状。如果2xx包含一个媒体磋商请求(基于上述的规则),ACK必须在包体中包含一个媒体磋商应答。如果2xx应答的媒体磋商请求不能被接收,UAC核心必须产生一个有合法的应答ACK,并且立刻发送一个BYE请求。

当ACK创建以后,[附件4]中规定的步骤用来检测对方地址,端口和transport。这个请求是直接交给通讯层进行通讯的,而不是交给一个客户事务层进行发送。这是由于UAC核心直接处理ACK的重发,而不是事务层进行重发的处理。每次收到一个重发的2xx终结应答的时候都必须发送一个ACK到通讯层。

UAC核心认为INVITE事务在接收到第一个2xx应答后的64×T1秒后完成。在这个时间点后,所有没有转换成为建立连接状态的早期对话都会被终止。一旦UAC确认INVITE事务完成了,那么缺省认为不会收到新的2xx应答了。如果,在相应了对INVITE请求的全部应答之后,UAC并不希望创建这个对话,那么UAC必须通过15节描述的那样发送BYE请求来结束对话。
13.3 UAS处理
13.3.1 处理INVITE
UAS核心从事务层收到INVITE请求。首先根据8.2节定义的步骤进行处理请求,8.2节中定义的是跟对话内外无关的请求的处理。如果处理顺利完成(没有产生应答),UAS核心根据如下步骤进行额外处理:
1、    如果INVITE请求包含一个Expires头域,UAS核心就设置一个时钟计数=这个头域值。如果时钟到了,这个邀请就过期了。如果在UAS尚未产生终结应答的时候就超时了,那么487(请求终止)应答应当产生给UAC。
2、    如果请求是一个对话中的请求,12.2.2节定义的方法无关的处理步骤将首先进行处理。这个处理可能会影响到会话;14节讲述了细节。
3、    如果请求的To头域包含了一个tag,但是对话的ID与现存的任何一个对话都不匹配,那么UAS可能是由于崩溃而重新启动的,或者是由于接收到了本应当发送给另外一个UAS的请求(或者就简单是由于请求填写错误)。12.2.2节提供了这种情况的处理指引。从这开始的处理将假定这个INVITE是在对话外的,并且INVITE请求的目的是建立一个新的会话。INVITE请求可能包含一个会话描述,在这种情况下是希望和UAS进行会话媒体的磋商。即使INVITE请求是对话外发出的,这个INVITE参与的用户也有可能正是那个会话中的参与方。这个是由于在多方会议中,某个正在会议中的用户,被其他参与方邀请参加。如果需要鉴别这样的情况,UAS可以使用会话描述来检查是否重复邀请。比如,SDP包含了会话的ID和版本号。如果这个用户本身就是会话中的一方,并且session参数包含的会话描述没有改变,UAS可能就悄悄接受这个邀请(就是说,在不提示用户的情况下发送2xx应答)。
如果INVITE并没有包含某个会话描述(session description),UAS就是被邀请创建一个会话,并且UAC已经希望UAS来提供这个会话offer。UAS必须在它的给UAC的第一个非失败的可靠消息中提供这个offer。在本规范中,给INVITE请求的2xx应答中就应当提供这个offer。
UAS可以提示进度,接受,转发,或者拒绝这个邀请。在这些情况下,它通过按照8.2.6节描述的步骤建立应答。
13.3.1.1 提示进度
如果UAS不能马上接受或者拒绝邀请,那么它可以提示某种形式的进度给UAC(比如提示一个回铃声等等)。这是通过一个101到199的临时应答实现的。这些临时应答建立了早期对话(early dialog)(通过8.2.6和12.1.1)。如果UAS愿意,UAS可以发送多个临时应答。每一个临时应答都必须包含相同的dialog ID。这些临时应答都并非可靠传送的。
如果UAS打算延长一点时间来响应这个INVITE请求,它需要请求一个”extension”来防止proxy来取消这个事务。proxy有权利来取消超过3分钟未完成的事务。要防止这个取消,UAS必须每分钟发送一个非100临时应答,防止由于1xx临时应答的非可靠传输导致的临时应答丢失。
如果呼叫出于等待状态(比如用户设置成为呼叫等待的)或者这个呼叫正在和PSTN电话系统进行通讯(PSTN系统允许呼叫没有应答),一个INVITE事务是可以被延长处理时间的。
13.3.1.2 INVITE请求转发
如果UAS决定转发这个呼叫,就需要发出3xx的应答。300(多重选择),301(永久转移),302(临时转移)应答中应当包含一个Contact头域,这个头域包含了一个或者多个表明需要重试的URI新地址。这个应答交给INVITE服务端事务层,由服务端事务层负责应答的重发。
13.3.1.3 INVITE请求的拒绝
拒绝INVITE请求的常见情景是被叫方不想或者不能在终端系统上接收这个呼叫。486(用户忙)应当在这样的情况下返回。如果UAS知道没有其他终端系统能够响应这个呼叫,就应当返回一个600(Busy Everywhere)。不过,通常情况下UAS是不太会知道这个情况的,并且这个应答也是罕见的。这些应答是交给INVITE服务端的事务层进行发送的,由这个事务层来保证应答的重发机制的。如果UAS拒绝的是INVITE请求包含的媒体磋商offer,UAS应当返回一个488(Not Acceptable Here)应答。这个应答应当包含一个Warning头域来解释为何offer被拒绝。
13.3.1.4 接受INVITE请求
UAS核心产生一个2xx应答。这个应答建立一个对话,然后遵循8.2.6节和12.1.1节的描述进行处理。
响应INVITE请求的2xx应答包含Allow头域和Supported头域,并且可能包含Accept头域。包含这些头域的目的是为了让UAC不需要再次请求就能够知道UAS的特性以及UAS的扩展支持。
如果INVITE请求包含了一个媒体磋商请求offer,并且UAS还没有发送应答,2xx应答中必须包含针对这个offer的应答。如果INVITE请求没有包含这个offer,而且UAS也尚未发出offer,2xx应答必须包含这个媒体磋商offer。
当应答构建好了以后,它会交给INVITE的服务端事务层进行发送。注意,INVITE的服务端事务将会由于收到这个终结应答并且交给通讯层进行发送而销毁。因此,有必要在没有收到ACK的时候,每隔一定的时间就直接交给通讯层进行发送。2xx交给通讯层进行发送的时间间隔是从T1秒开始,并且每次发送后就加倍,直到到达T2秒的时间间隔(T1和T2的时间间隔定义在17节)。当收到了针对这个应答的ACK请求之后,重发就终止了。这个是与使用什么通讯协议来发送这个应答是无关的。
由于2xx的重发是端到端的,并且在UAS和UAC之间存在采用UDP通讯的节点。所以要保证通过这些节点进行可靠的传送,就必须采用间隔时间重发的机制,哪怕UAS本身的通讯机制是可靠的。
如果服务端的对2xx应答的重发经过了64×T1秒还没有收到ACK请求,那么dialog就认为是confirmed,但是会话却应当终止。这个是用过15节描述的方法发送BYE请求来结束。
14 更改已经存在的会话
一个成功的INVITE请求(13节)既会创建一个基于两个用户之间的对话,也会基于请求/应答模式(offer-answer)创建一个会话。12节讲述了如何通过target refresh 请求来修改一个现存的会话(比如,修改对话的remote target URI)。本节描述如何修改实际的会话(session)。
这个修改可以包括修改地址或者端口、增加媒体流、删除媒体流等等。这是通过发起新的INVITE请求来完成的,并且这个新的INVITE请求是基于建立会话所相同的对话的。在一个现存对话中发出INVITE请求就是re-INVITE.
注意,单个的re-INVITE请求可以同时更改对话和会话的参数。
呼叫方或者被叫方都可以更改现存的会话。
在本协议中,UA检测本地媒体有效性是基于自身的策略的。但是,我们并不建议自动产生re-INVITE或者BYE请求,因为这样可能会导致网络上的阻塞。在任何情况下,如果某些消息将被自动发送,那么他们应当等待一个随机的时间间隔。
注意,上边的这些描述是特指自动产生的BYE和re-INVITE。如果用户由于媒体不兼容而挂机,UA应当正常发出BYE请求,而不视为自动产生的BYE。
14.1 UAC行为
与INVITE相同的会话描述磋商offer-answer模式(13.2.1节)在re-INVITE中也一样采用。假设UAS希望增进一个媒体流,那么UAC将会创建一个新的offer包含这个媒体流,并且发送INVITE请求给他的对方。特别需要注意的是,这个会话的全描述,而不是变化部分需要传送。这个支持无状态的会话处理,并且支持错误恢复机制。当然,UAC可以发送一个re-INVITE请求而不包含会话描述,在这样的情况下,就是在这个re-INVITE的第一个可靠的非失败的应答中将会包含这个会话描述offer(在这个规范中,就是2xx应答)。
如果会话描述格式具有版本号码,那么这个磋商的offer应当标志这个变化了的媒体描述版本。
re-INVITE请求中的To,From,Call-ID,Cseq,Request-URI头域应当和正常的在对话中的请求构造方法一样(12节)。
在re-INVITE请求中,UAC可以选择不增加一个Alert-Infor头域或者具有Content-Disposition=”alert”的消息体。因为UAS通常不会要求提示操作者来响应这个re-INVITE请求。
和INVITE不同的是,INVITE可以分支(分岔成为多份INVITE),re-INVITE是不会分支的,所以,只会由一个单个的终结应答。re-INVITE不会分岔的原因是因为Request-URI标志的是建立对话的UA的目标地址,而不是用户的address-of-record地址。
需要注意的是,在相同的对话中,UAC不能在上一个INVITE请求完成前(无论是那一方发起的INVITE)再次发起一个新的INVITE。
1、    如果有正在处理的INVITE客户事务,TU必须等待这个事务终结或者完成,才能初始化一个新的INVITE。
2、    如果有正在处理的INVITE服务事务,TU必须等待这个事务确认或者终结,才能开始处理一个新的INVITE。
不过,UA可以在INVITE事务正在处理的同时,处理一个普通的事务。也可以在一个普通事务正在处理的同时来初始化一个INVITE事务。如果UA接收到一个针对re-INVITE的非2xx终结应答,则会话参数不能改变,应当就像没有收到过这个re-INVITE请求一样。注意,就像在12.2.1.2节一开始讲的那样,如果非2xx终结应答是一个481(Call/Transaction Does Not Exists),或者一个408(Request Timeout),或者完全没有re-INVITE请求的应答(也就是说从INVITE客户事务端返回一个超时),UAC会终止这个对话。
如果UAC收到一个re-INVITE的491应答,他应当启动一个值为T的时钟,这个T的取值如下:
1、    如果UAC是这个dialog ID的Call-ID的拥有者。(也就是说是UAC产生的Call-ID),那么T取值为一个2.1到4秒的随机数,单位是10毫秒。
2、    如果UAC并非是dialog ID的Call-ID的拥有者,T应当取值是0到2秒的随机数,单位是10毫秒。
当这个时钟到了,如果UAC还希望再次尝试更改会话参数,UAC应当再次尝试re-INVITE请求一次。这个意思是说,如果这个呼叫已经被BYE所挂掉了,那么re-INVITE请求就没有再发的必要。
发送re-INVITE请求的规则,以及针对re-INVITE请求产生的2xx应答而产生的ACK请求的发送规则,等同于初始的INVITE请求(13.2.1节)。
14.2 UAS行为
13.3.1节描述了区分初始的INVITE请求和re-INVITE请求的方法,以及处理对现存的对话中处理re-INVITE请求的步骤。
UAS在发送第一个INVITE的终结应答之前,收到第二个INVITE请求,并且这个请求的Cseq序列号大于第一个INVITE请求,那么就应当给第二个INVITE请求返回一个500(服务器内部错误)应答,并且必须包含一个Retry-After头域,这个头域中应当包含一个0-10秒的随机数。
如果UAS正在处理一个INVITE请求的时候又收到了一个在同一个对话上的INVITE请求必须返回一个491(Request Pending)应答给接收到的INVITE。
如果UA接收到一个对现存的对话的re-INVITE请求,那么就必须检查有关会话描述(session description)的版本标志(version identifiers),或者,如果没有版本标志,那么就需要检查会话描述的正文看看有没有变化。如果会话描述改变了,UAS必须由此调整会话参数,在调整参数的时候可能会要求用户确认。

会话描述的版本可以用来提供给会议的新近加入者,增加或者删除媒体,或者由单点会议更改成为多方会议。
如果新的会话描述是不能被UA接受的,UAS可以用返回一个488(Not Acceptable Here)来拒绝这个re-INVITE请求。这个响应应当包含一个Warning头域(用来提供给请求方,提供这个拒绝的原因)。如果UAS产生一个2xx应答,但是没有收到ACK,它应当产生一个BYE来结束这个对话。
UAS可以不产生180(Ringing)应答给re-INVITE,因为UAC一般不展示这个信息给用户。同样的,UAS可以增加一个Alert-Info头域或者一个由Content-Disposition“alert”的消息体来应答给re-INVITE来让UAC展示这个信息。
如果UAS在2xx应答中提供了一个媒体磋商offer(因为INVITE没有包含一个offer),那么它应当当作新呼叫一样的构造这个offer,就像在[13]中SDP描述一样,遵循发送更改现有的会话的offer的限制。特别需要注意的是,这个意味着它应当包含全部的UA支持的媒体类型和媒体格式。UAS必须确保会话描述在媒体格式,通讯方式,或者其他要求对方支持的参数上,新的会话描述和旧的会话描述一样。这个可以避免对方拒绝这个会话描述。如果,UAC任然是不能接受这个会话描述,UAC应当产生一个它能够接受的会话描述应答,并且发送一个BYE来结束这个会话。

15 结束一个会话
本节描述了结束由SIP建立的会话的步骤。会话的状态和对话的状态是密切相关的。当一个会话由INVITE建立的时候,每一个由不同UAS的1xx或者2xx的应答创建一个对话,并且当完成了会话描述的请求/应答(offer/answer)交互之后,它也就创建了一个会话。这就是说,每一个会话都和单个对话”相关”-会话是对话所创建的。如果初始化的INVITE产生了非2xx的终结应答,它也终结了由本次请求创建的任何会话(如果有的话),并且终结了所有的本次请求创建的对话(如果有的话)。由于事务完整性的保证,一个非2xx的终结应答同样也防止了本次INVITE以后可能创建的会话。BYE请求用于终结指定的会话或者尝试建立的会话。在这里,特定的会话是一个和与之相对的对话的对方UA。当在对话中接收到了一个BYE,任何与该对话相关的会话都应当终止。UA禁止在对话外发送BYE请求。请求方UA可以在已经建立好的对话或者早期对话中发起BYE请求;被叫方只能在建立好的对话中发起BYE请求,不能在早期对话中发起BYE请求。
不过,在一个建立好的对话中,被叫方的UA不能在接收到对应2xx应答的ACK请求前发送BYE请求,或者不能在服务器事务超时前发送BYE请求。如果没有SIP扩展定义了和这个对话相关的其他应用层状态,这个BYE请求同样结束了对话。

在对话和会话中,给INVITE的非2xx的终结应答,使得使用CANCEL比较有吸引力。CANCEL是尝试强制给INVITE请求一个非2xx应答(比如,487应答)。因此,如果UAS希望放弃整个呼叫,它可以发送一个CANCEL。如果INVITE会有2xx终结应答,这个意味着UAS在CANCEL正在处理的时候,接收到一个邀请。UAC可以继续用这个2xx应答建立会话,也可以用BYE终结这个会话。(这个意思是说,一般情况下,如果UAC希望cancel 这个INVITE请求,那么就会发出CANCEL请求,如果接收到了非2xx的终结应答,就意味着CANCEL掉了,但是如果接收到的还是2xx应答,就说明没有CANCEL掉,没有CANCEL掉呢,就可以选择继续建立会话,或者说发送一个BYE来终结会话)

在SIP中,并没有一个很好的”hangin up”(挂机中)定义。它属于一个用户界面的普通常见的细节。通常,当用户挂机,它意味着结束建立会话的尝试,并且终止所有已经建立的会话。对于呼叫方的UA来说,如果没有收到初始INVITE请求的终结应答,这个可能是产生对初始INVITE请求的一个CANCEL请求,并且收到终结应答之后给每一个建立好的对话发出一个BYE。对于被叫方的UA,就是很普通的BYE;粗略来说,当用户(因为响应振铃)摘机,就会产生一个2xx应答,于是挂机会在收到ACK请求之后发送一个BYE。这不是说在收到ACK之前用户不能挂机,这只是表达在用户的电话中的软件,需要保持一小会儿状态,来正确释放状态。如果某个UI(用户界面)允许用户在不摘机的情况下拒绝呼叫,可以用403(Forbidden)来作为INVITE的应答,在这样的情况下,BYE就不能发送。
15.1 使用BYE请求终止一个会话
15.1.1 UAC行为
BYE请求就像其他在对话内的请求一样的构造,参见12节的描述。
当BYE请求创建好了之后,UAC核心处理部分创建一个新的非-INVITE客户端事务,并且用它来处理BYE请求。UAC必须认为当BYE请求一发送到客户端事务,会话就结束了(因此也就停止发送或者接收媒体流)。如果BYE请求的应答是481(Call/Transaction Does Not Exists)或者408(Request Timeout)或者BYE请求压根没有应答(就是说客户端事务返回一个超时),UAC必须认为会话和对话都已经结束。
15.1.2 UAS行为
UAS首先按照通用的UAS接收到请求的处理步骤进行BYE请求的处理(8.2节)。UAS核心处理部分接收到BYE请求以后,首先检查它是否和现存的对话匹配。如果BYE并不匹配现存的任何一个对话,那么UAS应当产生一个481(Call / Transaction Does Not Exists )应答,并且传送给服务器事务。
这个规则意味着如果UAC发送没有带tag标志的BYE请求会被拒绝。这个是一个对RFC2543的改动,RFC2543允许BYE不带tag标志。
UAS核心处理部分接收到BYE请求,并且发现和现存的对话匹配,那么它必须遵循12.2.2的步骤来处理请求。一旦处理完成,UAS应当终止这个会话(因此停止发送和接收媒体流)。唯一一个可以选择不终止的情况是多方会话,在多方会话中参与者允许对话中的其他参与方终止他们自己的会话。不管是否终止会话中的参与方,UAS核心处理都必须给BYE产生2xx的应答,并且必须由服务器的通讯层进行传输。
UAS必须依旧响应在这个对话中接收到的未决的请求。我们建议用487(请求终止)来给这些未决的请求以应答。
16 proxy行为
16.1 概述
SIP代理服务器是路由SIP请求到UAS的,并且路由SIP应答到UAC的。一个请求可能通过多个proxy到达UAS。每一个都会作出路由决定,在发送给下一个节点前对请求做一点修改。应答会通过和请求相同的proxy路径,只是顺序是逆序的。
proxy是一个SIP逻辑上的概念。当接收到一个请求,在做代理服务器之前,首先应该有一个部件来决定是否自身需要响应这个请求。例如,在作为代理服务器处理请求之前,首先判定请求可能是非法的或者请求需要一个信任状。这个元素可以使用任何合适的错误代码来响应这个请求。当直接应答请求的时候,这个元素(proxy)将作为UAS角色,并且必须遵循8.2节描述的UAS行为规范。
proxy对于每一个新的请求来说,既可以作为有状态的也可以作为无状态的模式来处理。当作为无状态的处理模式的时候,proxy就是简单的转发。它转发每一个请求下行到一个由请求所决定的目的地。并且简单转发从上行流取得的应答。一个无状态的proxy在处理完一个消息之后就会丢弃这个消息的相关资料。有状态的proxy会保留这些信息(尤其是事务信息),保留每一个接收的请求和每一个接收请求的应答的相关信息。它保留这些信息用于处理与这个请求相关的后续消息。一个有状态的proxy可以选择”分支”一个请求,路由它到多个地点。任何被路由到多个地点的请求都必须当作有状态的处理。在某些情况下,proxy可以用有状态的通讯协议(比如TCP)来转发请求,而不用自身成为事务有状态的。例如,proxy可以简单转发请求从一个TCP连接到另外一个TCP连接,而不用自身作为有状态的,只要它放置足够的信息在需要转发的消息里,使得能够正确把应答发送到接收到对应请求的连接发送出去。如果在不同传输通讯协议之间进行请求的转发,那么就必须要求proxy的TU采用某种手段来保证可靠的从一个协议有状态的转到另一个协议。
有状态的proxy可以在处理请求中的任何时候转换成为无状态的,只要它不作任何可能导致不能无状态的操作(比如分支,比如产生100应答)。当做这样的转换的时候,所有的状态就只是简单的废弃掉。proxy不应当发起一个CANCEL请求。
在作为无状态的或者有状态的时候,处理请求的步骤是一样复杂的。接下来的步骤是从有状态的proxy角度来些的。最后一节是讲述无状态proxy的区别。
16.2 有状态的proxy
作为有状态的proxy,它必须是一个纯粹的SIP事务处理引擎。它在这里的定义遵循17节讲述的服务端和客户端的事务处理规定。有状态的proxy有一个服务端事务,这个事务与一个或者多个客户端事务相关,这些客户端事务是由高层proxy处理元素产生的(图3),这些高层proxy处理元素就是proxy处理核心。一个输入的请求是通过一个服务端事务来处理的。请求由服务端事务交给proxy处理核心进行处理。proxy处理核心检查请求应当路由到哪里,选择一个或者多个下一个节点。每一个发往下一个节点的外发请求都由客户端事务进行处理。proxy处理核心从客户端事务中得到请求的应答并且把他们的应答交给服务端事务进行发送。
有状态的proxy为每一个接收到的新的请求创建一个服务端事务。任何请求的重复都是由这个服务端事务来处理(参见17节)。proxy处理核心必须遵循UAS的模式,发送一个直接临时应答(比如100 trying)到这个服务端事务上(8.2.6节)。因此,有状态的proxy不应当给非INVITE请求产生100(trying)应答。
这是一个proxy的模型,并非软件实现。在实现上可以扩展并且复用用这个模型定义。
对于所有的新请求来说,包括那些未知方法的请求,proxy处理请求必须:
1、    验证请求(16.3)
2、    预处理路由信息(16.4)
3、    决定请求的目的(targets)(16.5)


+--------------------+
|                        | +---+
|                        | | C |
|                        | | T |
|                        | +---+
+---+    |            Proxy        | +---+ CT = Client Transaction
| S |    |    "Higher" Layer    | | C |
| T |    |                        | | T | ST = Server Transaction
+---+    |                        | +---+
|                        | +---+
|                        | | C |
|                        | | T |
|                        | +---+
+--------------------+
Figure 3: Stateful Proxy Model
4、    转发请求到每一个目的地(16.6)
5、    处理所有的应答(16.7)
16.3 验证请求
在proxy转发请求之前,它必须检查消息的合法性。一个合法的消息必须经过如下的检查:
1、    合法的语法
2、    URI scheme
3、    最大转发次数
4、    (可选)循环检测(loop detection)
5、    proxy-require
6、    proxy-authorization
如果任何一步失败了,proxy都必须作为UAS(8.2)一样,应答一个错误码。
注意proxy并不要求检查合并的请求,并且不能把合并的请求当作一个错误的情况。终端接收到合并的请求会根据8.2.2.2节讲述的内容进行分解。
1、    合法的语法。
请求要能够被服务端事务处理,那么请求就必须是语法无误的。请求中的任何与检查相关的部分或者与请求转发节相关的部分都必须语法严格无误。在检查中,其他部分的严格与否,在检查中都被忽略,并且在转发消息过程中保持不变。例如,proxy不会由于非法的Date头域而拒绝请求。同样的proxy不会在转发请求的时候移去非法的Date头域。这个是为了设计成为可扩展的。以后的扩展可能定义新的方法、新的头域。proxy不能拒绝由于包含了不认识的方法或者不认识的头域而拒绝转发这个请求。
2、    URI scheme检查
如果Request-URI包含一个proxy所不能理解的URI形式,那么proxy应当通过返回一个416来拒绝这个请求(Unsupported URI scheme)。
3、    最大转发次数检查
最大转发次数Max-Forwards头域(20.22)是用来限制转发的次数的。如果请求没有包含Max-Forwards头域,那么这个检查将被忽略。如果请求包含了一个Max-Forwards头域,并且这个头域大于0,那么这个检查就跳过。如果请求包含一个Max-Forwards头域,并且这个头域为0,那么这个proxy不能转发这个请求。如果请求是OPTIONS请求,那么proxy可以作为最终响应者来响应这个请求,并且遵循11节讲述的产生应答。否则proxy应当返回一个483(too many hops)应答。
4、    可选的Loop Detection检查
proxy可以检查看看转发是否可能导致循环。如果请求包含一个Via头域,并且这个头域值,和proxy早先保留的请求的头域值相同,那么这个请求就是以前经过过本proxy。要么这个请求就是循环处理了,要么就是合法的循环处理。要检测请求是否循环处理,proxy可以用branch参数,根据16.6节的第8步来计算,并且和接收到的Via头域做比较。如果参数相等,那么请求就循环了。如果不等,那么请求就是合理的经过,并且继续处理。如果检测到了循环,那么proxy应当返回一个482(LoopDetechted)应答。
5、    Proxy-Require检查
本协议的以后的扩展可能会要求额外的proxy特性。所以终端会在请求中包含一个Proxy-Require头域来表明会使用到那些特性,这样proxy就可以根据Proxy-Require判定自己是否能够支持这些特性。
如果请求包含一个Proxy-Require头域(20.29)并且有一个或者多个本proxy不能理解的option-tags。那么这个proxy必须返回一个420(Bad Extension)错误,并且这个错误应答必须包括一个Unsupported(20.40)头域列明了那些option-tags这个proxy不能支持。
6、    Proxy-Authorization 检查
如果proxy要求在转发请求之前进行身份认证,那么必须根据22.3节中描述的那样进行请求的检查。22.3节也定义了proxy应当怎样处理检查失败的情况。
16.4 路由信息预处理
proxy必须检查请求中的Request-URI部分。如果Request-URI包含了一个本proxy早先放在Record-Route头域中的值(参见16.6,4),proxy必须用Route头域中的最后一个值来替换Request-URI,并且从Route头域中删去这个值。proxy必须接着按照个修改后的请求进行处理。
这个只会在某元素发送请求到proxy(proxy本身可能是一个终端),并且这个请求是基于严格路由的。在接收到请求后重写这个字段是必须的,因为要保持向后兼容性。同时也允许按照本规范实现的元素保护Request-URI通过严格路由的proxy(12.2.1.1节)。
这个要求并没有强制proxy保留状态来检查其早先放在Record-Route头域中的URI。作为替换的方法,proxy只需要保留足够的信息在那些URI里边,这样,在以后出现的时候就能识别了。
如果Request-URI包含了maddr参数,proxy必须检查这个参数来看看是否在proxy配置的可信任的地址列表或者可信任的区域列表中。如果Request-URI包含一个maddr参数,并且这个参数包含了一个proxy可以信任的地址,并且这个请求是通过Request-URI中(指明或者缺省)的端口和协议接收到的,proxy必须抽掉maddr和其他非缺省的端口和通讯参数,并且继续处理。

proxy可能收到一个带有匹配maddr的请求,但是不是在URI中指出的端口和transport接收到的。这个请求需要通过指明的port和transport转发到对应的proxy。

如果Route头域的第一个值就是这个proxy,那么proxy必须从请求中把它移去。

16.5 确定请求的目的
接着,proxy计算请求的目的(或者多个目的地)。目的地集合可以由请求的内容决定或者由绝对位置服务提供。目的地集合中的每一个目的地都由URI来表达。

如果请求中的Request-URI包含了maddr参数,必须把Request-URI放在目标集合中,并且是作为唯一一个目标URI,并且proxy必须按照16.6节中的约定进行处理。

如果Request-URI的区域并非本proxy负责的区域,那么Request-URI必须放在目标集合中,并且作为唯一一个目标URI,并且proxy必须按照16.6节中的约定进行处理。

有很多种情况都会导致proxy收到并非本proxy负责的区域的请求。一个防火墙proxy处理外发的请求(就像HTTPproxy处理外发的请求)就是一个典型的例子。

如果请求的目标集合没有像上边讲述的这样预先设定,那么这就意味着proxy是负责Request-URI所指明的区域的,并且proxy可以用任何机制来决定往哪里发送这个请求。这些机制都可以归结成为访问一个绝对位置服务的形式。这个可能由从SIP注册服务器创建的位置服务器获得信息、读取数据库、查阅服务器、利用其他协议、或者就简单的替代Request-URI来实现。当访问由注册服务器创建的位置服务的时候,在作为索引查询之前,Request-URI必须首先根据10.3节进行规范处理。这些机制的输出结果将作为目的地集合。
如果Request-URI没有提供足够的信息来让proxy能够产生目的地集合,它应当返回一个485(Ambiguous)应答。这个应答应当包含一个Contact头域包含一些应当尝试的新位置。比如,一个到sip:John.Smith@company.com的INVITE可能在某一个proxy是不明确的,因为这个proxy有多个JohnSmith。21.4.23节有细节描述。

任何与这个请求有关的,或者与proxy当前环境有关的信息都可以用来构造目的地集合。例如,由于请求的内容不同或者包头域的不同,可以有不同的目的地集合,又或者不同时间到达的请求也可以有不同的目的地集合,或者不同的时间间隔,上一次失败的请求,甚至是当前proxy的利用率都可以导致目的地集合的不同。

通过这些机制,我们可以有一个可能的目的地列表,他们的URI被增加到目的地集合。每一个目的地只能在目的地列表中出现一次。如果目的URI已经在这个集合中存在了(基于URI类型的相等定义),那么它不能再次增加。

如果原请求的Reuqest-URI指明的区域并非本proxy所负责的区域,那么本proxy不能增加任何额外的目的地到目的地集合。

如果proxy负责Request-URI所指明的区域,那么这个proxy只可以在转发的时候改变请求的Request-URI。如果proxy并非负责这个URI,那么它不会在3xx或者416应答的时候查生递归。

如果原始请求的Request-URI是属于本proxy负责的区域的,那么proxy可以在请求转发的时候增加目的地。他可以在处理过程中,用任何可以获得的信息来决定新的目的地。 例如,proxy可以选择把一个转发应答(3xx)所包含的联系地址合并到目的地集合中。如果proxy使用一个动态的信息源来构造目的地集合(例如访问SIP的注册服务器),它应当在处理请求的过程中监测这个信息源。当有新的目的地出现的时候,就应当加到这个目的地集合里边。就像上边说得,每一个URI只能在集合中出现1次。

只能出现1次的原因是可以降低网络冲突,在合并重定向请求的联系地址的情况下可以防止无限递归的出现。

举例来说,一个简单的位置服务是一个”no-op”(无操作的),返回的目的URI就是输入的请求URI。请求将送到一个特定的下一个节点proxy。在16.6节/6小节定义请求转发中,通过SIP或者SIPS URI表达的,下一个节点的身份, 将在Route的头域最上一层插入。
如果Request-URI是这个proxy所负责的,但是在本proxy中找不到,那么proxy必须返回404(Not Found)应答。

如果目的集合经过上边的处理依旧是空的,那么proxy必须返回一个错误应答,这个错误应答应当是408(暂时不可用)。

16.6 请求转发
当目的地集合不是空的时候,proxy可以开始转发这个请求。有状态的proxy可以按照任意的顺序处理这个目的地集合。它可以顺序处理多个目的地,上一个完成前下一个不能开始。也可以采用并行的处理多个目的地。也可以通过分组的形式,每组之间是串行的,组内是并行的。

通常的处理顺序机制是使用一个Contact头域的qvalue参数来处理(20.10节)。目的地从最高的qvalue开始处理到最低的qvalue。相同qvalue的目的地可以并行处理。

有状态的proxy必须包含针对目的地集合的一个接收到应答和转发出去的原始请求进行匹配的机制。为了完成这样的目的,这个机制是一个由proxy层在转发第一个请求前创建的”response context”(应答上下文)来保障的。

对于每一个目的地,proxy转发请求都遵循下列步骤:
1、    拷贝一个接收到的请求
2、    更新Request-URI
3、    更新Max-Forwards头域
4、    可选增加一个Record-Route头域
5、    可选增加附加的头域
6、    路由信息后处理
7、    决定下一个节点地址、端口、通讯协议。
8、    增加一个Via头域值
9、    如果需要,增加一个Content-Length头域
10、    转发这个新的请求
11、    设置定时器C
下面详细介绍每一步。

1、    拷贝请求
proxy首先把接收到的请求做一个拷贝。拷贝必须包含接收到的请求的全部头域。在接下来的处理步骤中未提及的头域不能删除。拷贝应当保留接收到的请求的头域的顺序。proxy不能用合并的域名来进行域值的重新排序(参见7.3.1)。proxy不能增加、修改、删除消息体。
实际上,在实现中并非只是做一个拷贝;首要的事情是为每一个下一个节点准备一个相同的请求。

2、    Request-URI
在拷贝好的请求中的Request-URI必须用目的地的URI进行替换。如果这个目的URI包含任何在Request-URI中所不能允许的参数,那么这些参数必须被删去。
这个步骤是proxy的本质步骤。proxy通过这个机制来把请求转发到目的地。在某些情况下,接收到的Request-URI会不作更改的添加到目的地集合中。对于这样的目的地来说,上边讲的替换就等于是没有任何操作。

3、    Max-Forwards
如果拷贝的头域包含一个Max-Forwards,proxy必须把这个域值减一。
如果拷贝的头域没有包含一个Max-Forwards头域,proxy必须自己增加一个头域,缺省值是70。现在有一些UA不会在请求中填写Max-Forwards头域。

4、    Record-Route
(假设proxy接收到的这个请求会创建一个对话的情况下),如果希望保留这个请求创建的对话中,后续的请求依旧是要经过本proxy,那么本proxy必须增加一个Record-Route头域值在这个拷贝中,并且增加的这个头域值应当是在其他现存的Record-Route头域之前。通过请求建立的对话可以包含一个预置的Route头域。
如果这个请求已经是一个对话的一部分,proxy如果希望以后这个对话的请求依旧经过本proxy,那么proxy应当增加一个Record-Route头域值。在12节描述的普通的终端操作中,这些Record-Route头域值不会对终端使用的路由集合造成任何影响。

如果请求本身已经在对话中的话,如果proxy不增加一个Record-Route头域在请求的包头,后续的请求也会经过本proxy。但是,如果当终端中断并且重新构造这个对话的时候,本proxy就会从对话所经过的节点中删去。

一个proxy可以在任何请求中增加这个Record-Route头域值。如果请求并没有初始化一个对话,终端将会忽略这个头域值。12节讲述了终端如何使用Record-Route头域来构造Route头域的。

在请求路径上的每一个proxy都是独立的决定是否增加一个Record-Route头域值的-在请求的Record-Route头域上的值并不影响这个proxy决定增加还是不增加Record-Route头域值。

在Record-Route头域中防止的URI必须是SIP或者SIPS URI。这个URI必须包含一个lr参数(参见19.1.1)。这个URI可以和请求将被转发的地方不同。这个URI不应当包含通讯参数,除非该proxy确认在后续请求将会经过的下行节点中,都支持这个通讯参数(比如本地网络等等)。

本proxy提供的这个URI可能会让其他元素(其他proxy)作出路由决定。本proxy,通常,并不知道其他节点的处理能力,所以,它必须严格律己,让自己遵循规范的SIP实现:SIP URI和TCP或者UDP通讯协议。

在Record-Route中的URI必须指向插入它的元素(或者替代元素),这个意思是说通过附件[4]的服务器定位步骤可以顺利找到这个元素,这样后续的请求才能顺利到达同一个SIP元素。如果Request-URI包含一个SIPS URI,或者Route头域的最上的值(经过后续第6步的处理)包含一个SIPS URI,那么插入Record-Route头域的值必须是一个SIPS URI。而且,如果请求不是基于TLS接受的,那么proxy必须增加一个Record-Route头域。在相似的情况下,proxy如果从TLS上接收的请求,但是产生的是一个在Record-Route中或者Route头域最上值中没有SIPS URI的请求(在第6步后处理之后),必须在Record-Route头域中增加一个非SIPS URI。

在安全范畴内的proxy必须在对话中保持这个安全范畴。

当Record-Route头域的URI在应答中又重新到达的时候,如果这个URI值需要重写的时候,这个URI必须是能够唯一确定的URI。(就是说,请求可能会经过这个proxy好几次,造成一个或者多个Record-Route头域值的增加)。16.7节的第8步提供了一个能够让这个URI唯一的一个机制。

这个proxy可以在Record-Route头域中增加一些参数。这些参数在某些请求的应答中会被反射(echo)回来,比如给INVITE请求的200(OK)应答。通过在消息的参数中保持状态比在proxy中保持状态更加有效。

如果proxy想在任何类型的对话中都保持在请求的路径上(比如在跨越防火墙的对话中),它需要给每一个接收到的请求中,都增加Record-Route头域,即使是它所不能理解的方法的请求也要增加,因为这些方法可能是对话相关的,具有对话语义的方法。

在Record-Route头域中增加的URI只是在当这个请求创建对话的时候有效。举一个例子,对于一个对话-有状态的proxy(dialog-stateful proxy),当在对话结束后,如果再收到一个请求,这个请求的Request-URI的值中包含这个URI,那么它就可以选择拒绝这个请求。一个对话状态无关的proxy,当然,没有对话结束的概念,但是他们可以再这个值中填写足够多的信息,这样就可以在以后的请求来到的时候做对话的ID的比较,并且可以选择拒绝不匹配这个信息的请求。终端不能在对话外使用这个对话中的Record-Route头域的URI。参见12节描述的终端使用Record-Route头域的细节。

当proxy需要查看所有对话中的消息的时候,我们就需要Record-routeing。但是,他会降低处理性能和影响扩展性,因此proxy应当只在特定情况下使用record-route。任何初始化一个对话的SIP请求都可以适用Record-Route。在本文档中,只有INVITE请求是可以适用的,以后的扩展文档可能包含其他的方法。

5、    增加附加的头域
在这一步,proxy可能增加其他适当的头域。

6、    处理路由信息
proxy可以有一个本地的策略,这个策略要求请求在传递到目的地之前,必须经历一个proxy集合。这样的proxy必须能够确保所有的类似的proxy都是松路由(loose routers)的。通常,只有当这些proxy都是在相同的区域管理的时候,我们才可能知道这些proxy是否都是松路由的。这个proxy的集合是通过一组URI的集合表示(每一个都包含一个lr参数)。这个集合必须被放置到Route头域中,并且放置在其他头域值之前。如果Route头域不存在,必须增加一个Route头域,包含这组URI的列表。
如果proxy有一个本地策略要求请求经过一个指定的proxy,在压栈Route头域之外的一个方法就是旁路下边的第10步的逻辑转发,而是直接发送这个请求到这个指定的proxy的地址,端口,和协议。如果请求有一个Route头域,这个额外的方法就不能用了,除非它知道下一个节点proxy是一个松路由的节点。否则,使用上边讲的增加Route头域的方法会更有效,更灵活,适应性更好,并且操作更一致。而且,如果Request-URI包含了一个SIPS URI,这个proxy必须用TLS来进行通讯。

如果请求的拷贝中包含了Route头域,这个proxy必须检查这个Route头域的第一个值。如果这个URI并没有包含lr参数,那么proxy必须根据下列步骤修改这个请求:

- proxy必须把Request-URI放在Route头域中的最后一个值。
- proxy必须把第一个Route头域的值放在Request-URI中,并且从Route头域中删去。

把Request-URI添加到Route头域的最后是为了让Request-URI的信息能够通过严格路由的proxy。”Popping”弹掉第一个Route头域值到Request-URI中是为了能够让严格路由元素能够接收到这个请求(并且用它自己的在Request-URI中的URI和在Route顶部下一个节点的URI)。

7、    确定下一个节点的地址,端口和通讯协议。
proxy可以有自己的策略来决定发送请求到特定的IP地址,端口和transport,可以和Route的值或者Request-URI的值无关。当本proxy不能确定对应ip,端口,transport的服务器是一个松路由(loose router)的时候,这样的策略就不能使用了。但是,除了Route头域应当像上边讲的这样使用,我们并不推荐这样的发送请求的机制。

在没有这样一个替代机制的时候,proxy应用附件[4]的步骤来决定应当向哪里发送这个请求。如果proxy重新规格化请求,并且发送到一个像上边6点讲的严格路由的元素,proxy必须应用这些步骤到请求中的Request-URI。否则,如果Route头域存在,proxy必须应用这些步骤到Route头域的第一个值;如果Route不存在,proxy必须应用这些步骤到Request-URI。这些步骤最终得到一个序列集合(地址,port,transport)。与使用那个URI作为附件[4]处理的输入,如果Request-URI指定了一个SIPS URI,那么proxy必须把输入[4]的URI当作是SIPS URI然后遵循[4]的处理步骤进行处理。

就像在附件[4]中讲述的,proxy必须尝试序列集合中的第一组元素,并且依次尝试序列集合中的每一组元素,直到成功为止。

对于每一组的尝试,proxy必须按照这组的通讯要求,对消息进行适当的格式化,并且用一个新的客户端事务(第8到第10点讲述的),进行请求的发送。

由于每一组的发送都是使用心得客户端事务,这就体现了一个新的分支。因而,第8步插入的Via头域中的分支参数必须每组发送的都不一样。

如果客户端事务报告发送请求失败,或者它自身的状态机超时,proxy就应当继续处理序列集合中的下一组元素。当遍历完序列集合之后,请求就不能发送到目的地集合。proxy不需要在应答上下文中放什么应答,然而在别的方面却需要就像从目的地集合收到一个408(Request Timeout)终结响应一样的操作。

8、    增加一个Via头域值
proxy必须在请求的拷贝中增加一个Via头域值,并且在其他Via头域值之前增加。这个值的构造可以参见8.1.1.7。这意味着proxy需要计算自己的分支参数,并且应当是全局唯一的分支,并且包含必要的magic cookie。注意这意味着如果请求循环经过本proxy的时候(也就是数次经过同一个proxy),每次的分支参数都不同。

在proxy构造分支参数的值上,有一个附加的约束,用来进行循环的检测。一个要检测循环的proxy应当创建一个由两部分组成的分支参数。第一部分必须满足8.1.1.7的约束。第二部分是用来做循环检测的,并且是从螺旋中判定是否存在循环(请求数次经过同一个proxy是正常的,这是螺旋,但是如果是循环,那就不正常了。假定proxy是X,CàX,XàY,YàZ,ZàX,XàA,Aà目的地是正常的,但是如果CàX, XàY, YàZ, ZàX, XàY, 这就是循环了)。

循环检测是通过这样的方法检测的:当请求返回给一个proxy,与处理请求相关的字段并未改变,那么这个就是循环了。这个分支参数的后一部分应当反应所有的这些头域(包括所有的Route,Proxy-Require和Proxy-Authorization头域)。这是确保如果请求从别处重新路由回来,而且这些字段改变了,那么这就是一个螺旋而不是循环(参见16.3)。通常建立这个比较值的方法是计算一个hash值,通过基于To tag,From tag,Call-ID头域,收到请求的Request-URI(而不是经过处理过后的Request-URI),Via头域的最上一个,Cseq头域的序列号,任何附加的Proxy-Require或者Proxy-Authorization头域。具体的hash算法是基于实现相关的,但是MD5(RFC1321[35]),用16进制表达,是一个有道理的选择。(基于64位表达的是不太合适的)。

如果proxy希望检测循环,那么”branch”参数必须用包含可能影响处理请求的全部信息构成,包括输入的Request-URI和其他可能会影响proxy处理路由的字段,通过计算得到。这是检测循环所必须的,因为如果请求在路由相关的字段改变以后,重新发回这个服务器,那么新的处理可能会发送到另外的地方,而不是造成一个循环。

在branch参数的计算上,请求的方法不能计算进去。但是作为特例,CANCEL和ACK请求(给非2xx应答的)必须和他们对应的请求有相同的branch值。branch参数用于在服务器处理这些请求的时候体现请求之间的相关性(17.2.3和9.2)

9、    如果需要,增加一个Content-Length头域
如果请求会通过一个基于流的通讯协议发送到下一个节点,并且发送的请求拷贝中没有包含一个Content-Length头域,那么proxy必须增加一个正确的请求包体大小在这个头域(20.14)。

10、    转发请求
一个有状态的proxy必须为这个请求创建一个新的客户端事务(根据17.1节描述的那样),并且指示事务层用第7步指定的地址,端口和协议进行发送。

11、    设定时钟C
为了能够处理INVITE请求没有产生终结应答的情况,TU使用一个定时器(称作定时器C)。当INVITE请求被转发的时候,必须为客户端事务设定一个定时器C。这个定时器C必须大于3分钟。16.7节的2步讲述了这个定时器是如何根据临时应答来更新的,并且16.8节讲述了定时器到时的处理步骤。

16.7 应答的处理
当proxy收到一个应答的时候,它首先尝试定位一个与这个应答匹配的客户端事务(17.1.3)。如果没有匹配,proxy必须作为无状态的proxy来处理这个应答(即使这个应答是信息性质的应答)。如果与应答匹配的客户端事务找到了,那么这个应答将转给这个客户端事务进行处理。

将应答转给对应的客户端事务(或者更通常的说法是发出请求的或者相关的事务),并不是为了更强大的处理能力,它是保证了”晚到”的给INVITE请求的2xx应答能够正确的转发。

当客户端事务把应答交给proxy层,将会执行下列步骤:
1、    寻找适当的应答上下文。
2、    用临时应答来更新定时器C
3、    从最上边移除Via
4、    在应答上下文中增加应答
5、    检查这个应答是否需要立刻发送
6、    如果需要,从应答上下文中选择最好的终结应答。

如果在与这个应答上下文相关的每一个客户端事务都结束的以后,还是没有终结应答转发,那么proxy必须选择从已经收到的应答中,选择转发”best”应答回去。

下列步骤必须在每一个被转发的应答上执行。就像每一个请求有超过一个应答被转发一样:至少有一个终结应答和0个或者多个临时应答。

7、    需要合并认证头域值。
8、    可选的重写Record-Route头域值
9、    转发应答
10、    产生合适的CANCEL请求。

上述每一步在下边有详细的描述:

1、    寻找上下文
proxy通过16.6节定义的方法来在寻找转发原始请求前创建的”应答上下文” 。在这个上下文中进行后续的处理步骤。

2、    为临时应答更新定时器C
对于INVITE事务,如果应答是一个返回码是101到199的临时应答(就是说,除了100的临时应答),proxy必须给这个客户端事务重新设置定时器C。这个定时器可以设置成为其他的值,和原始值不一样,但是这个数字必须大于3分钟。

3、    Via
proxy从应答中移去Via头域中最上的值。

如果在这个应答中没有这个Via头域值,那么应答的含义就是说这个应答不应当被这个proxy转发。本节描述的后续处理步骤也不需要继续处理,而是用8.1.3节定义的UAC处理规则进行处理(传输层处理已经进行)。

这种情况可能会发生,比如,当某一个元素产生一个第10节规定的CANCEL请求。

4、    增加应答到上下文
收到的终结应答都会保存在应答的上下文中,直到收到一个由服务端事务产生的针对这个上下文的终结应答为止。这个应答是从那个服务端事务中收到最佳终结应答的一个候选。即使这个应答不会被选中作为最佳应答,这个应答的信息也需要用来构造成为最佳应答。

如果proxy决定尝试调用3xx应答返回的联系地址,并且把他们添加到目的地集合,它必须在把这个应答添加到应答上下文之前把联系地址从应答中移除。不过,proxy不应当在源请求的Request-URI是一个SIPS URI的情况下,尝试调用非SIPS URI。如果proxy尝试每一个3xx应答给回的联系地址,proxy不应当把这个应答添加到应答上下文中。

从应答中删去联系地址的目的是为了防止下一个节点尝试本proxy已经尝试的地址。

3xx应答可能包含SIP,SIPS和非SIP URI。proxy可以自行决定自己调用那些SIP或者SIPS URI,并且把剩下的放在应答上下文中返回。

如果proxy收到一个对于一个Rquest-URI并非SIP URI的请求的416(不支持的URI scheme)应答,但是原始请求的Request-URI是SIP或者SIPS(就是说,proxy在转发请求的时候自己调换了请求的SIP或者SIPS为一个什么其他的东西),proxy应当增加一个新的URI到目的地集合。这个URI应当是刚才尝试的非SIP URI的SIP URI版本。对于电话URL来说,这个就是把电话URL的电话号码部分放在SIP URI的用户部分,并且设置SIP URI的主机部分成为当前请求发送者的区域。19.1.6节有电话URL到SIP URI的转换细节。

在3XX应答的情况下,如果proxy在416上会产生”递归”(因为尝试SIP或者SIPS URI而导致递归),那么应当在应答上下文中增加这个416应答。

5、    检查转发的应答
当终结应答到达服务端事务的时候,下列应答包必须立刻转发。
- 任何非100(trying)的临时应答
- 任何2xx应答。
如果收到一个6xx应答,那么就不立刻进行转发,如果是有状态的proxy,那么还需要cancel所有的依赖于这个事务的客户端(在10节中描述的那样),并且不能在上下文中创建新的分支。

这个是和RFC 2543的不同之处,2543要求proxy立刻转发6xx应答。对于一个INVITE事务来说,如果立刻转发6xx应答,会使得2xx应答到达别的分支。这个结果就是让UAC在2xx应答之后收到一个6xx应答,这个是不允许发生的。在新的规则下,基于接收到一个6xx应答,proxy应当产生一个CANCEL请求,那么这个会给所有等待的客户端事务一个487应答,这就是6xx应答应当给上行队列的一个结果。

在服务端事务上发送了终结应答之后,下列的应答应当立刻被发送:
- 任何给INVITE请求的2xx应答。

一个有状态的proxy必须不能立刻转发其他的应答。特别是,一个有状态的poxy必须不能转发任何100(Trying)应答。这些应答是作为后续将被转发”最佳”应答的候选,通过上边的”在上下文中增加应答”的步骤增加到应答上下文中。

任何将被立刻发送的应答都必须遵照”7、 需要合并认证头域值。”和”8、可选的重写Record-Route头域值”来处理。

这一步,合并下一步,确保有状态的proxy能够精确转发一个终结应答到一个非-INVITE请求,或者给一个INVITE请求的非2xx应答或者一个或者多个2xx应答。

6、    选择最佳的应答
对于一个有状态的proxy来说,如果根据上边的步骤,没有任何终结应答被立刻发送,并且在客户端事务中的所有的客户端服务都已经终结,那么这个proxy必须发送一个终结应答到一个应答上下文的服务端事务层。

那么这个有状态的proxy就必须从接收到的应答上下文中选择一个”最佳”的终结应答。如果在上下文中没有一个终结应答,那么proxy就必须返回一个408(请求超时)的应答到服务端事务层。

如果应答上下文中有终结应答,那么proxy就必须从这个应答上下文中取得应答来发送。如果应答上下文中有6xx应答,那么就必须选择这个6xx应答。如果没有6xx应答,那么proxy应当选择最小的应答(应答返回代码比较小)。proxy可以选择对应最小应答系列中的任意一个应答(比如2xx系列中的任意一个应答)。proxy应当给那些提供对影响请求的应答更多的机会,比如在4xx系列中,选择401,407,415,420或者484应该稍稍优先一些。

当proxy收到503(Service Unavailable)应答的时候,不应当转发到上行队列中,除非它能够知道这个后续的请求队列都能产生503的应答。换句话说,就是转发503就意味着proxy确实不能处理任何请求,不仅仅是Request-URI里边的这个地址不能处理请求。如果只有某个应答会产生503,proxy应当产生500应当到上行队列中。

被转发的应答都必须遵照”7、 需要合并认证头域值。”和”8、可选的重写Record-Route头域值”来处理。

例如,如果一个proxy转发一个请求到4个地方,并且收到了503,407,501,和404应答,它可能选择407(Proxy Authentication Required)应答。

1xx和2xx应答可能和建立对话有关。当请求没有包含一个To tag,UAC使用在应答中的To tag来区分请求创建的对话的多个应答。如果请求中没有包含To的tag,那么proxy必须不能为1xx或者2xx应答增加这个tag到To头域。一个proxy不能修改1xx或者2xx应答中的To头域的tag字段。

在请求的1xx应答中,如果应答没有To头域的tag字段的时候,由于proxy不能添加tag字段到这个To头域,它就不能产生它自己的非100临时应答。但是它可以把这个请求分支到其他一个UAS上,这个UAS可以和proxy共享同样的元素。这个UAS可以返回它自己的临时应答,进入请求创建早期对话中。这个UAS并没有必要作为一个proxy的严格处理步骤存在。它可以是一个在proxy内部的一个虚拟的UAS实现。

3到6xx的应答是节点到节点传递的。当产生了一个3-6系列的应答的时候,每一个节点都作为UAS一样,产生它自己的应答,通常基于下行队列的应答产生自己的应答。对于每一个节点来说,在转发3到6系列应答回去的时候,如果这个应答没有包含To tag,那么这个节点也应当不改变这个to tag。

当收到的应当包含了一个To tag,那么这个proxy不能够修改这个To tag。

恩,实际上在proxy转发3到6系列应答的时候,如果替换了To tag也不会让上行队列所经过的节点有影响,保留原始的tag值可以有助于调试。

当proxy需要合并多个应答的信息的时候,从这些应答中选取To tag的方式是任意的,并且产生一个新的To tag可能可以使得调试更加容易。举例来说,当合并401(Unauthorized)和407(Proxy Authentication Required)信息的时候,或者合并一个未加密的Contact值和未通过验证的3xx应答的时候,产生一个新的To tag就会让调试比较容易。

7、    合并认证头域值
如果选择的应答是401(Unauthorized)或者407(Proxy Authentication Required),那么proxy就必须从本应答上下文中的所有其他401(Unauthorized)和407应答中搜集WWWAuthenticate和Proxy-Authenticate 头域值。并且把这些信息增加到这个应答中。最后的401或者407应答中可能会包含多个WWWAuthenticate和Proxy-Authenticate头域值。

由于这个请求的一个或者多个目的地可能是需要请求身份验证的,所以这个搜集步骤就是必须的。客户端需要接收到这些所有目的者的应答并且在下一次尝试的时候,为每一个目的地提供相关的身份证明。在26节有相关的说明。

8、    Record-Route
如果最终发送的应答中包含Record-Route头域值,并且是这个proxy所原创提供的值,那么在发送这个应答前,proxy可能需要重写这个值。这提供了一个机制,让proxy能够给下一个上行节点或者下行节点提供非本机的一个URI地址。这种情况是很常见的,比如,在多地址主机系统就非常有用。

如果proxy是通过TLS收到请求的,并且通过非TLS转发出去,proxy必须重写在Record-Route头域中的URI,重写成SIPS URI。如果proxy通过非TLS接收到请求,转发是通过TLS转发的,那么proxy必须重写Record-Route请求头域的URI为一个SIP URI。

proxy提供的新的URI必须满足同样的Record-Route头域的URI约束(16.6节的第四步)。并且遵循下列的修改:

URI不应当包含通讯参数除非proxy知道下一个上行(同下行队列对应的)节点,对于后续的请求都支持相关的通讯参数。

如果proxy打算修改应答中的Record-Route头域,要做的一件事情就是定位插入的Record-Route头域值。如果请求是螺旋经过的,并且proxy在每次螺旋的时候都插入了Record-Route值,在应答中(必须在反向路径中的正确位置)找到正确的需要修改的值就需要一点技巧。上边的规则强调proxy在增加Record-Route头域值的时候是必须增加唯一的URI,这样才能找到正确的一个能够重写。我们推荐proxy为每一个URI的user portion增加一个唯一个proxy实例标志。

当应答到达的时候,proxy修改第一个和proxy实例标志匹配的Record-Route。这个修改导致产生一个在user portion部分去掉proxy实例的URI。到下一个循环回来处理的时候,同样的算法(用参数从上而下的寻找Record-Route头域值)会更改这个proxy插入的下一个Record-Route头域值。

对于proxy增加Record-Route头域值的请求来说,并非每一个应答都包含一个Record-Route头域。如果应答包含一个Record-Route头域,那么就包含这个proxy增加的值。

9、    转发应答
当”合并认证头域”和”Record-Route”步骤完成以后,proxy可以对这个应答做其他的附加处理。但是这个proxy不能增加、修改、删除消息体。并且除非另有指示,除了Via头域值(在16.7节3步)之外,proxy不能删除任何头域值。特别是,proxy不能删除任何可能增加到与处理和这个应答相关的下一个请求的Via头域值的”接收到”的参数。proxy必须把应答传递到跟这个应答上下文相关的服务端事务。这回导致应答发送到最上的Via头域值的地方。如果服务端事务不在处理这个发送,这个节点必须作为无状态proxy转发这个应答到服务端通讯层。服务端事务可能已经标志这个发送应答失败或者内部状态机已经设置成为超时状态。这些错误都应当记录下来用于诊断错误,但是协议并没有要求proxy做补救措施。

proxy必须维持应答上下文直到所有相关事务都已经终结,甚至在发送完成终结应答后还需要维持。

10、    产生CANCEL请求
如果转发的应答是一个终结应答,proxy必须给依赖于这个应答上下文的所有客户端事务,产生CANCEL请求。在收到6xx应答的时候,proxy同样应当为所有等待在这个应答上下文的客户端事务产生CANCEL请求。等待的客户端事务就是收到了临时应答,但是没有收到终结应答(还是出于处理中的状态),并且没有任何CANCEL请求与之相关的请求。产生CANCEL请求请参见9.1节。

对于要求基于转发终结应答而CANCEL的客户端事务并没有保证终端不会收到给一个INVITE的多个200(OK)应答。基于多余一个分支的200(OK)应答可能会在CANCEL请求处理前到达。进一步说,后续的扩展可能会改掉这个产生CANCEL请求的要求。

16.8 处理定时器C
如果定时器C 被出发了,proxy必须要么用另外一个数值重新设定定时器,要么终结客户端事务。如果客户端事务已经收到了临时应答,那么proxy必须产生一个与之匹配的CANCEL请求。如果客户端事务还没有收到临时应答,那么proxy必须就像收到一个408(Request Timeout)一样的处理。

允许proxy重设定定时器就意味着允许proxy基于当前条件(比如服务器利用率等等)动态的扩展事务的生命周期。

16.9 处理通讯层的错误
如果在转发请求(参见18.4)的时候,通讯层报告了一个错误,那么proxy必须就像收到了一个503(Service Unavailable)应答一样的处理。

如果proxy在转发应答的时候接收到错误,那么他就丢弃应答。proxy不能由于通讯的原因而cancel任何和这个应答上下文相关的客户端事务。

如果proxycancel了这些客户端事务,那么一个恶意的或者出错的客户端可以用一个Via头域导致所有的事务都失败。

16.10 CANCEL处理
一个有状态的proxy可以给他自己产生的其他请求在任何时候都产生CANCEL请求(参见9.1遵从接收到对应请求的一个临时应答)。在接收到一个匹配的CANCEL请求的时候,proxy必须取消任何与应答上下文相关的客户端事务。

当INVITE请求有一个Expires头域并且这个头域值已经超时的情况下,一个有状态的proxy可以对这个处于pending的INVITE客户端事务发出CANCEL请求。可是,通常来说,这是不必要的,因为相关的终端会发出结束事务的信号。

当有状态的proxy在它自己的服务端事务上处理CANCEL请求的时候,并没有新的应答上下文会创建。相反,proxy层寻找与这个CANCEL对应请求的现存的应答上下文。如果找到了对应的应答上下文,那么这个节点应当立刻返回一个200(OK)应答给这个CANCEL请求者。在这个情况下,这个节点就像8.2节定义的UAS一样的工作。进一步说,这个节点应当为每一个依赖于这个上下文的客户端事务产生一个CANCEL请求(就像在16.7节第10步描述的那样)。

如果一个应答上下文没有找到,这个节点就无法CANCEL这个请求。它就必须像无状态proxy一样转发这个CANCEL请求(可能这个节点把被CANCEL的请求在先前也当作无状态的proxy转发了)。

16.11 无状态的proxy
当作为无状态的时候,proxy就是一个简单的消息转发者。很多无状态的处理步骤和有状态的时候很类似。不同的地方在下边描述。

一个无状态的proxy并没有事务的概念,或者用于描述有状态proxy行为的应答上下文。相反的是,无状态的proxy处理消息,无论是请求还是应答,都是直接从通讯层处理的(参见18节)。当然,无状态proxy自己也不重发这些消息。他们只是转发他们收到的任何重发的消息(他们本身并没有能力来分辩那些消息是重发的,那些消息是原始消息)。进一步说,当无状态的处理一个请求的时候,这个节点并不产生它自己的100(Trying)或者其他临时应答。

无状态的proxy必须用16.3节描述的那样来验证一个请求。

无状态的proxy必须遵从16.4到16.5节定义的步骤来处理请求,有如下几点例外:
o 无状态的proxy必须从目的地集合中,选择一个并且只能选择一个目的地。这个选择必须是根据消息的头域并且是和服务器时间无关的。特别是,一个重发的请求必须能够每次都转发到相同的目的地。进一步说,CANCEL和非路由的ACK请求必须和他们相关的INVITE请求有相同的转发目的地。

一个无状态的proxy必须遵循16.6节定义的请求处理步骤,并且有下列的不同:

o无状态proxy的branchID来说,必须要求在时间上和空间上都是唯一的。也就是说,无状态的proxy不能简单的使用一个随机数产生器来计算branchID的第一个部分(16.6节8步)。这是由于请求的重发需要相同的值,并且无状态的proxy不能区分重发的请求和原始请求。因此,branch参数的组成部分要求唯一,这样使得重发的时候能够填写相同的值。对于无状态的proxy来说,branch参数必须作为一个重发无关的消息处理参数存在。

我们没有规定无状态proxy采用何种手段保证branchID的唯一性。不过,下列步骤是推荐的方法。proxy检查在接收到请求的最上Via头域值的branchID。如果它是由magic cookie打头的,那么branchID的第一个部分就是当作接收到的branchID的hash值。否则branchID的第一个部分就当作是Via头域的最上值、To头域的tag、From头域的tag,Call-ID头域,Cseq序列号(除了方法部分),接收到的请求的Request-URI的一个hash值。这些头域值在不同事务中总是不一样的。

o所有其他的消息转换(16.6节)必须保证转发重发的请求的时候能够转发到相同的节点。特别是,如果proxy在Record-Route头域中增加了值,或者在Route头域中增加了值,proxy必须在转发重发的请求的时候增加相同的值。至于Via 的branch参数,这就意味着转发必须是基于时间无关的配置或者请求重发无关的属性。

o一个无状态proxy决定转发的地点是像16.6节10步描述的有状态的proxy一样。但是请求是直接交给通讯层发送的,而不是交给客户端事务。

由于一个无状态的proxy必须转发重发的请求到相同的地方,并且增加标志性的branch参数,它只能用消息中本身的信息和时间无关的配置来计算。如果配置状态不是时间无关的(比如,如果路由表更新了),这个改变相关的请求,在这个改动开始以后,到在事务超时的时间范围内,就不能作为无状态的转发了。这个处理这段时间的请求是实现相关的。通常处理的方法,是把这些请求当作事务有状态的进行转发。

无状态的proxy必须不能对CANCEL做特别的处理。CANCEL的处理就像对其他请求的处理一样进行。特别是,一个无状态的proxy使用相同的Route头域来处理CANCEL请求,就像处理其他请求一样。

对于16.7节中定义的应答处理,对于无状态proxy来说,并不适用。当一个应答到达一个无状态proxy,proxy必须检查最上的Via头域值的sent-by参数。如果这个地址和这个proxy一样(就是和proxy插入的先前的请求中的值一样),那么这个proxy必须从应答中移除这个头域值,并且转发这个应答到下一个Via头域值。这个proxy必须不能增加,修改或者删除消息体。除非有特别的说明,proxy必须不能移除其他的头域值。如果地址不匹配本proxy,消息就必须简单的悄悄扔掉。

16.12 Proxy Route处理的总结
在没有本地策略的情况下,proxy对于包含Route头域的请求处理可以归结于如下的步骤:

1、    proxy会检查Request-URI。如果它指向的是本proxy所负责的区域,那么proxy会用位置服务的结果来替换这个URI。否则,proxy不改变这个URI。
2、    proxy会检查Route头域的最上URI。如果这个URI指向这个proxy,这个proxy从Route头域中移除(这个路由节点已经到达)。
3、    proxy会转发请求到最上的Route头域值所标志的URI,或者Request-URI(如果没有Route头域)。proxy通过附件[4]的步骤来产生地址,端口,通讯协议等等用来转发请求所必须的参数。

如果在请求的路径中,没有严格路由节点,Request-URI会始终标志着请求的目的地。

16.12.1例子
16.12.1.1 基本SIP四边形
本例子是一个基本的SIP四边传送,U1->P1->P2->U2,使用proxy来传送。下边是过程。

U1 发送:
INVITE sip:callee@domain.com SIP/2.0
Contact: sip:caller@u1.example.com
发给P1,P1是一个外发的proxy。P1并不管辖domain.com,所以它查找DNS并且发送请求到那里。它也增加一个Record-Route头域值:
INVITE sip:callee@domain.com SIP/2.0
Contact: sip:caller@u1.example.com
Record-Route: <sip:p1.example.com; lr>

P2收到这个请求。这是domain.com所以它查找位置服务器并且重写Request-URI。它也增加一个Record-Route头域值。请求中没有Route头域,所以它解析一个新的Request-URI来决定把请求发送到哪里。
INVITE sip:callee@u2.domain.com SIP/2.0
Contact: sip:caller@u1.example.com
Record-Route: <sip:p2.domain.com; lr>
Record-Route: <sip:p1.example.com; lr>

在u2.domain.com的被叫方接收到这个请求并且返回一个200OK应答:
SIP/2.0 200 OK
Contact: sip:  callee@u2.domain.com
Record-Route: <sip:p2.domain.com;lr>
Record-Route: <sip:p1.example.com;lr>

u2的被叫方并且设置对话的状态的remote target URI为:
sip:  caller@u1.example.com并且它的路由集合是:
(<sip:p2.domain.com;lr>,<sip:p1.example.com;lr>)

这个转发通过P2到P1到U1。现在U1设置它自己的对话状态的remote target URI为:sip:calle@u2.domain.com并且它的路由集合是:
(<sip:p1.example.com;lr>,<sip:p2.domain.com;lr>)

由于所有的路由集合元素都包含了lr参数,那么U1构造最后的BYE请求:
BYE sip:callee@u2.domain.com SIP/2.0
Route: <sip:p1.example.com;lr>,<sip:p2.domain.com;lr>

就像其他所有的节点(包括proxy)会做的那样,它会使用DNS来解析最上的Route头域的URI值,这样来决定往哪里发送这个请求。这就发到了P1。P1发现Request-URI中标记的URI不是它负责的域,于是它就不改变这个Request-URI。然后看到它是Route头域的第一个值,于是就从Route头域中移去,并且转发这个请求到P2:
BYE sip:callee@u2.domain.com SIP/2.0
Route: <sip:p2.domain.com;lr>
P2也发现它自己并非负责这个Request-URI的域(P2负责的是domain.com并非u2.domain.com),于是P2并不改变它。它看到自己在Route的第一个值,于是移去这个,并且向u2.domain.com转发(根据在Request-URI上查找DNS):
BYE sip:callee@u2.domain.com SIP/2.0

16.12.1.2 穿越一个严格路由proxy
在这个例子中,对话建立通过4个proxy,每一个增加Record-Route头域值。第三个proxy是由严格路由实现的(RFC 2543)。
U1->P1->P2->P3->P4->U2

INVITE请求到达U2包括了:
INVITE sip:callee@u2.domain.com SIP/2.0
Contact: sip:caller@u1.example.com
Record-Route: <sip:p4.domain.com;lr>
Record-Route: <sip:p3.middle.com>
Record-Route: <sip:p2.example.com;lr>
Record-Route: <sip:p1.example.com;lr>

并且U2返回了一个200 OK。接着,U2根据第一个Route头域值发送下边的BYE请求到P4:
BYE sip:caller@u1.example.com SIP/2.0
Route: <sip:p4.domain.com;lr>
Route: <sip:p3.middle.com>
Route: <sip:p2.example.com;lr>
Route: <sip:p1.example.com;lr>

P4并不管辖Request-URI指出的域,于是就不更改这个Reqeust-URI。它发现自己在第一个Route头域中,于是把自己从Route头域移除。然后准备发送请求到现在的第一个Route头域值:sip:p3.middle.com,但是它发现这个URI并没有包含lr参数,于是在发送前,它把这个请求更改成为:
BYE sip:p3.middle.com SIP/2.0
Route: <sip:p2.example.com;lr>
Route: <sip:p1.example.com;lr>
Route: <sip:caller@u1.example.com>
P3是一个严格路由,于是它转发到P2:
BYE sip:p2.example.com;lr SIP/2.0
Route: <sip:p1.example.com;lr>
Route: <sip:caller@u1.example.com>
P2看到Request-URI是它放在Record-Route头域中的值,于是在进一步处理前,它把这个请求改写为:
BYE sip:caller@u1.example.com SIP/2.0
Route: <sip:p1.example.com; lr>
P2自己并不管辖u1.example.com,于是它根据Route头域的值,转发这个请求到P1。

P1发现自己在Route头域的最上,于是把自己移除,得到:
BYE sip:caller@u1.example.com SIP/2.0
由于P1并不管辖u1.example.com并且没有其他的Route头域,P1会基于Request-URI转发这个请求到u1.example.com。

16.12.1.3 重写Record-Route头域值。
在这里例子中,U1和U2是在不同的私有域空间中,并且他们通过proxy P1开始一个对话,这个P1作为不同私有namespace的一个网关存在。
U1->P1->U2
U1发送:
INVITE sip:callee@gateway.leftprivatespace.com SIP/2.0
Contact: <sip:caller@u1.leftprivatespace.com>

P1使用自己的定位服务并且发送下边的信息到U2:
INVITE sip:callee@rightprivatespace.com SIP/2.0
Contact: <sip:caller@u1.leftprivatespace.com>
Record-Route: <sip:gateway.rightprivatespace.com;lr>

U2发送200 OK应答回给P1:
SIP/2.0 200 OK
Contact: <sip:callee@u2.rightprivatespace.com>
Record-Route: <sip:gateway.rightprivatespace.com;lr>

P1重写它的Record-Route头域参数,提供成为U1能够使用的参数,并且发送给P1:
SIP/2.0 200 OK
Contact: <sip:callee@u2.rightprivatespace.com>
Record-Route: <sip:gateway.leftprivatespace.com;lr>

稍后,U1发送接下来的BYE到P1:
BYE sip:callee@u2.rightprivatespace.com SIP/2.0
Route: <sip:gateway.leftprivatespace.com;lr>

P1转发到U2:
BYE sip:callee@u2.rightpriatespace.com SIP/2.0

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发起,所以在自己的事务中进行重传会比较有效。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
What is dip? dip is an application development and integration framework for Python and PyQt. dip supports Python v3.x, v2.6 and v2.7. dip is suitable for writing large, complex applications, even those that need to integrate substantial amounts of existing code without changing that code. It is also just as suitable for writing simple GUI utilities. Like any framework dip aims to make common tasks easy and uncommon tasks possible. dip encourages the development of applications based on reusable components. This applies both to the development of new components so that they can be reused in the future in a different context, and to the reuse of existing components that were developed independently of dip. Developers can choose to use the different parts of dip as required. dip does not require the wholesale adoption of the framework to develop an application. Developers can also choose to replace different parts of dip with their own implementations. dip's documentation can be found here. It includes tutorials on using the various dip modules and a complete API reference. Features dip includes the following features: the dip-builder utility that can be used to create a stub application that can be run immediately, and can create a packaged version of the application for easy deployment a plugin system that encourages the decoupling, and easy reuse, of components through the use of services and extension points a declarative type system where class attributes define the types of instance attributes that are created automatically when the class is instantiated the ability to define interfaces and to write adaptors that allow an object to appear to implement an interface without needing to change the object itself the ability to specify a user interface declaratively. When combined with the type system it is possible to create a user interface that allows the user to edit a data model with a single line of code dip's user interfaces are testable because a user's actions can be simulated programatically a framework for defining types of storage and data formats for reading and writing application objects a default user interface shell, based on QMainWindow that implements the menu hierarchies, context menus, toolbars etc. common to many applications support for alternative, Qt-based toolkits. For example, an application can automatically use KDE widgets rather than the corresponding Qt widgets if they are available.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值