在Vovida的基础上实现自己的SIP协议栈(三)

在Vovida的基础上实现自己的SIP协议栈(四)

卢政 2003/08/06

3.2.7.5 授权检查

a.示意图和信令部分:


SIP Headers
-----------------------------------------------------------------
sip-req: INVITE sip:93831073@192.168.36.180 SIP/2.0 [192.168.6.20:50753-
>192.168.36.180:5060]
Header: Via: SIP/2.0/UDP 192.168.6.20:5060
Header: From: sip:5120@192.168.6.20
Header: To: [sip:93831073@192.168.36.180]
Header: Call-ID: c2943000-23e062-2e278-2e323931@192.168.6.20
Header: CSeq: 100 INVITE
Header: Expires: 180
Header: User-Agent: Cisco IP Phone/ Rev. 1/ SIP enabled
Header: Accept: application/sdp
Header: Contact: sip:5120@192.168.6.20:5060
Header: Content-Type: application/sdp
Header: Content-Length: 218
-----------------------------------------------------------------
SDP Headers
-----------------------------------------------------------------
Header: v=0
Header: o=CiscoSystemsSIP-IPPhone-UserAgent 21012 9466 IN IP4 192.168.6.20
Header: s=SIP Call
Header: c=IN IP4 192.168.6.20
Header: t=0 0
Header: m=audio 25776 RTP/AVP 0 101
Header: a=rtpmap:0 pcmu/8000
Header: a=rtpmap:101 telephone-event/8000
Header: a=fmtp:101 0-11

407消息:
sip-res: SIP/2.0 407 Proxy Authentication Required
->[192.168.26.180:5060->192.168.26.10:5060]
Header: Via: SIP/2.0/UDP 192.168.26.10:5060
Header: From: [sip:6711@192.168.26.180:5060]
Header: To: [sip:6711@192.168.26.180:5060]
Header: Call-ID: c2943000-1e262-513-2e323931@192.168.26.10
Header: CSeq: 100 REGISTER
Header: WWW-Authenticate: Digest
realm=vovida.com,algorithm=MD5,nonce=966645751
Header: Content-Length: 0
下一个INVITE消息:
sip-req: INVITE sip:93831073@192.168.36.180 SIP/2.0 [192.168.6.20:50753-
>192.168.36.180:5060]
Header: Via: SIP/2.0/UDP 192.168.6.20:5060
Header: From: sip:5120@192.168.6.20
Header: To: [sip:93831073@192.168.36.180]
Header: Call-ID: c2943000-23e062-2e278-2e323931@192.168.6.20
Header: CSeq: 101 INVITE
Header: Authorization: Digest
username="6711",realm="vovida.com",uri="sip:192.168.26.180",response="fee2efef60a99b4576c
Header: Expires: 180
Header: User-Agent: Cisco IP Phone/ Rev. 1/ SIP enabled
Header: Accept: application/sdp
Header: Contact: sip:5120@192.168.6.20:5060
Header: Content-Type: application/sdp
Header: Content-Length: 218

