Kamailio实战
上QQ阅读APP看书,第一时间看更新

1.3 SIP

SIP(Session Initiation Protocol,会话初始协议)是一个控制发起、修改和终结交互式多媒体会话的信令协议。它是由IETF(Internet Engineering Task Force,Internet工程任务组)在RFC 2543[7]中定义的。最早发布于1999年3月,后来在2002年6月又发布了一个新的标准RFC 3261[8]

除此之外,还有大量相关的或是在SIP基础上扩展出来的RFC,如关于SDP的RFC 4566[9]、关于会议的RFC 4579[10]等。

关于SIP,笔者找到对Henning Schulzrinne[11]教授的一段采访[12]。采访中他回忆了SIP协议的诞生过程。

我最开始对音频和视频编码产生兴趣,是因为我的硕士论文与此有关。那时候PC声卡还没有广泛用于真正的音频采集,所以我只能在一台PDP-11/44迷你计算机上使用A/D转换器对信息进行转换。后来互联网和早期的SUN工作站(如SPARC)可以传输实时音视频了,我开始致力于开发和标准化用于在互联网上传输音视频的协议,如RTP和SIP。移动设备的广泛使用造就了“无处不在的系统”,包括现在广为人知的IoT。

我当时正在致力于一个被称为DARTnet的博士科研项目,这个项目可以在一个试验性的叠加网络中传输音频和视频,该网络当时使用了一个新的协议——ST-II(现在已弃用)。网络节点是早期可以实现网络路由器和叠加网络的SPARC工作站(来自SUN公司),这个系统需要配合相关工具和协议才能运行。我开始积极投入到音视频的各种技术工作中——从创造传输语音和视频的协议到开发支持流畅播放的算法。当时IETF为了支持组播骨干网(一种早期可以向千百个观众传输音视频的技术),开始开发所需协议,我也参与其中。这些工作促成了RTP的开发工作。RTP最初是针对组播设计的,后来被用于大多数“基于标准”的音视频传输,而不只限于组播。随着这项工作不断成熟,包括我在内参与这项工作的很多人认为,需要更好的方法来启动音视频会话,SIP因此诞生了。当时没人在意我们这个小组的研究,因为“真正的”电信工程师们正在研究时分复用电路交换机,而SIP是基于IP交换的。这反而成了我们的优势,没有人来指手画脚,我们的研究进程也较快。随着有线和无线行业开始认识到需要转入IP网络,SIP所需的底层协议标准实际上已经准备就绪了。

下面介绍SIP中一些基本概念。

1.3.1 SIP基础

SIP是一个基于文本的协议,从这方面来讲,SIP与HTTP、SMTP类似。一个典型的SIP请求如下:

请求由三部分组成:INVITE表示发起一次呼叫请求;seven@xswitch.cn为请求的地址(Request URI,RURI),又称SIP URI或AOR(Adress of Record,用户的公开地址);SIP/2.0是版本号。

SIP URI类似于一个电子邮件地址,其格式为“协议:名称@主机”。“协议”有SIP和SIPS(后者用于安全通信,如sips:seven@xswitch.cn)两种;“名称”可以是一串数字形式的电话号码,也可以是字母表示的名称;而“主机”可以是一个域名,也可以是一个IP地址。

一个SIP请求会得到一个响应,响应消息的第一行如下所示:

其中中间部分为状态码,由三位数字构成:

□ 1××表示临时响应。

□ 2××表示成功响应。

□ 3××表示重定向。

□ 4××表示客户端引起的错误(如请求一个不存在的地址时就会收到著名的404状态码)或需要客户端进一步提供的认证信息(如401等)。

□ 5××表示服务器端的错误(服务器脚本出错等)。

□ 6××表示全局错误。

其中临时响应(1××)不是必需的,其他所有的响应都称为最终响应,每一个SIP请求都应该有一个最终响应。

起始行以下为SIP消息头,如From和To等。有些SIP消息(如INVITE、200 OK)中会有更进一步的描述信息,这类信息称为正文(Body)。Body是可选的,并不是所有的请求或响应中都有Body字段。如果Content-Length为0或不存在,就没有Body。消息头中的Content-Length表示正文的长度。在所有的SIP头域结束后,会有一个空行,它标志着SIP头部的结束及消息正文的开始。