b.程序主体部分:
addOperator (new OpReInviteAuthenticated)
这个新的操作是指在接收到407(需要授权验证的Inivte)回应消息以后系统的操作。
const Sptr < State >
OpReInviteAuthenticated::process( const Sptr < SipProxyEvent > event )
{
Sptr < SipEvent > sipEvent;
sipEvent.dynamicCast( event );
… …
Sptr < SipMsg > sipMsg = sipEvent->getSipMsg();
assert( sipMsg != 0 );

Sptr < StatusMsg > msg;
msg.dynamicCast( sipMsg );

switch ( msg->getStatusLine().getStatusCode() )
{
case 407: // Proxy Authentication Required
break;
default:
{
return 0;
}
}

int cseq = sipMsg->getCSeq().getCSeqData().convertInt();
Sptr < UaCallInfo > call;
call.dynamicCast( event->getCallInfo() );
… …
//和上面Redirect消息的处理机制一样,找到源头的INVITE并且向Marshal Server 回送ACK
Sptr < Contact > origContact = call->findContact( *msg );
assert( origContact != 0 );

AckMsg ack( *msg );

Sptr< BaseUrl > baseUrl =
origContact->getInviteMsg().getRequestLine().getUrl();
assert( baseUrl != 0 );
if( baseUrl->getType() == TEL_URL )
{
assert( 0 );
}
// Assume we have a SIP_URL
Sptr< SipUrl > reqUrl;
reqUrl.dynamicCast( baseUrl );
assert( reqUrl != 0 );

SipRequestLine reqLine = ack.getRequestLine();
reqLine.setUrl( reqUrl );
ack.setRequestLine( reqLine );
sipEvent->getSipStack()->sendAsync( ack );

//如果前面已经接收到了一个407的状态回应,那么我们就认为密码错误,进入校验循环直接跳出
if( origContact->getStatus() == 407 )
{
… … return 0;
}

Sptr inviteMsg = new InviteMsg( origContact->getInviteMsg() );

//这个下面的过程和上面的Redirect非常相似,重新创建一个INVITE命令我们可以参考
//Redirect的下面的处理部分,这里不做累述
inviteMsg->setCallId( sipMsg->getCallId() );
newCSeq.setCSeq( ++cseq );
inviteMsg->setCSeq( newCSeq );

//建立授权,把密码和用户名称通过authenticateMessage
if(authenticateMessage( *msg,
*inviteMsg,
UaConfiguration::instance()->getUserName(),
UaConfiguration::instance()->getPassword() ))
{
// went OK
sipEvent->getSipStack()->sendAsync( *inviteMsg );
}
else
{
// xxx there was an authentication problem, so we need to abort out
}

call->setContactMsg(inviteMsg);


// Update the Ring INVITE message
call->setRingInvite( new InviteMsg( *inviteMsg ) );
return 0;
}

c.授权消息检查的各个子项目:
  这里是指的鉴别授权的方式我们可以看一下在RFC 2543中如何定义的请参看RFC 2543的6.42 WWW-Authenticate的介绍):

  在回应的状态消息中包含有401/407(Unauthentication)状态的时候,表示需要进行授权检查,在回应头部包含有一个或者多个的由密钥计算方法和密钥参数组成的域例如:

  Header: WWW-Authenticate: Digest//表示采用密钥检验的算法
  realm=vovida.com,//主叫和被叫方所使用的信任值,也就是授权值,表示主叫/注册服务器共享密码//域(就是所有的用户在这个区域内采用相同的密码,相当于公钥)。

  algorithm=MD5,//算法
  nonce=966645751//407/401消息回应的鉴别符,防止重放攻击。



  主叫接收到401/407消息以后,回应的下一个命令(例如INVITE)中必须包含鉴权因子在里面:

  Header: Authorization: Digest
  username="6711",realm="vovida.com",uri="sip:192.168.26.180",response="fee2efef60a99b4576c

  上面几个因子我就不详细说明意思了,大家应该一看意思就可以明白:

  那么我们根据RFC2543中14.3的Digest鉴别方式可以看出:最后在主叫部分的信任状(Respone)如何实现:

  response=username⊕realm⊕uri⊕realm⊕Password⊕Username
  最后在注册服务器端根据上述的算式重算response,并且判断结果主叫的response是否相等。
具体的程序按照下面的方式层次调用,具体代码不做详细的分析:

A.主叫端(客户端)

1.authenticateMessage( *msg, *inviteMsg,
UaConfiguration::instance()->getUserName(),
UaConfiguration::instance()->getPassword() )
... ... ...

void addWwwAuthorization(const StatusMsg& errorMsg, SipCommand& cmdMsg,
Data username,Data password)
... ... ...
void addAuthorization(const StatusMsg& errorMsg,
SipCommand& cmdMsg,
Data username,
Data password,
bool useProxyAuthenticate)