熟悉HTTP的读者会发现,SIP其实与HTTP类似。事实上SIP就是参考HTTP设计的,重用了很多HTTP中的内容,如Digest(摘要)认证等。HTTP是互联网最重要的协议,而SIP也在通信领域很快替代了二进制的H323协议成为主流。

1.3.2 SIP的基本概念和相关元素

SIP是一个对等的协议,类似于P2P。不像HTTP那样是“客户端-服务器”的结构,也不像传统电话那样必须有一个中心的交换机,SIP甚至可以在不需要服务器的情况下进行通信,只要通信双方都知道对方的地址(或者只有一方知道另一方的地址)。如图1-3所示,Bob拿起话机给Alice发送一个INVITE请求,说“一起吃饭吧”,Alice说“好的”,电话就接通了。

图1-3 SIP点对点通信

在SIP网络中,Alice和Bob的话机都称为UA[13]。UA是在SIP网络中发起或响应SIP处理的逻辑功能。UA是有状态的,也就是说,它维护会话(或称对话)的状态。UA有两种功能。一种是UAC(UA Client,用户代理客户端),它是发起SIP请求的一方,如图1-3中所示的Bob。另一种是UAS(UA Server,用户代理服务器端),它是接受请求并发送响应的一方,如图1-3中所示的Alice。由于SIP是对等的,所以当Alice呼叫Bob时(有时候Alice也会主动约Bob一起吃饭),Alice的话机就称为UAC,而Bob的话机会执行UAS的功能。一般来说,UA都会实现上述两种功能。

设想Bob和Alice是经人介绍刚刚认识的一对恋人。因为他们彼此还不熟悉,所以Bob想请Alice吃饭还需要一个中间人(M)传话,而这个中间人就称为代理服务器(Proxy Server),如图1-4所示。还有另一种中间人称为重定向服务器(Redirect Server),它以类似于这样的方式工作:中间人M告诉Bob,我也不知道Alice在哪里,但我爱人知道,要不然我告诉你我爱人的电话,你直接问她吧,我爱人叫W。这样,M就成了一个重定向服务器(把Bob对他的请求重定向到W,这样Bob接下来要直接联系W),而W是真正的代理服务器。这两种服务器都是UAS,它们主要为一对欲通话的UA提供路由选择功能,如图1-5所示。

图1-4 代理服务器示意图

还有一种被称为注册服务器的UAS。试想这样一种情况:Alice还是个学生,没有自己的手机,但她又希望Bob能随时找到她,于是当她在学校时就告诉中间人M说她在学校,如果有事找她可以打宿舍的电话;而当她回家时也通知M说有事打家里电话,哪天去姥姥家了,Alice也要把姥姥家的电话告诉M。总之,只要Alice换一个新的位置,它就要向M重新“注册”新位置的电话号码,以便M能随时找到她,这时候M就相当于一个注册服务器。注册服务器的另一个功能是“寻址”,比如Bob想要找Alice,那么他就要问M,用哪个电话号码可以联系到她,这时候M起的作用就是寻址,如图1-6所示。

图1-5 重定向服务器示意图

图1-6 注册服务器、寻址服务器示意图

还有一种特殊的UA称为B2BUA。需要指出,其实RFC 3261并没有定义B2BUA的功能,它只是实现一对UAS和UAC的串联。我们前面提到的FreeSWITCH就是一个典型的B2BUA。

此外,还有一个概念——边界会话控制器(Session Border Controller,SBC)。它主要位于一堆SIP服务器的边界,用于打通内外网的SIP通信、隐藏内部服务器的拓扑结构、抵御外来攻击等。SBC可能是一个代理服务器,也可能是一个B2BUA。其应用位置和拓扑结构如图1-7所示。

图1-7 SBC位置示意图

与FreeSWITCH相比,Kamailio是一个典型的代理服务器。不过,Kamailio也可以做注册服务器、SBC等。一般来说,Kamailio只处理SIP,但在某些场景中,如NAT穿越、拓扑隐藏等,也会配合MediaProxy或rtpengine(这两个都是媒体代理)一起工作。