void SipCommand::setAuthDigest(const Data& nonce, const Data& user,
const Data& pwd, const Data& method,
const Data& realm, const Data& requestURI,
const Data& qop, const Data& cnonce,
const Data& alg, const Data& noncecount,
const Data& opaque)
{
Sptr authorization;
myHeaderList.getParsedHeader(authorization, SIP_AUTHORIZATION_HDR);

SipDigest sipDigest;
//核心的MD5加密算法,主要计算出response
Data response = sipDigest.form_SIPdigest(nonce, user, pwd, method,
requestURI, realm, qop, cnonce, alg, noncecount);

cpLog(LOG_DEBUG_STACK, "setAuthDigest::Response = %s\n",
response.logData());

//set this as response in authorization.
authorization->setAuthScheme(AUTH_DIGEST);
以下部分是设定授权的头部的各个域的值
if(user != "")
{
authorization->setTokenDetails("username", user); //1
}
if(realm != "")
{
authorization->setTokenDetails("realm", realm); //2
}
if(nonce != "")
{
authorization->setTokenDetails("nonce", nonce); //3
}
if(response != "")
{
authorization->setTokenDetails("response", response); //4
}
if(qop != "")
{
authorization->setTokenDetails("qop", qop); //5
}
if(requestURI != "")
{
authorization->setTokenDetails("uri", requestURI); //6
}
if(cnonce != "")
{
authorization->setTokenDetails("cnonce", cnonce); //7
}
if(noncecount != "")
{
authorization->setTokenDetails("nc", noncecount); //8
}
if(opaque != "")
{
authorization->setTokenDetails("opaque", opaque); //9
}
if(alg != "")
{
authorization->setTokenDetails("algorithm", alg); // 10
}
}

Data SipDigest::form_SIPdigest( const Data& nonce,
const Data& user,
const Data& pwd,
const Data& method,
const Data& requestURI,
const Data& realm,
const Data& qop,
const Data& cnonce,
const Data& alg,
const Data& noncecount)

B.注册服务器端(Marshal Server)授权鉴定(不列举原代码只简单介绍过程)



3.2.7.6 OpFarEndAnswered处理接收到的OK回应
addOperator( new OpFarEndAnswered )
一个标准的OK回应的构成:

SIP Headers
-----------------------------------------------------------------
sip-res: SIP/2.0 200 OK [192.168.36.180:5060->192.168.6.21:5060]
Header: Via: SIP/2.0/UDP 192.168.6.21:5060
Header: From: [sip:5121@192.168.6.21:5060]
Header: To: [sip:5120@192.168.36.180:5060];tag=c29430002e0620-0
Header: Call-ID: c2943000-e0563-2a1ce-2e323931@192.168.6.21
Header: CSeq: 100 INVITE
Header: Contact: [sip:5120@192.168.6.20:5060]
Header: Record-Route:
[sip:5120@192.168.36.180:5060;maddr=192.168.36.180],
[sip:5120@192.168.36.180:5060;maddr=1]<92.168.36.180>
Header: Server: Cisco IP Phone/ Rev. 1/ SIP enabled
Header: Content-Type: application/sdp
Header: Content-Length: 218
-----------------------------------------------------------------
SDP Headers
-----------------------------------------------------------------
Header: v=0
Header: o=CiscoSystemsSIP-IPPhone-UserAgent 13045 2886 IN IP4 192.168.6.20
Header: s=SIP Call
Header: c=IN IP4 192.168.6.20
Header: t=0 0
Header: m=audio 30658 RTP/AVP 0 101
Header: a=rtpmap:0 pcmu/8000
Header: a=rtpmap:101 telephone-event/8000
Header: a=fmtp:101 0-11
const Sptr < State >
OpFarEndAnswered::process( const Sptr < SipProxyEvent > event )
{
Sptr < SipEvent > sipEvent;
sipEvent.dynamicCast( event );
Sptr < SipMsg > sipMsg = sipEvent->getSipMsg();
Sptr < StatusMsg > msg;
msg.dynamicCast( sipMsg );
if(msg->getStatusLine().getStatusCode() > 200)
{
return 0;
}
Sptr < UaCallInfo > call;
call.dynamicCast( event->getCallInfo() );
assert( call != 0 );
Sptr findContact = call->findContact( *msg );
//在OpInviteUrl和OpReInviteAuthenticated 需要有Invite命令发送的程序中有用到//call->setContactMsg来设定当前的Contact项目;在这里通过call->getContact的方法来
//进行对比,以检验是否和OK中返回的Contact项目相符合,如果不符合则表示必定不
//是当前的这个INVITE命令返回的内容。
Sptr getContact = call->getContact();
if ( *( call->findContact( *msg ) ) != *( call->getContact() ) )
{
return 0;
}
call->getContact()->update( *msg);
int status = msg->getStatusLine().getStatusCode();
if ( status >= 200 )
{ 发送ACK给被叫端
AckMsg ack( *msg );
sipEvent->getSipStack()->sendAsync( ack );
}
if ( status != 200 )
{
return 0; // Done
}
//如果返回的Contact有两个联络地址,那么建立新的路由,并且将该新路由项目保存
//在UaCallInfo中
call->setCallerRoute1List( msg->getrecordrouteList() );
int numContact = msg->getNumContact();
if ( numContact )
{
SipContact contact = msg->getContact( numContact - 1 );
Sptr < SipRoute > route = new SipRoute;
route->setUrl( contact.getUrl() );
call->addRoute1( route );
}
Sptr sdp;
sdp.dynamicCast ( sipMsg->getContentData(0) );
… …
//在UaCallInfo中保存远端回传的SDP
call->setRemoteSdp( new SipSdp( *sdp ) );

Sptr < SipSdp > localSdp = call->getLocalSdp();
Sptr < SipSdp > remoteSdp = call->getRemoteSdp();
//RSVP消息的发送和接受设置(下面做详细介绍)。
rsvpFarEndAnswered(localSdp, remoteSdp);
Sptr < UaStateMachine > stateMachine;
stateMachine.dynamicCast( event->getCallInfo()->getFeature() );
assert( stateMachine != 0 );
//转入SateInCall状态
return stateMachine->findState( "StateInCall" );
}

3.2.7.7在Vocal中如何实现RSVP资源预留协议:
  RSVP资源预留协议的具体内容我们在这里就不做详细的介绍了,如果对这个还不了解的可以看一下RFC 1633中对RSVP的具体定义,另外在draft-ietf-rsvp-rapi-01.txt中定义关于RSVP相关的基本API函数调用。

RSVP一般工作机理如下图所示:


  发送方发送PATH消息,消息中包含有数据业务特征,该消息沿所选的路径传送,沿途的路由器按照PATH准备路由资源,接收方接收到PATH消息以后,根据业务特征和所需要的QOS计算出所需要的资源,回送RESV消息,消息中包含的参数就包括请求预留的带宽,延PATH的原路途返回,沿途的路由器接收到RESV操作后才执行资源预留操作。发送方接收到RESV消息以后才发送用户数据。

一.在H.323协议中如何实现RSVP功能:
  对于一个H.323或者是SIP的多媒体通讯系统而言,为了保证实时通讯的质量,一般来说采用了很多方面来保证QOS,对于H.323来说方式没有SIP那样灵活,在H.323v3版本采用了一些几种方式来增强QOS保证:

a. 增强的RAS过程,在ARQ中指明了是否具备资源预留能力;
b. 增强的能力交换过程,收发端点都具备RSVP功能,通过能力交换过程可以双方具备RSVP能力(RSVP属于能力集合的一个部分),在OpenLogicalChannel原语中定义了一个参数qOSCapability来表示;
c. 增强的逻辑信道能力在逻辑信道打开过程中包含Path和Resv两个过程
下面我们用图来表示逻辑信道的打开过程和资源预留过程:


1. 发送端点向接受端点发送OpenLogicalChannel消息在qOSCapability中标明该信道的RSVP参数和综合业务类别。
2. 接收端点创建RSVP会话(调用Rapi_session API)向发送端点发送OpenLogicalChannel Ack。
3. 在OpenLogical Ack中包含FlowControl=0,抑制当前的媒体数据流。
4. 4和5表示发送端点和接收端点执行RSVP过程。
5. 接收端点接收到ResvConfirm以后知道预留成功。
6. FlowControl为最大的比特率,当前的媒体数据流为最大。

  要注意的一点是由于通讯是双向的实际上述的过程发送和接收方完全要对掉所以上述的过程要执行两遍。

二.在SIP中实现RSVP功能:

  Vocal的SIP协议栈软件中提供了一个非常简便的实现RSVP的方式,当然按照这个方式实现RSVP是相当的不成熟的,很多参量在应用程序都没有反馈并且处理,仅仅是在路由器之间相互的汇报,不过这个简单的方式实现RSVP的构架,所以仍然有一定的使用价值。

  一般说来在SIP中实现RSVP的步骤如下:


(点击放大)

  在上图中实线的部分是SIP命令,虚线部分是RSVP消息

Vocal中的RSVP实现过程:
1. 首先是主叫部分发送INVITE命令,我们知道命令中包含有主叫的会话描述(这里我们称为Remote SDP);
2. 被叫部分此时处于OpRing的状态中接收到主叫的INVITE消息以后,根据主叫的INVITE消息和主叫的SDP,得到主叫的地址和主叫的RSVP端口(主叫的RTP端口);被叫调用setupRsvp子程序发送包含有数据流标识和数据业务流特征的PATH消息到主叫,具体发送的业务流Tspec特征如下:
//Sender Tspec的定义:
rapi_tspec_t *tspec_ptr = &(snd_tspec);
qos_tspecx_t *qos_tspec = &(tspec_ptr->tspecbody_qosx);
qos_tspec->spec_type = QOS_TSPEC;//发送方业务流特征标示
qos_tspec->xtspec_r = 10000;// Token Rate (B/s)//业务流量
qos_tspec->xtspec_b = 200;// Token Bucket Depth (B)//标记存储桶宽度
qos_tspec->xtspec_p = 10000;// Peak Data Rate (B/s)//突发流量
qos_tspec->xtspec_m = 200;// Minimum policed unit//
qos_tspec->xtspec_M = 200; /* default 65535 */ Maximum SDU size
tspec_ptr->len = sizeof(rapi_hdr_t) + sizeof(qos_tspecx_t);
tspec_ptr->form = RAPI_TSPECTYPE_Simplified;

  这里似乎和RSVP的--呼叫方发送PATH消息的精神有一些违背,是被叫方发送PATH消息,其实二者没有什么不同,首先主叫方,没有收到被叫方的SDP所以不能确定被叫方接收RSVP消息的端口和IP地址,其次,媒体流是双向的,双方都必须在网路上通过PATH--Reserve的方式预流资源。

3. 在完成了一系列SIP命令和状态的交换(RING,OK过程)以后,呼叫方开始准备发送ACK消息了,也就是处于操作OpFarEndAnswered()的时候,调用rsvpFarEndAnswered发送Reserve消息,为什么要在这个时候发送Reserve消息呢?因为主叫在下一个过程(收到ACK消息后,打开RTP通道之前)的时候,已经保证了所有的主叫到被叫之间的路由器都已经收到了PATH预留消息,

4.第5,6两个消息是主叫端点向被叫端点之间的路由器发送PATH消息,并且接收对端的RESV消息的过程。和1,2,3的过程基本上一样,最后在双方的RTP通道打开之前,主叫/被叫之间的路由器实现稳定状态,也就是都收到主叫和被叫的资源预留的信息。