1.3.3 SIP的基本方法和头域

SIP定义了6种基本方法,如表1-1所示。

表1-1 SIP的基本方法

除此之外,SIP还定义了一些扩展方法,如SUBSCRIBE、NOTIFY、MESSAGE、REFER、INFO、PRACK[14]等。

另外,无论是基本方法还是扩展方法,所有SIP消息都必须包含表1-2所示的6个头域。

表1-2 SIP消息必备头域

1.3.4 SIP URI

如图1-8所示,192.168.1.9是Kamailio服务器,而Bob和Alice分别在另外两台机器上。

图1-8 Bob和Alice分别在另外两台机器上的情况

在图1-8所示情况中,Alice注册到Kamailio,Bob呼叫她时,使用她的服务器地址(因为Bob只知道服务器地址),即sip:Alice@192.168.1.9。Kamailio接到SIP呼叫请求后,查找本地数据库,发现Alice的实际地址(Contact地址,即联系地址)是sip:Alice@192.168.1.200,进而建立呼叫。

SIP URI除使用IP地址外,也可以使用域名,如sip:Alice@example.com。域名将使用DNS的A记录(对于IPv4)或AAAA记录(对于IPv6)进行查询,更高级及更复杂的配置则可能需要DNS的SRV记录,在此就不做讨论了。

这里再重复一下,Bob呼叫Alice时,Bob是主叫方,他已经知道服务器的地址,因此可以直接给服务器发送INVITE消息,因而他是不需要注册的[15]。而Alice不同,她是作为被叫的一方,为了让服务器能找到她,她必须事先通过REGISTER消息将自己“注册”到服务器上。

1.3.5 SDP和SOA

SIP负责建立和释放会话,一般来说,会话会包含相关的媒体,如视频和音频。媒体数据是由SDP(Session Description Protocol,会话描述协议)来描述的。SDP一般不单独使用,它与SIP配合使用时会放到SIP的正文(Body)中。

会话建立时,需要媒体协商,这样双方才能确定对方的媒体能力以交换媒体数据。Kamailio不处理媒体,但有时也可以配合rtpengine等做媒体代理、实现转码、完成NAT穿越等。在此,我们通过一个简单的FreeSWITCH例子介绍一下SDP是如何工作的。

我们来看一个FreeSWITCH参与的单腿呼叫的例子。客户端607呼叫FreeSWITCH默认的服务echo,它是一个回声服务,呼通后,主叫用户不仅能听到自己的声音,还能看到自己的视频(如果有的话)。为了更直观一些,我们使用Wireshark进行抓包和分析。图1-9显示了该SIP呼叫的流程。

由图1-9可知,客户端(192.168.1.118)呼叫FreeSWITCH(192.168.1.9),INVITE中带了SDP消息。其认证过程与我们上面讲到的类似。最后,FreeSWITCH回复200 OK对通话进行应答,然后双方互发RTP媒体流(G711A,即PCMA的音频和H264的视频)。

图1-9 带SDP的SIP呼叫

在图1-9中还可以看出,客户端的SIP端口号是35526,音频端口号是50452,视频端口号是52974;FreeSWITCH的端口号则分别是5060、31988和19008。到后面我们会在SIP消息中找到这些。

下面是一个完整的SIP INVITE消息:

对于SIP头部我们前面已经了解得差不多了。其中的Content-Length跟HTTP中的类似,表示正文的长度。这里的正文类型是用Content-Type表示的,在这里它是application/sdp,表示正文中是SDP消息。同样,一个空行把SIP头部(Header)与SIP正文(Body)部分隔开。(SIP头部的结束是以“\r\n\r\n”为标志的。)

下面我们主要讨论SDP部分。

□ v(Version),表示协议的版本号。

□ o(Origin),表示源。各项的含义依次是username、sess-id、sess-version、nettype、addrtype、unicast-address。

□ s(Session Name),表示本SDP所描述的Session的名称。

□ c(Connection Data),连接数据。两个字段分别是网络类型和网络地址,以后的RTP流就会发到该地址上。注意,在N AT环境中我们要解决透传问题,就是要看这个地址,这在后文中也会讲到。

□ b(Bandwidth Type),带宽类型。

□ t(Timing),起止时间。0表示无限。

□ m=audio(Media Type),媒体类型。audio表示音频,50452表示音频的端口号,应该跟图1-9所示一致;RTP/AVP是传输协议,这里是RTP;后面是支持的Codec类型,与RTP流中的Payload Type(负荷类型)相对应,在这里分别是8、0、98和101。8和0分别代表PCMA和PCMU,它们属于静态编码,因而有一一对应的关系。而对于大于95的编码都属于动态编码,需要在后面使用“a=”进行说明。

□ a(Attributes),属性,用于描述上面音频的属性,如本例中98代表8000Hz的ILBC编码,101代表RFC2833 DTMF事件。a=sendrecv表示该媒体流可用于收和发,其他的还有sendonly(仅收)、recvonly(仅发)和inactive(不收不发)。

□ m=video(Media Type),媒体类型。video表示视频。可以看出它的端口号52974也跟图1-9所示一致。而且H264的视频编码对应的也是一个动态Payload Type,在本例中是123。

FreeSWITCH收到上述的请求后,进行编码协商。这里我们省去SIP交互的中间环节,直接看200(应答)消息:

SIP头域我们就不多讲了,列在这里只是为了让消息完整。下面直接看200返回的SDP数据,我们也能找到音视频的IP地址是192.168.1.9,端口号分别是31988和19008。该SDP也携带了FreeSWITCH协商后的编码PCMA(8)以及a=ptime项,ptime表示RTP数据的打包时间,其实这里也可以省略,默认就是20(毫秒)。至此,双方都有了对方的RTP地址和端口信息,它们就可以互发RTP流了。

媒体流的协商过程称为SOA[16](Service Offer and Answer,提议/应答),即首先有一方提供它支持的Codec类型,另一方基于此进行选择。如本例中,607先提议:“我支持PCMA、PCMU和ILBC编码,你看咱俩用哪种通信比较好?”FreeSWITCH回复说:“那我们就用PCMA吧。”然后双方就可以互发RTP流进行媒体交换了。当然,根据现有的媒体协商标准,FreeSWITCH也可以说:“我支持PCMA和PCMU两个编码,随便你发,用哪个都行。”在这种情况下,双方就必须准备好能收发两种编码的RTP流,不管对方用哪个发,都必须能正确接收。不过,到目前为止,FreeSWITCH还不支持同时回复两个编码,但是如果FreeSWITCH是请求方,对方回复了两种编码,FreeSWITCH是可以正确支持的。虽然应答方回复多个编码会增加复杂性,但标准就是这么规定的。

1.3.6 SIP承载

大家已经熟知,HTTP是用TCP承载的[17],而SIP支持TCP和UDP承载(当然也支持TLS等其他承载方式)。事实上,RFC 3261规定,任何SIP UA必须同时支持TCP和UDP。我们常见的SIP都是用UDP承载的。由于UDP是面向无连接的,故在大并发量的情况下与TCP相比,可以节省TCP由于每个IP包都需要确认带来的额外开销[18]。不过,在SIP包比较大的情况下,如果超出了IP层的最大传输单元(MTU,即Maximum Transmit Unit,通常最大是1500字节)的大小,在经过路由器时可能会被拆包,使用UDP承载的SIP消息就可能会发生丢失、乱序等,这时候就应该使用更可靠的传输层协议TCP。

在需要对SIP加密的情况下,可以使用TLS[19]。TLS是基于TCP实现的。

在新的网络时代,又出了一个新的草案,名为SIP over WebSocket[20]。当前,主流浏览器如Chrome和Edge已经实现了WebSocket,从而可以通过它承载SIP;而这些浏览器大多也实现了WebRTC[21],这意味着它们可以通过Web浏览器与普通的SIP话机(甚至PSTN)进行音视频通话。SIP over WebSocket的承载为SIP/WS或SIP/WSS,其中后者是基于TLS实现的。WebRTC必须加密后才能传输,所以网上实际在用的信令协议都是SIP/WSS。