5.在被叫端点主要调用的函数:
a. void setupRsvp(SipSdp& localSdp, SipSdp& remoteSdp)
主要由被叫端调用,用于在收到主叫发送过来的INVITE消息以后根据主叫的SDP回送被叫资源预留PATH消息。
b. void rsvpFarEndAnswered(Sptr localSdp, Sptr remoteSdp)
主要由主叫端调用,用于在向被叫端发送ACK消息前向被叫发送RESV消息和开始主叫资源预留PATH消息。
c. void rsvpAckHandler(Sptr localSdp, Sptr remoteSdp)
主要由被叫端调用,用于在收到主叫发送过来的ACK消息以后根据主叫的SDP回送主叫资源预留RESV消息。

6. 如何改进Vocal的RSVP机制使它在广域网上应用:

  一.对于目前在Vocal中对于RSVP的处理过程是非常简单的,至少在用户端都没有对AdSpec和Tspec做任何具体的运算,仅仅是交给路径上的路由器去预留资源,这样做如果是一个简单的没有太复杂的网络状态的区域网内部,采用这种方法当然是无可厚非的,不过如果是在有复杂网络状态的广域网上这样就可以说是不是很行得通了,一般来说在主干网络上会运行DiffServ的机制(所有的流都分组为多个服务类别的方式),这样在骨干网上的RSVP消息当然就会被忽略,所以我们的PATH和SESV消息都要实现对Diffserv的映射,换一句话来说,就地让骨干网看起来象RSVP的一个节点,一般来说我们把DTOT(子网络到主干网络的传播延迟 在RFC2205中有定义)改变成透过骨干网的传播延迟和平均排队延迟的值(这个是由主干网罗入口/出口路由器做的工作),对于RESV来说上沿着已经建立的途径传递,那么这个问题就不存在了。

  另外有几个情况是在如果主干网络使用DiffSev需要注意的:

a. 如果有一个流不符合Tspec时--而这个时候路由器已经为所有的入口和出口规划了每一条虚链路的时候,一个不符合Tspec的流就足以毁坏同一类别所有其他留所争取的服务质量,例如入口处归纳低质量的视频/音频流时候,出现了高质量的视频流。
b. 分散的/突发的流合并到平缓的流中时候。
  不过一般来说每个路由器都具备检查流的Tspec的能力,特别是作为主干网络入口的路由器(例如一些大的网络(BGP/EBGP)的入/出口地方)。在运行视频会议或者是其他突发流很多的恶劣工作状况的时候:

  我们前面已经反复地阐述:在Vocal中只不过是实现了一个简单RSVP构架,最重要的一点就是它不能够实现软状态,也就是定期刷新消息的Tspec和Rspec,如果在视频信号的时候这样的情况出现得特别频繁,由于视频信号并不总是处于一种稳定的平缓的状态传输,以及当路由改变的时刻,RSVP消息需要能准确的沿着新的路由往复(这种情况是常常出现的,特别在大型网络中)。

  解决上述问题的途径首先就是要在RSVP建立保证服务预定,也就是要根据接收端根据发送端的AdSpec消息计算预留的带宽(而在Vocal中基本上没有处理AdSpec),AdSpec中参加带宽运算的主要是两个参数:Dtot和Ctot,第一个参数是最小路径延迟,第二个是路径带宽,通过这两个参数根据公式D=(b(存储桶深度)+Ctot)/p + Dtot计算出端到端之间的延迟:

例如:
PATH流的初始特征:
Tspec(p=10mbps,L=2kbps,r=1mbps,b=32kbps) AdSpec(Ctot=0,Dtot=0)
经过第一个路由器:
Tspec(p=10mbps,L=2kbps,r=1mbps,b=32kbps) AdSpec(Ctot=11,Dtot=0.05s)
经过第二个路由器:
Tspec(p=10mbps,L=2kbps,r=1mbps,b=32kbps) AdSpec(Ctot=55,Dtot=0.1s)
现在我们来计算Resv中Rspec项目:
最长的延迟为0.1s的延迟,我们在Rspec中所计算的预留带宽必须符合这个要求,那么根据公式:
D=(b+Ctot)/p + Dtot