1.3.7 事务、对话和会话

Kamailio在大多数情况下都被用作SIP代理(SIP Proxy),典型的应用场景是处理用户注册、呼叫路由、负载均衡等。要理解SIP代理,我们还需要进一步理解如下概念。

1.事务

事务(Transaction)是指一个请求消息以及这个请求对应的所有响应消息的集合。对于INVITE事务来讲,除包含INVITE请求和对应的响应消息外,在非成功响应的情况下,还包括ACK请求。Via头域中的branch参数能够唯一确定一个事务。branch值相同的,代表为同一个事务。事务是由方法(事件)来引起的,一个方法(Method)的建立和到来都将建立新的事务。实际上当收到新消息时,就是根据branch来查找对应事务的。

一个事务由5个必要部分组成:From、To、Via头域中的branch参数、Call-ID和CSeq。这5个部分一起识别某一个事务,如果缺少任何一部分,该事务就会设置失败。事务是逐一跳转(Hop by Hop,每一个路由节点称为一跳,即一个Hop)的关系,即路由过程中交互的双方包括一个请求及其触发的所有响应(即若干临时响应和一个最终响应)。事务的生命周期用于表示从请求产生到收到最终响应的完整周期。

2.对话

对话(Dialog)是两个UA之间持续一段时间的点对点的SIP连接,它使UA之间的消息变得有序,同时给出请求消息的正确的路由。Call-ID、from-tag以及to-tag三个值的组合能够唯一标识一次对话。对话只能由INVITE或SUBSCRIBE来创建。

对话是点到点(Peer to Peer)的关系,即真实的通信双方,其生命周期贯穿一个点到点会话的始终。

3.会话

会话(Session)是一次通信过程中所有参与者之间的关联关系以及它们之间的媒体流的集合,是端到端的。只有当媒体协商成功后,会话才能被建立起来。

如图1-10所示,根据前面的描述,图中有1个SIP对话和3个事务。从INVITE到200 OK是一个事务,从BYE到200 OK则是另一个事务。ACK是一个单独事务。在这个场景中,会话和对话是重合的。

图1-10 事务与对话关系图

图1-11描述的是两个UA经过代理服务器转发的情况。事务和对话只存在于直接相连的UA间,而会话是端到端的——Alice和Bob之间的通话是一个会话。

图1-11 事务、对话与会话关系图

4.CSeq

CSeq的生存期是一个会话。CSeq用于将一个会话中的请求消息进行序列化,以便对重复消息、“迟到”消息进行检测,以及对响应消息与相应请求消息进行匹配。它包含两部分:一个32位的序列号,一个请求方法。

通常在会话开始时确定一个初始值,其后在发送消息时将该值加1。主叫方与被叫方各自维护自己的CSeq序列,互不干扰。CSeq序列有点像TCP/IP中IP包的序列。

一个响应消息有与其对应的请求消息相同的CSeq值。

注意:SIP中CANCEL消息与ACK消息是比较特殊的。CANCEL消息的CSeq中的序列号总是跟其将要撤销(Cancel)的消息相同,而对于ACK消息,如果它所要确认的INVITE请求是非2××响应,则ACK消息的CSeq中的序列号与对应INVITE请求的相同;如果是2××响应,则不同,此时ACK被当作一个新的事务。

Call-ID、from-tag以及to-tag这三个值相同代表是同一个对话;branch值相同代表是同一个事务,否则代表不同的事务。

1.3.8 Stateless与Stateful

作为一个代理服务器,关键的作用就是路由SIP消息,即控制SIP消息从哪里来、到哪里去。当然,如果有必要的话,可以在中间修改SIP消息。

SIP代理服务器有两种工作状态——Stateless与Stateful,即无状态和有状态。在无状态情况下,代理服务器只是机械地路由消息,将收到的消息根据一定的规则转发到下一跳,它不关心会话、对话和事务。在这种情况下代理服务器不会维护状态机,因而比较轻量级,但同时,对于错误处理和计费应用来讲会有诸多限制。

在有状态的情况下,代理服务器在收到请求消息(如INVITE)时会启动一个状态机,跟踪一个事务,一直到收到200 OK或其他最终响应。所以,如果一个代理服务器在收到200 OK消息时知道与之关联的INVITE消息,那么该代理服务器就是有状态的。

在Kamailio中,无状态模式使用forward()转发消息,而有状态模式使用t_relay()转发,且可以在onreply_route()中处理响应消息。

在有状态的情况下,状态只维护在一个事务内,而不是整个对话。即状态只维护在从收到INVITE消息到200 OK消息的过程中,而不是在从INVITE到BYE的过程中。

有状态模式适合处理更复杂的应用,如语音信箱、会议、呼叫转移、计费等。

在Kamailio中,有状态模式的处理一般分为以下几个步骤。

(1)验证请求合法性。

□ 检查消息大小是否超长,消息是否完整。

□ 检查Max-Forward头域,看是否有循环请求。

(2)路由消息预处理。如果有Record-Route字段则对其进行处理。

(3)确定处理请求目的地时是否涉及如下问题。

□ 目标是本地注册用户吗?(可以在本地数据库中查到。)

□ 本机是最终目的地吗?

□ 是否需要转发到外部的域(其他服务器)?

(4)消息转发。调用t_relay()进行转发。Kamailio将会自动处理所有与状态相关的工作,如重发等。

(5)响应处理。如果收到响应消息,则进行处理,一般情况下这些都是自动完成的,但也可以在onreply_route()里进行处理,如“遇忙转移”业务,可以在收到“486 Busy Here”消息时,转到另一个号码或进入语音信箱进行处理。

1.3.9 严格路由和松散路由

Strict Router和Loose Router分别称为严格路由和松散路由。松散路由是SIP Version 2中才有的概念。

我们可以看到,在Router字段中设置的SIP URI经常有一个lr的属性,例如<sip:example.com;lr>,这就是表示这个地址所在的代理服务器是一个松散路由,如果没有lr属性,它就是一个严格路由。

松散路由实际上表示代理服务器依据RFC 3261处理Route字段的规则,而严格路由表示Proxy Server根据RFC 2357处理Route字段的规则。严格路由要求SIP消息的Request URI为其自身的地址。具体步骤如下。

(1)松散路由和严格路由首先都会检查Router字段的第一个地址是否为自己,如果是,则从Router字段中删除自己。

(2)严格路由在发往下一跳时将使用Router字段中的下一跳地址更新Request URI。

(3)松散路由首先会检查Request URI是否为自己。如果不是,则不做处理;如果是,则取出Route字段中最后一个地址作为Request URI地址,并从Route字段中删去最后一个地址。

(4)松散路由还会检查下一跳是否为严格路由。如果不是,则不做处理;如果是,则将Request URI添加为Route的最后一个字段,并用下一跳严格路由的地址更新Request URI。

由上可见,后面两步其实是松散路由为了兼容严格路由而做的额外工作。两者最大的区别体现在Request URI会不会变,如图1-12及图1-13所示。

图1-12 严格路由

图1-13 松散路由

1.3.10 Record-Route

当一个代理服务器收到一个SIP消息时,它可以决定是否留在SIP传输的路径上,即后续的SIP消息是否还要经过它。比如在A呼叫B时,如果代理服务器只起到“找到B”的作用,则它可以将第一个消息原样传送,B回送的消息将可以不经过代理服务器而直接回到A上,这种方式称为Forward,如图1-14所示。

图1-14 Forward示意图

如果代理服务器想保留在SIP路径上,则它在将消息转发到下一跳之前要把它自己的地址加到Record-Route头域中。那么,当B在回复响应消息的时候,就会将消息发回到Record-Route指定的地址上,这种方式称为Relay,如图1-15所示。

图1-15 Relay及Record-Route示意图

有了上述基础知识,下面我们就可以看看Kamailio的应用了。事实上,这些基础知识略显枯燥,也不是那么容易懂,大家可以先学习后面的内容,再回过来复习这部分,或许更有助于理解。