b:存储桶深度
计算出的D为0.185S我们在根据这个公式来计算预留带宽
R=((p-r)(L+Ctot)+(b-L)p)/((t-Dtot)(p-r)+b-L)
r:业务流量
t:所需要的延迟

  注意:所需要的延迟在0.1-0.185之间变化,这样我们通过上述公式得出一个比较确切的R值R=1.66Mbps。所以Rspec为:R=1.66Mbps;松弛项(S):用于指示Qos的富裕量,如果所有的路由器按照R预留,那么整个路径上的端到端的延迟会比要求的时延要少S毫秒:这里可以选择0.05S,具体的松弛项计算可以参看RFC2205定义。

  上面我们解决了服务预定的问题,但是它只不过让我们的终端程序运行进一步合理化,可以正确的规划需要预留的带宽,但是中间还是有关键问题没有解决,也就是我们上面说的软状态--定期刷新Tspec和Rspec,以及探测/感知主干路由的变化

  从程序上实现这些事实上并不困难,我们在打开媒体通讯的RTP信道以后,可以用一个进程定期的发送PATH和RESV消息,让流的接收者进行定期刷新,特别是在视频通讯阶段(采用H.263算法)出现帧间帧的时候,数据的流量必然会大大增加,这个时候,如果提前刷新预留的状态,那么我们可以在初期就避免网络出现阻塞的问题,当然,如果流量超过了路径所承受的标准,那么必然会依靠增加S(松弛度)来阻塞数据包,这个时候,必然通讯会出现一个比较明显的延迟,不过根据实验结果表明,这些还是可以接受的。

  如果IP路由发生了变化,那么上述的解决办法同样的适用,定期传送RSVP的消息可以重新根据流的路径进行新的定义,所以他们也会沿着新的路径进行传输,所以沿着相反路径的RESV消息将试图沿着新的路由进行预定,旧的预定就会超时然后取消。

  但是我们如果考虑到主干网络使用DiffServ就不会那么乐观了(当然主干网络的路由变化不会那么剧烈),主干网络上PATH项目中变化的部分就是AdSpec中的Dtot/Ctot,我们在上面已经说了如何定义Dtot的内容(子网络到主干网络的传播延迟的计算 在RFC2205中有定义)改变成透过骨干网的传播延迟和平均排队延迟的值,所以目前来说如果主干网络是功能强大的千兆路由器组成,那么PATH中的Dtot和Ctot可以在中继的时候得到更新,如果是ATM或者是MPLS网络的话,出入口也可以得到更新,这样的话你事实上完全不必操心。

  不过不管怎么说上述的数值如果是周期性计算并在RESV消息中更新的话,必然大大的加大路由器的运算开销,特别在跨洋多点视频会议的时候,这样用户接入服务供应商区域网入口路由器可能会发生"饿死"的情况(没有用户数据时候反而被大量无用的RESV所淹没),所以在使用主干网络的时候,我们必须有一个机制探测主干网络的传播延迟并且通报给服务供应商区域网入口路由器,最好是主干入口路由器本身就具备这样的功能,进一步简化主干网络为一个简单的RSVP节点,变成由主干网络的接入路由器通告服务供应商的路由器端对端的延迟 ,这样把计算的过程交给主干路由器*,而服务供应商区域网入口路由器只负责更新AdSpec,这样效率就可以得到大大的提高,不过协议的复杂性就增加了不少。**

3.2.8用户处于通话的StateInCall状态:

StateInCall::StateInCall()
{
addEntryOperator( new OpStartAudioDuplex );
addOperator( new OpAck );
addOperator( new OpConfTargetOk );
addOperator( new OpFwdDigit );
addOperator( new OpTerminateCall );
addOperator( new OpEndCall );
addOperator( new OpReInvite );//多方会议和呼叫等待
addOperator( new OpStartCallWaiting );

if ( UaConfiguration::instance()->getXferMode() != XferOff )
{
addOperator( new OpSecondCall );
addOperator( new OpRecvdXfer );
}

addExitOperator( new OpStopAudio );
}
  无论是主叫还是被叫,最后情况下都会进入到StateInCall状态中去,顾名思义,这个状态是打开媒体流(RTP/RTCP)通道,并且开始语音通讯的状态.

3.2.8.1 OpStartAudioDuplex主叫打开媒体通道端口,向被叫发送媒体信息

  首先我们来看一下在这个状态中加入的第一个操作OpStartAudioDuplex,作为主叫端,它打开了主叫的RTP/RTCP端口,开始向被叫发送媒体消息。

const Sptr < State >
OpStartAudioDuplex::process( const Sptr < SipProxyEvent > event )
{
Sptr < UaCallInfo > call;
call.dynamicCast( event->getCallInfo() );
assert( call != 0 );
Sptr < SipSdp > remoteSdp;
Sptr < SipSdp > localSdp;

  注释:*主干路由器大部分时候和用户接入服务供应商区域网入口路由器采用静态路由的方式接入,所以我们预想对于主干路由器来说事实上只要仅仅发送通告给服务供应商区域网入口路由器就可以了,

  **这个问题我们曾经在IETF的讨论中和很多通讯工程师进行讨论,完全可以增加此类的限制,不过上述的情况有不少人认为发生的可能性不大,我个人一直认为随着服务供应商所提供的多媒体业务的进一步扩展,这种危机可能在近几年就会要爆发出来。


//取得引起振铃的INVITE消息
Sptr < InviteMsg > inviteMsg = call->getRingInvite();

//取出当前的Contact字段中所引发的INVITE消息,在OpInviteUrl::process中会定义这//个当前的SipCOntact字段。
InviteMsg invMsg = call->getContact()->getInviteMsg();
//两者相比较,检验是否引起震铃的INVITE消息和当前Contact字段中的INVITE消息
//是否相同,相同的话就把对端和本地的SDP取出,
if ( *inviteMsg == invMsg )
{
remoteSdp = call->getRemoteSdp();
localSdp = call->getLocalSdp();
}
else
{//不相同的话以远端第二次回送的震铃消息为准,取出SDP
remoteSdp = call->getRemote2Sdp();
localSdp = call->getLocal2Sdp();
}
assert( localSdp != 0 );
//挂上本地设备事件的处理队列
Sptr < UaHardwareEvent > signal
= new UaHardwareEvent( UaDevice::getDeviceQueue() );
//定义所要处理的事件类型
signal->type = HardwareAudioType;
//向当前声音设备(Sounnd Card或者是Phone Card等等)控制台发送请求
struct HardwareAudioRequest* request = &(signal->signalOrRequest.request);
//开始声音发送,ResGwDevice::processSessionMsg中会调用这个状态的检测,并且会打
//开声音设备发送媒体信息,调用ResGwDevice::audioStart,稍后介绍。
request->type = AudioStart;
… …
//设定远端的Rtp接收发送端口
request->remotePort = remoteSdp->getRtpPort();
//设定本地的Rtp接收发送端口
// Set local host and port
request->localPort = localSdp->getRtpPort();
strcpy( request->localHost,
localSdp->getConnAddress().getData(lo) );
……
request->echoCancellation = true;
//设定RTP包的发送速率
request->rtpPacketSize = getRtpPacketSize(*localSdp);
… …
//停止震铃回送。
request->sendRingback = false;
//在本地设备事件的处理队列挂上当前的处理请求
UaDevice::getDeviceQueue()->add( signal );

return 0;
}

(未完待续)

在Vovida的基础上实现自己的SIP协议栈(五)

作者联系方法:lu_zheng@21cn.com

作者供稿 CTI论坛编辑