freeswitch 权威指南 --- 基础篇

FreeSWITCH 官网:https://signalwire.com/freeswitch
FreeSWITCH 中文站:https://www.freeswitch.org.cn
FreeSwitch 官方文档:http://rts.cn/freeswitch/FreeSWITCH-Explained
FreeSwitch:https://github.com/signalwire
FreeSwitch 案例:https://developer.signalwire.com/guides

内容来自 《FreeSWITCH 权威指南》:目录:https://juejin.cn/post/7020580794829635591
代码下载:https://book.dujinfang.com/download.html

1、freeswitch 基础

FreeSWITCH 新手指南

https://www.freeswitch.org.cn/blog/2009/11/beginners-guide/

FreeSWITCH是一个开源的电话软交换平台,主要开发语言是C。它有很强的可伸缩性,从最简单的软电话到商业级的软交换平台几乎无所不能。它支持SIP、H323、WebRTC等通信协议。另外,它还支持很多高级的SIP特性,如Presence、BLF、SLA以及TCP、TLS和sRTP 等。它可以作为SBC使用和多协议网关使用,也可以作为B2BUA连接其它的VoIP系统,如OpenSIPS、Kamailio、Asterisk等。

FreeSWITCH支持各种带宽的语音编解码,支持 8K、16K、32K及48KHz的高清通话,并可以在桥接不同频率的语音时自动进行转换。它可以运行在32位及64位的Windows、macOS、Linux以及Solaris等平台上,伸缩性极强,不管是一个信用卡大小的Raspberry Pi,还是数十、数百核的大型Linux服务器,都能运行 FreeSWITCH。此外,它还可以运行于Docker容器及K8S云原生环境中。

它支持 TTS(Text To Speach)、ASR(Automatic-Speach Recogonition)以及VAD(Voice Activity Detection)等。允许你使用Lua、Javascript、Python等嵌入式脚本语言来控制呼叫流程,或者你也可以通过 Event Socket 与 C、Go、Ruby、Erlang、Python、Perl、Java 等任何你所熟悉的语言进行交互。

为了避免重复发明轮子,它使用了相当多的第三方软件库。同时,为了方便编译和安装,很多第三方库代码都集成到了FreeSWITCH源代码树中。

它使用一种模块化、可扩展性的结构,只有必需的功能和函数才会加入到内核中,从而保证了其稳定。作为一款开源软件,它最大的好处就是你可以拿过来自己编译进行,并根据你的需要来开发自己的模块。

如果从源代码编译安装 FreeSWITCH 有困难,也可以试一下 XSwitch,它是一个已经编译好的FreeSWITCH,并且有专业的图形管理界面。

最快的学习FreeSWITCH的方法是使用 这个Docker镜象。如果不使用Docker镜象,也可以通过以下方式安装和使用FreeSWITCH。FreeSWITCH最理想的运行平台是64位的Linux。FreeSWITCH核心开发者大部分都使用 Debian。

  • 在Debian上安装FreeSWITCH的方法见:Debian | FreeSWITCH Documentation 。如果系统提示你需要一个PAT,可以参考如果创建一个FreeSWITCH PAT
  • 如果你使用 Windows,可以到 files.freeswitch.org 下载已编译好的安装包。如果安装有 Visual Studio 的开发环境,也可以下载源代码自己编译。
  • 也可以下载源代码编译。如果你提交BUG时,官方也只对 Git master分支中的代码提供支持。而且,从源代码安装FreeSWITCH,不需要PAT。

安装 FreeSWITCH 前需要安装一些依赖。在不同的平台上,依赖不同的包,如:

  • Debian/Ubuntu:apt-get -y install build-essential subversion automake autoconf wget libtool libncurses5-dev
  • CentOS:yum install -y subversion autoconf automake libtool gcc-c++ ncurses-devel make

FreeSWITCH 最新的源代码将 Sofia-SIP 和 SpanDSP 移出了 FreeSWITCH 代码仓库,分离到了独立的仓库中,在安装 FreeSWITCH 之前需要单独安装:

最新的mod_verto模块也需要libks,源代码可以从以下地址获取:

最好参考一下 http://www.freeswitch.org.cn/Makefile 以确定你的平台上应该安装哪些包。当然,该文件不是永远能保证最新的。

编译安装:

./bootstrap.sh # 初始化源码环境和工具
./configure    # 配置,如果将FreeSWITCH装到特定位置,可以使用 --prefix 指定
make           # 编译
make install   # 安装

详见:Installation GuideFreeSWITCH Explained.

在这里可以找到FreeSWITCH中文语音包

通信中名词解释

  • 信令:是指在通信系统中用于控制和管理通信过程的消息和协议 ( 相当于IP网络中的各种数据包,传统的通信是各种脉冲信号),主要传输控制信号。它在通信网络中扮演着关键的角色,用于建立、维护和终止通信会话。在传统电话通信中,信令用于控制呼叫建立、终止和管理呼叫过程中的其他功能,如转接、保持、转移等。通常,信令由 呼叫控制协议(如SS7)传输,这些协议定义了消息的格式、传输方式和交互过程。信令在IP网络和移动网络中也起着重要作用。例如,在VoIP(Voice over Internet Protocol)通信中,SIP(Session Initiation Protocol)是一种常用的信令协议,用于建立、修改和终止语音和视频通话。
  • 媒体(Media):信令主要传输一些控制信号,而通信双方需要听到对方的语音数据,这些语音数据就称为 媒体(Media)。随着通信系统能力的提高及更加高级、智能的终端设备的出现,通信系统所能传输的媒体类型也越来越丰富,比较典型的有语音,还有视频、文字信息(短消息)、传真等。
  • 呼叫控制协议(Call Control Protocol):用于控制和管理通信呼叫的协议。它定义了在通信系统中建立、修改和终止呼叫的过程和规则。几种常见的呼叫控制协议:SS7(Signaling System 7)又叫 7号信令:SS7是一种在传统电话网络中使用的信令协议,用于控制电话呼叫的建立、终止和管理。它提供了各种功能,如呼叫转移、呼叫等待和呼叫保持等。

    SIP(Session Initiation Protocol)中文可以翻译为 "会话初始协议、会话发起协议,也可粗略的叫做 呼叫控制协议":SIP是一种基于IP的呼叫控制协议,广泛用于VoIP(Voice over IP)电话系统和多媒体通信。它用于建立、修改和终止语音和视频通话,并支持会话管理、身份验证和媒体协商等功能。
    H.323H.323是一组标准,用于在IP网络上进行实时语音和视频通信。它包含了一系列协议,包括呼叫控制协议(H.225)、媒体协议(H.245)和音频/视频编解码协议(如G.711、H.264等)。
    MGCP(Media Gateway Control Protocol)MGCP是一种用于控制媒体网关的协议,它用于管理IP电话网络中的呼叫控制和信令传输。MGCP将信令控制从媒体网关中分离出来,使得呼叫控制可以集中管理。
  • ISDN(Integrated Services Digital Network)是一种数字化的综合服务数字网。通过使用数字信号传输数据,相比于传统的模拟电话线路,提供了更高的传输质量和更快的数据传输速度。它采用了基于交换机的数字技术,允许用户同时传输语音、图像、数据和视频等。随着更先进的通信技术的出现,如宽带互联网和VoIP等,ISDN 的使用逐渐减少。然而,在一些特定的应用场景中,仍然存在使用ISDN的需求,如某些企业、远程地区和特殊行业等。
  • PSTN:公共交换电话网(Public Switched Telephone Network)的缩写,也被称为传统电话网络。它是一种基于传统电路交换技术的全球公共通信网络,用于传输模拟语音信号和数字数据。PSTN是由电话运营商建立和维护的,包括电信公司、运营商和其他通信提供商。它利用铜线、光纤和其他传输介质,通过交换机和交换设备进行电话呼叫的路由和连接。PSTN提供了传统的电话服务,允许用户通过电话拨号进行语音通信。
  • VoIP(Voice over Internet Protocol)是一种利用互联网协议(IP)传输语音和多媒体通信的技术。它将传统的电话通信转换为基于IP网络的数字通信方式。通过VoIP,语音信号被数字化并分割成数据包,然后通过互联网进行传输。这些数据包在传输过程中,可以与其他数据包一起通过相同的网络基础设施进行传输,如电子邮件、文本消息等。在接收端,数据包再次被重新组装为原始的语音信号。H.323  SIP(Session Initiation Protocol)都属于 VoIP 领域的通信信令。SIP 在VoIP系统中起着核心的作用,它提供了一种标准化的方式来建立和管理多媒体会话。通过 SIP,用户可以进行语音通话、视频通话和即时消息等实时通信。它已成为现代通信领域的重要协议之一。
  • PBX:私有分支交换机(Private Branch Exchange)的缩写,它是一种电话系统,用于在企业中 "管理、路由、连接" 电话通信。PBX系统允许内部免费通话、呼叫保持、转接电话、转移呼叫、语音邮件等功能。比较高级的小交换机还可以提供自动总机、三方通话、语音信箱等。它可以与 公共电话网络(PSTN)互联网电话服务(VoIP)相连,使组织内部和外部通信更加高效和灵活。传统的PBX系统通常是基于硬件设备的,包括电话线路、交换机、控制单元和接口板等。随着技术的发展,现代的PBX系统也有基于软件的解决方案,称为IP-PBX。IP-PBX利用互联网协议(IP)传输语音数据,可以集成传统电话系统和互联网通信,提供更丰富的功能和更低的成本。PBX系统广泛应用于各种组织和企业,包括办公楼、学校、医院、政府机构等。它可以提供通信管理、成本控制、安全性和便利性等优势,使得电话通信更加高效、灵活和可靠。用户或企业的PBX要想打通外面的电话,或者外面的电话需要打进来,需要走运营商提供的中继线,以接入到 PSTN 网上去。理解中继线的概念对于理解PBX以及PSTN是非常重要的,中继的接入方式决定了我们如何拨号,
  • IVR:是交互式语音应答系统(Interactive Voice Response system)的简称。它是一种自动化电话系统,通过语音和按键输入与来电者进行互动和信息交流。IVR系统通常被用于提供自助服务、电话银行、预约管理、调查问卷等场景。来电者可以通过按键或语音指令选择所需的服务或获取信息。IVR系统能够提高客户服务效率,减少人工操作的负担,并可根据来电者的需求提供个性化的服务。
  • IMS 是 IP多媒体子系统(IP Multimedia Subsystem)的缩写,它是一种基于IP网络的通信架构,通过引入IP技术,将传统的电信网络与互联网相结合,实现了各种通信方式的融合和交互。它提供了丰富的业务接口,支持语音通话、视频通话、实时消息、多媒体会议、群组通信等功能。IMS的设计目标是将传统的语音、视频、消息和数据业务融合到一个统一的平台中,使用户能够以各种方式进行实时通信和互动。IMS还支持移动网络和固定网络之间的互操作,使得用户可以在不同类型的网络上使用相同的服务和应用。

freeswitch 产生原因

FreeSWITCH 的默认配置就是一个家用或小型企业级的 PBX,它是由纯软件实现的,基于IP网进行通信,因而又称为 IP-PBX

用户或企业的 PBX 要想打通外面的电话,或者外面的电话需要打进来,需要走运营商提供的 "中继线路",运营商的 中继线路 再接入到 PSTN 网上去。这样就间接实现了 PBX 接入到 PSTN。中继的接入方式决定了我们如何拨号。

假设一家新开的公司,需要7部电话,于是向运营商(在 PSTN交换机上)申请了7条模拟中继线。实际上就是7条普通的电话线,只是运营商在PSTN交换机端对这7条线(也可以解释为7个号码)做了特殊的设置,将其逻辑上分为一个组,并为该组设了一个总机号假设是88888888(可以是一个虚拟的号码,或者是其中某一条中继线的真实号码)。而其他的中继线号则可能是44440001~44440007。现在,把这7条线都接上话机。如果有人呼叫88888888,则PSTN交换机会从7条线中自动选择一条空闲的线路呼入,因此某个电话会就会振铃。如果有多个电话呼入,只要同时呼入的电话不超过7个则公司的7部电话就都有机会振铃,因而可以同时对外为7个人同时提供服务。一般来说,当有电话呼入时,交换机有两种选线策略"顺序选线、循环选线"。所谓顺序选线,就是每次都从44440001 开始,寻找一条空闲的线路进行呼入;而循环选线则是每次都从上一次呼叫的下一个开始选起,使用这种选线方式,每个话机接到的电话数会比较平均。公司安装的7部电话的结构示意如图所示。

为维护企业形象,当有人呼出时,不管是从哪个分机呼出,都显示总机号 88888888。当然,也可以设置显示单线的号码(如44440004),这个要在 PSTN 交换机 端设置,一旦设置后,用户端不能动态更改。一个月后,公司发展到 21个人,因此需要 21 部电话。但由于一般不会出现所有人同时都在打电话的情况,故安装21条线有些浪费。因此公司买了一个小交换机,把原来的7条中继线接到小交换机的外线接口上,而把每个人的话机接到小交换机的内线口上,这样,每个人就都有了一个分机号,从601到621,而PSTN端的配置不变,

当客户打总机号时,PSTN交换机仍然会选择一条线进入公司的小交换机,这时候,选线方式已经不像以前那样重要,因为现在是小交换机在接电话,对它来说,7条线哪条都一样。就这样小交换机接了电话,并播放“您好,欢迎致电某某公司,请直拨分机号,查号请拨 0......”如果客户按某一分机号,则对应分机振铃。电话接通。

有了小交换机,内部通话就免费了。但出现了另外一个问题,就是如果拨打外线,则需要先拨一个特殊的数字,一般是0或9。有的小交换机会送二次拨号音,即你拿起电话,听到小交换机的拨号音,拨了0之后,则听到外部 PSTN 交换机的拨号音,表明你可以拨打外线了。总之,小交换机会选择一条空闲的中继线对外呼叫。上述例子中,21:7称为集线比,即3:1。集线比是由话务量决定的,如果同时通话的人数比较多,那我们可能会把中继线增加到12条,集线比就降为21:12,约为2:1了。即使增加了线路,也经常会遇到这样的情况:由于打进来的电话太多,占用了太多的线路,经常一个电话都打不出,因此,我们联系运营商,将中继线分为三组,其中4条只进不出,4条只出不进,4条能出能进。在电信术语中,分别叫做单出,单入和双向,而北京联通则分别称为发专、受专和双向。当然,这种分配方式降低了总体线路的使用率,为此我们把每个组都增加1条线,现在中继线总共达到15条。

又过了几天,有客户反映这样的情况,正常上班期间打电话经常无人接听,需要打好几遍;而同时,内部也有人反映往外打电话时有时拨0没反应,再试一次就好了。我们没有处理这种问题的经验,只好请教 PBX专家,专家说可能是某条外线断了。因为,如果有一条线断了,当有电话呼入时,交换机仍会向主叫方送回铃音,跟被叫端没接话机是一样的。但到底是哪条线断了,却不好查。由于双方都是自动选线。我们只好将每条线都从小交换机上拔下来,接上话机试一试,以确定是哪条线断了。还算比较幸运,我们找到了断线的号码,联系运营商,很快修好了,把所有线路都插回小交换机,一切恢复正常。

几天后,老板又很幸运地搞到了一个新号码66666666,该号码并未加入中继线组,而是直接扯了根线拉到老板办公桌上。为了能拨打内线,他不得不在办公桌上放两部电话,另一部专门打内线。后来技术人员小张仔细阅读了 PBX 的说明书,发现该小交换机功能还比较强,就进行了以下设置:将66666666 这个号码接到小交换机上,仍给老板一个内线电话同时在小交换机上进行设置,只有老板打出时才走 66666666这个端口;而对于打入的电话也不播放“欢迎致电XX公司··.”,而是直接向老板电话振铃。这种拨入方式叫做 DID,即对内直接呼叫(Direct Inbound Dial)。

接下来,随着公司的发展,加入的中继线条数越来越多,维护起来更加复杂。比如,像我们刚才遇到的情况,其中有一条线断了,在很长的一段时间内根本不知道,即使知道了要找到是哪条线也非常麻烦。后来,当公司发展到 100 人的时候,购买了新设备,并将模拟中继线换成了两条E1数字中继线,可同时支持60路通话。
公司发展一帆风顺,电话量也越来越多,公司有了很多分支机构,也有了更多客户,需要更复杂的语音菜单及更智能的电话分配策略,而更换专门的电话系统不仅价格昂贵,而且跟现有业务系统进行集成难度也很大。在综合考虑了多种解决方案以后,技术人员开始学习 FreeSWITCH。。。

FreeSWITCH 的默认配置就是一个家用或小型企业级的PBX,它是由纯软件实现的,基于IP网进行通信,因而又称为 IP-PBX。IP-PBX 首先是一个 PBX(Private Branch eXchange),它具有传统PBX的绝大部分功能。另外,由于使用了 IP 通信,它能通过 IP 网提供语音、视频以及即时消息通信。这些通信不仅可以在企业内部网上进行,也可以通过 Internet 在外网甚至 PSTN(Public Switched Telephone Network) 电话间进行。由于大部分PBX功能都是用软件实现的,因而实现起来成本相对来讲都非常廉价,并且非常易于增加新功能,如多方会议、使用XML-RPC等控制正在进行的通话、IVR、TTS/ASR(Text To Speech/Automatic Speech Recognition)支持通过模拟或数字线路与PSTN网络对接,支持通过SIPIAX、H323 或Jingle(Google对XMPP 协议的扩展)以及其他协议与其他通信系统进行互联互通等。

IP-PBX 更易于部署,尤其是基于IP 的通信更加廉价。但是,IP-PBX并不是解决所有问题的良药,老技术向新技术过渡总要有些取舍。比如,在IP环境中,IP 电话终端 更加智能,如摘机检测、挂机检测、收号等,这些原来由 PBX或交换机实现的功能现在都在终端上实现了,因而在PBX上很难获得这些信息的细节。另外,当它与传统的PBX或PSTN 网络对接时,还需要相应的VP网关来实现。当然,现在国内某些运营商也开始试验性地提供基于IMS或SBC的SIP中继这样对接起来就方便多了。

呼叫中心

基于企业级的 PBX 和 IP-PBX 的通信还只是局限于基础的通信层。而随着企业规模的扩大及用户对服务要求的提高,企业更需要在业务逻辑和管理层方面为用户提供更好的服务。当这些服务可以通过远程电话支持的方式解决的情况下,一种称为 呼叫中心的业务(CallCenter)便产生了。

在呼叫中心中,有专门的话务员为客户提供服务。呼叫中心通常能同时处理大量的通话,并且为了给客户更好的服务体验,呼叫中心的通信系统通常通过技术手段与CRM(Customer Ralationship Management,客户关系管理)系统集成。

通俗地讲:呼叫中心是企业或机构建立的以电话为主要手段,为客户提供服务与沟通的部门组织及信息系统。就像 110、119、120 这些应急服务电话,及电信运营商的客服电话、电话银行等都是呼叫中心的具体应用。在呼叫中心中通常是由座席代表通过电话为客户提供相应的服务与沟通。根据呼叫中心业务量不同,可以同时处理的电话量和座席代表的人数也有所不同。较小规模的呼叫中心只有几个人,大规模的呼叫中心会达到几千人。

freeswitch 简介

FreeSwitch 是开源的软交换平台,多媒体通讯平台!官方的定义是世界上第一个跨平台的、伸缩性极好的、免费的、多协议的电话软交换平台。FreeSwitch 从一个简单的软电话客户端到运营商级别的软交换设备几乎无所不能。

FreeSWITCH 的默认配置就是一个家用或小型企业级的 PBX,它是由纯软件实现的,基于IP网进行通信,因而又称为 IP-PBX

FreeSWITCH 最典型的应用是作为一个服务器 (它实际上是一个背靠背的用户代理,B2BUA),并用 电话客户端软件(一般叫软电话)连接到它。

虽然 FreeSWITCH 支持 IAX、H323、Skype、Gtalk 等众多通信协议,但其最主要的协议还是 SIP。支持 SIP 的软电话有很多,最常用的是 X-Lite 和 Zoiper。这两款软电话都支持 Linux、MacOSX 和 Windows平台,免费使用但是不开源。在 Linux 上你还可以使用 ekiga 软电话。

FreeSWITCH 是一个 B2BUA(背靠背的用户代理),所以它能做的工作非常多。

一些典型应用场景:在国外,很多ISP和运营商把它作为关键的软交换设备,处理成千上万路的并发通话,也有的把它用于呼叫中心,与各种企业级的应用系统(如CRM、ERP 等)集成,在国内,也已经有很多应用案例,其被广泛用于金融、保险、电力、石油、煤炭等领域的呼叫中心、企业通信以及应急指挥调度平台等。从这一方面讲,它是传统的电话交换系统及商业的电话交换系统良好的替代品。除了简单的替代以外,它往往还提供更多的新功能、更灵活的数据集成能力和更快速的应用开发能力,在业务需求千变万化的今天显得格外有生命力。另外,在当今的移动互联、物联网与大数据、云计算盛行的时代,好多厂商和互联网的创业者也把 FreeSWITCH 用于通信领域的“云”平台。FreeSWITCH 诞生的年代和背景良好的设计架构以及活跃的技术支持社区都是它能在“云”平台上成功的坚实基础。

FreeSWITCH 典型功能:

  • 在线计费、预付费功能
  • 电话路由服务器
  • 语音转码服务器
  • 支持资源优先权和OoS的服务器
  • 多点会议服务器
  • IVR、语音通知服务器
  • VoiceMail 服务器
  • PBX应用和软交换
  • 应用层网关
  • 防火墙/NAT穿越应用
  • 私有服务器
  • 第三方呼叫控制应用
  • 业务生成环境运行时引擎
  • 会话边界控制器
  • IMS中的S-CSCF/P-CSCF/I-CSCF
  • SIP网间互联网关
  • SBC及安全网关
  • 传真服务器、T30到T38 网关

OpenSER ( OpenSIPS、Kamailio ) 和 FreeSWITCH

OpenSIPS、Kamailio、FreeSWITCH 区别:https://www.toutiao.com/article/6804706056887861771

SIP 服务器有很多种类型,典型是以下几类:

  • 注册服务器:即只管 Register 消息,这里相当于location也在这里了
  • 重定向服务器:给ua回一条302后,转给其它的服务器,这样保证全系统统一接入
  • 代理服务器:只做proxy,即对SIP消息转发
  • 媒体服务器:只做 rtp 包相关处理,即 media server
  • B2BUA:这个里包实际一般是可以含以上几种服务器类型

OpenSER 后来分家了,一家叫 Kamailio,另一家是 OpenSIPS。Kamailio、OpenSIPS 和 FreeSWITCH 都是流行的开源通信软件,用于构建和管理实时通信系统。OpenSER ( OpenSIPS、Kamailio ) 和 FreeSWITCH 三个SIP服务器 应用场景和区别

  1. Kamailio:Kamailio(以前称为OpenSER)是一个高性能、可扩展的SIP(会话初始化协议)服务器。它专注于提供稳定和高效的SIP路由、注册和身份验证功能。Kamailio主要用于大规模VoIP网络和运营商级别的部署,可以处理大量的并发呼叫和用户注册。

  2. OpenSIPS:OpenSIPS是另一个强大的SIP服务器,也是一个多功能的实时通信引擎。它提供了比Kamailio更多的功能和灵活性,包括SIP路由、负载平衡、故障转移、身份验证、会话保持等。OpenSIPS适用于中小规模的VoIP部署,也可以作为SIP代理服务器或SIP调度器使用。

  3. FreeSWITCH:FreeSWITCH是一个全功能的开源软交换平台,支持音频、视频、消息和数据通信。与Kamailio和OpenSIPS不同,FreeSWITCH不仅仅局限于SIP协议,还支持其他协议如WebRTC、XMPP等。它提供了更广泛的媒体处理功能,包括音频/视频会议、语音信箱、IVR等。FreeSWITCH适用于构建复杂的实时通信系统,特别是需要多媒体处理和高级应用功能的场景。
    FreeSWITCH 自带一个 fs_cli 用于一些日常的监控和维护。但是因为FreeSWITCH的用户属性-业务优先,所以它有个 event_socket 这个模块,简称为 esl (event socket library) ,让用户以自己的业务需求,通过 socket 编程实现相应的处理,当然其还支持一堆的 lua/python/perl 等各语言的原生调用模块。

总的来说,Kamailio 和 OpenSIPS 更专注于提供高性能的 SIP 路由和核心通信功能,适用于大规模和中小规模的 VoIP 网络。而 FreeSWITCH 则提供了更广泛的媒体处理功能,适用于更复杂的实时通信系统。选择适合自己需求的软件取决于具体的应用场景和功能需求。

freeswitch 架构

FreeSWITCH 由一个稳定的核心(Core)及一些外围模块组成。这些外围的模块根据其功能和用途的不同又分为 Endpoint、Codec Dialplan、Application 等不同的类别,如下图:

FreeSWITCH 内部使用线程模型来处理并发请求,每个连接都在单独的线程中进行处理,不同的线程间通过 Mutex 互斥访问共享资源,并通过消息和异步事件等方式进行通信。更重要的是,即使某路电话发生问题,也只影响到它所在的线程,而不会影响到其它电话。这种架构能处理很高的并发,并且在多核环境中运算能均匀地分布到多颗CPU或单CPU的多个核心上。

FreeSWITCH的核心非常短小精悍,这也是其保持稳定的关键。绝大部分应用层的功能都在外围的模块中实现。外围模块是可以动态加载(以及卸载)的,在实际应用中可以只加载用到的模块。外围模块通过核心提供的 Public API与核心进行通信,而核心则通过回调(或称钩子)机制执行外围模块中的代码。

  • 公共应用程序接口 (Public API)。FreeSWITCH 在核心层实现了一些 Public API。这些 Public API 可以被外围的模块调用。例如,当FreeSWITCH外围的 Endpoint 模块收到一个呼人请求时,该模块就可以调用核心的 switch_core_session_request 函数为该呼叫生成一个新的 Session,此后,该呼叫的生命周期就由该 Session 管理。如果呼叫挂断,就调用 switch_core_session_destroy 函数将该 Session 释放。一般来说,与 Session 相关的函数都是与呼叫相关的。进一步细分,可以认为它主要与信令层相关,因为一个呼叫的生命周期是由信令来控制的。
  • 接口(interface)。FreeSWITCH在核心中除实现了大量的 Public API 供外围模块调用外,还提供了很多抽象的接口,这些接口对同类型的逻辑或功能实体进行了抽象,但没有具体实现。具体的实现一般由外围的模块负责,核心层通过回调(钩子)方式调用具体的实现代码或函数。
  • 事件 (event)。除了使用 Public API 及接口回调方式执行内部逻辑和通信外,FreeSWITCH在内部也使用消息和事件机制进行进程间和模块间通信。消息机制完全是内部的。在FreeSWITCH 外部,也可以通过 Event Socket等接口订阅相关的事件,通过这种方式可了解FreeSWITCH 内部发生了什么,如当前呼叫的状态等。fs cli 就是一个典型的外部程序,它通过 Event Socket 与 FreeSWITCH 通信,可以对 FreeSWITCH 进行控制和管理,也可以订阅相关的事件对 FreeSWITCH 的运行情况进行监控。订阅事件最简单的方法是:
    fs cli> /event plain ALL 在 fs_cli 中执行上述命令可以订阅所有的事件。FreeSWITCH的事件主要有两大类:一类是主事件可根据事件的名字(Event-Name)来区分,如CHANNEL ANSWER(应答)CHANNELHANGUP(挂机)等;另一类是自定义事件,它们的 Event-Name 永远是 CUSTOM,不同的事件类型可根据子类型(Event-Subclass)来区分,如 sofia:register (SIP注册)、sofia::unregister(SIP 注销)等。在 fs_cli 中可以单独订阅某类事件,如:
    fs_cli> /event plain CHANNEL ANSWER
    fs_cli> /event plain CUSTOM sofia::register
    后面会细地讨论事件机制、函数及相关用法。

freeswitch 模块

FreeSWITCH 在核心(Core)中提供了很多抽象的接口,这些接口对同类型的逻辑或功能实体进行了抽象,但没有具体实现,具体的实现一般由外围的模块负责,核心层通过回调(钩子)方式调用具体的实现代码。

  • Core (核心):FS Core 是 FreeSWITCH 的核心,它包含了关键的数据结构和复杂的代码,但这些代码只出现在核心中,并保持了最大限度的重用。外围模块只能通过 API 调用核心的功能,因而核心运行在一个受保护的环境中,核心代码都经过精心的编码和严格的测试,最大限度地保持了系统整体的稳定。核心代码保持了最高度的抽象,因而它可以调用不同功能,不同协议的模块。同时,良好的 API 也使得编写不同的外围模块非常容易。
  • 终点 (End Points ):是终结 FreeSWITCH 的地方,也就是说再往外走就超出 FreeSWITCH 的控制了。它主要包含了不同呼叫控制协议的接口,如 SIP, TDM 硬件,H323 以及 Google Talk 等。这使得 FreeSWITCH 可以与众多不同的电话系统进行通信。如,可以使用 mod_skypopen 与 Skype 网络进行通信。另外,前面也讲过,它还可以通过 portaudio 驱动本地声卡,用作一个软电话。
  • 应用程序(Application,APP):mod_dptools模块提供了系统中大部分的APP。FreeSWITCH 提供了许多 App 使复杂的任务变得异常简单,如 mod_voicemail 模块可以很简单地实现语音留言,而 mod_conference 模块则可以实现高质量的多方会议。FreeSWITCH 本身是一个B2BUA,而实际上所有与 FreeSWITCH的通话(或通过FreeSWITCH的通话) 都是在与一个或多个 App 在交互。mod_dptools 模块提供了系统中大部分的 App。
  • 聊天计划 (Chatplan):类似于 Dialplan,但是 Chatplan 主要对文本消息进行路由,如:SIP SIMPLE、Skype Message、XMPP Message 等。它是在 mod_sms 中实现的。
  • 编解码器(Codec):FreeSWITCH 支持最广泛的 Codec,除了大多数 VoIP 系统支持的 G711、G722、G729、GSM 外,它还支持 iLBC,BV16/32、SILK、CELT等。它可以同时桥接不同采样频率的电话,以及电话会议等。
  • 数据库(DB):在核心中实现。FreeSWITCH 的核心除了使用内部的队列、哈希表存储数据外,也使用外部的关系型数据库存储数据。使用外部数据库的好处是查询数据不用锁定内存数据结构,这不仅能提高性能,还能降低死锁的风险保证了系统稳定。命令 show calls、show channels 等都可直接从
    数据库中读取内容并显示。FreeSWITCH 支持多种流行的关系型数据库。为了尽量减少对其他系统的依赖,FreeSWITCH 默认使用的数据库类型是 SOLite。SOLite 是一种嵌入式数据库,FreeSWITCH 可以直接调用它提供的库函数来访问数据。由于 SQLite 会进行读锁定,因此在使用 SOLite 时不建议通过外部应用直接读取核心数据库。FreeSWITCH使用一个核心的数据库(默认的存放位置是/usr/local/freeswitch/db/core.db)来记录系统的接口 (interfaces)、任务(tasks)以及当前的通道(channels)通话(calls)等实时数据。某些模块如mod sofia mod fifo等都有自己的数据库(表)一般来说这些模块也提供相关的 API用于从这些表里查询数据。必要时,用户也可以直接查询这些数据库(表)来获取数据(如mod sofia中的分机注册数据等)。系统对数据库操作做了优化,在高并发状态下,核心会尽量将几百条 SOL语一起执行,这可以大大提高系统性能。windows 下FreeSwitch 连接mysqlhttps://blog.csdn.net/qq_22626483/article/details/124248905
  • 拨号计划 (Dialplan)Dialplan主要提供查找电话路由的功能,默认的 Dialplan 由mod_dialplan_xml 提供。也支持 Asterisk 格式的配置文件。另外它也持 ENUM 查询。
  • 嵌入式语言(Embeded Language):通过 swig 可支持多种嵌入式语言进而控制呼叫流程。如 Lua、Javascript、Perl 等。
  • 事件消费者(Event Consumer):通过Event Socket可以使用任何其他语音(只要支持Socket),通过TCP Socket可控制呼叫流程、扩展FreeSWITCH的功能。
  • 格式、文件接口(Format,FileInterface)。支持不同格式的声音文件回放、录音。如 WAV、MP3 等。mod_sndfile 模块通过 libsndfile 库提供了对大部分音频文件格式的支持。MP3 格式是在 mod_shout 中实现的。
  • 语音识别(ASR)、语音合成(TTS):支持语音自动识别(ASR),文本-语音转换(TTS)。
  • 命令接口(FSAPI):即FreeSWITCH API,简称API,它是一种对外的命令接口,它的原理非常简单——输入一个简单的字符串(以空格分隔),该字符串由模块的内部函数处理,然后得到一个输出。系统中大部分的API都是由mod_commands模块提供。
  • 日志(Logger)日志可以写到控制台、日志文件、系统日志(syslog)以及远的日志服务器。实现日志功能的模块有 mod_console、mod_logfile、mod_syslog 等。
  • 分词短语(Say)
  • 定时器(Timer):实时的话音通话需要非常准确的定时器,在FreeSWITCH中,可以使用软时钟(soft timer)或内核提供的时钟来定时(如Linux中的timerfd或posixtimer)。FreeSWITCH 最理想的工作时钟频率是 1000Hz,而某些 Linux 发行版或虚拟机的内核的默认时钟可能是 100Hz或 250Hz,在这种情况下,可以使用内核提供的时钟接口,或者可以重新编译内核调整时钟频率。
  • XML接口(XML Interface):用来支持多种获取 XML 配置的方式,它可以是本地的配置文件,或从数据库中读取,甚至是一个能动态返回 XML 的远程 HTTP 服务器。对XML的解析和访问是在核心中实现的,但对XML的应用和扩展都是在外部模块中完成的。
  • 事件套接字 (Event Socket):可以使用任何其它语言通过 Socket 方式控制呼叫流程、扩展 FreeSWITCH 功能。

安装 freeswitch 

FreeSWITCH 默认的配置是一个 SOHO PBX ( 家用电话小交换机 ),可以实现分机互拨电话,测试各种功能,并通过添加一个 SIP-PSTN 网关拨打 PSTN 电话。

商业使用时都是使用 Linux 进行安装部署,但是今天是体验,所以使用 Windows。

最好从 github 直接下载源码编译安装。

  • Linux 默认安装位置在 /usr/local/freeswitch,源码安装成功后 freeswitch 源码目录最好保留,以便以后升级及安装配置其它组件。
  • windows 系统根据自己的系统选择不同的目录,然后就一直下一步下一步就可以了,安装完成后使用管理员执行 C:\Program Files\FreeSWITCH>FreeSwitchConsole.exe 便可启动,配置文件都在 C:\Program Files\FreeSWITCH\conf\

目录结构。windows 和 linux 的目录稍微有所不同。

  • bin:可执行程序目录。
    • freeswitch:服务器。
    • fs_cli:客户端。
    • fs_encode:将声音文件从一种编码转换到另一种编码。
    • fsxs:编译模块的辅助工具。不用Makefile也可编译,意味着不用源代码环境也可编译模块。
    • tone2wav:从一个TGML标记语言描述文件生成声音文件。
  • conf:配置文件目录。
    • autoload_configs:自动加载的模块的配置文件的目录。
      • *.conf.xml:一般每个模块一个配置文件,文件名格式为“不包含mod_的模块名.conf.xml”。
    • chatplan:聊天计划的配置文件的目录。
    • dialplan:拨号计划的配置文件的目录。
      • default.xml:默认Context,一般用于内部注册用户路由。
      • public.xml:一般用于外部来话路由。
    • directory:用户目录。支持多个域(Domain),每个域可以写到一个XML文件中。
      • default:默认域的SIP用户的配置目录。
        • *.xml:默认域的SIP用户,每个用户一个文件,文件名为用户名,依次为1000~1019。
      • default.xml:默认域。
    • freeswitch.xml:主配置文件,载入其他配置文件。
    • ivr_menus:IVR菜单配置目录。
    • jingle_profiles:连接Google Talk的相关配置的目录。
    • lang:多语言支持的配置目录。
      • en:英语。
      • fr:法语。
    • mrcp_profiles:MRCP的相关配置的目录,用于跟第三方语音合成和语音识别系统对接。
      • vestec-mrcp-v1.xml:Vestec支持MRCP的V1版本协议(RTSP)的配置。
    • sip_profiles:SIP配置文件目录。一般每个文件描述一个Profile。
      • external.xml:一般用于外部网关。
      • internal.xml:一般用于本地用户。
    • skinny_profiles:思科SCCP协议话机的配置文件目录。
    • vars.xml:定义一些全局变量。
  • db:数据库(sqlite)目录。将呼叫信息存放到数据中这样在查询时就无须对核心数据结构加锁
    • core.db:核心数据库。记录系统的接口(interfaces)、任务(tasks)以及当前的通道(channels)、通话(calls)等实时数据。
    • sofia_reg_*.db:SIP数据库。每个Profile一个文件。
  • grammar:语法文件目录,用于 ASR。
  • htdocs:HTTP Server根目录。
  • include:头文件目录。
  • lib:库文件目录。
    • libfreeswitch.so
  • log:日志文件及CDR话单目录。
    • cdr-csv:CSV话单目录。
      • Master.csv:话单汇总文件。轮替的文件加上.2006-01-02-15-04-05格式的后缀。
      • *.csv:每个独立分机一个话单文件。轮替的文件加上.2006-01-02-15-04-05格式的后缀。
    • cdr-pg-csv:话单写入ProstgreSQL失败时保存的本地文件话单目录。
    • freeswitch.log:日志文件。
    • freeswitch.xml.fsxml:完整的XML文档的镜像。
    • xml_cdr:XML话单目录。当话单POST请求失败时也会将话单保存至此。
  • mod:可加载的模块目录。
  • recordings:录音文件目录。record应用程序默认的存放路径。
  • run:运行目录,存放 FreeSWITCH 运行时的 PID
    • freeswitch.pid:存放FreeSWITCH运行时的PID。
  • scripts:嵌入式语言写的脚本的目录。lua、luarun、jsrun等应用程序默认的查找路径。
  • sounds:声音文件目录。playback 应用程序默认的查找路径。
    • en:英语提示音文件目录。
      • us:美式英语提示音文件目录。
        • callie:Callie(人名)录制的嗓音文件目录。每个子目录为某一类声音文件的目录。
          • ascii:ASCII字符提示音目录。每个子目录为某一采样率(Hz)的声音文件目录。
            • 8000:8000Hz语音的目录。
          • base256
          • conference
          • currency
          • digits:数字语音的目录。
          • directory
          • ivr:IVR提示音目录。
          • misc
          • phonetic-ascii
          • time
          • voicemail
          • zrtp
    • music:MOH保持音乐目录。
  • storage:语音信箱的录音文件及从其他HTTP服务器下载的语音文件缓存目录。

安装声音文件

在 Windows系统上,这些声音文件是默认安装的。声音文件有两种:

  • 提示音,用于通话期间的语音提示,如 VoiceMail 提示音、TTS功能的提示音等,
  • 音乐,用于在Hold 状态时播放,即所谓的 Music on Hold(MOH)。

在 Linux 或 Mac 上安装这些声音文件也异常简单。你只需在源代码目录中执行:

make sounds-install
make moh-install

安装过程中将自动从 files.freeswitch.org 下载相关的语音包,并解压缩到相关的安装路径中(默认安装在/usr/local/freeswitch/sounds下)。另外,FreeSWITCH 支持 8kHz、16kHz、32kHz及48kHz 的语音。与上面的声音文件相对应的高清声音文件可以选择安装。如以下命令安装 16kHz 的声音文件:

make cd-sounds-install
make cd-moh-install

freeswitch 对接 chatGPT

freeswitch-sip 电话和 chatGPT 连通:

freeswitch-chatGPT 是一个开源项目,旨在将 FreeSWITCH 与 OpenAI 的 Stream API 集成,并使用 FreeSWITCH 实现基于 MRCP 的 ASR(自动语音识别)和 TTS(文本转语音)。

freeswitch_chatGPT:https://github.com/laoyin/freeswitch_chatGPT

启动 freeswitch

在终端中执行 freeswitch 命令,可以将其启动到前台,启动过程中会有许多log输出,第一次启动时会有一些错误和警告,不用理会。启动完成后会进入到系统控制台(以下称称FS-Con)。并显示类似的提示符 "freeswitch@internal>" (以下简作 "FS> ")。通过在 FS-Con 中输入shutdown 命令可以关闭 FreeSWITCH。输入 help 可以查看帮助。

执行:C:\Program Files\FreeSWITCH>FreeSwitchConsole.exe 。执行成功后如图

如果想将 FreeSWITCH 启动到后台 (daemon,服务模式),执行 freeswitch -nc (No console)。后台模式没有控制台。

无论 FreeSWITCH 是在前台运行还是后台运行,都可以使用 fs_cli 连接 freeswitch 并进行控制。注意,在 fs_cli 中需要使用 fsctl shutdown 命令关闭 FreeSWITCH。当然也可以直接使用命令 freeswitch -stop 关闭。如果不想退出 FreeSWITCH 服务,只退出 fs_cli 客户端,则需要输入 /exit,或 Ctrl + D,或者直接关掉终端窗口。

freeswitch 配置文件

FreeSWITCH 配置文件默认放在 conf/

conf 是由一系列 XML 配置文件组成,手工编辑这些 XML 比较困难,有图形化的配置工具。系统还允许在某些 XML 节点上安装回调程序(函数),当这些节点的数据变化时,系统便自动调用这些回调程序。

XML配置文件:https://www.cnblogs.com/garvenc/p/freeswitch_learning_xml_configuration_file.html

FreeSWITCH默认的配置是一个SOHO PBX(家用电话小交换机)。

X-PRE-PROCESS标签是FreeSWITCH特有的,称为预处理指令,用于根据data参数设置(set)一些全局变量,以及将data参数指定的文件内容包含(include)到当前文件中。FreeSwitch在加载阶段只对其进行简单替换,并不进行语法分析,因此对它进行注释是没有效果的,解决办法是破坏X-PRE-PROCESS的定义(如将其替换为XPRE-PROCESS)。

可以$${VAR[:OFFSET[:LENGTH]]}形式引用全局变量,以${VAR[:OFFSET[:LENGTH]]}形式引用通道变量(局部变量)。OFFSET从0开始,如为负数表示从尾部开始。

配置文件的加载顺序如下:

freeswitch.xml
  |- vars.xml
  |- autoload_configs/*.xml
  | [autoload_configs/acl.conf.xml]
  | [autoload_configs/callcenter.conf.xml]
  | [autoload_configs/cdr_csv_conf.xml]
  | [autoload_configs/cdr_pg_csv.conf.xml]
  | [autoload_configs/conference.conf.xml]
  | [autoload_configs/distributor.conf.xml]
  | [autoload_configs/event_socket.conf.xml]
  | [autoload_configs/fifo.conf.xml]
  | [autoload_configs/ivr.conf.xml]
  |    `- ivr_menus/*.xml
  |      [ivr_menus/demo_ivr.xml]
  | [autoload_configs/local_stream.conf.xml]
  | [autoload_configs/lua.conf.xml]
  | [autoload_configs/modules.conf.xml]
  | [autoload_configs/nibblebill.conf.xml]
  | [autoload_configs/post_load_modules.conf.xml]
  | [autoload_configs/sofia.conf.xml]
  |    `- sip_profiles/*.xml
  |      [sip_profiles/external.xml]
  |         `- sip_profiles/external/*.xml
  |           [sip_profiles/external/example.xml]
  |      [sip_profiles/internal.xml]
  | [autoload_configs/switch.conf.xml]
  | [autoload_configs/tts_commandline.conf.xml]
  | [autoload_configs/xml_cdr.conf.xml]
  | [autoload_configs/xml_curl.conf.xml]
  | [autoload_configs/xml_rpc.conf.xml]
  |- dialplan/*.xml
  | [dialplan/default.xml]
  |    `- dialplan/default/*.xml
  | [dialplan/public.xml]
  |    `- dialplan/public/*.xml
  |- directory/*.xml
  | [directory/default.xml]
  |    `- directory/default/*.xml
  |      [directory/default/1000.xml]
  `- lang/en/*.xml
    [lang/en/en.xml]
       `- lang/en/demo/*.xml
         [lang/en/demo/demo-ivr.xml]

重要的配置文件:

  • autoload_configs/modules.conf.xml
  • autoload_configs/sofia.conf.xml
  • autoload_configs/switch.conf.xml
  • dialplan/default.xml
  • dialplan/public.xml
  • directory/default.xml
  • directory/default/1000.xml
  • freeswitch.xml
  • sip_profiles/external.xml
  • sip_profiles/external/example.xml
  • sip_profiles/internal.xml
  • vars.xml

freeswitch.xml 

最顶层、最重要的配置文件是 freeswitch.xml 。系统启动时 freeswitch.xml 会依次装入其它 XML 文件并最终组成一个大的 XML 文件 (将所有配置文件组合到一起)。组装后的 XML 非常大,而且已经对 X-PRE-PROCESS 标签进行了处理并隐藏了很多配置的细节。完整的 XML 文档分为以下几个重要的部分,每一部分又分别装入不同的 XML。

  • configuration (配置)
  • dialplan  (拨号计划)
  • chatplan (聊天计划)
  • directory (用户目录)
  • phrase (分词)

在加载 XML 时,FreeSWITCH 的XML解析器会先将预处理命令展开,在FreeSWITCH内部生成一个大的XML文档。log/freeswitch.xml.fsxml 是 FreeSWITCH 内部 XML 的一个内存镜像 ( 在log目录中,而不是在 conf 目录中,由于它是动态生成的,所以用户不应该手工编辑它)。它对调试非常有用。假设你不慎弄错了某个标签,又不知道它错在哪了,可以尝试让 FreeSWITCH 重新加载XML(执行命令:reloadxml ),这时在 FreeSWITCH 的日志中就可以看到 XML某一行出错的提示,在 freeswitch.xml.fsxml 就能很容易地定位到这一行。

为了便于了解,下面是精减的 freeswitch.xml 配置:

<?xml version="1.0"?>
<document type="freeswitch/xml">
	<X-PRE-PROCESS cmd="include" data="vars.xml"/>
	<section name="configuration" description="Various Configuration">
		<X-PRE-PROCESS cmd="include" data="autoload_configs/*.xml"/>
	</section>
	<section name="dialplan" description="Regex/XML Dialplan">
		<X-PRE-PROCESS cmd="include" data="dialplan/*.xml"/>
	</section>
	<section name="chatplan" description="Regex/XML Chatplan">
		<X-PRE-PROCESS cmd="include" data="chatplan/*.xml"/>
	</section>
	<section name="directory" description="User Directory">
		<X-PRE-PROCESS cmd="include" data="directory/*.xml"/>
	</section>
	<section name="languages" description="Language Management">
		<X-PRE-PROCESS cmd="include" data="lang/de/*.xml"/>
		<X-PRE-PROCESS cmd="include" data="lang/en/*.xml"/>
		<X-PRE-PROCESS cmd="include" data="lang/fr/*.xml"/>
		<X-PRE-PROCESS cmd="include" data="lang/ru/*.xml"/>
		<X-PRE-PROCESS cmd="include" data="lang/he/*.xml"/>
		<X-PRE-PROCESS cmd="include" data="lang/es/es_ES.xml"/>
		<X-NO-PRE-PROCESS cmd="include" data="lang/es/es_MX.xml"/>
		<X-PRE-PROCESS cmd="include" data="lang/pt/pt_BR.xml"/>
		<X-NO-PRE-PROCESS cmd="include" data="lang/pt/pt_PT.xml"/>
		<X-NO-PRE-PROCESS cmd="include" data="lang/sv/*.xml"/>
	</section>
</document>

可以看到它的根是 document,在 document 中,有许多 section,每个 section 都对应一部分功能。其中 X-PRE-PROCESS 是预处理指令,它们的作用是将 data 参数指定的文件包含(include)到本文件中来。由于它是一个预处理指令,FreeSWITCH 在加载阶段只对其进行简单替换,并不进行语法分析,因此,对它进行注释是没有效果的,这是一个新手常犯的错误因为 当 FreeSWITCH 预处理时,还没有到达 XML 解析阶段,也就是说它还不认识 XML 注释语法,而仅会机械地将预处理指令替换,由于 XML 的注释不能嵌套,因此便产生错误的XML。解决办法是破坏掉 X-PRE-PROCESS 的定义。常用下面两种方法:

<xX-PRE-PROCESS cmd="include" data="vars.xml"/>   <!-- 前面加一个x -->
<XPRE-PROCESS cmd="include" data="vars.xml"/>      <!-- 去掉一个 中划线 -->

由于 FreeSWITCH 不认识 xX-PRE-PROCESSXPRE-PROCESS,因此它会忽略掉该行,相当于注释掉了。

vars.xml

vars.xml 主要通过 X-PRE-PROCESS 指令定义了一些全局变量,它们在 FreeSWITCH 运行期间永远都是有效的。而后面可能还会遇到局部变量,它们通常在拨号计划中,在一个呼叫的生命周期中才有效。如果需要引用这些变量,则全局变量以 $${var} 表示,临时变量以 ${var} 表示。

在加载 vars.xml 之前,FreeSWITCH 就已经“算”出并设置了一些全局变量,也就是说有些变量是系统在运行时自动设置的,其有默认的值。

在实际使用中,可以使用 global_getvar 或这个API命令来查看这些变量的值,如

由于这些变量是在 vars.xml 加载前设置的,因而可以在 vars.xml 中覆盖它们,如
<X-PRE-PROCESS cmd="set" data="local_ip_v4=192.168.1.123"/>
上面的设置经常会用到,因为有时候 FreeSWITCH 自动“算”出的值可能不是你想要的,如上面的 local_ip_v4 的值,在服务器有多个网卡的情况下,可能希望它能得到另外一个网卡的IP地址,这时候就可以通过手动的方式设置该变量的 IP 来实现。

autoload_configs 目录

autoload_configs 目录下面的各种配置文件会在系统启动时装入。一般来说都是模块级的配置文件,每个模块对应一个。文件名一般以 模块名.conf.xxml 方式命名。其中 modules.conf.xml 决定了 FreeSWITCH 启动时自动加载哪些模块。

dialplan 目录 ( 拨号计划 )

定义 XML 拨号计划,用户对电话进行 路由(选路)。

directory 目录 ( XML用户目录 )

  • conf/directory/ 是用户xml目录,目录中的配置文件决定了当 FreeSWITCH 作为注册服务器时,哪些用户可以注册,即用于配置本地用户,其中的配置信息称为用户目录。FreeSWITCH 的用户目录支持多个域(Domain),每个域可以写到一个XML文件中。默认的配置(系统自带的配置) 文件为 default.xml,里面定义了 1000 ~ 1019 共 20 个用户 ( 实际上是 include(包括) 子目录中的20个配置文件)。
  • SIP 并不要求一定要注册才可以打电话,但是通话前的用户认证参数仍需要在用户目录中进行配置。
<include>
  <!--the domain or ip (the right hand side of the @ in the addr-->
  <domain name="$${domain}">
    <params>
      <param name="dial-string" value="{^^:sip_invite_domain=${dialed_domain}:presence_id=${dialed_user}@${dialed_domain}}${sofia_contact(*/${dialed_user}@${dialed_domain})},${verto_contact(${dialed_user}@${dialed_domain})}"/>
      <!-- These are required for Verto to function properly -->
      <param name="jsonrpc-allowed-methods" value="verto"/>
      <!-- <param name="jsonrpc-allowed-event-channels" value="demo,conference,presence"/> -->
    </params>

    <variables>
      <variable name="record_stereo" value="true"/>
      <variable name="default_gateway" value="$${default_provider}"/>
      <variable name="default_areacode" value="$${default_areacode}"/>
      <variable name="transfer_fallback_extension" value="operator"/>
    </variables>

    <groups>
      <group name="default">
        <users>
          <X-PRE-PROCESS cmd="include" data="default/*.xml"/>
        </users>
      </group>

      <group name="sales">
        <users>
          <!--
              type="pointer" is a pointer so you can have the
              same user in multiple groups.  It basically means
              to keep searching for the user in the directory.
          -->
          <user id="1000" type="pointer"/>
          <user id="1001" type="pointer"/>
          <user id="1002" type="pointer"/>
          <user id="1003" type="pointer"/>
          <user id="1004" type="pointer"/>
        </users>
      </group>

      <group name="billing">
        <users>
          <user id="1005" type="pointer"/>
          <user id="1006" type="pointer"/>
          <user id="1007" type="pointer"/>
          <user id="1008" type="pointer"/>
          <user id="1009" type="pointer"/>
        </users>
      </group>

      <group name="support">
        <users>
          <user id="1010" type="pointer"/>
          <user id="1011" type="pointer"/>
          <user id="1012" type="pointer"/>
          <user id="1013" type="pointer"/>
          <user id="1014" type="pointer"/>
        </users>
      </group>
    </groups>

  </domain>
</include>

一般来说,所有用户都应该属于同一个 domain(除非你想使用多 domain)。这里的 $${domain} 全局变量是在 vars.xml 中设置的,它默认是主机的 IP 地址,但也可以修改使用一个域名。

  • params 中定义了该 domain 中所有用户的公共参数。在这里只定义了一个 dial-string,这是一个至关重要的参数。当你在使用 user/user_name 或 sofia/internal/user_name 这样的呼叫字符串时,它会扩展成实际的 SIP 地址。其中 sofia_contact 是一个 API,它会根据用户的注册地址扩展成相应的呼叫字符串。
  • variables 则定义了一些公共变量,在用户做主叫或被叫时,这些变量会绑定到相应的 Channel 上形成 Channel Variable。
  • domain 中还定义了许多组(group),组里面包含很多用户(user)。在这里,组名 default 并没有什么特殊的意义,它只是随便起的,你可以修改成任何值。在用户标签里,又使用预处理指令装入了 default/ 目录中的所有 XML 文件。可以看到,在 default/ 目录中,每个用户都对应一个文件。

你也可以定义其它的用户组,组中的用户并不需要是完整的 XML 节点,也可以是一个指向一个已存在用户的 “指针”,如下图,使用 type="pointer" 可以定义指针。

  <group name="sales">
    <users>
      <user id="1000" type="pointer"/>
      <user id="1001" type="pointer"/>
      <user id="1002" type="pointer"/>
    </users>
  </group>

虽然我们这里设置了组,但使用组并不是必需的。如果你不打算使用组,可以将用户节点(users)直接放到 domain 的下一级。但使用组可以支持像群呼、代接等业务。使用 group_call 可以同时或顺序的呼叫某个组的用户。
实际用户相关的设置也很直观,下面显示了 alice 这个用户的设置:

<user id="alice">
  <params>
    <param name="password" value="$${default_password}"/>
    <param name="vm-password" value="alice"/>
  </params>
  <variables>
    <variable name="toll_allow" value="domestic,international,local"/>
    <variable name="accountcode" value="alice"/>
    <variable name="user_context" value="default"/>
    <variable name="effective_caller_id_name" value="Extension 1000"/>
    <variable name="effective_caller_id_number" value="1000"/>
    <variable name="outbound_caller_id_name" value="$${outbound_caller_name}"/>
    <variable name="outbound_caller_id_number" value="$${outbound_caller_id}"/>
    <variable name="callgroup" value="techsupport"/>
  </variables>
</user>

由上面可以看到,实际上 params 和 variables 可以出现在 user 节点中,也可以出现在 group 或 domain 中。 当它们有重复时,优先级由高到低顺序为:user > group > domain。

sip_profiles

定义了 SIP 配置文件,实际上它是由 mod_sofia 模块在 autoload_configs/sofia.conf.xml 中加载的。本身比较复杂又是核心的功能

初体验:软电话 (sip软件)

示例:软电话(sip软件)

在同一局域网上的其它机器上安装 软电话(sip软件)。也可以在同一台机器上安装。只是需要注意:软电话不要占用 UDP 5060 端口,因为 FreeSWITCH 默认要使用该端口。执行命令 netstat -an | grep 5060 可以知道 FreeSWITCH 监听在哪个IP地址上。FreeSWITCH 默认配置了 1000 ~ 1019 共 20 个用户,可以随便选择一个用户进行配置,默认密码都是 1234

接下来需要下载 sip 终端用于通话。

可用的 sip 软件有 X-Lite (已经变成 Bria)、Yate client、eyeBeam、Linphone。

  • 对于 mac 或 windows 可以使用 X-Lite (已经变成 Bria)、eyeBeam、YateClient
  • 对于 android 建议使用 Linphone。
  • 对于 iOS 可以使用 wave lite 。

eyebeam V1.5 下载:https://download.csdn.net/download/freeking101/88751188

eyebeam网络电话 中文版v1.5(附序列号)、使用教程:http://www.3h3.com/soft/147163.html

eyebeam V1.5 下载 :https://www.xtxz.com/soft/3893.html

也可以选择其他 sip 终端,只要是在同一网络下的多台设备,就可以相互通信。

网络电话软件SIP常见客户端:https://www.cnblogs.com/wuchangsoft/p/16744391.html

  • Zoiper 几乎全平台支持。免费版只有基本功能,专业版收费有更多功能。官网:https://www.zoiper.com/en/voip-softphone/download/current
  • X-Lite、eyeBeam、Bria 官网地址:http://www.counterpath.com/。X-Lite (现在也称为 Bria Solo Free) 是一款软电话软件,有一个免费版本和三个付费版本,适用于个人、团队和组织。可以帮助用户从传统的电话使用过渡到VoIP。免费的功能非常有限,因此,如果想体验更多,则必须购买。
  • Blink 号称是最好的开源SIP客户端。地址:http://icanblink.com/
  • Doubango网络电话软件 ( https://www.doubango.org/)
  • Telephone 是一款开源免费的电话,界面非常简洁,仅支持 Mac
  • PC-Telephone 利用互联网和ISDN/PSTN拨打电话,把你的电脑变成一部网络电话,一个SDN电话、一部传真机,提供语音邮件、传真、数据传输等。
  • 开源的Android版的SIP客户端,支持多种注册方式
  • LinPhone 是开源跨平台,支持Windows、Linux、Mac、Android、iOS,支持视频。
  • Jitsi (https://jitsi.org/Main/Download)用Java开发的SIP客户端,支持音视频,支持录音,它是跨平台的,是开源的。功能简单,中文显示易懂。
  • baresip 是一个协议栈,不过,它带了一个完整的命令行SIP客户端,也很好用,它是开源的,跨平台的,支持语音和视频。http://creytiv.com/baresip.html;http://creytiv.com/pub/(linux)。
  • Ekiga(http://www.ekiga.org)Ekiga,原名GnomeMeeting,支持Windows和Linux,是一个兼容SIP和H.323的视频会议程序,兼容VoIP,IP电话,通过Ekiga可以与使用任何SIP和H.323软硬件的远程用户进行视频和音频对话。
  • GSWave Grandstream出品的免费的Android和iOS上的客户端。支持会议(6人会议)、短信、sip信息跟踪等,视频支持H.264。
  • Yate Yate其实包含服务端和客户端,它是开源的,支持Window、Linux、Mac OS X。功能较为简单,不支持视频
  • LifeSize(https://www.lifesize.com/)支持Windows、Mac、Android、iOS,不开源的,支持视频,支持会议,支持SIP和H323 。
  • PJSIP(http://www.pjsip.org/) PJSIP是个协议栈,也有个客户端,它是开源的,支持语音和视频。支持STUN、ICE、WebRTC AEC等,很多客户端都是用它作协议栈。它的实现是为了能在嵌入式设备上高效实现SIP/VoIP。
  • TIPcon1TIPcon1是一个Mac版的客户端工具

软件电话 是安装在计算机上的程序,可以通过互联网拨打电话。传统上这是需使用固定电话才能完成,但随着技术世界的不断变化,用户可以通过更多方式与他人联系。虽然固定电话有很多好处,但软电话也有很多好处。

这是使用的 sip 终端是 eyeBeam,打开 eyeBeam 软件将 sip 终端注册到 freeswitch 上去,按 Windows + r ,输入cmd进入命令行。输入netstat -an | grep 5060

红框框里的内容就是我们要记住的地址。保证 windows 系统防火墙已经关闭。在 sip 终端上设置一个 sip 账号,用户名可以为 1000 - 1019,这是 freeswitch 默认的 20 个用户,密码是1234,也是默认的,domain中输入我们刚才从命令行中得到的地址。手机端也是类似过程,只不过需要另换一个账户,所有账户的默认密码都是1234

电脑端

这是注册成功的界面。

手机端也是类似过程。安卓手机安装 Linphone apk (一个sip客户端软件, 下载地址:https://f-droid.org/zh_Hans/packages/org.linphone/),安装成功后,设置sip账号,这里使用 1010 ,输入秘密 1234。所有账户的默认密码都是1234。

电脑端 sip 账号 (电话号码):1000
手机端 sip 账号 (电话号码):1010
然后就可以相互呼叫对方了。

从手机上呼叫电脑端

到这里,初探 freeswitch 完成。。。

freeswitch 常用默认号码

9664    保持音乐
9191    注册CluCon
9192    调用info在log中显示Channel信息
9195    echo, 回音测试,延迟5秒
9196    echo,回音测试
9197    milliwatte extention, 铃音生成
9198    TGML铃音生成示例
9180    铃音测试,使用远端生成的回铃音
9181    铃音测试,产生英式铃音
9182    铃音测试,使用音乐当铃音,彩铃
9183    先应答,然后发送英式铃音
9184    先应答,然后发送音乐铃音
9178    收传真
9179    发传真
5000    IVR实例
4000    听取主意信箱
33xx    电话会议,48Hz(其中xx为00~99,下同)
32xx    电话会议,32Hz(其中xx为00~99,下同)
31xx    电话会议,16Hz(其中xx为00~99,下同)
30xx    电话会议,8Hz(其中xx为00~99,下同)
2000-2002    呼叫组
1000-1019    默认分机号码(默认密码1234)

FreeSwitch 默认号码大部分是拨号计划的名称,具体定义在 conf/diaplan/default.xml中, 如9182定义如下:

<extension name="ringback_183_music_ring">
	<condition field="destination_number" expression="^9182$">
		<action application="set" data="ringback=$${hold_music}"/>
		<action application="bridge" data="{ignore_early_media=true}loopback/wait"/>
	</condition>
</extension>

freeswitch 添加 sip 用户

添加一个新的SIP用户,熟悉 FreeSWITCH 的配置文件。FreeSWITCH 默认设置了20个用户(1000-1019),如果需要更多的用户,或者想通过添加一个用户来学习 FreeSWITCH 配置,只需要简单执行以下三步:

  • 在 conf/directory/default/ 增加一个用户配置文件
  • 修改 拨号计划(Dialplan) 使其它用户可以呼叫到它
  • 重新加载配置使其生效

示例:添加用户 king ,分机号是 1234。只需要到 conf/directory/default 目录下,将 1000.xml 拷贝到 1234.xml。打开1234.xml,将所有1000都改为1234。并把 effective_caller_id_name 的值改为 king,然后保存退出。如:<variable name="effective_caller_id_name" value="king"/>

xml 格式化工具:https://c.runoob.com/front-end/710/

接下来,打开 conf/dialplan/default.xml,找到 <condition field="destination_number"
expression="^(10[01][0-9])$"> 一行,改为 <condition field="destination_number" expression="^(10[01][0-9]|1234)$">,然后保存。

熟悉正则表达式的人应该知道,"^(10[01][0-9])$" 匹配被叫号码 1000-1019。因此修改之后的表达式就多匹配了一个1234。FreeSWITCH 使用 Perl 兼容的正则表达式(PCRE)。现在,回到 FS-Con,或启动 fs_cli,执行 reloadxml 命令或按快捷键 F6,使新的配置生效。

在软电话中添加一个sip为 1234 ,然后重新注册,注册成功后就可以与 1010 相互拨打。

在同一台机器上运行多个软电话可能有冲突,可以直接进入 FreeSWITCH 控制台使用命令进行测试:originate (翻译:起源,产生,创始,开创)

FS> sofia status profile internal      (显示多少用户已注册)
FS> originate user/1000 &echo      (拨打1000并执行echo程序)


FS> originate sofia/internal/1000 9999        (相当于在软电话1000上拨打9999)
FS> originate sofia/internal/1000 9999 XML default       (同上)

其中,echo() 程序一个很简单的程序,它只是将你说话的内容原样再放给你听,在测试时很有用。

freeswitch 用作 软电话

FreeSWITCH 也可以简单的用作一个软电话,如 eyeBeam,配置相比 eyeBeam 略微麻烦一些,但是可以了解到 freeswitch 的配置流程。freeswitch 也是目前唯一支持 CELT 高清通话的软电话。FreeSWITCH 使用 mod_portaudio 支持你本地的声音设备。

执行 FS> load mod_portaudio  载入模块

如果得到“Cannot find an input device”之类的错误可能是你的声卡驱动有问题。如果是提示“+OK”就是成功了。模块载入成功后,才能使用 pa 命令

接着执行:FS> pa devlist

FS> pa devlist

API CALL [pa(devlist)] output:
0;Built-in Microphone;2;0;
1;Built-in Speaker;0;2;r
2;Built-in Headphone;0;2;
3;Logitech USB Headset;0;2;o
4;Logitech USB Headset;1;0;i

以上是在我笔记本上的输出,它列出了所有的声音设备。其中,0 和 3 最后的 “i” 和 “o” 分别代表声音输入(in) 和 输出(out)设备。每个电脑上可能不一样,如果想选择其它设备,可以使用命令:

FS> pa indev #0
FS> pa outdev #2

以上命令会选择我电脑上内置的麦克风和耳机。接下来你就可以有一个可以用命令行控制的软电话了。

FS> pa looptest    (回路测试,echo)
FS> pa call 9999
FS> pa call 1000
FS> pa hangup

现在你可以呼叫刚才试过的所有号码。现在假设想从SIP分机1000呼叫到你,那需要修改拨号计划(Dialplan)。用你喜欢的编辑器编辑以下文件放到conf/dialplan/default/portaudio.xml

<include>
  <extension name="call me">
    <condition field="destination_number" expression="^(me|12345678)$">
      <action application="bridge" data="portaudio"/>
    </condition>
  </extension>
</include>

然后在 FS-Con 中按 F6 或输入 reloadxml 使配置生效,在分机1000上呼叫“me”或“12345678”(你肯定想为自己选择一个更酷的号码),然后在FS-Con上应该能看到类似“[DEBUG] mod_portaudio.c:268 BRRRRING! BRRRRING! call 1”的输出(如果看不到的话按“F8”能得到详细的Log),这说明你的软电话在振铃。多打几个回车,然后输入“pa answer”就可以接听电话了。“pa hangup”可以挂断电话。

当然,你肯定希望在振铃时能听到真正的振铃音而不是看什么BRRRRRING。好办,选择一个好听一声音文件(.wav格式),编辑conf/autoload_configs/portaudio.conf.xml,修改下面一行:<param name="ring-file" value="/home/your_name/your_ring_file.wav"/>  然后重新加载模块:

FS> reloadxml
FS> reload mod_portaudio

再打打试试,看是否能听到振铃音

配置 SIP网关 拨打 外部电话

如果你拥有某个运营商提供的 SIP 账号,那么你就可以通过配置SIP 来拨打外部电话了。该SIP账号(或提供该账号的设备)在 FreeSWITCH 中称为SIP 网关(Gateway)。添加一个网关只需要在conf/sip_profiles/external/中创建一个XML文件,名字可以随便起,如:gw1.xml

<gateway name="gw1">
    <param name="realm" value="SIP服务器地址,可以是IP或IP:端口号"/>
    <param name="username" value="SIP用户名"/>
    <param name="password" value="密码"/>
    <param name="register" value="true"/>
</gateway>

如果你的 SIP 网关还需要其它参数,可以参阅同目录下的 example.xml,但一般来说上述参数就够了。然后重启 FreeSWITCH,或者不重启直接执行下面命令使配置生效。

FS> sofia profile external rescan reloadxml

FS> sofia status    查看状态。如果 gateway gw1 显示 REGED 表示成功注册到了网关上。

FS> originate sofia/gateway/gw1/xxxxxx &echo()    命令测试网关是否工作正常

通过网关 gw1 呼叫号码 xxxxxx(可能是你的手机号),被叫号码接听电话后,FreeSWITCH 会执行 echo() 程序,你应该能听到自己的回音。

从某一分机上呼出

如果网关测试正常,你就可以配置从你的 SIP 软电话或 portaudio 呼出了。由于我们是把 FreeSWITCH 当作 PBX 用,我们需要选一个出局字冠。常见的 PBX 一般是内部拨小号,打外部电话就需要加拨 0 或先拨 9 。当然这是你自己的交换机,你可以用任何你喜欢的数字(甚至是字母)。继续修改拨号计划,创建新 XML文件: conf/dialplan/default/call_out.xml

<include>
    <extension name="call out">
        <condition field="destination_number" expression="^0(\d+)$">
            <action application="bridge" data="sofia/gateway/gw1/$1"/>
        </condition>
    </extension>
</include>

其中,(\d+)为正则表达式,匹配 0 后面的所有数字并存到变量 $1 中。然后通过 bridge 程序通过网关 gw1 打出该号码。当然,建立该XML后需要在 Fs-Con 中执行 reloadxml 使用之生效。当添加网关对外注册时,FreeSWITCH 就相当于一个SIP 客户端

呼入电话处理

如果你的 SIP 网关支持呼入,那么你需要知道呼入的 DID (Direct Inbound Dial,对内直接呼入) 。 一般来说,呼入的 DID 就是你的 SIP 号码。

编辑以下XML文件放到 conf/dialplan/public/my_did.xml

<include>
    <extension name="public_did">
        <condition field="destination_number" expression="^(你的DID)$">
            <action application="transfer" data="1000 XML default"/>
        </condition>
    </extension>
</include>

在 FS-Con 执行 reloadxml 使之生效。上述配置会将来话直接转接到分机 1000 上。在后面会说明如何更灵活的处理呼入电话,如转接到语音菜单或语音信箱等。

FreeSwitch 帮助

bin/freeswitch 和 bin/fs_cli 是两个常用命令,可以创建符号链接放到  /usr/local/bin/ 下,如果 /usr/local/bin 不在你的搜索路径中,可以把 /usr/local/bin 换成 /usr/bin/。也可以不创建符号连接,直接通过修改 PATH 环境变量以包含该路径。

ln -sf /usr/local/freeswitch/bin/freeswitch /usr/local/bin/
ln -sf /usr/local/freeswitch/bin/fs_cli /usr/local/bin/

freeswitch -h

FreeSWITCH 是 Client-Server结构,不管 FreeSWITCH 运行在前台还是后台,都可以使用客户端软件 fs_cli 连接 FreeSWITCH。FreeSwitch 服务端用来提供服务。

使用 help 命令可以列出所有命令的帮助信息:freeswitch -h 或者 freeswitch -help  。某些命令也有自己的帮助信息,如 sofia。FreeSWITCH 的命令参数没有统一的解析函数,而都是由命令本身的函数负责解析的,因而不是很规范,不同的命令可能有不同的风格。所以使用时,除使用帮助信息外,最好还是查阅一下 Wiki 上的帮助(mod_commands | FreeSWITCH Documentation),那里大部分命令都有相关的例子。关于 APP,则可以参考 mod_dptools | FreeSWITCH Documentation。本书的附录中也有相应的中文参考。

常用命令:
        freeswitch -nc
        freeswitch -nonat
        freeswitch -nc -nonat
通过查看 log/freeswitch.log 跟踪系统运行情况

查看 freeswitch 是否运行

        ps -aux | grep freeswitch
        netstat -an | grep 5060
        netstat -anp | grep 5060

FreeSwitch 默认端口:Firewall | FreeSWITCH Documentation (signalwire.com)

TCP 5060:SIP internal 默认端口。
TCP 5080:SIP external 默认端口。
TCP 8021:Event Socket 默认端口。
UDP 5060:SIP internal 默认端口。
UDP 5080:SIP external 默认端口。
TCP 8080:FreeSWITCH Portal 默认端口。

用法:freeswitch [OPTIONS]
可以传递给 freswitch 的可选参数:
        -nf                    -- 不允许fork新进程
        -reincarnate           -- 异常退出时重新启动
        -reincarnate-reexec    -- 重新启动时运行execv(有助于升级)
        -u [user]              -- 启动后以非root用户user身份运行
        -g [group]             -- 启动后以非root组group身份运行
        -core                  -- dump cores
        -help            --帮助
        -version         --打印版本并退出
        -rp              --启用高(实时)优先级设置
        -lp              --启用低优先级设置
        -np              --启用正常优先级设置
        -vg              --在valgrind下运行
        -nosql           --禁用内部SQL记分板
        -heavy-timer     --更精确的时钟,但要付出代价
        -nonat           --禁用自动NAT检测
        -nonatmap        --禁用自动NAT端口映射
        -nocal           -- 禁用时钟 calibration
        -nort            -- 禁用时钟 clock_realtime
        -stop            -- 停止 freeswitch
        -nc              -- 后台方式运行,没有控制台
        -ncwait          -- 后台模式。等待系统初始化完成后再退出父进程。隐含 -nc 选项
        -c               -- 启动到控制台。默认
控制文件位置的选项:
        -base [basedir]          --指定基准目录,在配置文件中使用 $${base}
        -cfgname [filename]      --FreeSWITCH主配置文件的备用文件名
        -conf [confdir]          --FreeSWITCH配置文件的备用目录
        -log [logdir]            --日志文件的备用目录
        -run [rundir]            --运行时文件的备用目录
        -db [dbdir]              --内部数据库的备用目录
        -mod [moddir]            --模块的备用目录
        -htdocs [htdocsdir]      --htdocs的备用目录
        -scripts [scriptsdir]    --脚本的备用目录
        -temp [directory]        --临时文件的备用目录
        -grammar [directory]     --语法文件的备用目录
        -certs [directory]       --证书的备用目录
        -recordings [directory]  --记录的备用目录
        -storage [directory]     --语音邮件存储的备用目录
        -cache [directory]       --缓存文件的备用目录
        -sounds [directory]      --声音文件的备用目录

freeswitch 控制台、常用命令

不带参数会启动到控制台,在控制台上可以输入各种命令以控制或查询 FreeSWITCH 的状态。

常用命令

reloadxml      修改 xml 后,用来重新加载配置文件

shutdown      退出
help               显示帮助
version           显示当前版本
status             显示当前状态

sofia status     显示 sofia 状态
show codecs    显示编解码器

sofia status profile internal    查看连接状态
sofia status profile internal reg    查看当前已经注册的用户
sofia profile internal siptrace on    打开sip日志打开sip日志
console loglevel 7    开启控制台日志级别, 0-7, 数字越大日志越多 
originate user/1000 &echo    呼叫 默认账号,可以用来测试
originate user/1000 9195      呼叫 默认账号,可以用来测试
originate user/1008 &playback(file_string://D:/Temp/1.wav)    自动拨打音频文件测试
hupall    挂断所有通话

bgapi        放到后台执行(命令执行都是阻塞的,有些命令需要执行相对较长的时间才能返回,因而有时我们会把某些命令放到后台执行)。

官方文档:https://freeswitch.org/confluence/display/FREESWITCH/mod_commands

在 freeswitch 或 fs_cli 可执行以下内部命令:只输入 tab,然后 enter 可以查看所有命令

输入 tab 可补全命令,输入空格再输入 tab 可补全子命令只输入命令不输入参数,或输入错误的参数,都会提示用法。

  • bgapi API [ARG [ ...]]:将API命令放到后台执行。
  • callcenter_config agent add AGENT callback|uuid_standby:向呼叫中心添加坐席。
  • callcenter_config agent list:列出呼叫中心所有坐席。
  • callcenter_config agent set ATTRIBUTE AGENT VALUE:设置呼叫中心坐席的属性。
  • callcenter_config queue list:列出所有呼叫中心所有队列。
  • callcenter_config tire list:列出呼叫中心所有梯队。
  • cdr_csv rotate:令CSV话单文件轮替。
  • conference:查看会议命令的帮助。
  • conference CONFERENCE agc MEMBERID|all|last|non_moderator VALUE:启用自动增益控制(Auto Gain Control)。
  • conference CONFERENCE bgdial CALLURL [CALLERIDNUMBER] [CALLERIDNAME]:从会议呼出,令对方加入会议。其是非阻塞的。
  • conference CONFERENCE chkrecord [FILEPATH]:检查会议录音情况。
  • conference CONFERENCE clear-vid-floor:取消视频会议的floor。
  • conference CONFERENCE deaf MEMBERID|all|last|non_moderator:将会议成员禁听。可指定所有成员、最后加入的成员、非主席成员。
  • conference CONFERENCE dial CALLURL [CALLERIDNUMBER] [CALLERIDNAME]:从会议呼出,令对方加入会议。其是阻塞的。
  • conference CONFERENCE dtmf MEMBERID|all|last|non_moderator DIGITS:向会议成员发送DTMF。可指定所有成员、最后加入的成员、非主席成员。
  • conference CONFERENCE energy MEMBERID|all|last|non_moderator VALUE:设置会议成员的能量值(音量超过此值才能混音到会议桥中)。可指定所有成员、最后加入的成员、非主席成员。
  • conference CONFERENCE enter_sound file FILEPATH:设置会议成员进入时向其它成员播放声音的文件。
  • conference CONFERENCE enter_sound none:设置会议成员进入时向其它成员播放声音为静音。
  • conference CONFERENCE enter_sound off:设置会议成员进入时向其它成员播放声音为禁用。
  • conference CONFERENCE enter_sound on:设置会议成员进入时向其它成员播放声音为启用。
  • conference CONFERENCE exit_sound file FILEPATH:设置会议成员退出时向其它成员播放声音的文件。
  • conference CONFERENCE exit_sound none:设置会议成员退出时向其它成员播放声音为静音。
  • conference CONFERENCE exit_sound off:设置会议成员退出时向其它成员播放声音为禁用。
  • conference CONFERENCE exit_sound on:设置会议成员退出时向其它成员播放声音为启用。
  • conference CONFERENCE file_seek [+|-]VALUE MEMBERID:会议中播放声音文件的快进、快退。
  • conference CONFERENCE floor MEMBERID|last:将会议成员设置为floor。没什么用。
  • conference CONFERENCE get PARAM:获取会议参数的值。参数有max_members、sound_prefix、caller_id_name、caller_id_number、endconference_grace_time。
  • conference CONFERENCE hup MEMBERID|all|last|non_moderator:挂断会议成员。可指定所有成员、最后加入的成员、非主席成员。
  • conference CONFERENCE kick MEMBERID|all|last|non_moderator:从会议中踢出成员。可指定所有成员、最后加入的成员、非主席成员。
  • conference [CONFERENCE] list:列出会议的成员。
  • conference CONFERENCE lock:锁定会议,别人无法加入。
  • conference CONFERENCE mute MEMBERID|all|last|non_moderator:将会议成员禁言。可指定所有成员、最后加入的成员、非主席成员。
  • conference CONFERENCE nopin:取消会议的密码。
  • conference CONFERENCE norecord [FILEPATH|all]:停止会议录音。
  • conference CONFERENCE pause [FILEPATH]:暂停会议中声音文件的播放,或暂停会议录音。
  • conference CONFERENCE pin PIN:设置会议的密码。
  • conference CONFERENCE play FILEPATH [MEMBERID]:向会议中播放声音文件,可指定成员。
  • conference CONFERENCE record FILEPATH:对会议录音。
  • conference CONFERENCE recording check|pause|resume|start|stop FILEPATH|all:如在会议的Profile中设置了录音摸板,则可如此检查/暂停/恢复/开始/停止录音。
  • conference CONFERENCE relate MEMBERID1[,...] MEMBERID2[,...] clear:清除会议成员前者和后者之间可否听到的关系。
  • conference CONFERENCE relate MEMBERID1[,...] MEMBERID2[,...] nohear:令会议成员后者听不到前者。
  • conference CONFERENCE relate MEMBERID1[,...] MEMBERID2[,...] nospeak:令会议成员后者说话前者听不到。
  • conference CONFERENCE resume [FILEPATH]:恢复会议录音。
  • conference CONFERENCE say TEXT:在会议中使用TTS播放文本。
  • conference CONFERENCE saymember MEMBERID TEXT:在会议中仅向某个成员使用TTS播放文本。
  • conference CONFERENCE set PARAM VALUE:设置会议参数的值。参数有max_members、sound_prefix、caller_id_name、caller_id_number、endconference_grace_time。
  • conference CONFERENCE stop all|current|last [MEMBERID]:停止会议中声音文件的播放。可指定所有正在播放、当前正在播放、最后播放。
  • conference CONFERENCE tmute MEMBERID|all|last|non_moderator:切换会议成员的禁言/不禁言状态。可指定所有成员、最后加入的成员、非主席成员。
  • conference CONFERENCE transfer OTHERCONFERENCE MEMBERID[ ...]:将会议成员转到另一个会议。
  • conference CONFERENCE undeaf MEMBERID|all|last|non_moderator:取消对会议成员的禁听。可指定所有成员、最后加入的成员、非主席成员。
  • conference CONFERENCE unlock:解锁会议,允许加入。
  • conference CONFERENCE unmute MEMBERID|all|last|non_moderator:取消对会议成员的禁言。可指定所有成员、最后加入的成员、非主席成员。
  • conference CONFERENCE vid-floor MEMBERID|last [force]:将视频会议成员设置为持有floor,在视频会议中所有成员都看到持有floor成员的画面。如指定force则固定成员,不再根据声音大小自动切换画面。
  • conference CONFERENCE volume_in MEMBERID VALUE:设置会议成员的输入音量。
  • conference CONFERENCE volume_out MEMBERID VALUE:设置会议成员的输出音量。
  • conference [CONFERENCE] xml_list:列出会议的成员,XML格式。
  • console loglevel console|alert|crit|err|warning|notice|info|debug:设置控制台日志等级。
  • curl URL:与HTTP服务器交互。
  • db delete|insert|select:持久化数据库存储。
  • distributor LIST:从号码连选的列表中选择一个节点名称。
  • echo STR[ ...]:原样输出字符串。
  • esf_page_group [MULTICASTIP MULTICASTPORT CONTROLPORT] :发送组播RTP包。三个参数的默认值依次为:224.168.168.168、34567、6061。
  • expand API [ARG [ ...]]:将${}$${}引起的变量替换为实际值后再执行API命令。
  • expr EXPR[;...]:计算表达式结果。EXPR可为:
    • 数学表达式:如1+1。
    • 函数:
      • ceil(NUMBER):向上取整。
      • random(BEGIN,END,&SEED):获取区间之间的随机数。
      • randomize(&SEED):设置随机数种子。
  • fifo list [LIST]:查看呼叫队列的状态。
  • fifo reparse:重新解析呼叫队列的配置。
  • fifo_member add LIST CALLURL:动态增加呼叫队列坐席。
  • fifo_member del LIST CALLURL:动态删除呼叫队列坐席。
  • fsctl crash:令FreeSWITCH崩溃。
  • fsctl max_sessions [VALUE]:查看或设置呼叫最大并发数。
  • fsctl sps [VALUE]:查看或设置每秒最大呼叫数。
  • global_getvar VAR:查看全局变量的值。
  • hash delete/HASH/KEY:删除内存中哈希表数据结构的键值对。
  • hash insert/HASH/KEY/VALUE:插入内存中哈希表数据结构的键值对。
  • hash select/HASH/KEY:获取内存中哈希表数据结构的键值对的值。
  • help:查看帮助。
  • hupall:挂断所有通话。
  • jsrun JSFILEPATH:执行JavaScript脚本。
  • load MOD:加载模块。
  • lua LUAFILEPATH [ARG [ ...]]:执行Lua脚本。在当前线程执行,会阻塞当前线程。
  • luarun LUAFILEPATH [ARG [ ...]]:执行Lua脚本。在新线程执行,不会阻塞当前线程。
  • nat_map status:查看NAT端口映射状态。
  • originate CALLURL EXTEN|&APP(ARG[ ...]) [DIALPLAN] [CONTEXT] [CALLERIDNAME] [CALLERIDNUMBER] [TIMEOUTSEC]:发起对CALLURL的呼叫,接听后将另一端转入Dialplan路由至EXTEN或执行APP。当使用XML Dialplan时EXTEN为另一个号码;当使用inline Dialplan时EXTEN为[m:C:]APP[:ARG][,...,APPN[:ARGN]],如果ARG有空格则需使用单引号括起,如ARG有逗号则将分隔符替换为字符C。DIALPLAN默认为XML。inline Dialplan会忽略CONTEXT,Lua Dialplan的CONTEXT为Lua文件路径。CALLERIDNAME为来电显示的名字,CALLERIDNUMBERBER为来电显示的号码。TIMEOUTSEC为对方收到INVITE消息后不回复100 Trying的超时秒数。会阻塞并在收到媒体指示后返回(如183或200消息)。
  • pa answer:接听电话。
  • pa call USERNAME:呼叫用户。
  • pa devlist:列出portaudio模块发现的设备。
  • pa hangup:挂机。
  • pa indev #N:使用第N个设备作为输入设备。
  • pa looptest:echo回路测试。
  • pa outdev #N:使用第N个设备作为输出设备。
  • python PYFILEPATH:执行Python脚本。文件路径为相对于Python的查找目录(如/usr/lib/python2.7/dist-packages/)。
  • regex STR | REGEX [| REPLACEMENT]:测试字符串STR是否匹配正则表达式REGEX,如有REPLACEMENT则使用其替换匹配的内容。REPLACEMENT可以使用$N%N的形式引用匹配的分组。
  • reload MOD:重新加载模块。
  • reloadxml:重新加载XML配置文件。
  • rtmp status:查看RTMP状态。
  • rtmp_contact [PROFILE/]USERNAME[@DOMAIN]:返回使用RTMP的已注册用户的联系地址。
  • show channels:查看正在通话的通道。
  • show codec:查看可用的编解码类型及其支持模块。
  • show dialplan:查看可用的Dialplan及其支持模块。
  • show file:查看可用的文件类型及其支持模块。
  • show nat_map:查看NAT端口映射关系。
  • shutdown:停止FreeSWITCH。
  • sofia global capture on|off:开启/关闭所有Profile的Homer方式抓包。
  • sofia global siptrace on|off:开启/关闭所有Profile的SIP消息打印。
  • sofia help:查看帮助。
  • sofia loglevel all|default|tport|iptsec|nea|nta|nth_client|nth_server|nua|soa|sresolv|stun 0|1|2|3|4|5|6|7|8|9:设置Sofia底层协议栈中指定模块的日志级别。越大信息越详细。
  • sofia profile PROFILE capture on|off:开启/关闭指定的Profile的Homer方式抓包。
  • sofia profile PROFILE flush_inbound_reg USERNAME@DOMAIN|CALLID:将指定的Profile中指定的已注册用户清除。
  • sofia profile PROFILE killgw GATEWAY:删除指定的Profile中指定的网关。
  • sofia profile PROFILE rescan:刷新指定的Profile。隐含reloadxml,并不是所有配置参数都能生效。
  • sofia profile PROFILE register GATEWAY:令指定的Profile中指定的网关立即向外注册。
  • sofia profile PROFILE restart:重启指定的Profile。隐含reloadxml。
  • sofia profile PROFILE siptrace on|off:开启/关闭指定的Profile的SIP消息打印。
  • sofia profile PROFILE start:启动指定的Profile。隐含reloadxml。
  • sofia profile PROFILE stop:停止指定的Profile。隐含reloadxml。
  • sofia profile PROFILE unregister GATEWAY:令指定的Profile中指定的网关立即向外注销。
  • sofia recover:从数据库中读出FreeSWITCH崩溃前的通话信息并恢复。
  • sofia status|xmlstatus:查看Sofia状态。
  • sofia status|xmlstatus gateway GATEWAY:查看指定网关的状态。
  • sofia status|xmlstatus profile PROFILE:查看指定的Profile的状态。
  • sofia status|xmlstatus profile PROFILE reg:查看指定的Profile所有已注册用户。
  • sofia status|xmlstatus profile PROFILE reg USERNAME:查看指定的Profile中指定的已注册用户。通过SIP的Contact头过滤。
  • sofia status|xmlstatus profile PROFILE user USERNAME@DOMAIN:查看指定的Profile中指定的已注册用户。
  • sofia tracelevel console|alert|crit|err|warning|notice|info|debug:console只会打印到控制台,不会写入日志文件。
  • sofia_contact [PROFILE/]USERNAME[@DOMAIN]:返回已注册用户的联系地址。
  • sofia_count_reg USERNAME@DOMAIN:查看该用户使用多少个客户端注册。在允许多点注册的情况下会有多个。
  • sofia_dig IP:返回其他服务器的地址和端口。类似DNS的dig。
  • sofia_presence_data list USERNAME@DOMAIN:列出指定用户的Presence信息。
  • sofia_presence_data status USERNAME@DOMAIN:列出指定用户的Presence状态。
  • sofia_presence_data user_agent USERNAME@DOMAIN:列出指定用户的Presence的user agent信息。
  • sofia_username_of USERNAME@DOMAIN:返回已注册用户的用户名。
  • status:查看服务器状态。
  • strepoch:显示当前的Unix时间戳。
  • strftime [FORMAT]:将当前时间格式化显示。
  • stun STUNSERVER:使用STUN服务器检测公网IP和端口。
  • system CLI [ARG [...]]:调用系统命令。
  • uptime:查看服务启动的秒数。
  • uuid_bridge CHANNELUUID1 CHANNELUUID2:使用UUID桥接两个通话Channel。
  • uuid_debug_media CHANNELUUID read|write|both|vread|vwrite|vboth on|off:打开/关闭指定通话Channel的媒体流调试信息。read为收,write为发,both为收发,v开头为可打印视频媒体流。每行输出包括以下信息:R或W表示收或发,呼叫字符串,b=表示RTP包大小(含包头),本地IP和端口,远端IP和端口,pt=表示载荷类型,ts=表示时间戳,m=表示RTP的Marker。
  • uuid_kill CHANNELUUID:释放通话Channel。
  • uuid_record CHANNELUUID start FILENAME:对指定的通话Channel开始录音。如不指定扩展名,则以原生格式录音。
  • uuid_record CHANNELUUID stop FILENAME|all:对指定的通话Channel停止录音。如使用all则停止通话Channel的所有录音。如不指定扩展名,则以原生格式录音。
  • uuid_setvar CHANNELUUID VAR VALUE:对指定的通话Channel设置通道变量。
  • uuid_transfer(CHANNELUUID CALLURL|lua:LUAFILENAME [DIALPLAN [CONTEXT]]):将通话重新转移到ROUTING阶段,重新去Dialplan中进行路由。
  • version:查看服务器版本。
  • xml_curl debug_on:打开mod_xml_curl模块的调试。会将每次请求得到的XML文件存放到系统临时目录中。

有些命令可以使用COMMAND help查看帮助。示例:

originate user/1000 &echo
originate user/1000 1001
originate user/1000 1001 XML
originate user/1000 echo
originate user/1000 echo inline
originate {origination_caller_id_name=test}{effective_caller_id_name=haha}user/1000 &bridge(user/1001)

快捷键

为了调试方便,FreeSWITCH 还在 conf/autoload_configs/switch.conf.xml 中定义了一些控制台快捷键。可以通过 F1-F12 来使用它们(不过在某些操作系统上,有些快捷键可能与操作系统的相冲突,那你就只直接输入这些命令或重新定义他们了)。

fs_cli -h

FreeSWITCH 是 Client-Server结构,不管 FreeSWITCH 运行在前台还是后台,都可以使用客户端软件 fs_cli 连接 FreeSWITCH。fs_cli 是一个类似 Telnet 的客户端,它使用 FreeSWITCH 的 ESL(Event Socket Library)库与 FreeSWITCH 通信。当然,需要加载模块 mod_event_socket。该模块是默认加载的。如果 fs_cli 启动失败,检车FreeSWITCH 有没有启动或 mod_event_socket 没有正确加载,检查TCP端口8021端口是否处于监听状态或被其它进程占用。

输入 /help <enter> 查看命令列表
用法: ./fs_cli [-H <host>] [-P <port>] [-p <secret>] [-d <level>] [-x command] [-t <timeout_ms>] [profile]
    -?,-h --help                    帮助
    -H, --host=hostname             freeswitch服务端的IP 
    -P, --port=port                 freeswitch服务端的端口 (1 - 65535)
    -u, --user=user@domain          user@domain
    -p, --password=password         密码
    -i, --interrupt                 允许 Control-c 中断
    -x, --execute=command           执行命令后,在退出
    -l, --loglevel=command          Log 级别
    -U, --log-uuid                  log 输出中包括 UUID
    -S, --log-uuid-short            在日志输出中包含缩短的UUID
    -q, --quiet                     禁用日志
    -r, --retry                     连接失败时重试
    -R, --reconnect                 断连是重新连接
    -d, --debug=level               Debug 级别 (0 - 7)
    -b, --batchmode                 Batch mode
    -t, --timeout                       API命令超时时间 (in milliseconds)
    -T, --connect-timeout           socket连接超时时间 (in milliseconds)
    -n, --no-color                  禁用彩色显示
    -s, --set-log-uuid              设置 UUID 来过滤 log events

-x 参数,它允许执行一条命令后退出。示例

bin/fs_cli -x "version"
bin/fs_cli -x "status"

进入控制台后,可执行以下命令:

  • 在 fs_cli 中,有几个特殊的命令是以 / 开头,这些命令并不直接发送到 FreeSWITCH,而是先由 fs_cli 处理。其它一些 / 开头的指令与 Event Socket 中相关的命令相同,如:
            /help        帮助
            /quit、/bye、/exit、Ctrl + D  都可以退出 fs_cli
            /event plain|json|xml EVENTTYPE [SUBCLASS]:订阅事件。
            /noevents   关闭事件接收
            /nixevent    除了特定一种外,开启所有事件
            /log       设置 log 级别,如 /log info 或 /log debug 等
            /nolog     关闭 log
            /filter    过滤事件
  • 另外,一些 "重要" 命令不能直接在 fs_cli 中执行,如 shutdown 命令,在控制台上可以直接执行,但在 fs_cli 中,需要执行 fsctl shutdown。除此之外,其它命令都与直接在 FreeSWITCH 控制台上执行是一样的。它也支持快捷键,最常用的快捷键是 F6(reloadxml)、F7(关闭 log输出)、F8(开启 debug 级别的 log 输出)。

日志级别可使用数值或字符串表示。数值越大显示越详细,字符串不区分大小写:

  • 0 - EMERG/CONSOLE
  • 1 - ALERT
  • 2 - CRIT
  • 3 - ERR
  • 4 - WARNING
  • 5 - NOTICE
  • 6 - INFO
  • 7 - DEBUG

uuid_kill <uuid>        # 挂断(结束)一个当前活动的通话
fsctl hupall               # 挂断所有当前活动的通话
fsctl hupall normal_clearing        # 挂断并发送正常挂断原因
fsctl hupall USER_BUSY              # 挂断并发送用户忙原因
fsctl hupall ORIGINATOR_CANCEL      # 挂断并发送发起者取消原因

fs_cli 连接 远程 fs

使用 fs_cli,不仅可以连接到本机的 FreeSWITCH,也可以连接到其它机器的 FreeSWITCH,通过在用户主目录下编辑配置文件 .fs_cli_conf(注意前面的点 "." ),可以定义要连接的多个机器:

[server1]
host     => 192.168.1.10
port     => 8021
password => secret_password
debug    => 7

[server2]
host     => 192.168.1.11
port     => 8021
password => someother_password
debug    => 0

一旦配置好,就可以这样使用它:

bin/fs_cli server1
bin/fs_cli server2

如果要连接到其他机器,要确保目标机器的 FreeSWITCH 的 Event Socket 是监听在真实网卡的IP地址上,而不是 127.0.0.1,可以修改 conf/autoload configs/event_socket.conf.xml 中的IP地址改成为服务器IP 或"0.0.0.0"实现,当然这可能带来潜在的安全性问题。如果你的服务器运行在公网上,则需要考虑你是否确实需要这样做,或者至少考虑设置一下ACL或防火墙规则只允许特定的IP 地址访问。当然,记得改完后在控制台上要执行 "reload mod_event_socket"

core.db

show 命令的大部分内容基于这些表。包含的表有:

  • aliases:别名表,用于存储命令行别名。
  • basic_calls:一个视图,基于channels和calls表,提供基本的呼叫信息。
  • calls:呼叫表,在bridge的呼叫中,用于关联Channel表中的两条腿。
  • channels:存储所有当前的Channel。
  • complete:存储所有Tab Complete数据。
  • detailed_calls:一个视图,基于channels和calls,提供详细的呼叫信息。
  • interfaces:存储所有的Interface。
  • nat:存储当前的NAT映射关系。
  • recovery:在使用系统恢复功能时,该表存储所有呼叫的详细信息。
  • registrations:存储注册用户的信息。与sofia_reg_PROFILE.db的sip_registrations表中有些数据是重复的。由于不止SIP用户需要注册,其它模块的用户也可能需要注册,此处存储的是统一的注册信息。
  • tasks:当前的任务表,如heartbeat(心跳)、检测IP地址变化等。

sofia_reg_PROFILE.db。每个Profile都使用一个单独的数据库(PROFILE替换为实际的Profile名字)。包含的表有:

  • sip_authentication:SIP认证相关。
  • sip_dialogs:SIP对话相关。
  • sip_presence:SIP呈现相关。
  • sip_registrations:SIP用户的注册信息。
  • sip_shared_appearance_dialogs:SIP SLA相关,记录当前的Dialog。
  • sip_shared_appearance_subscriptions:SIP SLA相关,记录订阅信息。
  • sip_subscriptions:SIP订阅相关。

sofia 命令

FreeSWITCH 的 sofia 命令用于管理和配置其 SIP 组件,如注册用户、呼叫路由、呼叫转移等。

下面是一些常用的 sofia 命令及其功能:

  • sofia status:显示 SIP 组件的状态信息,包括已注册用户、呼叫路由等。
  • sofia profile:管理 SIP 配置文件的配置选项。
    sofia profile internal start:启动内部配置文件。
    sofia profile external stop:停止外部配置文件。
  • sofia global:管理全局 SIP 配置选项。
    sofia global siptrace on:开启 SIP 跟踪。
    sofia global siptrace off:关闭 SIP 跟踪。
  • sofia register:管理 SIP 注册用户。
    sofia register <profile> <username> <password> <server>:注册一个 SIP 用户。
    sofia register <profile> unregister <username>:取消注册一个 SIP 用户。
  • sofia contact:管理 SIP 联系方式。
    sofia contact list:列出所有已注册的 SIP 用户。
    sofia contact kill <contact-id>:断开指定的 SIP 联系。
  • sofia gateway:管理 SIP 网关。
    sofia gateway list:列出所有已配置的 SIP 网关。
    sofia gateway kill <gateway-name>:停止指定的 SIP 网关。
  • sofia profile inbound_reg:管理入站 SIP 注册。
    sofia profile inbound_reg start:启动入站 SIP 注册。
    sofia profile inbound_reg stop:停止入站 SIP 注册。
  • sofia profile outbound_reg:管理出站 SIP 注册。
    sofia profile outbound_reg start:启动出站 SIP 注册。
    sofia profile outbound_reg stop:停止出站 SIP 注册。

这只是一些常用的 sofia 命令示例,FreeSWITCH 的 sofia 命令非常强大且灵活,还有很多其他选项和功能可以探索和使用。

发起呼叫

在 FreeSWITCH 中使用 originate 命令发起一次呼叫。示例:用户 1000 已经注册,那么:originate user/1000 &echo  命令在呼叫 1000 这个用户后,便执行 echo 这个程序。echo 是一个回音程序,即它会把任何它 "听到" 的声音(或视频)再返回(说)给对方。因此,如果这时候用户 1000 接了电话,无论说什么都能听到自己的声音。

呼叫 字符串 (Dial String)

user/1000 称为 "Dial String 呼叫字符串" 或 "呼叫 URL"。user 是一种特殊的呼叫字符串。

示例,假设:

  • FreeSWITCH  UA 的地址为 192.168.4.4:5050,
  • alice  UA 的地址为 192.168.4.4:5090,
  • bob  UA 的地址为 192.168.4.4:26000。

若 alice 已向 FreeSWITCH 注册,在 FreeSWITCH 中就可以看到她的注册信息:freeswitch@xxxx> sofia status profile internal reg

Registrations:
============================================================
Call-ID:        ZTRkYjdjYzY0OWFhNDRhOGFkNDUxMTdhMWJhNjRmNmE.
User:           alice@192.168.4.4
Contact:        "Alice" <sip:alice@192.168.4.4:5090;rinstance=a86a656037ccfaba;transport=UDP>
Agent:          Zoiper rev.5415
Status:         Registered(UDP)(unknown) EXP(2010-05-02 18:10:53)
Host:           du-sevens-mac-pro.local
IP:             192.168.4.4
Port:           5090
Auth-User:      alice
Auth-Realm:     192.168.4.4
MWI-Account:    alice@192.168.4.4

============================================================

FreeSWITCH 根据 Contact 字段知道 alice 的 SIP 地址 sip:alice@192.168.4.4:5090。当使用 originate 呼叫 user/alice 这个地址时,FreeSWITCH 便查找本地数据库,向 alice 的地址 sip:alice@192.168.4.4:5090 发送 INVITE 请求(实际的呼叫字符串是由用户目录中的 dial-string 参数决定的)。

呼叫字符串格式为(此处使用<>引起表示内容可选,/表示使用左侧或右侧内容,...表示重复之前内容。):<{GLOBALVAR=VALUE<,...>}...><[LOVALVAR=VALUE<,...>]>DIALSTR<,/|...>

{}括起的通道变量是全局的,作用于呼叫字符串中每一条腿;[]括起的通道变量为局部通道变量,只作用于呼叫字符串中靠近它的某一条腿。通道变量VALUE中的逗号需要使用\转义,或在VALUE的开头使用^^C来将分隔符替换为指定的字符C。DIALSTR使用,隔开表示同振,使用|隔开表示顺振。

DIALSTR可为(一般来说,每种Endpoint都会提供相应的呼叫字符串,每一种呼叫字符串的类型都属于一个Endpoint Interface):

  • freetdm/SPAN/CHANNEL/USERNAME:mod_freetdm呼叫字符串。CHANNEL为a表示从小到大选择一个空闲时隙,为A表示从大到小选择一个空闲时隙。
  • h323/USERNAME@IP:mod_h323的h323呼叫字符串。
  • loopback/USERNAME:loopback呼叫字符串。
  • opal/h323:USERNAME@IP:mod_opal的h323呼叫字符串。
  • rtmp/UUID/USERNAME@IP[:PORT]:rtmp呼叫字符串。
  • sofia/PROFILE/[sip:]USERNAME[@IP[:PORT]]:指定PROFILE的用户。
  • sofia/gateway/GATEWAY/USERNAME:网关用户。
  • user/USERNAME[@DOMAIN]:本地用户。

示例:

{origination_caller_id_name=test}{effective_caller_id_name=haha}user/1000
{origination_caller_id_name=test}{effective_caller_id_name=haha}user/1000|sofia/internal/1001
{origination_caller_id_name=test}[effective_caller_id_name=haha]user/1000,[effective_caller_id_name=hehe]sofia/internal/1001

API 与 APP

originate 命令(Command)用于控制 FreeSWITCH 发起一个呼叫。FreeSWITCH 的命令不仅可以在控制台上使用,也可以在各种嵌入式脚本、Event Socket (fs_cli 就是使用了 ESL库)或 HTTP RPC 上使用,所有命令都遵循一个抽像的接口,因而这些命令又称 API Commands。

echo() 则是一个程序(Application,简称 APP),它的作用是控制一个 Channel 的一端。我们知道,一个 Channel 有两端,在上面的例子中,alice 是一端,别一端就是 echo()。电话接通后相当于 alice 在跟 echo() 这个家伙在通话。另一个常用的 APP 是 park()

示例:originate user/alice &park()

初始化了一个呼叫,在 alice 接电话后对端必须有一个人在跟也讲话,否则的话,一个 Channel 只有一端,那是不可思议的。而如果这时 FreeSWITCH 找不到一个合适的人跟 alice 通话,那么它可以将该电话“挂起”,park() 便是执行这个功能,它相当于一个 Channel 特殊的一端。

park() 的用户体验不好,alice 不知道要等多长时间才有人接电话,由于她听不到任何声音,实际上她在奇怪电话到底有没有接通。相对而言,另一个程序 hold()则比较友好,它能在等待的同时播放保持音乐(MOH, Music on Hold)。

示例:originate user/alice &hold()

也可以直接播放一个声音文件:originate user/alice &playback(/root/welcome.wav)

或者 直接录音: originate user/alice &record(/root/voice_of_alice.wav)

以上的例子实际上都只是建立一个 Channel,相当于 FreeSWITCH 作为一个 UA 跟 alice 通话。它是个一条腿(one leg,只有a-leg)的通话。

在大多数情况下,FreeSWITCH 都是做为一个 B2BUA 来桥接两个UA 进行通话的。在 alice 接听电话以后,bridge()程序可以再启动一个 UA 呼叫 bob:originate user/alice &bridge(user/bob) 。终于,alice 和 bob 可以通话了。我们也可以用另一个方式建立他们之音的通话:

originate user/alice &park()
originate user/bob &park()
show channels
uuid_bridge <alice_uuid> <bob_uuid>

在这里,我们分别呼叫 alice 和 bob,并把他们暂时 park 到一个地方。通过命令 show channels 我们可以知道每个 Channel 的 UUID,然后使用 uuid_bridge 命令将两个 Channel 桥接起来。与上一种方式不同,上一种方式实际上是先桥接,再呼叫 bob。

上面,我们一共学习了两条命令(API),originate 和 uuid_bridge。以及几个程序(APP) - echo、park、bridge 等。可以发现 uuid_bridge API 和 bridge APP 有些类似,他们一个是先呼叫后桥接,另一个是先桥接后呼叫,那么,它们到底有什么本质的区别呢?

api 和 app 区别

  • 一个 APP 是一个程序(Application),它作为一个 Channel 一端,与另一端的 UA 进行通信,相当于它工作在 Channel 内部;在 dialplan 中执行的程序都是 APP( dialplan 中也能执行一些特殊的 API)。APP 通过 mod_dptools 模块加载,因而 APP 又称为拨号计划工具(Dialplan Tools)。
  • 一个 API 则是独立于一个 Channel 之外的,它只能通过 UUID 来控制一个 Channel。通常在控制台上输入的命令都是 API;大部分公用的 API 都是在 mod_commands 模块中加载的;
  • 某些模块(如 mod_sofia)有自己的的 API 和 APP。某些 APP 有与其对应的 API,如上述的 bridge/uuid_bridge,还有 transfer/uuid_transfer、playback/uuid_playback等。UUID 版本的 API 都是在一个 Channel 之外对 Channel 进行控制的,它们不参与到通话中,但是却可以对正在通话的 Channel 进行操作。场景:例如 alice 和 bob 正在畅聊,有个坏蛋使用 uuid_kill 将电话切断,或使用 uuid_broadcast 给他们广播恶作剧音频,或者使用 uuid_record 把他们谈话的内容录音等。

呼叫流程、相关概念

在 FreeSWITCH 中,每一次呼叫都由一条或多条 "腿" (Call Leg) 组成,每一条腿又称为一个 Channel(信道),每一个 Channel 都有好多属性,用于标识 Channel 的状态,性能等。

总结:只要发起呼叫,就会产生一条腿,或者一个 Session (也可以叫做Channel通道)

来话 (Inbound call)、去话 (Outbound call)

典型的呼叫流程有以下两种:

  • 单腿呼叫:这也是比较常见的呼叫,通常用于语音通知,IVR 等业务场景。单腿呼叫只有一个call leg,这里FreeSWITCH自身作为一方与UA进行“通话”。
  • 两方呼叫:这是最常见的呼叫,它用于完成两个用户 UAa 和 UAb之间的通话。
    在通过 FreeSWITCH 建立的两方呼叫中,包含两个 call leg;
    FreeSWITCH(作为B2BUA)分别与UAa建立一个call leg(a-leg) 和 UAb 建立一个call leg(b-leg)。建立后,FreeSWITCH将两个 call leg 桥接起来,UAa 和 UAb 两方就可以进行通话了。
    这一种又有一种变种:如市场上有人利用上、下行通话的不对称性,卖电话回拨卡获取不正当利润:UAa 呼叫 FreeSWITCH,FreeSWITCH 不应答,而是在获取 UAb 的主叫号码后直接挂机;然后 FreeSWITCH 回拨 UAb;UAb 接听后 FreeSWITCH 启动一个 IVR 程序指示 UAb 输入 UAa 的号码;然后 FreeSWITCH 呼叫 UAa

呼叫腿 (Call legs) 是呼叫的基本构成,一个呼叫中可能包含一个呼叫腿(单腿呼叫),也可能包含两个呼叫腿(两方呼叫),甚至多个呼叫腿(多方呼叫)。对 FreeSwitch 而言,在一个呼叫中,作为B2BUA,它和每个对端的 SIP UA 之间的连接都构成了一个 call leg。

根据 SIP呼叫发起的方向 ( 都是以 FreeSWITCH 作为参照物),call leg 可以分为来话和去话两种

  • 来话(Inbound call):对端为主叫,FreeSwitch是被叫
    示例:"用户UAa ---> FreeSWITCH" 的通话称为 来话
  • 去话(Outbound call):对端是被叫,FreeSwitch是主叫。
    示例:"FreeSWITCH ---> 用户UAb" 的通话称为 去话。

在两方通话中,根据call leg建立的先后关系,call leg常被称为A leg 和 B leg

  • a-leg:通常是先建立的 call leg,对应主动发起呼叫的一方。
  • b-leg:通常是后建立的 call leg, 对应被动接受呼叫一方。
  • 通俗理解:相对于 freeswitch 来说,进来的是 a-leg,出去的是 b-leg

注:无论来话还是去话,对每一次呼叫 FreeSWITCH 都会启动一个Session (会话) 用于控制整个呼叫,它会一直持续到通话结束。在FreeSWITCH中,一个channel 代表一个call leg。

每个 Session 都控制着一个 Channel (通道,又称信道),Channel 是一对UA间通信的实体,相当于FreeSWITCH的一条腿(leg),每个 Channel 都用一个唯一的 UUID 来标识,称为Channel UUID。另外,Channel 上可以绑定一些呼叫参数,称为 Channel Variable (通道变量)。Channel 中可能包含媒体(音频或视频流)也可能不包含。通话时,FreeSWITCH 的作用是将两个 Channel (a-leg 和 b-leg,通常先创建的或占主动的叫a-leg)桥接(bridge)到一起,使双方可以通话。这两路桥接的通话(两条腿)在逻辑上组成一个通话,称为一个 Call。

呼叫 "基本概念、发起模式"

有两种典型的呼叫发起模式

  • 1PCC(First-party call-control)
  • 3PCC(Third party call control)

如上图所示,1PCC 与 3PCC 关键的区别在于第一段呼叫的发起

  • 1PCC模式中,第一段呼叫(a-leg)是由Caller SIP UA通过SIP INVITE发起的,在FreeSwitch中呼叫是由SIP协栈(Sofia)触发的。
  • 3PCC模式中,第一段呼叫(a-leg)在FreeSwich中是应用(即所谓third pary)触发的(通过originate命令),SIP呼叫是FreeSwitch主动发起的。

在1PCC模式中,第一段呼叫(a-leg)中,FreeSwitch是被叫,因此这段呼叫是来话(inbound call),第二段呼叫(b-leg)中,FreeSwitch是主叫,因此这段呼叫是去话(outbound call)。

在3PCC模式中,两段呼叫(a-leg,b-leg)中,FreeSwitch都是主叫,因此这两段呼叫都是去话(outbound call)。

FreeSWICH 呼叫控制

来话处理(呼入)

来话处理,即对呼入呼叫的处理,分为以下几个阶段

呼叫发起&初始化

  1. 在一个 SIP Profile 的 SIP 端口(例如5060)上收到INVITE,
  2. FreeSwitch 首先创建新的 channel(A-leg)。
    进行ACL鉴权,通过以后根据当前SIP Profile的conext参数确定呼叫路由上下文,否则
    根据主叫号码查询user directory得到用户信息,完成用户名密码鉴权,并根据用户信息中的context 配置确定呼叫路由上下文

呼叫路由

  1. 根据呼叫初始化阶段确定的呼叫路由上下文,解析响应的dialplan配置脚本
  2. 使用当前呼叫信息匹配dialplan extention,确定要执行的actions
  3. 根据actions中的app的类型进行呼叫处理;或者进一步路由到脚本或外部应用中进行处理,如:
    <action application="lua" data="test.lua"/> 将来话路由给Lua脚本(test.lua)进行处理。
    <action application="socket" data="127.0.0.1:8040"/> 将来话路由到event socket接口上,交给外部应用(TCP Server)来处理。

呼叫处理

1. 根据dianplan actions中的app(或者从脚本或外部应用发送的api命令)进行呼叫处理

2. 这些app/api分为两类

去话处理(外呼)

FreeSWITCH发起外呼有两种方式:

  • brigde :在既有呼叫(a-leg)中发起新的呼叫腿(b-leg);可能在来话处理逻辑中触发。
  • originate:创建新的呼叫(a-leg);只能通过命令行或ESL触发。

无论那种方式发起呼叫,首先要指定呼叫目的方。呼叫目的方是通过Dialstrings 拨号字符串来标识的。拨号字符串以要使用的模块标识开头,后跟(可选)特定于模块的其他信息,最后是目标号码。拨号字符串的组件由“/”分隔。

FreeSWITCH中针对不同类型的呼叫目的方的 Dial-Strings。示例如下

通过指定SIP UA呼叫对端:sofia/external/17896061576@202.112.121.2001
通过gateway发起呼叫:sofia/gateway/gw/18005551212
呼叫注册用户:user/1000 

回铃音与 Early Media

假定A与B不在同一台服务器上(如在PSTN通话中可能不在同一座城市),中间需要经过多级服务器的中转:

在 PSTN 网络中,A 呼叫 B,B 话机开始振铃,A 端听回铃音(Ring Back Tone)。在早期,B 端所在的交换机只给 A 端交换机送地址全(ACM)信号,证明呼叫是可以到达 B 的,A 端听到的回铃音铃流是由 A 端所在的交换机生成并发送的。但后来,为了在 A 端能听到 B 端特殊的回铃音(如“您拨打的电话正在通话中…” 或 “对方暂时不方便接听您的电话” 尤其是现代交换机支持各种个性化的彩铃 - Ring Back Color Tone 等),回铃音就只能由 B 端交换机发送。在 B 接听电话前,回铃音和彩铃是不收费的(不收取本次通话费。彩铃费用一般是在 B 端以月租或套餐形式收取的)。这些回铃音就称为 Early Media(早期媒体)。它是由 SIP 的183(带有SDP)消息描述的。

理论上讲,B 接听电话后交换机 b 可以一直不向 a 交换机发送应答消息,而将真正的话音数据伪装成 Early Media,以实现“免费通话”。

Dialplan (拨号计划)

freeswitch 的拨号规则配置:https://www.cnblogs.com/einyboy/archive/2012/11/21/2780539.html

Dialplan (拨号计划) 主要作用就是对电话进行 路由 (选路、寻路)。就是当一个用户拨号时,对用户所拨的号码进行分析,进而决定下一步该做什么。当然,实际上它所能做的比你想象的要强大的多。

Dialplan 支持多种不同的格式,系统支持的拨号计划(Dialplan)及对应的模块:

  • asterisk:mod_dialplan_asterisk。
  • enum:mod_enum。
  • inline:mod_dptools。
  • LUA:mod_lua。
  • XML:mod_dialplan_xml。用得最多的还是 XML格式的 Dialplan (拨号计划)

xml Dialplan 解析

XML Dialplan 由一系列的 XML 配置文件组成,这些 XML可以是静态配置的,也可以使用动态配置方式从其他服务器或脚本中动态获取。Dialplan 有特定的结构,FreeSWITCH 通过解析相关的结构对 Dialplan 进行路由的呼叫,决定执行何种动作或流程。拨号计划的配置文件在 conf/dialplan 中,是通过 freeswitch.xml 装入:<X-PRE-PROCESS cmd="include" data="dialplan/*.xml"/>

拨号计划

  • 由多个 Context (上下文/环境)组成。context 可以类比为一个公司。
  • 每个 Context 中有多个 Extension (分支,在简单的 PBX 中也可以认为是分机号,但很显然,Extension 涵盖的内容远比分机号多)。所以,Context 就是多个 Extension 的逻辑集合,它相当于一个分组,一个 Context 中的 Extension 与其它 Context 中的 Extension 在逻辑上是隔离的。Extension 可以类比为公司的一个分机号。

注意:在处理 Dialplan 时是顺序的对每一项Extension 进行正则表达式匹配是非常影响效率的。所以,在生产环境中,往往要删除这些默认的 Dialplan,而只配置有用的部分。现在可以暂时不删,因为里面有好多例子可以学习。

下面是 Dialplan 的完整结构:

Extension 相当于路由表中的表项。

dialplan xml 解析

<extension name="incoming_call">
	<condition field="destination_number" expression="^1234$">
		<action application="answer" data=""/>
		<action application="sleep" data="1000"/>
		<action application="ivr" data="welcome"/>
	</condition>
</extension>

呼叫目标号码 "1234" 时,自动接听电话,并在1秒后播放欢迎语音(通过 "ivr" 应用程序实现)。就可以听到配置的IVR菜单了。注意,在实际应用中,为了能接受外部来的呼叫,可能要把这里的 1234 改成实际的 DID(Direct Inbound Dial)号码。

  • <extension>:定义一个扩展(extension),即一个电话呼入的处理逻辑。
  • name="incoming_call":扩展的名称,可能是用于标识并区分不同的扩展。
  • <condition>:定义一个条件,用于判断是否应该执行下面的一系列操作。
  • field="destination_number" expression="^1234$":设置条件的字段为 destination_number,并且要求其值为正则表达式 ^1234$ 匹配的内容。即如果用户拨打1234时,就会在 Dialplan 中路由到这里。
  • <action>:定义一个操作,即在满足条件时执行的动作。三个action,分别指定接下来要执行的动作。
  • application="answer" data="":执行 answer 应用程序,用于接听呼入电话。就是 执行answer对来话进行应答(必须先应答才能向对方播放声音);
  • application="sleep" data="1000":执行 sleep 应用程序,使当前线程暂停1000毫秒(1秒)。使用sleep暂停一秒,以防止由于声音通路没建立好(有时候,特别是当主叫与被叫之间的链路比较复杂时,声音链路的建立需要一个过程)而丢失声音;
  • application="ivr" data="welcome":执行 ivr 应用程序,并传递 welcome 作为数据。

每一个 Extension 都有一个 name 属性。它可以是任何合法的字符串,本身对呼叫流程没有任何影响,但取一个好听的名字,有助于在查看 Log 时发现它。在 Extension 中可以对一些 condition (条件)进行判断,如果满足测试条件所指定的表达式,则执行相对应的 action (动作)。

FreeSWITCH 安装时,提供了很多例子,为了避免与提供的例子冲突,强列建议在学习时把自己写的 Extension 放到第一个 Extension 的位置。示例:编辑 conf/dialplan/default.xml 并将下面 Extension 作为第一个 Extension

<extension name="My_Echo_Test">
	<condition field="destination_number" expression="^echo|1234$">
		<action application="echo" data=""/>
	</condition>
</extension>

保存后,重新载入配置文件,设置日志级别、最后呼叫 1234

  • 在 FreeSWITCH 命令行上(Console 或 fs_cli)执行 reloadxml 或按 F6键,使 FreeSWITCH 重新读入你修改过的配置文件。
  • 按 F8 键将 log 级别设置为 DEBUG 从而显示详细日志。
  • 然后注册软电话,并拨叫 1234 或 echo (大部分软电话都能呼叫字母,如 Zoiper,X-lite 可以使用空格键切换数字和字母)。

debug 查看 log

重点提示遇到 Dial plan 的问题,按 F8 打开 DEBUG 级别的日志

一个合法的 XML 文件,所有的标签都是成对出现的。如果有的标签比较简单,可以使用简写的形式来关闭标签,即大于号前面的 /,如果不小漏掉,在 reloadxml 时将会出现类似 "+OK [[error near line 3371]: unexpected closing tag ]" 之类的错误,而实际的错误位置又通常不是出错的那一行。这是在编辑 XML 文件时经常遇到的问题,又比较难于查找。因此在修改时要多加小心,推荐使用具有语法高亮的功能的编辑器来编辑 xml,例如:notepad++、vscode、sublime text、等。

在XML加载阶段,FreeSWITCH的XML解析器会先将预处理命令展开,在FreeSWITCH内部生成一个大的XML文档。log/freeswitch.xml.fsxml 是 FreeSWITCH 内部 XML 的一个内存镜像(注意,它在log目录中,而不是在 conf 目录中,由于它是动态生成的,所以用户不应该手工编辑它)。它对调试非常有用:假设你不慎弄错了某个标签,又不知道它错在哪了,可以尝试让 FreeSWITCH 重新加载XML(reloadxml),这时在 FreeSWITCH 的日志中就可以看到 XML 某一行出错的提示,在 freeswitch.xml.fsxml 就能很容易地定位到这一行。

查看上面示例 Log,注意如下的行:

省去了 Log 中的日期以及其它不关键的一些信息。

  • 第2行 Processing ,说明是在处理 Dialplan,my_sip_name 是我的的 SIP 名字,1000 是我的分机号, 1234 是我所拨叫的号码,这里,我直接拨叫了 1234。它完整意思是说,呼叫已经达到路由阶段,要从 XML Dialplan 中查找路由,该呼叫来自 my_sip_name ,分机号是1000,它所呼叫的被叫号码是 1234 (或 echo,如果你拨叫 echo 的话)。
  • 第3行 呼叫进入 parsing (解析XML) 阶段,它首先找到 XML 中的一个 Context,这里是 default(它是在 user directory 中定义的。如果 1000 这个用户发起呼叫,则它的 context 就是 default,所以要从 XML Dialplan 中的 default 这个 Context 查起)。它首先找到的第一个 Extension 的 name 是 My_Echo_Test。continue=false 的意思后面再讲。
  • 第4行,由于该 Extension 中有一个 Condition,它的测试条件是 destination_number,也就是被叫号码,所以, FreeSWITCH 测试被叫号码(这里是 1234)是否与配置文件中的正则表达式相匹配。 ^echo|1234$ 是正则表达式,它匹配 echo 或 1234。所以这里匹配成功,Log 中显示 Regex (PASS) 表示匹配成功,它就开始执行动作 echo(它是一个 APP),所以你就听到了自己的声音。

在这个例子中,我们呼叫 1234 时创建了一个单腿的呼叫,与其说我们跟 FreeSWITCH 在通话,还不如说我们在跟 FreeSWITCH 中的一个 App 在通话。而 XMI Dialplan 只是帮助我们找到这个(些) App。上面所说的是最简单的路由查找,实际上,系统自带的一些 Dialplan 的例子很有代表性。

用户目录配置文件中有一项 <variable name="user_context" value="default">,说明,如果 1000 这个用户发起呼叫,则它的 context 就是 default,所以,待呼叫进行到路由阶段后,要从XML Dialplan 中的 default 这个 Context 查起。

默认的配置文件结构

系统默认提供的配置文件包含三个 Context:default、features 和 public,它们分别在三个 XML 文件中。

  • default 是默认的 dialplan,一般来说注册用户都可以使用它来打电话,如拨打其它分机或外部电话等。
  • public 则是接收外部呼叫,因为从外部进来的呼叫是不可信的,所以要进行更严格的控制。如,你肯定不想从外部进来的电话再通过你的网关进行国内或国际长途呼叫。当然,这么说不是绝对的,等你熟悉了 Dialplan 的概念之后,可以发挥你的想象力进行任何有创意的配置。
  • 在 default 和 public 中,又通过 include 预处理指令分别加入了 default/ 和 include/ 目录中的所有 XML 文件。 这些目录中的文件仅包含一些额外的 Extension。由于 Dialplan 在处理是时候是顺序处理的,所以,一定要注意这些文件的装入顺序。通常,这些文件都按文件名排序,如 00_,01_等等。如果你新加入 Extension,可以在这些目录里创建文件。但要注意,这些文件的优先级比直接写在如 default.xml 中低。可以将新加的 Extension 加在 Dialplan 中的最前面,以便于说明问题。

Channel (通道、信道) Variables、、及相关操作

在 FreeSWITCH 中,每一次呼叫都由一条或多条 "腿" (Call Leg) 组成,每一条腿又称为一个 Channel(信道),每一个 Channel 都有好多属性,用于标识 Channel 的状态,性能等,这些属性称为 Channel Variable(信道变量),简写为 Channel Var 或 Chan Var 或 Var。

通过使用 info 这个 APP,可以查看所有的 Channel Var。我们先修改一下 Dialplan

<extension name="Show Channel Variable">
	<condition field="destination_number" expression="^1235$">
		<action application="info" data=""/>
	</condition>
</extension>

加入 default.xml 中,这次可以放在 My_Echo_Test 这个 Extension 后面然后保存,

执行 reloadxml 重新载入配置。从软电话上呼叫 1235,可以看到有很多Log输出,还是从绿色的行开始看:

在处理 Dialplan 时是顺序的对每一项Extension 进行正则表达式匹配可以看到,由于呼叫的是 1235,它在第三行测试 My_Echo_Test 的 1234 的时候失败了,接在接下来测试 1235 的时候成功了,便执行相对应的 Action - info 这个APP。它的作用就是把所有 Channel Variables 都打印到 Log 中。所有的 Channel Variable 都是可以在 Dialplan 中访问的,使用格式是 ${变量名},如 ${destination_number}。将下列配置加入 Dialplan 中保存配置,

<extension name="Accessing_Channel_Variable">
	<condition field="destination_number" expression="^1236(\d+)$">
		<action application="log" data="INFO Hahaha, I know you called ${destination_number}"/>
		<action application="log" data="INFO The Last few digists is $1"/>
		<action application="log" data="ERR This is not actually an error, just jocking"/>
		<action application="hangup"/>
	</condition>
</extension>

执行 reloadxml 重新载入配置。这次呼叫 1236789,看看结果,跟前面一样,我们还是从绿色的行开始看。

1236789 匹配了正则表达式 ^1236(\d+),并将 789 存储在变量 $1 中。然后看到它解析出的四个 Action(3个 log,1个 hangup),到这里为止,Channel 的状态一直没有变,还处在路由查找的阶段。在所有 Dialplan 解析完成后,Channel 状态才进行 Standard Execute 阶段。理解这一点是非常重要的,后面再做详细说明,在这里你要记住:路由查找(解析)和执行分属于不同的阶段。当 Channel 状态进入执行阶段后,它才开始依次执行所有的 Action。log() 的作用就是将信息写到 Log 中,它的第一个参数是 leglevel,就是 Log 的级别,有 INFO、Err、DEBUG 等,不同的级别能以不同的颜色显示。(详细的级别请参考mod_logfile | FreeSWITCH Documentation)。

这次实验特意增加了几个 Action。一个 Action 通常有两个参数,一个是 application,代表要执行的 APP,另一个是 data,就是 APP 的参数。当 APP 没有参数时 data 可以省略。

读到这里,你或许还有疑问,既然我们在 info APP 的输出里没看到 destination_number 这一变量,它到底是从哪里来的呢?是这样的,它在 info 中的输出是 Caller-Destination-Number,但你在引用的时候就需要使用 destination_number。还有一些变量,在 info 中的输出是 variable_xxxx,如 variable_domain_name,而实际引用时要去掉 variable_ 前缀。不要紧张,这里有一份对照表: Channel Variables | FreeSWITCH Documentation

在Dialplan中,可以对通道变量(Channel Variable)进行各种操作。

  • set App 可以给变量赋值:<action application="set" data="my_var=my_value"/>
  • export 可以对 a-leg 和 b-leg 同时赋值(即使此时 b-leg 还不存在):<action application="export" data="my_var=my_value"/>

以上两条命令都可以将 my_var 变量的值设置为 my_value。不同的是 set 程序仅会作用于当前的Channel(aleg),而 export 程序则会将变量设置到两个Channel(a-leg和b-leg)上,如果当时 b-leg 还没有创建,则会在创建时设置。另外,export 也可以通过 nolocal 参数将变量值限制仅设置到 b-leg上:<action appliction="export" data="nolocal:my_var=my_value"/>

在实际应用中,如果a-leg上已经有一些变量的值(如var1、var2、var3),但想同时把这些变量都复制到bleg上,可以使用以下几种办法:

<action application="export" data="var1=$var1"/>
<action application="export" data="var2=$var2"/>
<action application="export" data="var3=$var3"/>

或者使用如下等价的方式:
<action application="set" data="export_vars=var1,var2,var3">

所以,set 也具有能往b-leg上赋值的能力。其实,它和 export 一样,都是操作 export_vars 这个特殊的变量。只不过 export 的语法更直观一些。

另外,我们也可以随时取消某些Variable的定义。取消Variable定义只需对它赋一个特殊的值 "_undef_" 或使用 unset App,如,下面两行代码是等价的:

<action application="set" data="var1=_undef_"/>
<action application="unset" data="var1"/>

在实际使用时,还常常会用到截取Variable值的部分操作。在FreeSWITCH中,可以使用特殊的语法取一个通道变量值的子字符串,使用格式是“${var:位置:长度}”。其中“位置”从0开始计数,若为负数则从字符串尾部开始计数;如果“长度”为0或小于0,则会从当前“位置”一直取到字符串结尾(或开头,若“位置”为负的话)。例如var的值为1234567890,那么各种取值方法见下面的例子:

官方文档:https://freeswitch.org/confluence/display/FREESWITCH/Channel+Variables+Catalog

通道变量与info应用程序输出变量的对应关系见官方文档:https://freeswitch.org/confluence/display/FREESWITCH/Channel+Variables

  • ani:主叫的ANI,一般与caller_id_number相同。
  • aniii:主叫的ANI II,如果有的话。
  • answered_time:应答时间。
  • billsec:计费时长。从应答开始到呼叫结束。
  • bridge_hangup_cause:记录b腿bridge失败的原因。
  • bypass_media:来话是否启用媒体绕过模式。
  • caller_id_name:主叫名称。
  • caller_id_number:主叫号码。
  • channel_name:Channel名称。
  • context:Dialplan当前的Context。
  • continue_on_fail:当bridge失败时是否继续执行后面的action。还可以为挂断原因的逗号分隔的列表,挂断原因见originate_disposition。
  • created_time:创建时间。
  • destination_number:被叫号码。
  • detect_speech_result:语音识别结果。
  • dialplan:使用的Dialplan模块的名字,如XML、YAML、lua、enum、asterisk、lcr等。
  • direction:呼叫方向,Inbound或Outbound。
  • domain:租户域。
  • domain_name:租户域的名字。
  • dtmf-type:DTMF类型。inband为使用带内DTMF,info为使用SIP的INFO消息。
  • duration:呼叫时长。从呼叫开始到呼叫结束。
  • effective_caller_id_number:设置在a-leg上,但影响b-leg的主叫号码显示。
  • enable_file_write_buffering:录音时是否先写到内存缓冲区中,以减少IO访问。默认为true。默认缓冲区大小是65536字节。
  • end_stamp:结束时间。
  • execute_on_answer:应答时的钩子。格式为:APP [ARG[ ...]]。
  • fifo_bridge_uuid:呼叫取回时指定来话Channel。
  • fifo_caller_exit_key:退出呼叫队列等待的按键。
  • fifo_caller_exit_to_orbit:退出呼叫队列等待时是否转至其它号码。如否则直接退出。
  • fifo_chime_freq:呼叫队列等待时给来话播放提示音的间隔秒数。
  • fifo_chime_list:呼叫队列等待时每隔一段时间给来话播放的提示音。
  • fifo_music:呼叫停泊时播放的音乐。
  • fifo_orbit_announce:坐席接听时先给来话播放的提示音。
  • fifo_orbit_context:等待超时转至的Context。
  • fifo_orbit_dialplan:等待超时转至的Dialplan。
  • fifo_orbit_exten:等待超时转至的号码。格式为USERNAME:TIMEOUTSEC。
  • fifo_override_announce:坐席接听后在来话通话前给坐席播放的提示音。
  • fifo_position:来话在当前队列中的位置。执行过程自动产生。
  • fifo_priority:将来话放至呼叫队列的优先级。1至10,默认为5,越大优先级越高。
  • fifo_serviced_by:为来话服务的坐席Channel。执行过程自动产生。
  • hangup_after_bridge:在bridge正常完成后是否挂机,不继续执行后面的action。
  • hangup_time:挂机时间。
  • ignore_early_media:忽略早期媒体,避免被认为已接听。
  • instant_ringback:见《FreeSWITCH权威指南》P219。
  • is_loopback:通道是否是由loopback呼叫字符串创建的。
  • leg_timeout:等待返回媒体(如183或200)的超时时间。
  • loopback_bowout:是否令loopback呼叫字符串创建的那条额外的腿在完成使命后尽快释放。
  • network_addr:主叫的IP。
  • nibble_account:计费模块的计费账号。
  • nibble_rate:计费模块的每分钟费率。
  • origination_caller_id_name:可以设置在a-leg上,也可以设置在b-leg上,影响本leg的主叫名称显示。
  • origination_caller_id_number:可以设置在a-leg上,也可以设置在b-leg上,影响本leg的主叫号码显示。
  • origination_cancel_key:取消协商转的按键。
  • originate_disposition:记录b腿bridge失败的原因,只读。其值为挂机原因的枚举值。
  • outbound_caller_id_name:设置在a-leg上,但影响b-leg的主叫名称显示。
  • playback_delimiter:file_string中多个文件的分隔符。
  • playback_sleep_val:file_string中多个文件的播放时间间隔。
  • profile_index:Profile Index。
  • read_codec:读Codec。
  • read_rate:读采样率。
  • record_sample_rate:录音时的采样率,进行实时的转码。可为:8000、12000、16000、24000、32000、11025、22050、44100、48000。
  • RECORD_ANSWER_REQ:是否等通话被应答后才开始录音,可以防止录制不必要的早期媒体。默认为false。
  • RECORD_APPEND:如录音文件已存在,是否在文件后追加录音而不是覆盖文件,可用于电话中断重新建立后重新录音。默认为false。
  • RECORD_ARTIST:文件元信息之一。
  • RECORD_BRIDGE_REQ:是否等当前的Channel与其它Channel桥接后才开始录音,可以防止录制不必要的早期媒体。
  • RECORD_COMMENT:文件元信息之一。
  • RECORD_COPYRIGHT:文件元信息之一。
  • RECORD_DATE:文件元信息之一。
  • RECORD_FINAL_TIMEOUT_MS:录音过程中检测不到声音的超时毫秒数,超时则停止录音。
  • RECORD_HANGUP_ON_ERROR:录音失败时是否挂断电话。默认为false。
  • RECORD_INITIAL_TIMEOUT_MS:从录音开始检测不到声音的超时毫秒数,超时则停止录音。
  • RECORD_MIN_SEC:最小录音秒数。如小于该值,则删除录音文件。默认为3。
  • RECORD_READ_ONLY:是否只录读方向的录音。
  • RECORD_SILENCE_THRESHOLD:静音能量阈值,小于该值认为是静音。默认为200。
  • RECORD_SOFTWARE:文件元信息之一。
  • RECORD_STEREO:是否录制立体声。
  • RECORD_STEREO_SWAP:是否录制立体声,同时将左右声道互换。
  • RECORD_TITLE:文件元信息之一。
  • RECORD_WRITE_ONLY:是否只录写方向的录音。
  • rdnis:原被叫号码,一般用于呼叫转移时。
  • rtp_auto_adjust:是否启用RTP自动调整。
  • source:呼叫来源,来自哪一个FreeSWITCH模块,如mod_sofia、mod_portaudio、mod_freetdm等。
  • start_stamp:开始时间。
  • state:当前Channel的状态。
  • state_number:当前Channel的状态整数值。
  • transfer_after_bridge:通话结束后再转移到呼叫此号码,重新等待。
  • transfer_ringback:见《FreeSWITCH权威指南》P218。
  • transfer_time:转移时间。
  • tts_engine:TTS引擎。
  • tts_voice:TTS嗓音。
  • username:用户名。
  • uuid:Channel的UUID。
  • write_codec:写Codec。
  • write_rate:写采样率。

$${var} 与 ${var}

在每一个 Channel 上都可以设置好多 Variable,称为信道变量。FreeSWITCH 呼叫过程中,会根据这些变量控制 Channel 的行为。

${var}  是在dialplan、application 或 directory中设置的变量
        它会影响呼叫流程并且可以动态的改变。
$${var} 是全局的变量,它仅在预处理阶段(系统启动时,或重新装载 - reloadxml时)被求值。
        一般用于设置一些系统一旦启动就不会轻易改变的量。
        如 $${domain} 或 $${local_ip_v4} 等。
所以,两者最大的区别是:
    $${var} 只求值一次,
    ${var} 则在每次执行时求值(如一个新电话进来时)。
  • $variable_xxxx:会发现,有些变量在显示时(可以使用dp_tools 中的 info() 显示,后面会讲到)是以 "variable_" 开头的,但在实际引用时要去掉这开头的 "variable_"。如 "variable_user_name",引用时要使用 "${user_name}"。Channel Variables | FreeSWITCH Documentation 列举了一些常见的变量显示与引用时的对应关系。

  • 给 Variable 赋值:在 dialplan 中,有两个程序可以给 Variable 赋值:
    <action application="set" data="my_var=my_value"/>
    <action application="export" data="my_var=my_value"/>
    以上两条命令都可以设置 my_var 变量的值为 my_value。不同的是 -- set 程序仅会作用于“当前”的 Channel (a-leg),而 export 程序则会将变量设置到两个 Channel (a-leg 和 b-leg)上,如果当时 b-leg 还没有创建,则会在创建时设置。另外,export 还可以只将变量设置到 b-leg 上:
    <action appliction="export" data="nolocal:my_var=my_value"/>
    在实际应用中,如果 a-leg 上已经有一些变量的值(如 var1、var2、var3),而想同时把这些变量都复制到 b-leg 上,可以使用以下几种办法:
    <action application="export" data="var1=$var1"/>
    <action application="export" data="var2=$var2"/>
    <action application="export" data="var3=$var3"/>
    或者使用如下等价的方式:
    <action application="set" data="export_vars=var1,var2,var3">
    所以,其实 set 也具有能往 b-leg 上赋值的能力,其实,它和 export 一样,都是操作 export_vars 这个特殊的变量。

  • 取消 Variable 定义:只需设置一个特殊的值 _undef_:<action application="set" data="var1=_undef_">

  • 截取 Variable 的一部分:可以使用特殊的语法取一个 Variable 的子串,格式是“${var:位置:长度}”。其中 “位置” 从 0 开始计烽,若为负数则从字符串尾部开始计数;如果“长度”为 0 或小于 0,则会从当前“位置”一直取到字符串结尾(或开头,若“位置”为负的话)。例如 var 的值为 1234567890,那么:
    ${var}      = 1234567890
    ${var:0:1}  = 1
    ${var:1}    = 234567890
    ${var:-4}   = 7890
    ${var:-4:2} = 78
    ${var:4:2}  = 56

关于 set 和 export : set 是将变量设置到当前的 Channel 上,即 a-leg。而 export 则也将变量设置到 b-leg 上。

Conditions (测试条件)

一个简单的测试条件:<condition field="destination_number" expression="^1234S">,它使用正则表达式匹配测试一个变量是否满足预设的正则表达式。大部分的测试都是针对被叫号码(destination_number) 的,也可以对其他变量进行测试,如 IP 地址 ( 注意由于在正则表达式中 . 具有特殊意义,所有需要用 \ 进行转义) :<condition field="network_addr" expression="^192\.168\.7\.7$">

除此之外,测试还接受用户在用户目录中设置的变量,但要注意必须使用 ${} 对变量进行引用。

示例:toll_allow 就是在用户目录中设置的

<condition field="${toll_allow}" expression="international">

如果在你的用户目录中设置了以下变量

<user id="1000">
	<variables>
		<variable name="my_test_var" value="my_test_value"/>
	</variables>
</user>

则可以在 Dialplan 中通过下面的 condition 进行相关测试。

<condition field="${my_test_var}" expression="^my_test_value$">

一般来说,测试条件不可以嵌套,但可以迭加,如下面的例子并不能达到你预期的效果:

<extension name="Testing Stacked Conditions">
	<condition field="network_addr" expression="^192\.168\.7\.7$">
		<condition field="destination_number" expression="^1234$">
			<action application="log" data="INFO Hahaha, I know you called ${destination_number}"/>
		</condition>
	</condition>
</extension>

但以下配置是正确的:

<extension name="Testing Stacked Conditions">
	<condition field="network_addr" expression="^192\.168\.7\.7$"/>
	<condition field="destination_number" expression="^1234$">
		<action application="log" data="INFO Hahaha, I know you called ${destination_ number}"/>
	</condition>
</extension>

两个 condition 是迭加的关系。因此就构成了一个简单的 "逻辑与" 的关系,即只有在IP地址和被叫号码两个条件都匹配的情况下才执行此处的 Action,否则就跳过本 extension,继续解析下一项。因此说这两个 "平行" 的 condition 是 "逻辑与" 的关系。

除此之外,两个迭加在一起的 condition 还可以构成其他关系,使你可以只用 XML 就能完成比较复杂的路由配置,而无须编程。break 参数就是用于实现这个功能的,它有以下几个值(为方便讨论,假设两个条件分别为A和B):

  • on-false:在第一次匹配失败时停止(但继续处理其他的extension),这是默认的配置。结果相当于A and B。
  • on-true:在第一次匹配成功时停止(但会先完成对应的Action,然后继续处理其他的extension),不成功则继续,所以结果相当于((not A)and B)
  • always:不管是否匹配,都停止。
  • never:不管是否匹配,都继续。

通过使用 break 参数,你可以写出类似 if-then-else 的结构。如上面的例子(因为没有break参数,所以默认是 on-false):

if(network_addr 正则匹配 /^192\.168\.7\.7$/) then
    if(destination_number 正则匹配 /^1234$/) then
        //wirte log
    end
end

再来看一个例子,假设你呼叫1234,如果来自 192.168.7.7 这个IP,就播放早上好,如果来自192.168.7.8 就说晚上好,这时你可以这样设置:

<extension name="Testing Stacked Conditions">
	<condition field="destination_number" expression="^1234$">
		<condition field="network_addr" expression="^192\.168\.7\.7$"/>
		<action application="playback" data="good-morning.wav"/>
	</condition>
</extension>
<extension name="Testing Stacked Conditions">
	<condition field="destination_number" expression="^1234$">
		<condition field="network_addr" expression="^192\.168\.7\.8$"/>
		<action application="playback" data="good-night.wav"/>
	</condition>
</extension>

通过使用break参数,你就可以将两种情况写到一个extension里:

<extension name="Testing Stacked Conditions">
	<condition field="destination_number" expression="^1234$"/>
	<condition field="network_addr" expression="^192\.168\.7\.7$" break="on-true">
		<action application="playback" data="good-morning.wav"/>
	</condition>
	<condition field="network_addr" expression="^192\.168\.7\.8$">
		<action application="playback" data="good-night.wav"/>
	</condition>
</extension>

这种情况跟上面两个extension的情况是等价的。虽然看起来不如第一种情况容易理解,但它把相似的功能逻辑上放到一个extension里,却比较直观。如果你从192.168.7.7上呼叫1234,它会首先匹配1234,进而匹配网络地址,匹配成功,便不再往下进行。但是,它会先把已经匹配到的条件中的Action执行完毕,所以播放goodmorning.wav。若你从192.168.7.8上呼叫,则它不能匹配192.168.7.7,但由于break参数的值是on-true,所以在这里,它不会中止,而是继续尝试匹配下面的condition,进而播放good-night.wav。所以,它相当于:

if(destination_number 正则匹配 /^1234$/) then
    if(network_addr 正则匹配 /^192\.168\.7\.7$/) then
        // play good morning
    else if(network_addr 正则匹配 /^192.168.7.8$/) then
        // play good night
    end
end

always 和 never 不常用,发挥想象力可以造出类似 if a then b end 或 if c then d end之类的条件。

Action (动作)、Anti-Action (反动作)

除使用 condition 的 break 机制来完成复杂的条件以外,还可以使用 "反动作"(Anti-Action)来达到类似的目的,如:

<extension name="Anction and Anti-Action">
	<condition field="destination_number" expression="^1234$"/>
	<condition field="network_addr" expression="^192\.168\.7\.7$">
		<action application="playback" data="good-morning.wav"/>
		<anti-action application="playback" data="good-night.wav"/>
	</condition>
</extension>

上述代码它说明,如果呼叫来自192.168.7.7,则播放 good-morning.wav,否则播放 good-night.wav(不管是不是来自192.169.7.8)。因此,读者可以看到,它没有condition条件那么强大,但在简单的条件下也经常使用,它相当于:

if(destination_number 正则匹配 /^1234$/) then
    if(network_addr 正则匹配 /^192\.168\.7\.7$/) then
        // play good morning
    else
        // play good night
    end
end

dialplan 工作流程

在 Hunting 时,只解析 Dialplan,并不执行任何 Action 而是将所有满足条件的 Action 都放到一个列表中,待呼叫流程进行到 EXECUTE 阶段时,再依次执行列表中的 Action。了解 Dialplan 的工作机制之前,先来看一下 Channel 的状态机。

新建(NEW) 一个 Channel 时,它首先会进行 初始化(INIT),然后进入路由(ROUTING)阶段,也就是查找解析 Dialplan 的阶段。这里注意一个专门的术语 Hunting(译为 "选线、选路")。找到合适的路由入口后,Hunting 会执行(EXECUTE)一系列动作,最后无论哪一方挂机,都会进入挂机(HANGUP)阶段。后面的报告(REPORTING)阶段一般用于进行统计、计费等,最后将 Channel 销毁(DESTROY),释放系统资源。

在 EXECUTE 状态,可能会发生转移(Transfer,该转移跟通常说的呼叫转移不太一样),它可以转移到同一 context 下其他的 extension,或者转移到其他 context 下的 extension,但无论发生哪种转移,都会重新进行路由,也就是重新进入ROUTING (图中虚线部分),重新 Hunt  Dialplan。

一定要记住 ROUTING 和 EXECUTE 是属于两个不同阶段的,只有 ROUTING 完毕后才会进行EXECUTE 阶段的操作。当一个 Channel 进入 ROUTING 阶段时,它首先会到达 Dialplan(英文叫 Hit the Dialplan),然后对预设的 Dialplan 进行解析(是的,每个电话都会重新解析Dialplan),解析 Dialplan 的这一过程称为 Parsing 或 Hunting。解析完毕(成功)后,会得到一些 Action,然后 Channel 进入 EXECUTE 阶段, 依次执行所有的 Action。

示例:用另一种方式实现类似上面的 good-morning/good-night 中的例子,但这一次稍有不同:用户呼叫1234,如果来自192.168.7.7,则播放good-morning.wav;如果来自192.168.7.8,则播放goodnight.wav;否则,什么都不做。( 注意: 这个例子是错误的,不会出现你预期的结果, 但这是新手常常犯的错误,而遇到这种情况,大多数新手都会以为FreeSWITCH出问题):

<extension name="Testing Hunting and Executing" continue="true">
	<condition>
		<action application="set" data="greeting=no-greeting.wav"/>
	</condition>
</extension>
<extension name="Testing Hunting and Executing" continue="true">
	<condition field="network_addr" expression="^192\.168\.7\.7$">
		<action application="set" data="greeting=good-morning.wav"/>
	</condition>
</extension>
<extension name="Testing Hunting and Executing" continue="true">
	<condition field="network_addr" expression="^192\.168\.7\.8$">
		<action application="set" data="greeting=good-night.wav"/>
	</condition>
</extension>
<extension name="Testing Hunting and Executing">
	<condition field="destination_number" expression="^1234$"/>
	<condition field="${greeting}" expression="^good">
		<action application="playback" data="${greeting}"/>
	</condition>
</extension>

在前三个 extension 中都增加了一个参数 continue="true"。如果没有该参数,则默认为 false。在默认的情况下,在 Dialplan 的 Hunting 阶段,一旦根据前面介绍的 condition 匹配规则找到对应的extension,就执行相应的 Action,而不会再继续查找其他的 extension了, 不管后面的 extension是否有可能匹配。

但有些情况下,Dialplan 中会有多个 extension 满足匹配规则,而我们希望所有对应的 Action 都能得到执行,这时我们就要使用 continue="true" 参数了。

此外,我们这次还在第一个 extension 中用了一个空的 condition,这个空的condition没有匹配规则,因此它被认为匹配所有规则,故称其为绝对条件(Absolute Condition)。所以从表面上看,它等价于:

$greeting = no-greeting.wav
if (network_addr 正则匹配 /^192\.168\.7\.7$/) then
    $greeting = good-morning.wav
else if (network_addr 正则匹配 /192\.168\.7\.8$/) then
    $greeting = good-night.wav
end
if (destination_number 正则匹配 /^1234$/ and $greeting 正则匹配 /^good/) then
    play $greeting
end

但实际上两者不是等价的。原因在于,对 Dialplan 的 Hunting 和 Executing 分属于不同的阶段。在Hunting 阶段,只解析 Dialplan,并不执行任何动作,而是将所有满足条件的 Action 都放到一个动作列表(队列)中,待呼叫流程进行到 Executing 阶段时,再依次执行动作列表中的动作。所以上述的 XML 实际上等价于:

$action_list[0] = "$greeting = no-greeting.wav"
if (network_addr 正则匹配 /^192\.168\.7\.7$/) then
    $action_list[1] = "$greeting = good-morning.wav"
else if (network_addr 正则匹配 /^192\.168\.7\.8$/) then
    $action_list[1] = "$greeting = good-night.wav"
end
if (destination_number 正则匹配 /^1234$/ and $greeting 正则匹配 /^good/) then
    $action_list[2] = "play $greeting"
end
// 开始执行命令列表中的命令
foreach $action in $action_list do
    execute $action
end

到了这里你应该找出问题所在了。可以看到,在Hunting阶段,greeting这个变量始终是空值,因为没有任何动作设置这个值。所以在测试greeting与正则表达式/^good/是否匹配时,永远是不通过的。因此最后的动作列表中,也只有两个动作action_list[0]和action_list[1]。虽然在执行阶段$greeting的值最终会被设置为no-greeting、good-morning或good-night,但它再也不会回到Hunting阶段了(使用transfer除外),因而也不可能有机会通过/^good/测试条件进而再执行真正的playback动作以播放声音。

可反复阅读这个小节,理清前后关系,若依然不明白,建议把这些 XML 放到你的机器上,观察 Lg的输出,并改一下某些参数对比一下 Log 有什么不同 (如把 true 改成false把IP 换成你自已的P地址等)。

inline ( 内联执行 )

上面讲了一个错误的例子。原因是在 Dialplan 的 Hunting 阶段不执行 Action,但你却认为它应该执行。当然,如果你学会了看 Log,完全可以从 Log 中看出这一问题。但实际上,大多数的新手都不会仔细地去看 Log,而且即使仔细看了,由于经验比较少,也不一定能找到问题所在。

FreeSWITCH 的开发者和老手们为了解决这个问题,便在 Action 上增加了一个 inline 属性。
好了,现在我们就把所有带 set 的 Action 都加上 inline="true" 这一属性,问题就迎刃而
解了,如:<action inline="true" application="set" data="greeting=no-greeting.wav"/>
,改完后就是你期望的结果。在Hunting 阶段,如果发现带有 inline 的 Action,FreeSWITCH 便会直接执行它,而不用等到 EXECUT 阶段。

改好后,认真对比一下 Log 输出与前面有什么不同。相信到这里你基本上完全理解 Dialplan 了。
当然,并不是所有的 App 都能用 inline 执行。以 inline 方式执行的 App 实际上相当于不遵守正常的执行流程,因而使用起来会有一些限制:

适合 inline 执行的 App 必须能很快地执行,一般只是很快地存取某个变量,并且不能改变当前 Channel 的状态。满足这样条 件的 App 有:check_acl、eval、event、export、enum、log、presence、set、set_global、lcr、set_profile_var、set_user、sleep、unset、nibblebill、verbose_events、cidlookup、curl、easyroute、odbc_query。

当然,inline 参数也不是解决所有问题的万能钥匙,由于它会打乱执行顺序,所以使用不当也可能会产生非预期的结果。考虑下面的例子:

<action inline="true" application="set" data="var=1"/>
<action application="info"/>
<action inline="true" application="set" data="var=2"/>
<action application="info"/>

乍一看,第一个 info 输出结果中应该有 var iable_var=1,第二个info输出结果中variable_var=2。而实际上,在最后的输出中,两个info的输出结果都会显示variable_var=2。这是为什么?因为 set是以 inline 方式执行的,而 info 要等到 EXECUTE 阶段才被执行,所以实际执行的顺序是:set ---> set ---> info ---> info,很显然,执行到 info 时变量的值已经是2了。

案例解析:Local_Extension

以上涵盖了 Dialplan 的大部分概念,当然要活学活用还需要一些经验。下面看几个真实的例子。这些例子大部分来自默认的配置文件 (conf/dialplan/default.xml )。要看的第一个例子是 Local_Extension。 FreeSWITCH 默认的配置提供了 1000 - 1019 共 20 个 SIP 账号,密码都是 1234 。

用正则表达式 (10[01][0-9])$ 来匹配被叫号码,它匹配所有 1000 - 1019 这 20 个号码。这里我们假设在 SIP 客户端上,用 1000 和 1001 分别注册到了 FreeSWITCH 上,当 1000 呼叫 1001 时,FreeSWITCH 会建立一个 Channel,该 Channel 构成一次呼叫的 a-leg(一条腿)。初始化完毕后,Channel 进入 ROUTING 状态,即进入 Dialplan。由于被叫号码 1001 与这里的正则表达式匹配,所以,会执行下面这些 Action。另外,由于在正则表达式中使用了 (),因此,匹配结果会放入变量 $1 中,因此,在这里,$1 = 1001。

<action application="set" data="dialed_extension=$1"/>
<action application="export" data="dialed_extension=$1"/>

set 和 export 都是设置一个变量,该变量的名字是 dialed_extension,值是 1001。关于 set 和 export : set 是将变量设置到当前的 Channel 上,即 a-leg。而 export 则也将变量设置到 b-leg 上。这里 b-leg 还不存在。所以在这里它对该 Channel 的影响与 set 其实是一样的。因此,使用 set 完全是多余的。但是除此之外,export 还设置了一个特殊的变量,叫 export_vars,它的值是 dialed_extension。所以,实际上。上面的第二行就等价于下面的两行:

<action application="set" data="dialed_extension=$1"/>
<action application="set" data="export_vars=dialed_extension"/>

继续往下看:

其中 bind_meta_app 的作用是在该 Channel 上绑定 DTMF。上面四行分别绑定了 1、2、3、4 四个按键,它们都绑定到了 b-leg 上。注意,这时候 b-leg 还不存在。所以请记住这里,后面我们再讲。继续往下看:

<action application="set" data="ringback=${us-ring}"/>

此处,设置回铃音是美音(不同国家的回铃音是有区别的),${us-ring}的值是在vars.xml中设置的。接下来:

<action application="set" data="transfer_ringback=$${hold_music}"/>

以上是设置如果发生呼叫转移时,用户听到的回铃音。

下面这些变量会影响呼叫流程,关于它们的详细说明参见下文的bridge部分。

call_timeout 是设置呼叫超时的变量。

<action application="set" data="call_timeout=30"/>

设置呼叫超时。

<action application="set" data="hangup_after_bridge=true"/>
<!--<action application="set"
data="continue_on_fail=NORMAL_TEMPORARY_FAILURE,USER_BUSY,NO_ANSWER,TIMEOUT,
NO_ROUTE_DESTINATION"/> -->
<action application="set" data="continue_on_fail=true"/>

继续往下看:

<action application="hash" data="insert/${domain_name}-call_return/${dialed_extension}/${caller_id_number}"/>
<action application="hash" data="insert/${domain_name}-last_dial_ext/${dialed_extension}/${uuid}"/>
<action application="hash" data="insert/${domain_name}-last_dial_ext/${called_party_callgroup}/${uuid}"/>
<action application="hash" data="insert/${domain_name}-last_dial_ext/global/${uuid}"/>

hash是内存中的哈希表数据结构。它可以设置一个键-值对(Key-Value Pair)。如上面最后一行向${domain_name}-last_dial_ext 这个哈希表中插入一个 global 键,它的值是 ${uuid},即 本Channel的唯一标志[14]。

不管是上面的set,还是hash,都是保存一些数据为后面做准备的。不同的是set将变量存绑定到Channel上,以通道变量的形式存在,而hash保存到内存的哈希表数据结构中。

继续往下看,还是设置通道变量:

<action application="set" data="called_party_callgroup=${user_data(${dialed_extension}@${domain_name} var callgroup)}"/>
<!--<action application="export" data="nolocal:sip_secure_media=${user_data(${dialed_extension}@${domain_name} varsip_secure_media)}"/>-->

上面最后一行默认是注释掉的,因此不起作用。nolocal 的作用之前也讲过,它告诉 export 只将该变量设置到 b-leg上,而不要设置到 a-leg上。

下面,还是往哈希表中插入数据:

<action application="hash" data="insert/${domain_name}-last_dial/${called_party_callgroup}/${uuid}"/>

再往下看,终于到了一个干实事的地方了:

<action application="bridge" data="{sip_invite_domain=$${domain}}user/${dialed_extension}@${domain_name}"/>

这里 bridge 是最关键的部分。其实上面除bridge以外的Action都可以省略,只是会少一些功能而已(如同组代答、监听等)。

bridge 相当于一座桥,它的作用就是把两条腿 1000 和 1001 给桥接起来。在这里,为了能连接到1001,FreeSWITCH 作为一个SIP UAC,向1001这个SIP UA(UAS)发起一个 INVITE 请求,并建立一个新 Channel,就是我们的 b-leg。1001 开始振铃,bridge 把回铃音传回到 1000,因此 1000 就能听到回铃音(如果 1001 有自己的回铃音,则 1000 也能听到,否则将会听到默认的回铃音${us-ring})。

当然,实际的情况比我们所说的要复杂,因为在呼叫之前,FreeSWITCH 首先要查找 1001 这个用户是否已经注册,否则,会直接返回 USER_NOT_REGISTERED,而不会建立 b-leg。

bridge 的参数是一个标准的呼叫字符串(Dial string),domain 和 domain_name 都是预设的变量,默认就是服务器的 IP 地址。user 是一个特殊的 Endpoint,它指本地用户。所以这里的呼叫字符串翻译出来就是(这里假设IP是192.168.7.2):

{sip_invite_domain=192.168.7.2} user/1001@192.168.7.2

其中,"{}" 里是设置通道变量。由于 bridge 在这里要建立 b-leg,因此这些变量只会建立在 b-leg上。这与 set是 不一样的,但它等价于下面的 export:

<action application="export" value="nolocal:sip_invite_domain=192.168.7.2"/>
<action application="bridge" value="user/1001@192.168.7.2"/>

好了,到此为止电话路由基本上就完成了,我们已经建立了 1000 到 1001 之间的呼叫,1001 已经开始振铃,就等着有人来接电话了。接下来可能会有以下几种情况:

被叫应答
被叫忙
被叫无应答
被叫拒绝
其他情况

先来看一下被叫应答的情况。1001 接电话,与1000畅聊。在这个时候 bridge 一直是阻塞的,也就是说,bridge 这个App会一直等待 b-leg(1001)挂机(或者其他错误)后才返回,这时才有可能继续执行下面的 Action。好吧,让我们先休息一下,等他们两个聊完吧。

最后,无论哪一方挂机,bridge 就算结束了。如果 1000(主叫)先挂机,则 FreeSWITCH 会将挂机原因(Hangup Cause,一般是 NORMAL_RELEASE,即正常释放)发送给1001,同时释放b-leg。由于 a-leg 已经没了,Dialplan 就再也没有往下执行的必要,因此会产生计费信息,并销毁a-leg。

如果1001先挂机,b-leg 就这样消失了。但 a-leg 依然存在,所以还有戏看。

b-leg 会将挂机原因传到 a-leg。在 a-leg 决定是否继续往下执行之前,会检查一些变量,以决定该怎么做。其中,我们在前面设置了 hangup_after_bridge=true。它的意思是,如果 bridge 正常完成后,就挂机。因此,a-leg 到这里就释放了,它的挂机原因是参考 b-leg 得出的。

但由于种种原因 1001 可能没接电话,如 1001 可能会拒接(返回CAlL_REJECTED,但某些 SIP UA 会在用户拒接时返回 USER_BUSY,隐藏真实原因)、忙(USER_BUSY)、无应答(NO_ANSWER或NO_USER_RESPONSE)等。出现这些情况时,FreeSWITCH 认为这是不成功的 bridge,因此就不管用 hangup_after_bridge 变量了(由于没有成功的 bridge,也就也没有 "after" 一说了)。这时候它会检查另一个变量 continue_on_fail。由于我们上面设置的continue_on_fail=true,因此在 bridge 失败后会继续(continue)执行下面的 Action。

这里值得说明的是,通过给 continue_on_fail 不同的值,可以决定在什么情况下继续,如下面的设置将只在用户忙和无应答的情况下才会继续呼叫流程:

<action application="set" data="continue_on_fail=USER_BUSY,NO_ANSWER"/>

其他可能的值有:NORMAL_TEMPORARY_FAILURE(临时故障)、TIMEOUT(超时,一般是SIP超时)、NO_ROUTE_DESTINATION(呼叫不可达)等。

当然,此处的 continue_on_fail=true 表示无论什么原因导致 bridge 失败(没法联系上1001),都决定继续执行。言归正传,接着往下看:

<action application="answer"/>

Dialplan 执行到这里,answer App 首先会使 FreeSWITCH 给主叫1000 回送应答消息,以建立真正的媒体流,准备对其播放声音。接下来:

<action application="sleep" data="1000"/>
<action application="voicemail" data="default ${domain_name} ${dialed_extension}"/>

sleep 表示暂停,1000 表示暂停的毫秒数(即1秒),然后转到 1001 的语音信箱 (Voicemail,在国内用得不多)。另外,值得注意的是,FreeSWITCH默认配置文件中是通过 transfer 加 loopback Endpoint 的方式转到 voicemail 的,还没有学到 loopback,因为到这里学到的新东西已经够多了,所以为了方便说明,直接改成了voicemail App的形式,它们在这里的作用是等价的。

回声、延迟回声

关于回声,没什么需要多解释的。如果拨9196,就能听到自己的回声。Dialplan 如下:

与 echo 类似,delay_echo 可以使用回声有一定延迟(其中 5000 是毫秒数)。

会议、通话双方转入会议

会议

下面的 Dialplan 可以将来话转入一个会议。

nb_conference 的意思是 Narrow Band Conference,就是默认的 8000Hz 的窄带电话会议。正则表达式 ^(30)$ 匹配以30开头的4位数字的被叫号码。一般测试时都可以直接呼叫3000或3001。

所有呼叫 30xx(30+两位数字)的电话均可以进入一个会议(conference),大家可以畅所欲言。其中,conference 是一个App,它的参数扩展开就是(假设呼叫3000,IP地址是192.168.7.2):3000-192.168.7.2@default

其中,3000-IP地址是会议的名字,@后面的default表示一个会议的profile,它定义了这个会议的相关参数。后面还会专门讲到多人电话会议,这里只需要知道呼叫3000能进入一个会议。

通话双方转入会议

再回头看 bind_meta_app 3 这一行:

bind_meta_app 是一个App,它有一系列参数,其中,

  • 3 表示绑定 DTMF 按键3;
  • b表示要绑到b-leg上;
  • s 表示 Same。
  • 整体意思:即如果在 b-leg 上按下 “*3”(必须先按“*”),则执行 execute_extension  这个App。execute_extension 是临时去别的地方执行一些 Dialplan 指定的App,在这里它的参数是 cf XML features,表示要去 XML Dialplan 的f eatures 这个 Context 中找一个匹配 cf 的 extension。

假设 1000 呼叫 1001,则 1001 摘机后可以与1000通话。这时,由于1001是b-leg,因而它可以通过按“*3”这个DTMF按键触发 execute_extension 动作,我们接下来看会发生什么。

features 这个 Context 在conf/dialplan/features.xml 中定义:

首先,answer 这个App在这里没什么用,因为呼叫已经应答了。transfer是一个App,它会将当前通话重新转移到ROUTING阶段,重新去Dialplan中进行路由。这里它的第一个参数是-both,表示要将它自己和a-leg都转到Dialplan中重新路由。路由的Dialplan类型仍然是XML,Context是Default,被叫号码30${dialed_extension:2}中的dialed_extension变量来自于:<action application="export" data="dialed_extension=$1"/>

注意,上面是用export把变量设置到b-leg上的,实际上就是最初的被叫号码1001,${dialed_extension:2}的意思是从1001的第2个位置开始截取,一直截取到字符串结尾的字符串,由于位置是从0开始计数的,因而1001:2最终的结果就是01,因而最终transfer的一行就等价于:<action application="transfer" data="-both 3001 XML default"/>

其中,-both 表示将两条腿都分别转到 3001 这个 extension上。所以,重新路由后就到了会议的情况。最终 1000 和 1001 都进入名为3001-192.168.7.2的会议。

inline Dialplan (内连拨号计划)

XML Dialplan支持非常丰富的功能,但在测试或编写程序时,经常用到一些临时的或者很简单的Dialplan,如果每次都需要修改XML,不仅麻烦,而且执行效率也会有所降低。这时便需要一种短小、轻便的 Dialplan 以便更高效地完成任务,所以 inline Diaplan 便因此而产生。通过使用 inline Dialplan,可以很方便地在脚本中生成动态的 Dialplan 而无须使用复杂的 reloadxml 以及mod_xml_curl 技术等。inline Dialplan 称为内联拨号计划。

  • XML Dialplan Action 中的 inline属性(内联执行)是 在路由阶段就立即执行,不用再等到execute 阶段执行
  • inline Dialplan 没有 Extension,也没有复杂的 Condition,只是像 XML Dialplan 中那样简单地叠加 Action。语法格式:app1:arg1,app2:arg2,app3:arg3  从语法可以看出,它只是多个App以及其参数组成的字符串,App之间用逗号分隔,而App与参数之间用冒号分隔。如果参数中有空格,则整个字符串都需要使用单引号引起来。

在上面的例子中,通过拨打 9196 来找到对应的 XM Ldalplan,在这里可以直接在命令行上写出对应的 inline Dialplan 的形式:

originate user/1000 echo inline
originate user/1000 answer,echo inline

这里你可能要问,它与 originate user/1000 &echo 有什么区别呢?在回答这个问题之前,先看一下 originate 的语法:

originate <call_url> <exten>|&<application_name>(<app_args>)
[<dialplan>] [<context>] [<cid_name>] [<cid_num>] [<timeout_sec>]

首先它的第一个参数 call_url 是呼叫字符串。第二个参数可以是一个 exten,或者&加上一个App,App的参数要放到括号里,如:

originate user/1000 &echo
originate user/1000 &playback(/tmp/sound.wav)
originate user/1000 &record(/tmp/recording.wav)

首先 originate 会产生一个Channel,它会呼叫user/1000这个用户。注意这是一个单腿的通话,因此只有一个Channel。但一个Channel有两端,一端是1000这个用户,另一端是FreeSWITCH(实现上是1000在跟FreeSWITCH的某个App通话,如这里的echo、playback或record)。在 user/1000 接电话后(严格说是收到它的 earlymedia 后),FreeSWITCH 即开始在该 Channel 上执行&后面的App。但这种形式只能执行一个App,如果要执行多个,就需要将电话转入Dialplan,命令:originate user/1000 9196  等价于 originate user/1000 9196 XML default

  • 解释:在 user/1000 接听电话后,电话的另一端(也就是FreeSWITCH)需要对电话进行路由,在这里它要将电话路由到 9196 这个 Extension 上,第一条命令由于没有指定是哪个Dialplan,因此它会在默认的 XML Dialplan 中查找,同时 XML Dialplan 需要一个Context,它默认就是 default。它的效果跟直接用软电话拨打9196这个分机一样(不同的是呼叫方向的问题,这种情况相当于回拨)。

还可以加一些可选的参数,用于指定来电显示(Caller ID)的名字(cid_name)和号码(cid_number),以及呼叫超时的秒数,如:originate user/1000 9196 XML default 'Seven Du' 9196 30

学了 inline Dialplan 后,所以也可以用下面这种形式:originate user/1000 echo inline 。请注意,在XML Dialplan中,9196 是一个分机号,而 inline Dialplan 中的 echo 是一个App,当然你也可以顺序执行多个 App。示例:

  • 命令分别执行 answer(应答)、playback(播放语音)、record( 录音):originate user/1000 answer,playback:/tmp/pleace_leave_a_message.wav,record:/tmp/recording.wav inline
  • 命令分别执行 playback 和 bridge:originate user/1000 playback:/tmp/beep.wav,bridge:user/1001 inline

有时候,App的参数中可能会有逗号,因而会与默认的App间的逗号分隔符相冲突,以下的m语法形式将默认的逗号改为^分隔(相当于转义):

originate user/1000 'm:^:playback/tmp/beep.wav^bridge:{ignore_early_media=true,originate_caller_id_number=1000}user/1001' inline

其中,"m:^" 表示将 App 间的分隔符临时改为了 "^",所以后面的 bridge 中的参数中的逗号就不会误认为是 App 间的分隔符了。

当然也可以把 inline Dialplan 用在任何需要Dialplan的地方,如(以下两行实为一行):

uuid_transfer 2bde6598-0f1a-48fe-80bc-a457a31b0055'set:test_var=test_value,info,palyback:/tmp/beep.wav,record:/tmp/recording.wav'

命令会将一个 Channel 转入一个 inline Dialplan,它首先执行 set 以设置一个 test_var 的通道变量,然后执行 info 在日志中打印当前 Channel 的详细情况,接下来播放一个声音文件(如beep应该是“嘀”的一声),然后开始录音。

其他 dialplan

除了以上介绍的 Dialplan外,还有其他的 Dialplan形式,要查看你的系统支持多少Dialplan,可以使用如下命令:freeswitch> show dialplan

命令列出了系统默认支持的Dialplan以及实现它们的模块。

常用的 dialplan app

前面熟悉了各种 Dialplan,并知道 Dialplan 最终会找到并返回一组App,实际的呼叫行为全部是由这些App来控制的。FreeSWITCH中有超过140个App,不过最常用的不是很多,在此我们对几个常用的、有代表性App进行讲解。

  • set。用于设置一个通道变量,如:<action application="set" data="my_var=123456"/>
  • echo。回声,在调试的时候比较有用,例如:<action applicatioin="echo"/>
  • info。调试的时候比较有用,打印全部的通道变量,例如:<action applicaiton="info"/>
  • answer。用于应答一路呼叫。FreeSWITCH做被叫时,如果想给主叫方放音,则必须应答后才可以(在SIP中是200消息),例如:<action application="answer"/>。有些App(如conference 或 fifo 等)会隐含应答,因而不需要明确的应答。但有些App(如 playback 和 ivr等)则不会隐含应答,因而需要明确的应答(answer)。bridge 是一个比较特殊的 App,它会再发起一路呼叫 b-leg,如果 b-leg 应答,它会将 b-leg 的应答传递给 a-leg,进而给主叫用户发送应答消息。一般来说,运营商都把应答时间作为计费的开始时间。所以在上面的例子中,如果A呼叫B,只有B应答后,应答消息才会发给A,因此只有B应答了才开始计费。
  • bridge。负责桥接另一条腿(b-leg),它的参数是一个呼叫字符串:<action application="bridge" data="user/1000"> bridge 操作是阻塞的,它会一直等到 b-leg 释放后才继续往下走
  • playback。用于给 Channel 放音。比如,如果需要对主叫放音,可以这样使用 playback:<action application="playback" data="/tmp/test.wav"/> 它的参数是声音文件的路径。一般的声音文件是.wav格式的。如果要播放多个文件可以串联操作,如:
    <action application="playback" data="/tmp/test1.wav"/>
    <action application="playback" data="/tmp/test2.wav"/>
    <action application="playback" data="/tmp/test3.wav"/>
    也可以通过 filestring 协议来串联,如:<action application="playback" data="file_string:///tmp/test1.wav!/tmp/test2.wav!/tmp/test3.wav"/> 其中,“!”是文件名的分隔符,如果想使用其他分隔符,可以在playback之前使用以下Chan Var指定。另外,也可以指定多个文件间播放的时间间隔,如下代码会将文件名分隔符改成“|”,并把时间间隔改为500毫秒:<action application="set" data="playback_delimiter=!"/>
    <action application="set" data="playback_sleep_val=500"/>
    另外,通过 mod_shout 模块,也可以支持本地或远程HTTP或Shout Cast服务器上的.mp3格式的文件,如:
    <action application="playback" data="/tmp/test.mp3"/>
    <action application="playback" data="http://localhost/test.mp3"/>
    <action application="playback" data="shout://localhost/test.mp3"/>
    在播放过程中,用户可以按“*”号键停止播放,或在播放前通过Chan Var选择其他按键完成此功能:<action application="set" data="playback_terminators=1"/>
    也可以使用如下方法取消按键中断的方式:<action application="set" data="playback_terminators=none"/>
  • sleep。用于设置可以等待/暂停的一段时间,单位默为毫秒:<action application="sleep" data="1000"/>
  • ring_ready。用于在SIP中给对方回180消息,即通知对方可以振铃了:<action application="ring_ready" data="1000"/>
  • pre_answer。用于在SIP中给对方回183消息,后续的playback之类的动作将作为早期媒体(Early Media) 给对方发过去,如彩铃音:<action application="ring_ready" data="1000"/>
    <action application="playback" data="/tmp/music.wav"/>  注意,虽然FreeSWITCH可以把媒体发给对方,但如果在一定时间内(通常60秒)没有应答,对端通常也会挂断该通话。
  • read。用于实现播放声音并且等待接收DTMF按键,它的格式是:
    <min> <max> <sound file> <variable name> <timeout> <terminators>  其中:
    min:最少收号位数。
    max:最大收号位数。
    sound file:要播放的声音文件。
    variable name:收到用户按键后存到哪个变量里。
    timeout:等待每一位的输入超时毫秒数。
    terminators:收号小于min位时,按该键可以提前结束,通常是#。
    例如,我们可以播放“您好,请输入身份证号,以#号结束(身份证号是15或18位的)”
    <action application="answer"/>
    <action application="read" data="15 18 /tmp/input-id-card.wav id_card_num 5000 #"/>
    <action application="log" data="INFO ID Card Number: ${id_card_num}"/>
    <action application="info"/>
    <action application="hangup"/>
  • play_and_get_digits 与 read 类似但它比 read 更高级,先看参数。方括号内是可选参:
    <min> <max> <tries> <timeout> <terminators> <file> <invalid_file>
    <var_name> <regex> [<digit_timeout>] ['<failure_ext> [failure_dp[failure_context]]']
    它的作用也是播放一段语音等待收号,与read不同的是,它收到后会跟正则表达式比较,如果不匹配,则播放invalid_file所指定的声音(如“输入错误,请重新输入”),然后重新等待输入。重试次数由tries参数指定。
    play_and_get_digits 大部分参数都与read的类似,具体如下
    min:最少收号位数。
    max:最大收号位数。
    tries:重试次数。
    timeout:收齐全部位数的超时。
    terminators:结束符。
    file:要播放的声音文件。
    invalid_file:输入错误时播放的提示音文件。
    var_name:变量名。
    regex:正则表达式。
    digit_timeout(可选):位间间隔。
    failure_ext(可选):若最后输入错误则转到该Extension。·failure_dp(可选):输入错误时转到的Dialplan。
    failure_context(可选):输入错误时转到的Dialplan Context。
    示例:如上面输入身份证明的例子,我们可以允许用户重试3次,使用play_and_get_digits实现的方法如下:<action application="play_and_get_digits"
    data="15 18 3 10000 # /tmp/input-id-card.wav /tmp/invalid_num.wav
    id_card_num (^\d{15}$)|(^\d{17}([0-9]|X)$)"/>
    <action application="log" data="INFO ID Card Number: ${my_digits}"/>

官方文档见:https://freeswitch.org/confluence/display/FREESWITCH/mod_dptools

APP又称拨号计划工具(Dialplan Tools)。可使用以下APP(括号中为参数列表,参数如有空格需用''引起):

  • answer([is_conference]):给主叫回送应答消息(200)。FreeSWITCH做被叫时,如果想给主叫放音,必需应答后才可以。is_conference表示支持RFC4579视频会议控制标准。

  • att_xfer(CALLURL):协商转。

  • bind_meta_app(NUM a|b s APP::ARG[ ...]):绑定DTMF,按键后执行APP。NUM为按键(需先按*键激活功能),可指定a腿或b腿,s表示在收到按键的相同的腿上执行。

  • bridge(CALLURL):桥接至另一条腿(b-leg)。其是阻塞的,直到b-leg释放后才继续往下走。

  • callcenter(QUEUE):将呼叫转到呼叫中心。

  • conference(CONFERENCE[@PROFILE][+PIN][+flags{FLAG1|FLAG2}]):将来话转入会议。@之前为会议的名字,之后为会议的参数。竖线为字面值,不表示“或”的意思。

  • curl(URL):与HTTP服务器交互。

  • delay_echo(MS):延迟若干毫秒回声。

  • detect_speech(ASRENGINE GRAMMERFILENAME GRAMMERFILENAME|pause|resume|params MODELFILENAME DICTIONARYFILENAME):语音识别开启/暂停/重启/设置参数。文件名可没有扩展名。

  • directory(default DOMAINNAME default):按姓名呼叫用户。当系统提示输入名字时,通过输入用户名字的前几位进行拨叫。

  • echo:回音。

  • endless_playback(FILEPATH):无限循环播放声音文件。

  • erlang(ERLANGCALL NODE):连接外部Erlang节点。

  • execute_extension(EXTENSION DIALPLAN CONTEXT):临时执行其他Dialplan下的context的extension,不会重新进入ROUTING阶段。

  • export([nolocal:]VAR=VALUE):设置变量,并会使用VAR设置特殊的变量export_vars。如使用nolocal,则只设置到b-leg上;否则,设置到两条腿上。

  • fifo(LIST in|LIST out [wait|nowait]):从一个先进先出队列中进行呼叫停泊/取回。使用out时可指定wait(offhook坐席)或nowait(onhook坐席),默认为wait。

  • hangup:挂断通话。

  • hash(insert/HASH/KEY/VALUE|delete/HASH/KEY|select/HASH/KEY):操作内存中哈希表数据结构的键值对。

  • hold:将通话挂起,并播放MOH保持音乐。

  • info:在日志中打印所有通道变量。

  • intercept([-bleg] CHANNELUUID):代接。

  • ivr(IVR):进入IVR菜单。

  • javascript(JSFILEPATH):执行JavaScript脚本。

  • log(LEVEL MESSAGE):打印日志。(originate命令执行此APP时,不知道怎么设置LEVEL,括号里面都当成MESSAGE。)

  • loop_playback(+N FILEPATH):循环播放声音文件若干次。

  • lua(LUAFILEPATH [ARG [ ...]]):执行Lua脚本。如为相对路径,则在安装目录的scripts目录下。

  • park:将通话挂起,对端听不到任何声音。

  • phrase(PHRASE[,INPUT]):播放Phrase宏。

  • play_and_detect_speect(MUSIC detect:ASRENGINE GRAMMARFILENAME):播放一段声音并进行语音识别。文件名可没有扩展名。

  • play_and_get_digits MINDIGITS MAXDIGITS MAXATTEMPTS TIMEOUTMS TERMINATORS MUSIC INVALIDMUSIC VAR REGEX [INTERDIGITTIMEOUTMS ['FAILEXTENSION [FAILDIALPLAN [FAILCONTEXT]]']]:播放声音并等待接收DTMF按键,与read类似,但更强大。MINDIGITS为最少收号位数,MAXDIGITS为最大收号位数,MAXATTEMPTS为重试次数,TIMEOUTMS为收齐全部位数的超时毫秒数,TERMINATORS为收号小于MINDIGITS位时的提前结束按键(可多个,通常是#),MUSIC为播放的提示音(用法见playback的MUSIC),INVALIDMUSIC为输入错误时播放的提示音(用法见playback的MUSIC),VAR为收到用户按键后存放的变量名,REGEX为收号后进行匹配的正则表达式,INTERDIGITTIMEOUTMS为位间间隔毫秒数,FAILEXTENSION为若最后输入错误时转到的extension,FAILDIALPLAN为输入错误时转到的Dialplan,FAILCONTEXT为输入错误时转到的Dialplan Context。

  • play_fsv(FILEPATH):播放录像。

  • playback(MUSIC):播放声音。MUSIC可为:

    • FILEPATH,文件名。不带扩展名会查找与本Channel编码一致的原生文件。
    • file_string://MUSIC[!...]:串联多个声音。
    • http://PATH:HTTP接口。可将远程文件缓存到本地。
    • local_stream://LOCALSTREAM,本地文件流,每个流在系统中只有一个实例。LOCALSTREAM为conf/autoload_configs/local_stream.cof.xml中定义的名字。
    • phrase:PHRASE[:INPUT]:Phrase宏。
    • say:[TTSENGINE:TTSVOICE:]TEXT:使用TTS根据文本内容放音。TTSENGINE和对应TTSVOICE用法见speak
    • shout://SERVER/PATH:SHOUTcast协议,播放服务器上的文件(可为MP3文件)。
    • silence_stream://MS[,CN],静音流,MS等于0为无限长,CN为舒适噪音,范围为1至10000,通常使用600至1400,值越小噪声越大,-1则产生绝对静音。
    • tone_stream://%(ONMS,OFFMS,HZ1[,HZ2])[;%(ONMS,OFFMS,HZ1[,HZ2])];loops=N]:铃流,使用TGML语音生成信号音。ONMS为通毫秒数,OFFMS为断毫秒数,HZ1、HZ2为交替的频率,N为循环次数,-1为无限循环。
    • unimrcp:SERVER:MRCP协议。
    • vlc://FILEPATH|HTTPURL:使用libvlc播放。
  • pre_answer:给对方回复消息(183),后续的playback之类的action将作为早期媒体给对方发送过去。

  • python(PYFILEPATH):执行Python脚本。文件路径为相对于Python的查找目录(如/usr/lib/python2.7/dist-packages/)。

  • read MINDIGITS MAXDIGITS MUSIC VAR INTERDIGITTIMEOUTMS TERMINATORS:播放声音并等待接收DTMF按键。MINDIGITS为最少收号位数,MAXDIGITS为最大收号位数,MUSIC为播放的提示音(用法见playback的MUSIC),VAR为收到用户按键后存放的变量名,INTERDIGITTIMEOUTMS为等待每一位的输入超时毫秒数,TERMINATORS为收号小于MINDIGITS位时的提前结束按键(可多个,通常是#)。

  • record(FILEPATH):对当前Channel进行录音。阻塞,只用于单腿的呼叫。如不指定扩展名,则以原生格式录音。

  • record_fsv(FILEPATH):录像,并将视频回显。只用于单腿的呼叫。

  • record_session(FILEPATH):对当前Channel进行录音。非阻塞,可用于单腿的呼叫也可用于桥接的呼叫。如不指定扩展名,则以原生格式录音。

  • ring_ready:给对方回复消息(180),通知对方可以振铃。

  • rxfax(FILENAME):收传真。文件格式为TIFF。

  • say(LANGUAGE SAYTYPE SAYMETHOD TEXT):使用Say接口将预先录好的声音对特定的内容放音,支持多语言。LANGUAGE为语言,SAYTYPE为播放的数据类型,SAYMETHOD为播放的读法,TEXT为播放的内容。

    SAYTYPE可为:

    • ACCOUNT_NUMBER:账号。
    • CURRENCY:货币。
    • CURRENT_DATE:当前日期。
    • CURRENT_DATE_TIME:当前日期时间。
    • CURRENT_TIME:当前时间。
    • EMAIL_ADDRESS:Email地址。
    • IP_ADDRESS:IP地址。
    • ITEMS:项目。
    • MESSAGES:消息。
    • NAME_PHONETIC:姓名拼写。
    • NAME_SPELLED:姓名。
    • NUMBER:数字。
    • PERSONS:人名。
    • POSTAL_ADDRESS:邮寄地址。
    • SHORT_DATE_TIME:短日期时间。
    • TELEPHONE_EXTENSION:分机号。
    • TELEPHONE_NUMBER:手机号码。
    • TIME_MEASUREMENT:时间。
    • URL:网址。

    SAYMETHOD可为(并不是所有语言都实现了全部读法):

    • COUNTED:以序数词方式读出。
    • ITERATED:逐个数字读出。
    • PRONOUNCED:以基数词方式读出。
  • sched_hangup(+SECOND):若干秒后挂断通话。

  • set(VAR=VALUE):设置通道变量到当前Channel(a-leg)上。如VALUE为_undef_则取消VAR的赋值。

  • sleep(MS):暂停若干毫秒。

  • socket(IP:PORT [async] [full]):使用Event Socket的外连模式连接外部服务器。async表示异步执行,默认是同步执行。full表示外部应用可以使用全部的API,默认只有少量API是有效的。

  • speak([TTSENGINE|TTSVOICE|]TEXT):使用TTS播放声音。此参数使用竖线隔开各字段。可用的TTSENGINE和对应TTSVOICE有:

    • flite:awb、kal、rms、slt。
    • tts_commandline
  • start_dtmf:打开带内DTMF检测。

  • transfer([-bleg] USERNAME DIALPLAN CONTEXT):盲转。将当前通话重新转接到ROUTING阶段,重新去Dialplan中进行路由,呼叫USERNAME。

  • txfax(FILENAME):发传真。文件格式为TIFF。

  • unset(VAR):取消变量的赋值。

  • verbose_events:令事件包含所有variable_开头的字段。

  • voicemail(CONTEXT DOMAINNAME EXTENSION):语音信箱。

在 Dialplan 中使用 API 命令

Dialplan 中一般执行的是App,前面介绍App与API的区别时也讲到过,API本身与Channel或 Session是不相关的,其是一个“第三者”。不过,在某些特殊情况下,在Dialplan中也需要调用某些API提供的能力,这可以通过类似变量引用的方法来实现,如:${status()},它与变量引用的区别就是多了一对 “()”,看起来类似于函数调用,括号内可以填写API命令的参数。
这些API调用一般是通过set 来执行的,如:

<action application="set" data="api_result=${status()}"/>
<action application="set" data="api_result=${version()}"/>
<action application="set" data="api_result=${strftime()}"/>
<action application="set" data="api_result=${expr(1+1)}"/>

以上命令都很直观,它们都是执行一个API,并将API输出的结果作为变量的值。其中,最后一行中的 expr 类似于UNIX中的expr命令,它计算一个表达式并输出结果,

如:freeswitch> expr 1+1
2

呼叫 总结

上面主要讲述了 FreeSWITCH 中的几种拨号计划,并重点讲解了XML拨号计划。XML的描述能力很强,因而便于写出比较复杂的拨号计划。事实上,FreeSWITCH 默认的配置在这里已经发挥得淋漓尽致了。如果有时间可以好好看下 默认配置

拨号计划有三个核心要素: Dialplan、 Context 和 Extension。系统中有一些 Endpoint模块,这些模块一般都能配置相关的拨号计划以便对来话进行处理和路由。某些 App 或 API,如 transfer 和 uuid_transfer 等,需要的参数也是 Dialplan。在不同的场景下这些参数也会有默认值,

如:<action application="transfer" data="1234"/>

表示 transfer 到 1234 这个 Extension 使用 XML Dialplan,且 Context为default,等价于:

<action application="transfer" data="1234 XML default"/>

理解了这些要素和参数,就可以举一反三,写出更强大的呼叫流程。不管是 XML Dialplan 还是inline Dialplan,拨号计划的最终目的就是为了给我们返回一组 App 以及它们的参数。一旦返回,拨号计划就完成了它的使命,实际呼叫的应答、放音、收号,录音等行为全部是由这些App后续控制的。当然,这些 App 也可以在适当的时候进行转移(transfer),从而要求重新进行路由(见图6-1中的Transfer部分),以获取一组新的App,进而执行新的动作。

execute_extension 与 transfer类似,都需要 Dialplan的 "三要素" 作为参数,但不同的是,前者是临时执行一些 Dialplan 指定的 App,它不会重新进入 ROUTING 阶段。

灵活掌握Dialplan中一些常用的App以及在Dialplan中执行API的方法和技巧,可以在以后的配置维护和应用开发中事半功倍。

挂机原因

官方文档见:Hangup Cause Code Table | FreeSWITCH Documentation

挂机原因有:

  • CALL_REJECTED:拒接。
  • NO_ANSWER:无应答。
  • NO_ROUTE_DESTINATION:空号或无法路由。
  • NORMAL_CLEARING:呼叫正常释放。
  • NORMAL_TEMPORARY_FAILURE:普通临时失败。
  • USER_BUSY:用户忙。
  • USER_NOT_REGISTERED:用户未注册。

事件

  • API:执行API。
  • BACKGROUND_JOB:使用bgapi后台API执行完成。
  • CHANNEL_ANSWER:Channel被应答。两条腿均会产生该事件。
  • CHANNEL_BRIDGE:Channel与另一个Channel桥接成功。只在主动发起bridge的那条腿上产生该事件。
  • CHANNEL_CALLSTATE
  • CHANNEL_CREATE:来话或去话产生。
  • CHANNEL_DESTROY:Channel完全释放。
  • CHANNEL_EXECUTE:一个APP开始执行。
  • CHANNEL_EXECUTE_COMPLETE:一个APP执行完成。
  • CHANNEL_HANGUP:挂机。
  • CHANNEL_HANGUP_COMPLETE:挂机完成。信息更详细。
  • CHANNEL_PARK:通话挂起。
  • CHANNEL_PROGRESS:收到100或180消息。
  • CHANNEL_PROGRESS_MEDIA:在应答之前收到早期媒体,如收到183消息。
  • CUSTOM:自定义。已约定的Subclass有:
    • conference::maintenance
    • fifo:info
    • sofia::register
    • sofia::unregister
  • DTMF:双音多频按键信息。
  • HEARTBEAT:心跳。每20秒产生一次。
  • MODULE_LOAD:模块加载。
  • MODULE_UNLOAD:模块卸载。
  • PLAYBACK_START:放音开始。
  • PLAYBACK_STOP:放音结束。
  • RECORD_START:录音开始。
  • RECORD_STOP:录音结束。
  • SHUTDOWN:系统关闭。
  • STARTUP:系统启动。

Channel事件发生顺序:

  1. CHANNEL_CREATE
  2. CHANNEL_PROGRESS
  3. CHANNEL_PROGRESS_MEDIA
  4. CHANNEL_ANSWER
  5. CHANNEL_BRIDGE
  6. CHANNEL_EXECUTE
  7. CHANNEL_EXECUTE_COMPLETE
  8. CHANNEL_HANGUP
  9. CHANNEL_HANGUP_COMPLETE
  10. CHANNEL_DESTROY

sip 协议

下面的协议文本都是从 FreeSWITCH 中实际抓出来的,看起来比较直观。获取这些 SIP 消息的方法是 在FreeSWITCH 命令行上执行如下命令:freeswitch> sofia global siptrace on

然后,打电话看一下能不能在控制台上看到信令。有时候,注册的电话多了以后,信令消息比较多,看起来就比较累。在这种情况下笔者一般是执行 fs_cli 连接到 FreeSWITCH,打开 siptrace,打一个测试电话后就立即用 Ctrl+D(或/exit)退出 fs_cli,然后慢慢分析。当然对初学者而言还是建议找一个清净的环境学习,不要注册太多分机。如果需要关闭 trace,可执行如下命令:freeswitch> sofia global siptrace off

sip 简介

SIP 协议是 FreeSWITCH 的核心协议,理解SIP协议对理解FreeSWITCH的设计理念和使用方法很有帮助。SIP 即 会话初始协议(Session Initiation Protocol)是一个控制发起、修改和终结交互式多媒体会话的信令协议。

SIP是一个基于文本的协议,这一点与 HTTP 和 SMTP 类似。

对比一组简单的HTTP请求与SIP请求。

HTTP: GET /index.html HTTP/1.1
SIP:  INVITE sip:seven@freeswitch.org.cn SIP/2.0

两者类似,请求均有三部分组成:

  • 在HTTP请求中,GET指明一个获取资源(文件)的动作,/index.html则是资源的地址,最后HTTP/1.1是协议版本号;
  • 在SIP中,INVITE表示发起一次呼叫请求,seven@freeswitch.org.cn为请求的地址,也称为SIP URI 或 AOR(Adress of Record,用户的公开地址),第三部分的 SIP/2.0 也是版本号。其中,SIP URI 类似一个电子邮件地址,其格式为“协议:名称@主机”。这里SIP URI格式中的“协议”与 HTTP和HTTPS 相对应,也有SIP和SIPS两种(后者是加密的,如sips:seven@freeswitch.org.cn);“名称”可以是一串数字的电话号码,也可以是字母表示的名称;而“主机”可以是一个域名,也可以是一个IP地址。

简单示例来看一下HTTP协议。CURL 是一个HTTP的命令行工具和客户端,相当于一个命令行版的浏览器。命令行工具与图形界面的工具比起来,前者使用起来更灵活,并且通常更容易展现工具“背后”的东西。CURL工具使用-v命令可以看到HTTP协议的通信情况,它与HTTP服务器的交互流程如下:

用 CURL 请求网站 www.freeswitch.org.cn 的首页。

  • 以 “*” 开头的都是CURL的调试信息,
  • 以 “>” 开头的行是客户端向服务器发出的请求,
  • 以 “<” 开头的行是服务器对客户端的响应

SIP协议也与HTTP协议类似,如果熟悉其中的一种,可以对比着进行学习

SIP是一个对等的协议,类似 P2P。它和HTTP不一样是 "sip 不是客户端服务器结构的";也不像传统电话那样必须有一个中心的交换机,它可以在不需要服务器的情况下进行通信,只要通信双方都彼此知道对方地址(或者只有一方知道另一方的地址)即可,这种情况称为点对点通信。如图所示,Bob 给 Alice 发送一个 INVITE 请求,说“Hi,一起吃饭吧…”,Alice说“好的,OK”,电话就接通了。

在 SIP 网络中,Alice 和 Bob 都称为用户代理(User Agent,UA)。UA是在SIP网络中发起或响应SIP处理的逻辑实体。UA是有状态的,也就是说,它维护会话(或称对话)的状态。UA有两种:

  • 一种是UAC(UA Client),它是发起SIP请求的一方,比如图中的Bob;
  • 另一种是UAS(UA Server),它是接受请求并发送响应的一方,比如图中的Alice。

由于SIP是对等的,当Alice呼叫Bob时,Alice 就称为 UAC,而 Bob 则实现 UAS 的功能。一般来说,UA 都会实现上述两种功能。

假设Bob和Alice是经人介绍认识的,而他们还不熟悉,Bob想请Alice吃饭就需要一个中间人(M)传话,而这个中间人就叫代理服务器(Proxy Server)。

还有另一种中间人称为重定向服务器(Redirect Server),它以类似于这样的方式工作──中间人M告诉Bob,我也不知道Alice在哪里,但我朋友知道,要不然我告诉你我朋友的电话,你直接问她吧,我朋友叫W。这样,M就成了一个重定向服务器(把Bob对他的请求重定向到他的朋友,这样Bob接下来要直接联系他的朋友),而他的朋友W是真正的代理服务器。这两种服务器都是UAS,它们主要是提供一对欲通话的UA之间的路由选择功能。

还有一种UAS称为注册服务器。试想这样一种情况:Alice还是个学生,没有自己的手机,但它又希望Bob能随时找到她,于是当她在学校时就告诉中间人M说她在学校,如果有事找她可以打宿舍的固定电话;如果她要回家,也通知M说有事打家里电话;或许某一天她要去姥姥家,也要把她姥姥家的电话告诉M。总之,只要Alice换一个新的位置,它就要向M重新“注册”,以让M能随时找到她,这时候M就相当于一个注册服务器。

还有一种特殊的UA称为背靠背用户代理(Back-to-Back UA,B2BUA)。为了理解B2BUA,我们来看上述故事的另一个版本。M和W是一对恩爱夫妻。M认识Bob而W认识Alice。M和W有意撮合两个年轻人,但见面时由于两人太腼腆而互相没留电话号码。事后Bob想知道Alice对他感觉如何,于是打电话问M,M不认识Alice,就转身问爱人W(注意这次M没有直接把W的电话给Bob),W紧接着打电话给Alice,Alice说印象还不错,W就把这句话告诉M,M又转过身告诉Bob。这样,M和W一个面向Bob,一个对着Alice,他们两个合在一起,称为B2BUA。在这里,Bob是UAC,因为他发起请求;M是UAS,因为他接受Bob的请求并为他服务;我们把M和W看做一个整体,他们背靠着背(站着、坐着、躺着都行),W是UAC,因为她又向Alice发起了请求,最后Alice是UAS。其实这里UAC和UAS的概念也不是那么重要,重要的是要理解这个背靠背的用户代理。因为事情还没有完,Bob一听说Alice对他印象还不错,开心得不得了,便想请抽空请Alice吃饭,他将这一想法告诉M,M告诉W,W又告诉Alice。然后Alice问去哪里吃啊,W又只好问M,M再问Bob……在这对年轻人挂断电话之前,M和W只能“背对背”不停地工作,如图所示。

可以看出,四个人其实全是UA。在SIP世界中,所有UA都是平等的。具体到实物,则M和W就组成了实现软交换功能的交换机,它们对外说的语言是SIP,而在内部它们使用自己家的语言沟通。Bob 和 Alice 就分别成了我们常见的软电话,或者硬件的SIP话机。

SIP 定义了6种基本方法

除此之外,SIP 还定义了一些扩展方法,如SUBSCRIBE、NOTIFY、MESSAGE、REFER、INFO等,等到我们用到时会简单介绍。另外,无论是基本方法还是扩展方法,所有 SIP 消息都必须包含以下6个头域,

sip 注册

普通的固定电话网中,电话的地址都是固定的,而因特网是开放的,所以Alice可能在家也可能在学校,甚至可能在世界上任何角落,只要能上网,她就能与全世界通信。当然,如果她作为被叫的一方,为了让我们的FreeSWITCH服务器能随时找到她,她的UA必须向我们的服务器注册。

通常的注册流程是,Alice 向 FreeSWITCH 发起注册(REGISTER)请求,FreeSWITCH 返回 401消息对 Alice 发起 Challenge(挑战),Alice 将自己的用户名密码信息与收到的 Challenge 信息进行计算,并将计算结果以加密的形式附加到下一个 REGISTER 请求上,重新发起注册,FreeSWITCH 收到后对本地数据库中保存的 Alice 的信息使用同样的算法进行计算和加密,并将其与Alice发过来的计算结果相比较。如果计算结果相匹配,则认证通过,Alice 便可以正常注册。交互流程如图所示。

现在用真正的注册流程进行说明。

SIP 消息是在真正的 FreeSWITCH 中跟踪(trace)出来的。其中 FreeSWITCH 服务器的IP地址是 192.168.4.4,它使用默认的端口号 5060,在这里使用的 SIP 承载方式是UDP。Alice 使用的UA是Zoiper,端口号是 5090(这里是与FreeSWITCH在同一台机器上,所以不能再使用端口5060)。其中,每个消息短横线之间的内容都是FreeSWITCH中输出的调试信息,不是SIP的一部分。recv(Receive的缩写形式)表示FreeSWITCH收到的消息,send表示发出的消息,下同。

在Alice发起注册时,下面是FreeSWITCH收到的第一条消息(为了便于说明,增加了行号):

SIP是类似HTTP的纯文本的协议,所以很容易阅读

  • 第1行的REGISTER表示这是一条注册消息。
  • 第2行的Via是SIP的消息路由,如果SIP经过好多代理服务器转发,则会有多条Via记录。
  • 第3行,Max-forwards指出消息最多可以经过多少次转发,主要是为了防止产生死循环。
  • 第4行, Contact是Alice的联系地址,即相当于Alice家的地址,本例中FreeSWITCH应该能在192.168.4.4这台机器上的5090端口找到她。
  • 第5和第6行的To和From表示以Alice注册。
  • 第7行,Call-ID是本次SIP会话(Session)的标志。
  • 第8行,CSeq是一个序号,由于UDP是不可靠的协议,在不可靠的网络上可能丢包,所以有些包需要重发,该序号则可以防止重发引起的消息重复。
  • 第9行,Expires是说明本次注册的有效期,单位是秒。在本例中,Alice的注册信息会在一小时后失效,它应该在半小时内再次向FreeSWITCH注册,以免FreeSWITCH“忘记”她。实际上,大部分UA的实现都会在几十秒内重新发一次注册请求,这在NAT的网络中有助于保持连接。
  • 第10行,Allow是说明Alice的UA所能支持的功能,某些UA功能丰富,而某些UA仅有有限的功能。
  • 第11行,User-Agent是UA的型号。
  • 第12行,Allow-Events说明她允许哪些事件通知。
  • 第13行,Content-Length是消息正文(Body)的长度,在这里只有消息头(Header),没有消息正文,因此长度为0。

FreeSWITCH需要验证Alice的身份才允许她注册。在SIP中,没有发明新的认证方式,而是使用已有的HTTP摘要(Digest)方式来认证。这里它给Alice回送(发,send)401响应消息,消息内容如下:

401消息表示未认证,它是FreeSWITCH对Alice请求的响应。各消息头的含义基本上与请求中的含义是一样的。其中,“CSeq:1 REGISTER”表示它是针对刚刚收到的CSeq为1的REGISTER请求的响应。同时,它在本端生成一个认证摘要(WWW-Authenticate),一起发送给Alice。

Alice收到带有摘要的401后,重新发起注册请求,这一次加上了根据收到的摘要和她自己的用户名密码生成的认证信息(Authorization头)。同时,下面你可能也会注意到,CSeq序号变成了2。下面是重发的注册请求信息:

FreeSWITCH收到带有认证的注册消息后,核实Alice身份,如果认证通过,则向Alice回应200 OK消息,表示注册成功了。返回的200 OK消息如下:

如果认证失败(可能是Alice的密码填错了),则回应403 Forbidden或其他失败消息,消息内容如下:

可以看到,在整个注册过程中,Alice的密码是不会直接在SIP消息中传送的,因而最大限度地保证了认证过程的安全。如果Alice注册成功,则FreeSWITCH会将Alice在SIP消息中的联系地址(Contact字段)记录下来。以后如果有人呼叫Alice,FreeSWITCH就可以向该联系地址发送SIP消息以建立呼叫。

sip 呼叫流程 (几个典型示例)

示例 1:UA间直接呼叫

SIP的UA都是平等的,如果一方知道另一方的地址,就可以通信。实验:在机器上启动两个软电话(UA),一个是Bob的 X-Lite,另一个是Alice的 Zoiper。它们的IP地址都是192.168.4.4,而端口号分别是26000和5090,当Bob呼叫Alice时,它只需直接呼叫Alice的SIP地址sip:Alice@192.168.4.4:5090 。然后 Alice 的电话正在振铃。详细的呼叫流程如图

首先 Bob 向 Alice 发送 INVITE 消息请求建立 SIP 会话。Alice 的 UA 回 100 Trying 消息,意思是说我收到你的请求了,先等一会儿。接着 Alice 的电话开始振铃,并给对方回消息180 Ringing,说我这边已经振铃了,Alice听到后可能一会就过来接电话。Bob的UA收到该消息后即可以播放回铃音,以提示Bob对方的话机正在振铃。接着Alice接了电话,她发送200 OK消息给Bob,该消息是对INVITE消息的最终响应(所有大于1的状态码都是最终响应),而先前的100和180消息都是临时状态,只是表明呼叫进展的情况。Bob收到200后向Alice回复ACK证实消息。INVITE- 200-ACK完成“三次握手”的操作,保证了呼叫可以正常进行。其中,INVITE-1xx-200等消息合在一起称为一个事务(Transaction)。这时候Bob已经在跟Alice通话了,他们通话的内容(语音数据)是在SIP之外的RTP包中传递的,我们后面再详细讨论RTP。

最后,Alice挂断电话,向Bob发送BYE消息,Bob收到BYE后回送200 OK,通话完毕。其中BYE和200 OK也是一个事务,而上面的所有消息,则称为一个对话(Dialog,也译作会话)。

反过来也一样,Alice可以直接呼叫Bob的地址sip:Bob@192.168.4.4:26000进行通话。

上面描述了一个最简单的SIP呼叫流程。实际上,SIP还有其他一些消息。SIP消息大致可分为请求和响应两类。请求由UAC发出,到达UAS后,UAS回送响应消息。某些响应消息需要证实(ACK),以完成三次握手。其中请求消息包括基本的INVITE、ACK、OPTIOS、BYE、CANCEL、REGISTER,以及re-INVITE、PRACK、SUBSCRIBE、NOTIFY、UPDATE、MESSAGE、REFER等一些扩展。而响应消息则都包含一个状态码和一个原因短语(Reason Phrase)。与HTTP响应类似,状态码由三位数字组成:

  • 1xx组的响应为临时状态,表明呼叫进展的情况;
  • 2xx表明请求已被成功收到、理解和接受;
  • 3xx为重定向,表明SIP请求需要转向到另一个UAS处理;
  • 4xx表明请求失败,这种失败一般是由客户端或网络引起的,如密码错误、空号等,客户端应该重新修改请求,然后重发;
  • 5xx为服务器内部错误,表明服务器出错,不能响应合法的请求;
  • 6xx为全局性错误,如600 Busy Everywhere。

状态码后面跟着一个原因短语(如200 OK中的OK及刚才讲到的Busy Everywhere),它是对前面的状态码的一个简单解释。

示例 2:通过 B2BUA 呼叫

在真实世界中,Bob和Alice可以会经常改变位置,那么它们的SIP地址也会相应改变,并且,如果他们之中有一个或两个处于NAT的网络中时,直接通信就更困难了。所以,他们通常会借助于一个服务器来实现通信。这样Bob和Alice通过注册到服务器上即可获得一个服务器上的公有SIP地址。注册服务器的地址一般是不变的,因此他们的SIP地址就不会发生变化,他们也就总是能够进行通信了。现在,作为例子,让他们两个都注册到FreeSWITCH上。上面已经说过,FreeSWITCH监听的端口是SIP默认的5060端口。Bob和Alice注册后,他们分别获得了一个服务器的地址(SIP URI):sip:Bob@192.168.4.4和sip:Alice@192.168.4.4(默认的端口号5060可以省略)。

下面是Bob呼叫Alice的流程。需要指出,如果 Bob 只是发起呼叫而不接收呼叫,他并不需要向FreeSWITCH注册(有些软交换服务器规定需要先注册才能发起呼叫,但SIP是不强制这么做的)。首先,Bob向FreeSWITCH发送INVITE消息,请求建立一个呼叫,发送的INVITE消息如下:

上面的消息中省略了SDP的内容,关于SDP的部分我们将留到本章后面再探讨。在此,Bob的UAC通过 INVITE消息向FreeSWITCH发起请求。Bob的UAC用的是X-Lite(User-Agent),它运行在端口26000上(由Contact字段给出。实际上,它默认的端口也是5060,但由于实验环境下它与FreeSWITCH运行在一台机器上,5060端口已被FreeSWITCH占用,因此需要选择另一个端口)。其中,From为主叫用户的地址,To为被叫用户的地址。此时 FreeSWITCH作为一个UAS接受请求并进行响应。但在此之前,它先通过100 Trying消息通知Bob它已经收到了他的请求,消息内容如下:

FreeSWITCH通过100 Trying消息告诉Bob:“我已经收到你的消息了,别着急,我正在设法联系Alice……”,该消息称为呼叫进展消息。

但就在此时,FreeSWITCH发现它还不认识Bob,即它还不确定Bob是否有权通过它发起呼叫,因而它需要确认Bob的身份。在SIP中,它通过回送带有Digest验证信息(Proxy-Authenticate头)的407消息来通知Bob(类似于注册流程中的401),消息内容如下:

Bob回送ACK证实消息向FreeSWITCH证实已收到认证要求,至此,从INVITE开始发起的事务就结束了。下面是FreeSWITCH收到的ACK消息:

然后,Bob重新发送INVITE请求。这次还附带了Proxy-Authorization验证信息,该信息是根据上次收到的Proxy-Authenticate中的信息与Bob提供的认证用户名和密码计算出的结果。该INVITE重新开始了一个事务,消息内容如下:

这里也省略了SDP消息正文。FreeSWITCH收到INVITE请求后,重新回送100 Trying,通知Bob呼叫进展情况,消息内容如下:

然后FreeSWITCH对Bob发来的认证信息进行验证,发现Bob是本地的授权用户,因而呼叫可以继续进行。

至此,Bob与FreeSWITCH之间的通信已经初步建立,这种通信的逻辑通道称为一个Channel。该Channel是由Bob的UA和FreeSWITCH的一个UA构成的,我们称之为FreeSWITCH的一条腿,又称a-leg。

接下来,呼叫进入路由阶段,FreeSWITCH根据路由,发现Bob要呼叫Alice,由于FreeSWITCH是一个B2BUA, 因而它需要要建立另外一条腿去呼叫Alice,与a-leg相对,这条新腿就称为b-leg。

FreeSWITCH通过查找本地数据库,得到了Alice的位置(Alice的联系地址,Alice必须事先已经向它注册),接着启动一个UA(用作UAC),向Alice发送一个新的INVITE消息,内容如下(注意,这完全是一个新的事务,理论上与a-leg没有任何关系):

可以看到,该INVITE的Call-ID与上一次INVITE消息中的不同,说明这是另一个SIP对话(Dialogue)。另外,消息中还多了一个Remote-Party-ID,它主要是用来支持主叫号码显示功能(俗称来电显示,有了这个功能,Alice的话机上就可以显示Bob的号码,知道是Bob打来的)。与普通的POTS通话不同,在SIP通话话中,不仅能显示电话号码(这里是Bob),还能显示一个可选的名字(Bob)。这也说明了FreeSWITCH这个B2BUA本身是一个整体,它虽然是以一个单独的UA呼叫Alice,但还是跟负责Bob的那个UA有联系——就是这种背靠背的串联关系,使得Alice这一侧的Channel(b-leg)知道Bob那一侧(a-leg)的一些信息(如这里的主叫号码)。

同理,Alice会回送100 Trying消息通知FreeSWITCH它已经正常接收到该INVITE请求,正准备进行下一步处理。消息内容如下:

虽然我们曾经说过,所有的SIP UA都是对等的。但Alice的身份与FreeSWITCH不同,它作为一个被叫用户,不需要对FreeSWITCH发起的呼叫进行认证,因而,Alice的话机直接振铃,并向FreeSWITCH回送180 Ringing消息,通知FreeSWITCH Alice的话机已经开始振铃了。FreeSWITCH收到的180消息如下:

上述代码中,180也是呼叫进展消息,它说明 Alice 的电话已经开始振铃了,如果Alice听到了,则过一会她可能会接听电话。FreeSWITCH在收到180以后,有以下两个选择。

  • 直接给Bob回180消息。Bob的话机收到180后,知道Alice那边已经振铃了。注意,这里只是Bob的话机知道,Bob并不知道收到了180。因而Bob的话机会自己产生一个回铃音,播放给Bob听,以提示Bob对方正在振铃。
  • 给Bob回183消息。与180不同,183消息包含媒体SDP。这时,FreeSWITCH就有机会产生一个回铃音,通过RTP发送给Bob。Bob的话机就不需要自己再产生回铃音。通过这种技术,FreeSWITCH可以为呼叫Alice的用户产生个性化的回铃音,也称彩铃。

不过,在此我们先不深入讨论。不管是180还是183,Bob都会听到回铃音。FreeSWITCH默认是183,消息内容如下:

这里省略了SDP消息。再看Alice这端,她听到电话响了,一看主叫号码是Bob,心里非常甜蜜,便迫不及待地接起电话。在她接起电话的一刹那,她的SIP话机给FreeSWITCH回送了200 OK消息,表示Alice已经接听了。消息内容如下:

这里省略了SDP消息。FreeSWITCH向Alice回送ACK证实消息,证实它已经收到200 OK了。至此,FreeSWITCH与Alice这一端的通话已建立完毕。ACK消息内容如下:

当然,FreeSWITCH作为中间人也不敢怠慢,它立即向Bob的话机发送200 OK消息,并切断原先自动产生的回铃音,把Alice甜美的声音接进来。200 OK消息如下:

这里省略了SDP消息。多半这时Alice已经开始说话了:“Hi,Bob,你好……”,Bob的UA在收到200消息后也启动本地的麦克风,至此,他们两个就可以对话了。当然,Bob作为一个有责任的男人,他还顺便通过ACK消息通知FreeSWITCH他确实收到了上面的200消息。ACK消息如下:

至此,a-leg和b-leg的通话分别建立完毕,FreeSWITCH作为一个中间人将两个leg桥接起来,使得双方可以正常通话,通话进入稳定阶段。

一般来说,在需要计费的场合,FreeSWITCH可以对Bob进行计费。计费的开始时间是以FreeSWITCH给Bob发送200的时间为准的。当然,FreeSWITCH本身并不做任何计费处理,它只是记录通话的起止时间,实际的费率和计费工作由其他软件完成。

通话进入稳定阶段后,一般不再有SIP消息交互,所有的语音数据都在RTP中传送。

“电话粥”也不能煲太久,终于Alice挂断了电话,它的话机会给FreeSWITCH发送BYE消息,通知FreeSWITCH要再见(拆线)了。消息内容如下:

FreeSWITCH回送200 OK,并释放b-leg。消息内容如下:

同时,FreeSWITCH也给Bob发送BYE消息,通知Bob要拆线了。在下面的消息中,它还包含了挂机原因(Hangup Cause,在Reason头中给出),此处NOMAL_CLEARING表示正常释放。BYE消息内容如下:

Bob的话机回送200 OK消息,FreeSWITCH收到后,也释放a-leg,通话结束。FreeSWITCH收到的200 OK消息如下:

上面描述的是一个典型的通过FreeSWITCH进行呼叫的流程。虽然有点长,但是并不复杂,图示可以很形象地描述出 FreeSWITCH 的 两条 "腿" a-leg 和 b-leg。

完整的呼叫流程如图所示。实际上有4个UA(两对)参与到了通信中,4个UA组成了通信中的两条腿。

深入理解 SIP

HTTP协议与SIP协议文本本身的格式很类似,不同的只是SIP的交互流程更丰富一些。假设有三台机器,其中192.168.1.9 是 FreeSWITCH 服务器,而 Bob 和 Alice 分别在另外两台机器上,

Alice 注册到 FreeSWITCH,Bob 呼叫她时,使用她的服务器地址(因为Bob只知道服务器地址),即sip:Alice@192.168.1.9,FreeSWITCH接到请求后,查找本地数据库,发现 Alice 的实际地址(Contact地址,又叫联系地址)是 sip:Alice@192.168.1.200,便可以建立呼叫。

SIP URI 除使用 IP 地址外,也可以使用域名,如sip:Alice@freeswitch.org.cn。更高级及更复杂的配置可能需要DNS的SRV记录,在此就不做讨论了。

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

SDP 和 SOA

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

会话建立时,需要媒体协商,双方才能确定对方的媒体能力以交换媒体数据。后面会专门讲媒体协商,在此我们通过一个简单的例子介绍一下SDP是如何工作的。

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

客户端(192.168.1.118)呼叫FreeSWITCH(192.168.1.9),INVITE中带了SDP消息。其认证过程与我们上面讲到的类似。最后,FreeSWITCH回复200 OK对通话应答,然后双方互发RTP媒体流(G711A,即PCMA的音频和H264的视频)。并且,在图中可以看出,客户端的SIP端口号是35526,音频端口号是50452,视频端口号是52974;FreeSWITCH端的端口号则分别是5060、31988和19008。后面我们会在SIP消息中找到这些端口,这里就不再赘述了,大家只要知道用到的是这些端口就可以了。

在前面的例子中,为了节省篇幅,都省略了SDP部分。下面是一个完整的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(会话ID)、sess-version(会话版本号)、nettype(网络类型)、addrtype(地址类型)、unicast-address(单播地址)。
  • s=:Session Name,表示本SDP所描述的Session的名称。
  • c=:Connecton Data,连接数据。其中值域中以空格分配的两个字段分别是网络类型和网络地址,以后的RTP流就会发到该地址上。注意,在NAT环境中如果我们要解决RTP穿越 [2]问题就要看这个地址。NAT问题将在第9章节中讲到。
  • b=:Badwidth Type,带宽类型。
  • t=:Timing,起止时间。0表示无限。
  • m=:audio Media Type,媒体类型。audio表示音频, 50452表示音频的端口号,跟图7-10中的一致; RTP/AVP是传输协议 [3];后面是支持的Codec类型,与RTP流中的Payload Type [4](载荷类型)相对应,在这里分别是8、0、98和101,8和0分别代表PCMA和PCMU,它们属于静态编码,因而有一一对应的关系,而对于大于95的编码都属于动态编码,需要在后面使用“a=rtpmap”进行说明。
  • a=:Attributes,属性。它用于描述上面音频的属性,如本例中98代表8000Hz的ILBC编码,101代表RFC2833 DTMF事件。a=sendrecv表示该媒体流可用于收和发,其他的还有sendonly(仅收)、recvonly(仅发)和inactive(不收不发)。
  • v=:Video,视频。可以看出它的端口号52974也是跟图7-10中是一致的。而且与H264的视频编码对应的也是一个动态的Payload Type [5],在本例中是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 [6](Service Offer and Answer,提议/应答)。通俗地讲,它首先由一方提供支持的Codec类型,由另一方选择。如本例中,607先在INVITE中提议:“我支持PCMA、PCMU和ILBC编码(在m=audio 50452 RTP/AVP 8 0 98 101行中说明),你看咱俩用哪种通信比较好?”,FreeSWITCH在200 OK中回复说:“那我们就用PCMA吧”(m=audio 31988 RTP/AVP 8 101)。具体SOA流程及FreeSWITCH选择Codec的策略和依据我们将在第8章中详细解释。

3PCC。并不是所有的 INVITE 请求都是带SDP的,在 3PCC 中可能没有SDP。

3PCC(Third Party Call Control,第三方电话呼叫控制)指的是由第三方控制者(Controller)在另外两者之间建立的一个会话,由控制者负责会话双方的媒体协商。3PCC是一种非常灵活的会话控制方式。在PSTN网中,第三方呼叫控制通常用于会议、接线业务(接线员创建一个连接另外双方的呼叫)。同样,使用SIP协议也可以借助3PCC来完成PSTN网中的一些相关业务,例如点击拨号、通话过程中放音等等,而且实现起来非常方便。

3PCC的实现关键就在于控制者如何使用SOA在会话双方之间使用SDP消息协商即将建立的会话。RFC3725 [7]描述了几种实现3PCC的方法,我们在此以第一种为例简单介绍一下。详细了解3PCC已超出本书的范围,在此笔者仅以此为例帮助读者理解不带SDP的INVITE呼叫的交互流程,而对3PCC则避重就轻。一个典型的3PCC呼叫如图

其中:

  • 1)Controller向A发送一个不带SDP的INVITE请求。
  • 2)A给Controller回送200 OK,其中的offer指的是SDP消息,其中还有描述与A相关的媒体处理能力的数据,详见SDP协议。
  • 3)Controller向B发送INVITE请求,这其中带有A的SDP相关描述。
  • 4)B给Controller回送200 OK应答,其中的answer指的是SDP消息,其中描述与B相关的媒体处理能力的数据。
  • 5)Controller给B回送ACK,不含任何SDP信息,因为在前面的INVITE中已经有了。
  • 6)Controller对A回送ACK,其中带有B的SDP相关信息,这其实就可使媒体流在A和B之间传输了,而不用Controller去转发媒体流。

这就是用一个第三方的Controller进行呼叫控制的例子。FreeSWITCH默认不支持没有SDP的INVITE请求,如果需要响应这种请求,则可以尝试在SIP Profile中开启enable-3pcc参数。

SIP承载。大家已经熟知,HTTP是用TCP承载的,而SIP则支持TCP和UDP承载。事实上,RFC3261规定,任何SIP UA必须同时支持TCP和UDP [8]。我们常见的SIP都是用UDP承载的,由于UDP是面向无连接的,在大并发量的情况下与TCP相比可以节省TCP由于每个IP包都需要确认带来的额外开销。不过,在SIP包比较大的情况下,如果超出了IP层窗口的大小(通常是1500),在经过路由器的时候可能会被拆包,使用UDP承载的SIP消息就可能发生丢失、乱序等,这时候就应该使用TCP。在需要对SIP加密的情况下,可以使用TLS。TLS是基于TCP的。

媒体 (音频、视频、文字等)

SIP 信令用于建立会话。建立会话的目的是 "通话",即通话的双方要能互相听到对方的声音。通话时,要通过采样、量化、编码将传输的声音变成数字信号,以便在数字线路上传输,把这些数字信号称为媒体(Media)。

从模拟信号变成数字信号的过程称为模数转换(Analog Digital Convert,AD)。AD转换要经过采样、量化、编码三个过程。编码(Code)就是指按照一定的规则将采样所得的信号用一组二进制或者其他进制的数来表示。经过编码后的数据便于在数字网络上传输,到达对端以后,再通过解码(Decode)过程变成原始信号,进而经过数模转换(Digital Analog Convert,DA)再恢复为模拟量,即转换为人们能够感知的信号。

一般来说,编码与解码过程都是成对出现的,所以习惯上人们把它们合起来说,称为编解码(Codec),即 Co(de)与 Dec(ode) 的合成写法。

使用 PCM方式对原始声音信号进行采样、量化后得到线性编码,然后再进行压缩,这种编码方式就称为PCM编码。PCM的两种压缩方式 A律和μ律(alaw和ulaw)对应的编解码名称分别为 PCMA 和 PCMU。PCM编码是在 ITU-T Recommendation G.711 标准中定义的,因而又称为G.711 编码。PCM 编码的采样频率为8000Hz,而随着技术的进步及人们对声音质量要求的提高,各种高清(High Definition,HD)编码也纷纷涌现,如 G.722 等。

如果在网络上传输语音,则要将编码后的语音数据打包。通常使用的打包时间间隔为20ms,即将20ms 的音频数据放到一个数据包里传送,也可以理解为每20ms打一个包。那么1秒就能传输1000ms/20ms=50个包,如果采样频率是8000Hz,那么每个包携带 8000/50=160个采样数据。在PCMA或PCMU方式中,每个采样数据占1字节,因此一个20ms的PCM包的数据净荷就是160字节。

FreeSWITCH支持的语音编码

与其他压缩算法不同的是,G.729携带的不是真正语音数据的压缩结果,而是使用CS-ACELP预测模型得到的声音码表。CS-ACELP的全称是Conjugate-Structure Algebraic-Codec-Excited Liner,它利用数学模型模拟人类的各种发声方式,可以建立声音码表,用发送对应的声音代码代替实际的声音采样。对端收到后再根据这些声音代码合成语音。

Global IP Sound,后改为Global IP Solutions,在语音编解码方面颇有建树,它们的解决方案在抗丢包及回声消除方面做得特别好,广泛应用于我们所熟知的产品,如Skype及QQ。 2010年被Google收购,后来开源了iSAC。

在FreeSWITCH中,编码名称的格式为 "名称@xxh@yyi"。

  • h 代表 Hz,即采样频率;
  • i 则代表 ptime,即打包间隔;
  • xx 和 yy 分别代表实际的数值,
  • 示例:speex@16000h@20i 表示16000Hz、20ms 的 Speex 编码。

表列举了 FreeSWITCH 中各种编码的一些详细参数

如果要查看FreeSWITCH支持的编码类型,可以在控制台上使用 show codec 命令

每一行代表某一个 codec(编解码),每一行使用逗号进行分割

  • 第1列:表示是一个 codec(编解码)
  • 第2列:相关的编码名称及参数
  • 第3列:具体实现的模块。

可以看到 G.711编码 A律、μ律 (alaw、ulaw) 都是在核心模块CORE_PCM_MODULE中实现的。

在实践中,当不清楚某种编码所提供的各种参数时,可以尝试重新加载所属模块。比如 SILK 编码属于 mod_silk 模块,则可以使用 reload mod_silk 命令实现

iLBC 编码也是由独立的模块提供的,重新加载的命令和输出如下:reload mod_ilbc

音频编码最基本的两个技术参数就是采样频率和打包周期。采样频率越高,声音就越清晰,保留的细节就越多,当然它会占用更大的带宽。对于普通“人声”通话来讲,8000Hz就够了,但对于高品质的音乐来讲,就需要更高的采样率才能保证“悦耳”,比如我们通常说的CD音质的声音使用的就是44.1kHz的采样率。打包周期跟传输有关,打包周期超短,延迟越小,相对而言传输开销就会越多,因而需要更大的带宽;打包周期超长,带来的延

迟就越大,如果传输过程中有丢包,对语音质量的影响也就越大。大部分编码都支持多种打包周期,最常见的是20ms,iLBC、G.723等默认使用30ms,更长的打包周期如60~120ms,通常用于卫星链路等高延迟、低带宽的场合。

注意,在FreeSWITCH中,有些编码不是默认安装的。如果要使用这些编码,需要自己编译,如在源代码目录下可执行以下命令来安装相应的编码

媒体 工作机理

在基于SIP的通信中,媒体数据是在RTP流中传输的。

以PCM音频数据为例,上面已经讲过,如果打包周期是20ms,则每个音频包得到160字节的数据,在这些数据前面加上12字节的RTP包头,就成为了一个RTP包。RTP包头中携带了音频的编码类型及时间戳等数据,以便于对方收到后进行解码回放及同步等。

RTP包使用与SIP不同的UDP端口传送,因而在实际传输前需要先通过SIP信令与对方 "协商" 好该往哪个端口传送,一个高度简化的 SIP/RTP 交互流程示意图如图所示。

从图中可以看到,A和B首先通过SIP信令“协商”好双方将要使用的RTP端口号(当然还有双方的IP地址,这里省略了),然后双方互相向对方发送RTP媒体流。

SIP 可使用TCP或UDP承载,但 RTP 数据一般只使用UDP承载。下面先来看一下FreeSWITCH中与媒体相关的配置。

相关配置

在默认的配置中,SIP Profile 支持的媒体列表是在 vars.xml 文件中配置的,如:

<X-PRE-PROCESS cmd="set" data="global_codec_prefs=G722,PCMU,PCMA,GSM"/> 
<X-PRE-PROCESS cmd="set" data="outbound_codec_prefs=PCMU,PCMA,GSM"/>

可以看到默认支持的编码

如果要增加其他编码的支持(如G729),可以将上述配置项改为:

<X-PRE-PROCESS cmd="set" data="global_codec_prefs=G722,PCMU,PCMA,GSM,G729"/> 
<X-PRE-PROCESS cmd="set" data="outbound_codec_prefs=PCMU,PCMA,GSM,G729"/>

然后需要重启 FreeSWITCH 以使设置生效。如果在学习过程中需要频繁做实验,不妨直接修改Profile 配置中的相关项,如在 internal.xml 中,将下面的行:

<param name="inbound-codec-prefs" value="$${global_codec_prefs}"/>
改为:
<param name="inbound-codec-prefs" value="PCMU,PCMA,G729,iLBC,H264,VP8"/>

然后只需在 FreeSWITCH 控制台上或 fs_cli 中执行如下命令使刚才的配置生效:

freeswitch> sofia profile internal rescan

以下命令可以列出当前与Profile相关的配置情况:freeswitch> sofia status profile internal

其中,CODECS IN、CODECS OUT行显示了呼入、呼出的编码设置。

媒体协商

世界是丰富多彩的,不同的 SIP 终端有不同的特性。支持不同的语音编码。所以不同的SIP终端进行通信时需要先与支持的编码进行“协商”,以便双方互相能够“理解”对方发来的媒体流中的数据。

来看一个最简单的编码协商过程。如果在 FreeSWITCH 中,按 F8 打开详细的 Log,打一个电话,就会在 Log 中看到如下类似的协商过程:

Audio Codec Compare [iLBC:97:8000:60:0]/[G722:9:8000:20:64000]
Audio Codec Compare [iLBC:97:8000:60:0]/[G729:18:8000:20:8000]
Audio Codec Compare [iLBC:97:8000:60:0]/[PCMU:0:8000:20:64000]
Audio Codec Compare [iLBC:97:8000:60:0]/[PCMA:8:8000:20:64000]
Audio Codec Compare [iLBC:97:8000:60:0]/[GSM:3:8000:20:13200]

在上面的例子中,SIP客户端呼入时使用 iLBC 编码,打包周期是 60ms,Payload Type是97;而服务器端提供G722、PCMU、PCMA、GSM等选择,但都是20ms的。所以我们在上述Log中可以看出FreeSWITCH会将客户端提供的编码与本地提供的编码逐一比较。由于客户端与服务器提供的编码没有交集,故上面的协商是不成功的,在这种情况下,FreeSWITCH会返回SIP 488消息,失败原因INCOMPATIBLE_DESTINATION(目的地不兼容)说明找不到兼容的编码。

SIP采用 Offer/Answer(请求/应答)机制来协商。请求发起的一方提供(Offer)自己支持的媒体编码列表,被请求的一方比较自己支持的媒体列表最终选择一种(或几种)编码以应答(Answer)方式通知请求者,然后它们就可以使用兼容的编码进行通信了。下面是一次成功的协商Log(客户端使用 PCMA,最终成功协商 PCMA):

Audio Codec Compare [PCMA:8:8000:20:64000]/[G722:9:8000:20:64000]
Audio Codec Compare [PCMA:8:8000:20:64000]/[PCMU:0:8000:20:64000]
Audio Codec Compare [PCMA:8:8000:20:64000]/[PCMA:8:8000:20:64000]
switch_core_media.c:3213 Audio Codec Compare [PCMA:8:8000:20:64000]
++++ is saved as a match
Set Codec sofia/internal/1006@192.168.7.6 PCMA/8000 20 ms 160 samples 64000

从上面的协商Log中可以看到,它也是将客户端的编码与服务器端的编码进行逐一比较,最后一行的“Set Codec…PCMA”表示本次协商成功并把该Channel的编码设为PCMA编码。

SDP及其在编码协商中的作用

为了更直观地理解SIP协议中的编码协商过程,下面我们来详细看一下相关的SIP信令。

SIP信令格式类似HTTP协议,由请求/应答行、消息头及消息体构成。一般的SIP INVITE消息体都包含SDP,来看一个INVITE请求(消息头部分做了删节),消息如下:

INVITE sip:3000@192.168.1.102 SIP/2.0
To: <sip:3000@192.168.1.102>
From: "1000"<sip:1000@192.168.1.102>;tag=53604628
Content-Type: application/sdp
Content-Length: 222

v=0
o=- 1364745810658806 1 IN IP4 192.168.1.102
s=Bria 3 release 3.5.0b stamp 69410
c=IN IP4 192.168.1.102
b=AS:2064
t=0 0
m=audio 59820 RTP/AVP 8 18 101
a=rtpmap:101 telephone-event/8000
a=fmtp:101 0-15
a=sendrecv

其中,媒体类型是由SDP消息(Session Description Protocol,会话描述协议)描述的,SDP消息的MIME类型是application/sdp,它是在SIP头中以 Content-Type 标志,并在SIP消息的正文(Body,又称消息体,消息头中的Content-Length指示Body的长度)中携带。以下我们将忽略SIP消息头部分,只讨论SDP消息。

SDP消息从v=0一行开始。“C=IN”一行表示媒体所在机器的IP地址。“m=audio”一行描述音频数据,其中59820就是RTP的端口号,该端口用于收、发RTP数据;RTP/AVP表示使用RTP承载的Audio/Video Profile;8和101表示音频编码的代号,它是由 IANA 负责分配和管理的,因而又称为IANA代码,在此8代表PCMA,18代表G729。a=sendrecv表示双向收发,其他的还有sendonly(只发)、recvonly(只收)及inactive(不发不收)等。

当FreeSWITCH收到后,即启动协商过程。如上一节的Log所示,服务器端提供PCMU和PCMA,因此当比较到PCMA时协商成功,FreeSWITCH将返回如下SIP消息:

SIP/2.0 200 OK
...
Content-Type: application/sdp
Content-Length: 249
v=0
o=FreeSWITCH 1364717298 1364717299 IN IP4 192.168.1.102
s=FreeSWITCH
c=IN IP4 192.168.1.102
t=0 0
m=audio 28512 RTP/AVP 8 101
a=rtpmap:8 PCMA/8000
a=rtpmap:101 telephone-event/8000
a=fmtp:101 0-16
a=silenceSupp:off - - - -
a=ptime:20

以上消息中的SDP说明FreeSWITCH这端的RTP端口号是28512,使用PCMA编码。至此,协商完毕,双方互相知道了对方的IP地址和端口号,就可以互发音频RTP包了。

注意,在本例中,FreeSWITCH回复的是200消息以此作为对Offer的Answer,当然具体回复的消息要视具体情况而定,如果在早期媒体的情况下则需要先回复,这时可能是183消息。我们第7章已经讲过,1xx消息是呼叫进展消息。当UA发出一个请求后,如果对方回复的是180,证明呼叫进展顺利,主叫这一端即自己产生回铃音给用户一个有效的反馈。如果对方回复的是183,则183是可以携带SDP的,从而可以在协商成功后把自己个性化的媒体也传

送过来,如彩铃或呼叫失败之前播放的录音通知等,这些媒体称为早期媒体(即Early Media,与正常的通话数据相比,它来得早了那么一点点)。对方在回送180或183时,自己的话机开始振铃,如果有人接听,则切断回铃音,并发送200 OK消息,一般来说交换机会在此时开始计费,因为正常的通话开始了。

另外,上面SDP消息中有一行“a=rtpmap:8 PCMA/8000”,它是对其上一行中音频代码列表中“8”的描述,在本例中,这一行不是必需的。但是IANA代码只规定了96以下的代码的一一对应关系,大于或等于96的称为动态编码,动态编码不能只用代码来区分,还必须同时使用其名称,因而,对于动态编码来讲,rtpmap行就是必需的。而UA在进行编码协商时,对于大于或等于96的编码,必须使用其名称来对比(如iLBC)。也许更糟糕的是,若双方约定了iLBC,一方的代码是96,另一方的代码是98,那么在RTP中,它们就必须使用自己的代码来发送,却需要以对方指定的代码来接收。当然,虽然代码不同,但双方都知道这是iLBC,所以编码和解码都没有问题。很遗憾的是,有一些UA没有充分理解这一点,因而在互联互通时可能会出问题。

协商时机与策略

FreeSWITCH的协商可分为早协商和晚协商。当呼叫到达一个SIP Profile时,即某端口收到INVITE请求而未到达路由阶段时,就先行协商的,称为早协商。而如果等到路由阶段,到达拨号计划时再进行协商的,称为晚协商(Later Negotiation)。通过使用晚协商,用户可以有更多的自由去确定协商策略。系统默认为晚协商,如果要使用早协商,可以在SIP Profile中设置以下参数:<param name="inbound-late-negotiation" value="false"/>

这里需要注意的是,如果启用了inbound-zrtp-passthru(默认),则隐含设置inbound-late-negotiation为true。

FreeSWITCH支持的协商策略有:

  • generous(普通的):优先考虑客户端的编码,默认的协商策略。假设客户端提供的编码列表为PCMA、G729,而FreeSWITCH支持的列表优先顺序为G729、PCMU、PCMA。那么当FreeSWITCH收到请求时将优先考虑客户端的感受,因而优先使用PCMA编码。
  • greedy(贪婪的):优先考虑服务器的编码。上面的例子中FreeSWITCH会优先考虑自己的编码,因而会选择G729。
  • scorooge(吝啬的):优先考虑服务器的编码,且强制使用服务器的采样率。上面示例中 FreeSWITCH 会更加强势,它除了具有greedy所有的特性外,还将强制使用自己的采样率。

上面提到的是UA跟FreeSWITCH进行单腿协商的情况。我们总是提到FreeSWITCH是一个B2BUA。在预计通话会有两条腿的情况下,启用晚协商会得到更好的效果。比如,在SIP Profile中设置 disable-transcoding为true时,FreeSWITCH 将首先呼叫 bleg,并仅 Offer aleg 提供的编码,从而可以避免转码,以节约CPU资源。

其他媒体相关的问题

FreeSWITCH有很强的媒体处理能力,也支持非常灵活的配置。

在完成SIP及SDP协商后,真正的语音数据是在RTP协议中传送的。即双方在前面的SDP协商中都已经获得了对方的IP地址、端口号,以及支持的媒体类型。接下来,就是将本地的媒体数据以指定的编码格式进行编码并通过RTP协议发送到对方去。

  • RTP 协议的全称是 Real-time Transport Protocol,即实时传输协议,它是由IETF的多媒体传输工作小组在RFC 3550 [2]中定义的。RTP协议详细说明了在互联网上传递音频和视频的标准数据包格式。它一开始被设计为一个多播协议,但后来被用在很多单播应用中。RTP协议是建立在UDP协议之上的,常用于流媒体系统、视频会议和一键通(Push to Talk)系统(配合H.323或SIP),它已成为IP电话产业的技术基础。
  • RTCP 是实时传输控制协议(Real-time Transport Control Protocol或RTP Control Protocol,RTCP)是RTP的一个姊妹协议,前者由RFC 3551定义。RTP使用一个偶数UDP端口,而RTCP则使用RTP的下一个相邻的奇数端口。RTCP除为RTP媒体流提供信道外(out-of-band)的控制。RTCP本身并不传输数据,但会和RTP一起协作将多媒体数据打包并发送出去。RTCP定期在多媒体会话参加者之间传输控制数据。RTCP的主要功能是为RTP提供的服务的质量(Quality of Service)提供反馈信息。RTCP收集相关媒体连接的统计信息,例如传输字节数、传输分组数、丢失分组数及拉动(jitter)、单向和双向网络延迟等等,网络应用程序即可利用RTCP的统计信息来控制传输的品质,比如当网络拥塞比较严重时,可以限制信息流量或改用压缩率较高的编解码器。

RFC 3550中规定的RTP封包格式如下:

一般来说,在没有任何扩展的情况下,RTP包头的长度为12字节。其中主要字段的含义简介如下:

  • version(V):1 bit。标明RTP版本号。RFC3550中规定的版本号为2。
  • padding(P):1 bit。如果该位被设置,则将在该packet末尾包含额外的附加信息,附加信息的最后一个字节表示额外附加信息的长度(包含该字节本身)。该字段之所以存在是因为一些加密机制需要固定长度的数据块,或者为了在一个底层协议数据单元中传输多个RTP packets。
  • extension(X):1 bit。如果该位为1,则在固定的头部后存在一个扩展头部,格式定义在RFC3550中。
  • CSRC count(CC):4 bit。在固定头部后存在多少个CSRC标记。
  • marker(M):1 bit。该位的功能依赖于RTP中实际传输媒体类型的Profile的定义。Profile可以改变该位的长度,但是要保持marker和payload type总长度不变(一共是8 bit)。一般来说,音频跟视频中该位的定义是不同的。在视频中,marker值为1通常表示一帧图像的结束 [3]。
  • payload type(PT):7 bit。标记着RTP packet所携带信息的类型,标准类型列在RFC3551中。如果接收方不能识别该类型,必须忽略该包。
  • sequence number:16 bit。序列号,每个RTP包发送后该序列号加1,接收方可以根据该序列号检测丢包或重新排列数据包顺序等。
  • timestamp:32 bit。时间戳。反映RTP包中所携带信息包中第一个字节的采样时间。
  • SSRC:32 bit。数据源标识。在一个RTP Session期间每个数据流都应该有一个不同的SSRC。如果视频源发生变化,则应该改变SSRC,以让接收端感知到该变化。
  • CSRC list:0到15项,每个源标识为32 bit。贡献数据源标识。只有存在多路混发的时候才有效。多路混发的情况,如一个将多声道的语音流合并到一个RTP流中发送,在这里就可以列出原来每个声道的SSRC。

转码 (a-lg、b-leg)

FreeSWITCH 是一个B2BUA,因而在桥接两条腿时,如果两条腿分别使用不同的编码,则需要经过一个转码过程分别转成对方需要的编码。

当需要转码时,FreeSWITCH会将收到的音频数据转成一种中间格式,称为 L16,即线性16位的编码。这种格式可以与其他各种编码进行转换。另外,即使呼叫的双方采用同样的编码,但如果有IVR或 录、放音 等中间环节时,也需要转码。有一些编码,如G729,由于专利原因,不能自由使用,因此FreeSWITCH默认不支持对它的转码。如果确实需要转码,则可以购买商业版的转码模块。

FreeSWITCH默认的配置不支持自动转码, 可以用默认的配置做一个实验:A呼叫B,A端只有PCMA,B端只支持 PCMU。编码设置:

而 FreeSWITCH 使用默认的编码,即 "G722,PCMU,PCMA,GSM"。

A端呼叫到达FreeSWITCH后,FreeSWITCH又向B发送INVITE,其中的SDP如下:

1 v=0
2 o=FreeSWITCH 1374385260 1374385261 IN IP4 192.168.7.8
3 s=FreeSWITCH
4 c=IN IP4 192.168.7.8
5 t=0 0
6 m=audio 21582 RTP/AVP 8 101 13
7 a=rtpmap:101 telephone-event/8000
8 a=fmtp:101 0-16
9 a=ptime:20

可以看出,FreeSWITCH提供的编码类型只有8(第6行,其中8代表PCMA,后面的101和13,分别代表 RFC2833和静音包支持,不是实际的音频编码),而由于B端仅支持PCMU,所以协商失败,对方返回488消息,表示没有找到兼容的媒体类型。测试中收到的488消息内容如下:

SIP/2.0 488 Not Acceptable Here
Via: SIP/2.0/UDP 192.168.7.8;rport=5060;branch=z9hG4bKH1t8Fv9Xr7XgK
To: <sip:1002@192.168.7.8:51570;rinstance=59ddd96a3228b9f6>;tag=95be460b
From: "Extension 1000"<sip:1000@192.168.7.8>;tag=QQaKDZ709NHtF
Call-ID: 36dd7df4-6c9d-1231-82ad-cb44f52397c6
CSeq: 46867357 INVITE
User-Agent: Bria 3 release 3.5.0b stamp 69410
Warning: 305 devnull "SDP: Incompatible media format: no common codec"
Content-Length: 0

其中的 Warning 是一个可选的消息头,SIP终端(这里是Bria)带了这个消息头便于我们查找问题原因。最后,经过实验发现,如果要使协商成功,只需要在 internal Profile 里将下面一行注释掉:<param name="inbound-zrtp-passthru" value="true"/>  需要将该Profile重启使之生效,重启命令如下:freeswitch> sofia profile internal restart 

读者也可以看一下重启前后该配置项的区别(省略其他输出,中文注释为笔者所加):

尝试再打一个电话,可以看到,这次FreeSWITCH向B提供的编码列表就多了。下面是FreeSWITCH发给B的INVITE消息中的SDP部分:

其中,“8 9 0 3”分别代表PCMA、G722、PCMU、GSM,由于A提供的编码是PCMA,而FreeSWITCH提供的列表是G722、PCMU、PCMA、GSM,基于FreeSWITCH默认的协商策略(generous),FreeSWITCH在向B发送SDP的时候,尊重客户端A的选择,因此将PCMA排在了最前面,最后的列表成了PCMA、G722、PCMU、GSM。这时候B收到INVITE请求后,由于它仅支持PCMU,因此它在后续的200消息中告诉FreeSWITCH,FreeSWITCH将电话接通后,A便向FreeSWITCH发送PCMA编码的RTP媒体流,FreeSWITCH在收到后会自动转换为PCMU格式的媒体流并发送给B。同理,B向FreeSWITCH发送PCMU,FreeSWITCH转成PCMA再发给A。也就是说,FreeSWITCH可以在A与B端编码不同的情况下进行自动编码转换,简称转码。

通过该例子应能了解到,FreeSWITCH使用该协商策略的好处是,由于FreeSWITCH做SDP Offer的时候把PCMA放到了最前面,如果B同时也支持PCMA,而且也采取类似FreeSWITCH的协商策略,选用Offer列表中第一个编码,那么最终协商的结果还是PCMA,最终还是不用转码,从而有效节省了服务器的资源。

在实际应用中,可以使用late-negotiatioin、disable-transcoding等方法来尽量避免发生转码,以节省系统资源。也可以更进一步设置Bypass Media,让媒体流不经过FreeSWITCH而直接在两个SIP客户端间点对点传输,则可以更进一步节约FreeSWITCH的资源。当然,在NAT环境下,SIP客户端间的点对点传输可能不可用,那么这时就不能设置Bypass Media了。

透传、媒体绕过、媒体代理

一般情况下,RTP 媒体流也是经过 FreeSWITCH 转发的(图中的虚线部分)。在G729不支持转码的情况下,只要通话的双方都支持G729,FreeSWITCH仍然是可以建立通话的,其中一种方式就是透传(Passthru)。透传是指在不经过转码的情况下,将从一方收到的媒体流原样转给另一方。这种情况下,仍然可以建立通话,但在需要媒体处理的情况下,如在 IVR、录音时,就会有些限制。

另一种极端的情况是采用媒体绕过(Bypass Media)技术,即真正的媒体流使用点到点传输,根本不经过FreeSWITCH。即使是 FreeSWITCH 从来没听说过的编码,只要通话的双方都支持,FreeSWITCH 也能正常建立通话,而 RTP 则通过点对点直接传输,如图中粗实线部分的RTP(2)路径。当然,使用媒体绕过与透传一样,要求通话的双方必须支持同样的媒体编码。

还有一种情况称为媒体代理(Proxy Media),即不管 FreeSWITCH 是否支持对该种编码转码,它都对 RTP 数据在不进行任何处理的情况下发给另一方,注意这里它跟透传的区别是,它只改变SDP中的“c=”部分,而不对 RTP 进行任何改变,如仍然保留其时间戳等数据。

Media 监听

为了解决监听和录音问题,FreeSWITCH在媒体流路径上放了一个 Media Bug (这里的bug不是指软件缺陷,而是 监听(bugging) 的意思),它的作用相当于我们水管上的一个三通,如图所示,媒体流不仅在A(Alice)和B(Bob)之间交换,还通过一个三通连接到C(Carl)。

技术上讲,C可能是另一个SIP Channel,在做监听。Media Bug 的媒体流也是双向的,因而C也可以发送音频数据形成三方通话。FreeSWITCH 提供 eavesdrop 和 three_way 两个App来分别完成该功能。实际上,Media Bug 的用处还有很多,如音信号检测(Tone Detect)就是使用 Media Bug 来检测是否有特殊音频的,如传真音或 inband DTMF 信号等。当然,录音也是通过这种机制来实现的(这时候C就相当于一个录音机)。

视频

FreeSWITCH 也支持视频呼叫和会议。目前,所有编码的视频流都是透传的,

一个视频SDP的例子。在如下的SDP中,可以看到除m=audio外,还有一个m=video,它表示这是一个视频流,IANA编码123(注意,它属于大于等于96的动态编码)及后面的rtpmap属性说明该视频流是H264编码的。

v=0
o=FreeSWITCH 1364797393 1364797394 IN IP4 192.168.1.102
s=FreeSWITCH
c=IN IP4 192.168.1.102
t=0 0
m=audio 24786 RTP/AVP 8 101
a=rtpmap:8 PCMA/8000
a=rtpmap:101 telephone-event/8000
a=fmtp:101 0-16
a=silenceSupp:off - - - -
a=ptime:20
a=sendrecv
m=video 22252 RTP/AVP 123
a=rtpmap:123 H264/90000

视频编码的协商过程与音频类似,不同的是,由于视频不支持转码,因而必须保证两端都使用相同的视频编码才可以互相看到视频。

排错

在实际应用中,媒体的相关的问题可能会较多,各种客户端的实现也五花八门,所以具体的查错和分析方法需要根据当时的情况具体问题具体分析,有一定经验可以帮助你更快地解决问题。这里给出笔者的经验:遇到媒体协商的问题,打个电话并在Log中查找与“ Audio Codec Compare”相关的行,一般能比较快地定位到问题。

另外,如果在日志中查找不到原因,也可以使用外部的抓包工具(如tcpdump或Wireshark)来抓包分析。关于如何使用这些工具排查错误我们将在后面讲到。这里值得一提的是,FreeSWITCH也提供了一个实用工具—— uuid_debug_media,可以比较方便地查看是否有媒体流的收发,它是一个API命令,使用格式如下:uuid_debug_media <uuid><read|write|both><on|off>

如以下命令可以在终端上打印当前收到的RTP媒体流的信息:uuid_debug_media f87f1d62-ec54-44bd-ab37-cf2fb5b6a5a7 read on

另外,在read/write/both前面加上v就可以打印视频媒体流的信息,如:uuid_debug_media f87f1d62-ec54-44bd-ab37-cf2fb5b6a5a7 vread on

SIP 模块

前面学习了SIP协议的基础知识,并了解到SIP协议是现代VoIP通信的一个主要协议。事实上,它也是FreeSWITCH支持的一个核心协议。FreeSWITCH中大部分的功能和应用都是基于SIP的。现在来看一下在 FreeSWITCH 中是怎么实现和使用SIP的,以及如何来配置和使用它。

一些与SIP相关的基本概念。

  • Sofia-SIP:FreeSWITCH的SIP (Sofia-SIP是由诺基亚公司开发的SIP协议栈,它以开源的许可证LGPL发布) 功能是在mod_sofia模块中实现的。FreeSWITCH并没有开发新的SIP协议栈,而是使用了比较成熟的开源SIP协议栈Sofia-SIP,以避免“重复发明轮子” 。
  • Endpoint:在FreeSWITCH中,实现一些互联协议接口的模块称为Endpoint。Free-SWITH支持很多类型的Endpoint,如SIP (有的人可能会问,那么实现SIP的模块为什么不支持mod_sip呢?这是由于FreeSWITCH的Endpoint是一个抽象的概念,你可以用任意技术来实现。实际上mod_sofia只是对Sofia-SIP库的一个黏合和封装。除Sofia-SIP外,还有很多开源的SIP协议栈,如pjsip、osip等。最初选型的时候,FreeSWITCH的开发团队也对比过许多不同的SIP协议栈,最终选用了Sofia-SIP。FreeSWITCH是一个高度模块化的结构,如果你喜欢其他协议栈,可以自己实现如mod_pjsip或mod_osip等,它们是互不影响的。这也正是FreeSWITCH架构设计的精巧之处。)、H232等。这些不同的Endpoint主要是使用不同的控制协议跟其他的Endpoint通话。所以说,Endpoint一般是跟通话相关的。
  • mod_sofia:mod_sofia 实现了SIP中的注册服务器、重定向服务器、媒体服务器、呈现服务器、SBC等各种功能。它的定位是一个B2BUA,它不能实现SIP代理服务器 (实现SIP代理服务器的开源软件有OpenSIPS、Kamailio等。它们可以很好的与FreeSWITCH配合工作
    ) 的功能。
  • SIP Profile:在mod_sofia中,有一个概念是SIPProfile,它相当于一个SIPUA,通过各种不同的配置参数可以配置一个UA的行为。一个系统中可以有多个SIP Profile,每个SIP Profile都可以监听不同的IP地址和端口对。
  • Gateway:一个SIP Profile中有多个Gateway,Gateway可以直译为网关,它主要用于定义一个远端的SIP服务器,使FreeSWITCH可以与其他服务器通信。FreeSWITCH可以作为一个SIP客户端(UAC)向远端的网关进行注册;当然也可以不注册,而是使用与远端服务器对等的方式(俗称SIPTrunk,即SIP中继)相互通信(我们将在第14章讲到FreeSWITCH与其他系统相连的各种拓扑结构)。
  • 本地SIP用户:FreeSWITCH可以作为注册服务器,这时候,其他的SIP客户端就可以向它注册。FreeSWITCH将通过用户目录(Directory)中的配置信息对注册用户进行鉴权。这些SIP客户端所代表的用户就称为本地SIP用户,简称本地用户。
  • 来话和去话:牢记FreeSWITCH是一个B2BUA。如果Alice通过FreeSWITCH给Bob打电话,Alice首先向FreeSWITCH发起呼叫,对FreeSWITCH而言,这路通话就称为来话(InboundCall);然后FreeSWITCH再去呼叫B,这路通话称为去话(OubtoundCall)。如果来、去话都是本地的,又称为本地来话和本地去话。
  • 中继来话或中继去话:如果来、去话不是本地的,双方通话需要以中继方式进行,就称为中继来话或中继去话。但是,中继的叫法只是沿用传统的PSTN网络中的概念,在SIP术语中,本来是没有中继的概念的。

了解了这些基本概念,下面我们来看一下它们在 mod_sofia 的配置文件中是怎么实现的。

Sofia 配置文件

Sofia 的配置文件是 conf/autoload_configs/sofia.conf.xml 一般不需要直接修改它,因为该文件仅有少数的配置参数,大部分的配置参数实际上是直接在上述配置文件中使用下面的预处理指令装入conf/sip_profiles/目录中的XML文件中的配置:<X-PRE-PROCESS cmd="include" data="../sip_profiles/*.xml"/> Sofia 的配置文件的总体结构是这样的:

其中,global_settings 中存放了一些全局配置参数。下面的 profiles 标签中又包含多个 profile 标签。每个 profile 标签的详细信息都是在 conf/sip_profiles/ 下的配置文件中配置的。

Sofia 支持多个 Profile,而每个 Profile 相当于一个 SIP UA,在启动后它会监听一个 “IP地址:端口”对。从物理上来讲 freeswitch 只是一个UA,但由于它同时支持多个Session,在逻辑上就是相当于两个UA,简单来讲,一个 "IP地址:端口" 唯一标志一个UA。

FreeSWITCH 默认的配置带了三个Profile(也就是三个UA),这里不讨论 IPv6,仅讨论 internal 和 external 两个(它们分别是在 internal.xml 和 external.xml 中定义的)。其实 internal 和 external 最大的区别就是:

  • internal.xml 运行在 5060 端口。internal 翻译为 "内部的",用于配置 FreeSWITCH 内部通信和呼叫路由。它包含了内部 SIP 端口、SIP 用户、呼叫路由等设置。
  • external.xml 运行在 5080 端口。external 翻译为 "外部的",用于配置 FreeSWITCH 外部呼叫路由,例如 PSTN、VoIP 网关等。它包含了外部 SIP 端口、外部呼叫路由等设置。

在 FreeSWITCH 中,internal.xmlexternal.xml 是两个重要的配置文件。它们分别用于配置 FreeSWITCH 内部通信和外部呼叫路由。

internal.xml

internal.xml 中定义了一个Profile(名字就是internal),它里面有大量的配置参数。这些参数往往与具体的部署方式和呼叫流程有关。到现在为止,还没有接触到太多与呼叫流程有关的内容。但是若先讲流程的话,对这里的参数又不熟悉,也不便理解。因此,这就变成了一个是先有鸡还是先有蛋的问题。综合考虑,先把一些重要的配置参数都大体讲一遍,到后面的实战部分在讲具体。

在熟悉了Sofia的配置文件结构以后,再来看一下 Profile 的配置文件。由于Sofia Profile 的参数比较多,因此默认把不同的 Profile 配置存放在单独的文件中。

以 internal.xml 为例:

<profile name="internal">
	<aliases></aliases>
	<gateways></gateways>
	<domains>
		<domain name="all" alias="true" parse="false"/>
	</domains>
	<settings>
		<param name="debug" value="0"/>
		<param name="sip-trace" value="no"/>
		<param name="sip-capture" value="no"/>
		<param name="watchdog-enabled" value="no"/>
		<param name="watchdog-step-timeout" value="30000"/>
		<param name="watchdog-event-timeout" value="30000"/>
		<param name="log-auth-failures" value="false"/>
		<param name="forward-unsolicited-mwi-notify" value="false"/>
		<param name="context" value="public"/>
		<param name="rfc2833-pt" value="101"/>
		<!-- port to bind to for sip traffic -->
		<param name="sip-port" value="$${internal_sip_port}"/>
		<param name="dialplan" value="XML"/>
		<param name="dtmf-duration" value="2000"/>
		<param name="inbound-codec-prefs" value="$${global_codec_prefs}"/>
		<param name="outbound-codec-prefs" value="$${global_codec_prefs}"/>
		<param name="rtp-timer-name" value="soft"/>
		<!-- ip address to use for rtp, DO NOT USE HOSTNAMES ONLY IP ADDRESSES -->
		<param name="rtp-ip" value="$${local_ip_v4}"/>
		<!-- ip address to bind to, DO NOT USE HOSTNAMES ONLY IP ADDRESSES -->
		<param name="sip-ip" value="$${local_ip_v4}"/>
		<param name="hold-music" value="$${hold_music}"/>
		<param name="apply-nat-acl" value="nat.auto"/>
		<param name="apply-inbound-acl" value="domains"/>
		<param name="local-network-acl" value="localnet.auto"/>
		<param name="record-path" value="$${recordings_dir}"/>
		<param name="record-template" value="${caller_id_number}.${target_domain}.${strftime(%Y-%m-%d-%H-%M-%S)}.wav"/>
		<param name="manage-presence" value="true"/>
		<param name="presence-hosts" value="$${domain},$${local_ip_v4}"/>
		<param name="presence-privacy" value="$${presence_privacy}"/>
		<param name="inbound-codec-negotiation" value="generous"/>
		<param name="tls" value="$${internal_ssl_enable}"/>
		<param name="tls-only" value="false"/>
		<param name="tls-bind-params" value="transport=tls"/>
		<param name="tls-sip-port" value="$${internal_tls_port}"/>
		<param name="tls-passphrase" value=""/>
		<param name="tls-verify-date" value="true"/>
		<param name="tls-verify-policy" value="none"/>
		<param name="tls-verify-depth" value="2"/>
		<param name="tls-verify-in-subjects" value=""/>
		<param name="tls-version" value="$${sip_tls_version}"/>
		<param name="tls-ciphers" value="$${sip_tls_ciphers}"/>
		<param name="inbound-late-negotiation" value="true"/>
		<param name="nonce-ttl" value="60"/>
		<param name="auth-calls" value="$${internal_auth_calls}"/>
		<param name="auth-subscriptions" value="true"/>
		<param name="inbound-reg-force-matching-username" value="true"/>
		<param name="auth-all-packets" value="false"/>
		<param name="ext-rtp-ip" value="$${external_rtp_ip}"/>
		<param name="ext-sip-ip" value="$${external_sip_ip}"/>
		<param name="rtp-timeout-sec" value="300"/>
		<param name="rtp-hold-timeout-sec" value="1800"/>
		<param name="force-register-domain" value="$${domain}"/>
		<param name="force-subscription-domain" value="$${domain}"/>
		<param name="force-register-db-domain" value="$${domain}"/>
		<param name="ws-binding"  value=":5066"/>
		<param name="wss-binding" value=":7443"/>
		<param name="challenge-realm" value="auto_from"/>
	</settings>
</profile>

<profile name="internal"> 该行定义了一个Profile,它的名字就叫 internal,这个名字本身并没有特殊的意义,也不需要与文件名相同,你可以改成任意你喜欢的名字,但是必须记住它,因为很多地方要使用这个名字。也可以为 Profile 起个别名

<aliases>
    <alias name="default"/>
</aliases>

如果有了这个别名,就可以在呼叫字符串中使用这个别名。比如,如果配置文件中有上述配置,就可以使用类似这样的呼叫字符串sofia/default/13912345678@192.168.1.12去呼叫相应的SIP地址。关于呼叫字符串的知识后面会讲到。

下面继续看配置文件。在一个Profile中,可以配置多个网关(Gateway)。网关是在<gateways></gateways>标签中定义的。默认的配置中只是装入了相关目录下的网关配置文件。配置如下:

<gateways>
	<X-PRE-PROCESS cmd="include" data="internal/*.xml"/>
</gateways>

上面的配置代码定义了一些网关。既然 Profile 是一个UA,它就应该可以注册到别的SIP服务器上去,它要注册的远端SIP服务器对于该Profile来说,就称为网关,而这里的网关配置就是对远端SIP服务器的一些描述,即网关的配置参数由远端的SIP服务器来决定。一般不在 internal 这个 Profile 上使用 Gateway,大多数在 external 上使用。

接下来,便定义该Profile所属的域(Domain)。Domain可以是一个IP地址或一个DNS域名。需要注意,直接在操作系统hosts文件中设置的IP到域名的映射可能不好用,如果使用域名,大多数SIP UA都需要有一个真正的DNS服务器。Domain的配置代码如下:

<domains>
	<!--<domain name="$${domain}" parse="true"/>-->
	<domain name="all" alias="true" parse="false"/>
</domains>

一说到Domain,可能大家都会想到域名。不过在这里,Domain可以是任意的字符串,可以简单把它理解为一个域。FreeSWITCH支持多Domain,也就是逻辑上可以把一些资源(如用户)分到多个“域”中。

在默认的配置文件中,vars.xml 中定义了一个名为domain的全局变量,它的默认值来自于local_ip_v4 这个全局变量,也就是说,它是一个IP地址。当然,话又说回来,domain 就是一个字符串,它可以是IP地址,也可以是其他值,只是默认的配置中是一个IP地址。该全局变量在很多地方可以引用,如上面配置例子中的第2行(该行默认是注释掉的,因而不生效)。

上面配置中第3行的含义是:在该Profile中定义一个Domain,name="all" 表示检查看所有的用户目录中定义的 Domain(默认是在conf/directory目录下定义的Domain,如果name等于一个特定的Domain,则它仅会检查指定的 Domain),然后执行下列动作:

如果 alias="true",则会为所有的 Domain 取一个别名,放到与Profile同等重要的位置,以便后续在构造或查询呼叫字符串时能找到正确的用户。执行 sofia status命令

其中,Type为alias表示是一个别名。

如果 parse="true"(注意默认的配置中此处为false),则FreeSWITCH会解析该Profile中所有的网关。

注意上面的 alias和parse属性都具有排它性,即在有多个Profile的情况下,它们的值只能有一个为true,否则会产生冲突。

几个重要参数

Sofia 的 Profile 有很多可配置参数,它们可以影响某一 Profile 所代表的 SIP UA 的行为。也就是说,对于来话而言,所有到达某一 Profile(如internal.xml)的呼叫都会进行统一的处理;对于去话而言也类似,所有从某一Profile出去的通话也都有类似的行为(如 auth-calls 参数)。下面看下Profile 中的一些主要参数,这些参数大部分可以在 internal.xml 找到相应的例子。

inbound-bypass-media 用于设置入局呼叫是否启用“媒体绕过(Bypass Media)”模式,它的取值有“true”和“false”两种。如:<param name="inbound-bypass-media" value="true"/>

什么叫Bypass Media?图示 Bob和Alice通过FreeSWITCH使用SIP接通了电话,他们谈话的语音(或视频)数据要通过 RTP 包传送。RTP可以像SIP一样经过FreeSWITCH转发。但是,RTP与SIP相比占用很大的带宽,如果FreeSWITCH不需要“偷听(或录音)”他们谈话,为了节省带宽,完全可以让RTP直接在两者之间点对点传送,这种情况对 FreeSWITCH 来讲就是没有媒体的,在FreeSWITCH 中就称为 Bypass Media(媒体绕过)。

所以,如果将 inbound-bypass-media 的值设为“true”,那么对于任何来话(如Alice打给Bob,电话要先进入FreeSWITCH,对FreeSWITCH来说该电话就称为来话)都会自动采用Bypass Media模式。

与 Bypass Media 相关的参数。

  • media-option:媒体选项,它有两个可能的取值:resume-media-on-hold、bypass-media-after-att-xfer。前者的意思是,如果FreeSWITCH是没有媒体(No Media/Bypass Media)的,那么若设置了该参数,当你在话机上按下Hold键时,FreeSWITCH将会回到有媒体的状态;后者是跟Attended Transfer有关的,Attended Transfe即出席转移,又称协商转移,它需要媒体配合才能完成工作。但如果在执行转接之前没有媒体(处于Bypass Media状态),则该参数能让转接执行时通过re-INVITE请求要回媒体,等到转接结束后再回到Bypass Media的状态。我们将在15.1.1节详细讲解Attended Transfer。
  • context:设置来话将进入Dialplan中的哪个Context进行路由。<param name="context" value="public"/> 需要指出的是,如果用户注册到该Profile上(或是经过认证的用户,即本地用户),则用户目录(directory)中设置的user_contex优先级要比这里设置的高。
  • dialplan:设置默认的dialplan类型,即在该Profile上有电话呼入后到哪个Dialplan中进行路由。<param name="dialplan" value="XML"/>
  • inbound-codec-prefs:设置支持的来话媒体编码,用于编码协商。默认值引用了 vars.xml 中的一个 global_codec_prefs 变量,该变量的默认值是“G722,PCMU,PCMA,GSM”。<param name="inbound-codec-prefs" value="$${global_codec_prefs}"/>
  • outbound-codec-prefs:设置去话语音编码。<param name="outbound-codec-prefs" value="$${global_codec_prefs}"/>
  • rtp-ip:设置RTP的IP地址。<param name="rtp-ip" value="$${local_ip_v4}"/>
  • sip-ip:设置SIP的IP地址。<param name="sip-ip" value="$${local_ip_v4}"/>
  • sip-port:该Profile启动后监听的SIP端口号。默认的配置中引用了var.xml中定义的一个变量internal_sip_port,默认是5060。<param name="sip-port" value="$${internal_sip_port}"/>
  • auth-calls:设置是否对来电进行鉴权。默认是需要鉴权,即所有从该 Profile 进来的 INVITE请求都需要经过 Digest 验证。<param name="auth-calls" value="$${internal_auth_calls}"/>
  • ext-rtp-ip和ext-sip-ip:用于设置NAT环境中公网的RTP IP和SIP IP。该设置会影响SDP中的IP地址。<param name="ext-rtp-ip" value="auto-nat"/>
    <param name="ext-sip-ip" value="auto-nat"/>
    其中,value的取值有以下几种可能:
            一个IP地址,如12.34.56.78;
            一个STUN服务器,它会使用STUN协议获得公网IP,如stun:stun.server.com;
            一个DNS名称,如host:host.server.com;
            auto,它会自动检测IP地址;
            auto-nat,如果路由器支持NAT-PMP或uPnP,则可以使用这些协议获取公网IP。
    这些设置的值可以使用sofia status命令显示:sofia status profile internal

external.xml

external.xml 是另一个UA配置文件,它定义了另一个名为 "external" 的 UA,默认使用5080端口。

<profile name="external">
  <!-- http://wiki.freeswitch.org/wiki/Sofia_Configuration_Files -->
  <!-- This profile is only for outbound registrations to providers -->
  <gateways>
    <X-PRE-PROCESS cmd="include" data="external/*.xml"/>
  </gateways>

  <aliases>
    <!--
        <alias name="outbound"/>
        <alias name="nat"/>
    -->
  </aliases>

  <domains>
    <domain name="all" alias="false" parse="true"/>
  </domains>

  <settings>
    <param name="debug" value="0"/>
    <!-- If you want FreeSWITCH to shutdown if this profile fails to load, uncomment the next line. -->
    <!-- <param name="shutdown-on-fail" value="true"/> -->
    <param name="sip-trace" value="no"/>
    <param name="sip-capture" value="no"/>
    <param name="rfc2833-pt" value="101"/>
    <!-- RFC 5626 : Send reg-id and sip.instance -->
    <!--<param name="enable-rfc-5626" value="true"/> -->
    <param name="sip-port" value="$${external_sip_port}"/>
    <param name="dialplan" value="XML"/>
    <param name="context" value="public"/>
    <param name="dtmf-duration" value="2000"/>
    <param name="inbound-codec-prefs" value="$${global_codec_prefs}"/>
    <param name="outbound-codec-prefs" value="$${outbound_codec_prefs}"/>
    <param name="hold-music" value="$${hold_music}"/>
    <param name="rtp-timer-name" value="soft"/>
    <!--<param name="enable-100rel" value="true"/>-->
    <!--<param name="disable-srv503" value="true"/>-->
    <!-- This could be set to "passive" -->
    <param name="local-network-acl" value="localnet.auto"/>
    <param name="manage-presence" value="false"/>

    <!-- used to share presence info across sofia profiles
         manage-presence needs to be set to passive on this profile
         if you want it to behave as if it were the internal profile
         for presence.
    -->
    <!-- Name of the db to use for this profile -->
    <!--<param name="dbname" value="share_presence"/>-->
    <!--<param name="presence-hosts" value="$${domain}"/>-->
    <!--<param name="force-register-domain" value="$${domain}"/>-->
    <!--all inbound reg will stored in the db using this domain -->
    <!--<param name="force-register-db-domain" value="$${domain}"/>-->
    <!-- ************************************************* -->

    <!--<param name="aggressive-nat-detection" value="true"/>-->
    <param name="inbound-codec-negotiation" value="generous"/>
    <param name="nonce-ttl" value="60"/>
    <param name="auth-calls" value="false"/>
    <param name="inbound-late-negotiation" value="true"/>
    <!--
        DO NOT USE HOSTNAMES, ONLY IP ADDRESSES IN THESE SETTINGS!
    -->
    <param name="rtp-ip" value="$${local_ip_v4}"/>
    <param name="sip-ip" value="$${local_ip_v4}"/>
    <param name="ext-rtp-ip" value="$${external_rtp_ip}"/>
    <param name="ext-sip-ip" value="$${external_sip_ip}"/>
    <param name="rtp-timeout-sec" value="300"/>
    <param name="rtp-hold-timeout-sec" value="1800"/>
    <!--<param name="enable-3pcc" value="true"/>-->

    <!-- TLS: disabled by default, set to "true" to enable -->
    <param name="tls" value="$${external_ssl_enable}"/>
    <!-- Set to true to not bind on the normal sip-port but only on the TLS port -->
    <param name="tls-only" value="false"/>
    <!-- additional bind parameters for TLS -->
    <param name="tls-bind-params" value="transport=tls"/>
    <!-- Port to listen on for TLS requests. (5081 will be used if unspecified) -->
    <param name="tls-sip-port" value="$${external_tls_port}"/>
    <!-- Location of the agent.pem and cafile.pem ssl certificates (needed for TLS server) -->
    <!--<param name="tls-cert-dir" value=""/>-->
    <!-- Optionally set the passphrase password used by openSSL to encrypt/decrypt TLS private key files -->
    <param name="tls-passphrase" value=""/>
    <!-- Verify the date on TLS certificates -->
    <param name="tls-verify-date" value="true"/>
    <!-- TLS verify policy, when registering/inviting gateways with other servers (outbound) or handling inbound registration/invite requests how should we verify their certificate -->
    <!-- set to 'in' to only verify incoming connections, 'out' to only verify outgoing connections, 'all' to verify all connections, also 'subjects_in', 'subjects_out' and 'subjects_all' for subject validation. Multiple policies can be split with a '|' pipe -->
    <param name="tls-verify-policy" value="none"/>
    <!-- Certificate max verify depth to use for validating peer TLS certificates when the verify policy is not none -->
    <param name="tls-verify-depth" value="2"/>
    <!-- If the tls-verify-policy is set to subjects_all or subjects_in this sets which subjects are allowed, multiple subjects can be split with a '|' pipe -->
    <param name="tls-verify-in-subjects" value=""/>
    <!-- TLS version ("sslv23" (default), "tlsv1"). NOTE: Phones may not work with TLSv1 -->
    <param name="tls-version" value="$${sip_tls_version}"/>
    <!-- TLS ciphers default: ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH -->
    <param name="tls-ciphers" value="$${sip_tls_ciphers}"/>
  </settings>
</profile>

从 external.xml 中可以看到,其中的大部分参数都与 internal.xml 中相同。最大的不同是 auth-calls 参数。 在 internal.xml 中,auth-calls 默认值是 true;而在 external.xml 中,默认值是 false。也就是说,客户端发往 FreeSWITCH 的5060端口的SIP消息需要鉴权(一般只对REGISTER和INVITE消息进行鉴权),而发往 5080 的消息则不需要鉴权。一般把本地用户都注册到 5060 上,所以它们打电话时要经过鉴权,保证只有授权(在我们用户目录中配置的)用户才能注册和拨打电话。而5080 不同,任何人均可以向该端口发送 SIP INVITE 请求。

如图所示,本地用户1000注册到5060端口上,每次它向外打电话时(呼叫本地用户1001时也一样),它向FreeSWITCH的5060端口发送INVITE请求,FreeSWITCH对其鉴权,验证通过后(可以是IP地址验证或Digest验证),才允许通话继续进行─如果呼叫1001,则FreeSWITCH继续向用户1001发送INVITE请求;如果呼叫外部电话,则通过端口5080向外部网关发送INVITE请求。

例子中,FreeSWITCH也可以通过5060端口向外部网关发送INVITE请求,效果是一样的。也就是说对于去话(Outbount Call,出局通话)而言,两者在这里没有区别。但对于来话(Inbound Call,入局通话)就不同了。为了让 FreeSWITCH 能接收来话,一般有两种方式:

  • FreeSWITCH作为一个UAC(这里可以理解为软电话,如X-Lite)从本地的5080端口通过SIP注册到外部网关上,这样外部网关就可以通过SIP消息中的Contact字段知道该 UAC 所在的 IP地址和监听的端口(这里是5080)。当有电话进来时,外部网关就向 FreeSWITCH 所在IP的5080端口发送INVITE请求。
  • 某些网关是把来话和去话分开的,一般不需要或者根本不允许注册。那它们怎么知道FreeSWITCH的IP和端口呢?在这种情况下,它们一般是在申请的时候事先配置好的,就比方你去装电话时,告诉电信公司你家的门牌号一样,所不同的是这里你告诉人家的是你FreeSWITCH 服务器的IP地址和端口号(在这里是5080)。如果有来话,外部网关就按照事先配置好的地址发送INVITE请求。

不管是使用以上哪种方式,当外部网关的 INVITE 请求到达 FreeSWITCH 时,我们不能要求对这些消息进行鉴权,因为外部网关不是FreeSWITCH的本地用户,它们不知道该如何给FreeSWITCH提供鉴权信息。所以我们在external这个Profile中把auth-calls参数设为false,对来话不鉴权。

读到这里,大概就明白多了。如果你使用5060端口向外部网关进行注册的话,那么外部网关的来话请求将被发送到FreeSWITCH的5060端口上,由于FreeSWITCH会对发送到该端口的所有来话进行鉴权,很显然外部的网关不知道怎么才能通过FreeSWITCH的验证,所以最终FreeSWITCH会拒绝所有来话。当然可能你还是有疑问,既然5080端口允许所有来话,那么怎么保证安全呢?这个在后面讲安全的时候介绍。

Gateway

FreeSWITCH 需要通过外部网关向外打电话,而这个外部网关就称为 Gateway。在 external.xml中可以看到它使用预处理指令将 external 目录下的所有的 XML 配置文件都装入到了该 Profile 的gateways 标签中:

<gateways>
    <X-PRE-PROCESS cmd="include" data="external/*.xml"/>
</gateways>

这样做的好处是可以把每个网关配置写到不同的文件中。默认的配置中包含了一个example.xml

<include> extension
    <gateway name="asterlink.com">
        <param name="username" value="cluecon"/>      用户名 *必需*
        <<param name="realm" value="asterlink.com"/>  认证 realm(域): *可选*。 如果为空,则使用 gateway name
        <param name="from-user" value="cluecon"/>     设置SIP消息中From字段的值,如果省略,则默认与username相同。
        <param name="from-domain" value="asterlink.com"/>  设置From字段中的domain值,默认与realm相同。
        <param name="password" value="2007"/>        密码 *必需*
        <param name="extension" value="cluecon"/>    设置来话中的分机号,即被叫号码,默认与username相同。
        <param name="proxy" value="asterlink.com"/>  如果需要代理服务器,则设置该proxy的值,默认与realm相同。
        <param name="register-proxy" value="mysbc.com"/>  如果需要注册到代理服务器,则设置该register-proxy的值,默认与realm同。
        <param name="expire-seconds" value="60"/>  设置注册时SIP消息中Expires字段的值,默认为3600秒。
        
        如果网关不需要注册,则设为false,默认为true。
        有些网关必须注册了才能打电话;而有的则不需要。
        另外,注册到网关上还允许从网关设备呼入我们的FreeSWITCH。
        <param name="register" value="false"/>
        
        <param name="register-transport" value="udp"/>  设置SIP消息使用udp还是tcp来承载。
        <param name="retry-seconds" value="30"/>        设置如果注册失败或超时,则多少秒后再重新注册。
        
        将主叫号码(要发给对方的)放到SIP的From字段中。
        默认会放到Remote-Party-ID字段中(有些终端从From字段中获取主叫号码)。
        <param name="caller-id-in-from" value="false"/> 
        
        <param name="contact-params" value=""/>   设置在SIP协议中Contact字段中额外的参数。
        <param name="extension-in-contact" value="true"/>  把分机号放入到Contact中
        <param name="ping" value="25"/>  每x秒发送一个ping探测,失败将注销,或标记并关闭注册
        <param name="cid-type" value="rpid"/>
        <param name="rfc-5626" value="true"/>
        <param name="reg-id" value="1"/>  在 contact 添加额外的sip params
    </gateway>
</include>

example.xml 的默认参数都是注释掉的,不起作用这里为了说明,注释全部都取消了,并在后面加上对应解释。实际使用时,可以根据自己情况,只选择自己需要的取消注释并进行设置

里面有很多配置选项。先从最简单的配置讲起。添加一个新网关只需要在 external 目录中新建一个 XML 文件,名字可以随便起,但需要以.xml结尾,如:gw1.xml

<gateway name="gw1">
    <param name="realm" value="SIP"/>
    <param name="username" value="SIP"/>
    <param name="password" value=""/>
</gateway>

每个网关都有一个名字,由第一行的 name 属性指定。该名字可以与XML文件的名字相同,也可以不同。在 FreeSWITCH 内部,将以该名字唯一确定该网关。在整个FreeSWITCH中,网关名字必须唯一,否则除第一个外,其他的都将被忽略。

realm 指定SIP网关服务器的地址,可以是一个合法的域名或IP地址,如果端口号不是5060,则后面需要加上“:端口号”,如“1.2.3.4:5080”。该参数是可选的,如果没有,则默认跟网关的名字(name)相同(此时网关的名字应该写成一个IP地址或者是域名,如192.168.7.7)。

username 和 password 分别指用户名和密码,这两个参数也是必需的。值得注意的是,有些网关使用IP地址验证,而不需要用户名和密码。在FreeSWITCH中,你也必须设置这两个参数,但它们的值将被忽略,所以可以填上任意值

sip 常用命令

mod_sofia 提供了一个API命令:sofia,它有很多参数,可以提供很多功能。比如 sofia status 会列出 sofia 的运行状态。在 FreeSWITCH 控制台上输入 sofia 或 sofia help 命令,将得到命令的帮助信息。帮助信息如下:

下面,我们对主要的参数进行讲解。

sofia status profile internal      列出某个Profile的状态。
sofia status profile internal reg  列出某个Profile上所有已注册用户。
sofia status profile internal reg 1000   过滤某些符合条件的用户。
sofia status profile internal user 1000  列出某个特定用户。
sofia status gateway gw1    列出网关状态。

以上的命令都可以将 status 用 xmlstatus 来替代,

列出XML格式的状态,这样比较容易用其他程序解析,可以自行比较它与 sofia status 命令输出的异同。

除 sofia 外,mod_sofia 还提供了一些其他 API 命令,这些命令一般用于显示一些相关的信息

sofia_username_of  返回注册用户的 username。

sofia_contact。返回注册用户的联系地址。

sofia_count_reg。在允许多点注册的情况下(开启multiple-registrations时),计算有多少客户端注册。

sofia_dig。类似于DNS的 dig,返回其他服务器的服务地址和端口号,如下命令显示了指定IP上都有哪些 SIP 服务。

profile 相关命令

Sofia 支持多个 Profile,而每个 Profile 相当于一个 SIP UA,在启动后它会监听一个 “IP地址:端口”对。从物理上来讲 freeswitch 只是一个UA,但由于它同时支持多个Session,在逻辑上就是相当于两个UA,简单来讲,一个 "IP地址:端口" 唯一标志一个UA。

profile 相关的命令都是针对某个 profile 进行的操作。一般来说,下面这些指令在需要读取XML时都会隐含reloadxml,因而如果要修改XML配置文件中的某个参数,就不需要明确的reloadxml指令了。常用的启动、停止、重启某个Profile的命令分别如下:

freeswitch> sofia profile internal start      启动
freeswitch> sofia profile internal stop       停止
freeswitch> sofia profile internal restart    重启

有时候,修改了某个 Profile 的某个参数( 如 outbound-codec-prefs、inbound-late-negotiation等),不需要重启(因为重启是影响通话的)。可以使用下列命令让 FreeSWITCH 重读 sofia 的配置(并不是所有的配置参数都能生效):

freeswitch> sofia profile internal rescan

添加了一个新的 gateway 以后,也可以使用 rescan 指令读取:

freeswitch> sofia profile external rescan

如果是修改了一个网关,则可以先将该网关删除,再 rescan,如:

freeswitch> sofia profile external killgw gw1
freeswitch> sofia profile external rescan

下列命令可以指定某个网关立即向外注册或注销:

freeswitch> sofia profile external register gw1
freeswitch> sofia profile external unregister gw2 
freeswitch> sofia profile internal siptrace on   # 开启该 Profile 的SIP跟踪功能抓SIP包:

有时候,希望将已经注册的用户清理掉。可以使用如下方法实现,如:

freeswitch> sofia profile internal flush_inbound_reg 1000@192.168.1.7

也可以通过找到该用户的 call-id 来清理,如下面的例子,再重新查看状态时,发现已经被清除了(Total items变成了0):

freeswitch> sofia status profile internal reg 1000
Registrations:
===================================================
Call-ID: ZWIyNDVmMTU1ODIyMDA1ZDl
User: 1000@192.168.1.123
...
Total items returned: 1
freeswitch> sofia profile internal flush_inbound_reg ZWIyNDVmMTU1ODIyMDA1ZDl
+OK flushing all registrations matching specified call_id
freeswitch> sofia status profile internal reg 1000
Total items returned: 0

有的时候可能会发现记录根本没有清除,出现这种情况可能是因为输错了命令或参数,但更可能是因为记录已经清除了,但客户端又重新注册了(因而又产生了一条新记录)。在 flush_inbound_reg 时,FreeSWITCH 只是简单地清理本地数据库中用户的注册信息,它无法防止客户端重新注册。如果确实不想让该客户端再注册了,可以给该用户改一个密码,让它注册不上来,或直接修改 iptables 防火墙,不允许相关的IP注册。

SIP 抓包

FreeSWITCH内置了Homer Capture Agent用于SIP抓包。Homer 是一个使用 HEP、HEP2 和 IPIP 协议的抓包分析工具。它逻辑上由 Capture Agent、Capture Node(又称Capture Server)和webHomer 三部分组成。其中,Captuer Agent 运行于 FreeSWITCH 内部,用于将收到的 SIP 包进行封装并通过 HEP/HEP2 或 IPIP 协议发送到远端的 Capture Node 上。Capture Node 收到封装后的 SIP 包以后,进行分析并将分析结果存储到数据库中。然后,技术人员就可以使用webHomer 在浏览器中查看各种统计和分析结果了,如图所示。

使用 Capture 功能前,需要先在 sofia.conf.xml 中配置 Captcher Node 的地址,如:<param name="capture-server" value="udp:192.168.0.100:6060"/>

通过在 Profile 中配置下列参数,可以配置该功能默认情况下在 Profile 加载时是打开的还是关闭的,如:<param name="sip-capture" value="yes"/>

当然,也可以在运行阶段通过命令动态开关,如:

freeswitch> sofia profile internal capture on
freeswitch> sofia profile internal capture off

global 相关

在使用sofia命令的siptrace子命令进行抓包时,用户经常搞不清该对哪个Profile进行抓包。因此,FreeSWITCH 的作者为这部分用户加入了这个特性:通过使用global参数,使 siptrace子命令对所有 Profile 都有效,如以下两条命令可以分别打开和关闭全局的SIP消息跟踪:

freeswitch> sofia global siptrace on
freeswitch> sofia global siptrace off

以下两条命令可以分别打开和关闭全局的SIP捕获(Homer方式抓包):

freeswitch> sofia global captuer on 
freeswitch> sofia global capture off

debug 相关

有时候,可能是协议栈更底层的原因引起的问题,由于收到或发送非法的消息会导致协议栈出错,这可能会使消息丢弃,当然也可能是协议栈层的Bug,在这种情况下即使开启了详细的FreeSWITCH日志以及SIP跟踪(siptrace)也查找不到问题的原因。这时候可以使用如下命令打开更低级别的调试器:freeswitch> sofia loglevel all 9

以上命令将开启详细的Sofia SIP底层调试信息,在控制台上打印日志。其中日志级别为0到9。如果你对sofia比较熟悉,也可以开启相关模块的日志。

例如,如下命令可以仅开启nua的调试信息:freeswitch> sofia loglevel nua 9

loglevel的其他的参数可以在sofia的命令帮助中找到。在默认的情况下,Sofia 层的日志级别是console,它会直接打印相关信息到控制台上,而不会写到日志文件中(如log/freeswitch.log)。如果需要将这些日志也写到日志文件中去,可以为这些日志指定一个级别。例如,下面的命令可以分别将 Sofia 的日志映射到 debug 和 notice 级别:

freeswitch> sofia tracelevel debug
freeswitch> sofia tracelevel notice

最后,可以使用如下命令关闭这些调试:freeswitch> sofia loglevel all 0

NAT 相关知识

默认安装的 FreeSWITCH 也能很好地工作在 NAT 环境下,但不可否认,用户的网络环境可能五花八门,因而也不可避免地会遇到NAT问题。适当地解决在NAT网络环境下的内、外网通信问题,就称为 NAT 穿越。

NAT涉及的知识比较多,用户不仅要了解FreeSWITCH、SIP协议及跟踪调试技巧,还要了解网络拓扑结构、使用网络设备(如交换机和路由器)的相关参数和特性等。不建议大家花费时间去研究和解决NAT问题,而是建议在局域网环境中学习FreeSWITCH,等有了一定基础以后再研究NAT环境。

NAT的全称是Network Address Translation(网络地址转换),它最初是为了克服IPv4网络地址不足出现的一项技术。众所周知,IPv4使用32位(4个8位,即4字节)地址空间,因而最多可以表示2 32个地址。随着Internet的迅速发展,人们需要大量的IP地址,大大超出了IPv4所能提供的地址数量。为了解决IPv4地址空间紧张的问题,RFC1918 [1]规定了一些私有的IP地址,这些私有的地址存在于路由器后面,它们本身形成一个私有的网络,称为内部网(Intranet,简称内网)。当内网的IP需要与外界的IP(又称公网)通信时,通过路由器提供的网络地址转换(NAT)功能转换成一个合法的外网IP地址来与外界通信。不同的内部网间彼此独立,因而可以复用这些私有的IP地址,这大大提高了IPv4网络的接入能力。

虽然 NAT 有效解决了IPv4网络上的IP地址短缺问题,但对于飞速发展的互联网来说,还不是终极的解决方案。因此,人们很早就发明了IPv6,它使用128位的地址空间,能表示2的128方个地址,也就是说将来任何可以联网的智能设备,包括家用的微博炉、手表等都可以获取一个IPv6地址。为了推动IPv4向IPv6的过渡,国际IP地址分配机构ICANN 早在几年前就提出了IPv4地址预警,称地址将很快耗尽,但遗憾的是,这项工程还是迟迟没有进展。Anthony Menissale《FreeSWITCH 1.2》中说过:“世界改变得越快,人们就越容易怀旧。技术的进步和革命也是如此。我们的汽车仍然假装有速度指针,你喜欢的网站上仍然使用过时的按钮和开关图片,我们作为社会的一分子,仍然信奉一句话:东西,如果它不坏,就不要去修它”

所以,我们还是整天生活在NAT的环境中,不管是在办公室,还是在家里通过路由器上网时。大多数人并未意识到NAT的存在,因为大多数的Web应用都是基于TCP的,NAT对TCP的影响不是很大。但基于SIP的语音通信中,SIP一般用UDP承载,UDP是无连接的协议,因而在NAT穿越方面就更加困难。而更为复杂的是,媒体(语音或视频)数据是在RTP包中传递的,它是与SIP路径不同的另一路径(甚至是更多的路径,比如同时有语音和视频的情况)。虽然SIP也可以通过TCP承载,但RTP为了保持实时性,还需要用UDP传输。因而,我们就不能忽略NAT的影响了。

图示:一个典型的NAT结构。其中,路由器有两个网络接口,一个用于连接外网,其IP是1.2.3.4,一个用于连接内网,其IP是192.168.0.1。内网的主机IP从192.168.0.2到192.168.0.5,它们把192.168.0.1作为一个网关,即所有与外网的通信都需要经过192.168.0.1转发。如果内网主机要与外界通信,路由器会将内网主机的请求转换成外网的IP地址,因而不管是哪个内网的主机与外界通信,外界的主机看起来都是从1.2.3.4这个路由器的外网IP发出的。同时,路由器会维护一个地址与内网主机间的映射关系,以保证回来的IP能到达相应的主机。这个映射关系是在内网主机首次向外网发包时建立的,此后外网的主机才可以向内网的主机发送信息。建立该映射关系的过程好像是在NAT设备上打了一个“洞”(因而该技术也称为UDP Hole Punching,即打洞),通过该“洞”进行内外网的通信。该洞是有生命周期的,如果在一段时间内没有数据通过,则洞会自动消失。

NAT有三种类型:

  • 静态NAT(Static NAT)。静态NAT设置起来最简单;内部网络中的每个主机都被永久映射成外部网络中的某个合法的地址,
  • 动态地址NAT(Pooled NAT)。动态地址NAT则是在外部网络中定义了的一系列的合法地址,采用动态分配的方法映射到内部网络;
  • 网络地址端口转换(Network Address Port Translation,NAPT)。NAPT则是把内部地址映射到外部网络的一个IP地址的不同端口上。

前两种类型实际上都不能节省IP地址,这里就不讨论了。NAPT是比较熟悉的一种转换方式,它普遍应用于接入设备中,可以将中小型的网络隐藏在一个合法的IP地址后面,这个优点使得这种方式在小型办公室内非常实用,通过从 ISP 处申请的一个公网IP地址,可以将多台电脑或网络设备通过NAPT 接入 Internet。

一般来说,NAPT有四种类型,但它实际上又分为两大类:锥型NAT 和 对称NAT。其中锥型NAT又分为三类,因而一共是四类。简单起见,沿用四类的说法来简单讲解(其中前三类属于锥型,最后一类属于对称型)。

  • Full Cone NAT(全锥型NAT)。内网主机建立一个UDP socket(表示为LocalIP:LocalPort,如192.168.0.2:5060),第一次使用这个socket给外部主机发送数据时,NAT设备(如我们上面讲的路由器)会给其分配一个公网IP、端口对(PublicIP:PublicPort,如1.2.3.4:5060),并记住它们之间的映射关系。以后用这个socket向外面任何主机发送数据都将使用这对PublicIP:PublicPort。此外,任何主机只要知道这个PublicIP:PublicPort就可以给它发送数据,NAT设备会根据它已经记住的映射关系将收到的数据转发到相应的内部主机的LocalIP:LocalPort上。其实简单来说就一句话: 内部主机向外打了一个洞,外网的任何主机都可以利用这个洞与它通信。
  • Restricted Cone NAT(限制锥型NAT)。内网主机建立一个UDP socket(LocalIP:LocalPort),第一次使用这个socket给外部主机发送数据时NAT设备会给其分配一个公网的PublicIP:PublicPort,以后用这个socket向外面任何主机发送数据都将使用这对PublicIP:PublicPort。此外,如果外部主机想要发送数据给这个内网主机,除了需要知道这个PublicIP:PublicPort外,内网主机在这之前必须用这个socket曾向这个外部主机的IP发送过数据。也就是说,如果内网的主机从来没有往某一公网IP发送过数据,则这个公网IP是不能往内网主机发送数据的,即使它知道PublicIP:PublicPort也不行。按洞的理论来说,就是内部主机向某一外部主机打了一个洞后,只有该外部主机才能利用这个洞。
  • Port Restricted Cone NAT(端口限制锥型NAT)。这种NAT与Restricted Cone类似,唯一不同的是,如果外部主机想要给内网主机发送数据,它除了必须知道PublicIP:PublicPort外,而且内部的主机必须事先向该外部主机的IP:Port发送过数据,并且该公网主机必须使用相应的IP:Port通过PublicIP:PublicPort给内网主机发送数据。可以看出,Poxt Restricted Cone NAT与Restricted Cone相比增加了对外网主机使用的端口的限制。即内部主机向外部主机上一某一程序(一个端口)打了一个洞,则只有该程序可以利用这个洞,其他的不行。
  • Symmetric NAT(对称型NAT)。锥型NAT对内网主机的同一socket(LocalIP:Localport)发往任何外网主机的数据均分配一个PublicIP:PublicPort,而对称型NAT会对内网主机同一socket(LocalIP:Localport)发往外部不同主机的数据分配不同的PublicIP:PublicPort映射关系。详细来讲,内网主机建立一个UDP socket(LocalIP:LocalPort),当用这个socket第一次给外部主机1发送数据时,NAT设备会为其映射一个PublicIP-1:Port-1,以后内网主机发送给外部主机1的所有数据都用这个PublicIP-1:PublicPort-1。如果内网主机同时用这个socket给外部主机B发送数据,第一次发送时,NAT设备会为其分配一个PublicIP-2:PublicPort-2,以后内网主机发送给外部主机2的所有数据都用这个PublicIP-2:Port-2。如果任何外部主机M想要发送数据给这个内网主机N,N必须曾经用这个socket向这个外部主机M的IP发送过数据,并且M需要知道N向M发送数据时NAT设备为其映射的PublicIP:PublicPort。这样这个外部主机M就可以用自己的IP:AnyPort(即任何端口)给PublicIP:PublicPort发送数据。对称型NAT相当于对同一内部主机联系不同的外部主机时都需要打不同的洞。

FreeSWITCH 的拓扑结构

FreeSWITCH一般有三种拓扑结构,如图所示。

  • 图 1:FreeSWITCH运行在公网上,与公网上的其他落地网关设备对接。SIP 话机一般位于NAT内部。这种情况一般用于运营 VoIP 业务的情况。
  • 图 2:FreeSWITCH运行在内网上,穿过NAT与公网上的设备对接。SIP话机也在内网上。一般用于公司内部的IP-PBX的情况。
  • 图 3:公网上的 用户(B) 也可以穿过NAT向FreeSWITCH注册,或通过FreeSWITCH打电话,而其外网用户通常是公司的外勤或出差人员。这种拓扑结构通常比较复杂,尤其是在希望B能在内网和外网间自由切换的情况下(有时内勤有时外勤,但不希望电脑上的SIP软电话注册地址改来改去)。甚至处于另外一个NAT后面的用户(如C)也希望向FreeSWITCH注册,这种情况称为双重NAT(Double Nat),就更加复杂了。

NAT 怎么影响 SIP/RTP 通信

图 1 拓扑结构(下面除非特别说明都以这种拓扑结构为例)。假设SIP话机的 IP 地址是192.168.0.2,而路由器的外网IP是 1.2.3.4,FreeSWITCH的IP地址是1.2.3.5。如果 SIP 话机向FreeSWITCH 注册,它将发送以下 REGISTER 消息(省略了其他不需要的字段):

REGISTER 1000@1.2.3.5 SIP/2.0

Contact: 1000@192.168.0.2:5060

该消息是从 192.168.0.2:5060 发出的,由于 NAT 设备进行了网络地址转换,因此 FreeSWITCH 在收到请求后会认为该消息是从 1.2.3.4:5060 发出的,

众所周知,SIP话机向FreeSWITCH注册是为了让FreeSWITCH记住自己的Contact地址。但如果FreeSWITCH记住了192.168.0.2,由于它没法连通该地址,因此如果有人呼叫1000这个用户就会出现电话打不通的情况,这就是典型的NAT引起的问题。

解决这一问题通常有两种思路:

  • 如果FreeSWITCH足够聪明,那么它应该知道192.168.0.2是一个内网地址,并且由于它收到的消息是从1.2.3.4:5060发出的,因而它可以记住后者,而不是那个内网地址。
  • 可以从客户端解决,让客户端想办法知道被NAT设备映射完以后的外网地址和端口号,即1.2.3.4:5060。这个一般靠STUN服务解决。

针对这两种思路的具体的解决办法稍后还会讲到。下面再来看一下RTP的情况。

SIP走通了以后,如果RTP不通,就会出现电话打通了但没有声音的情况(或者是单通,即一方有声音,另一方没有)。前面已经学过,RTP的地址是由SDP信息描述的,具体如下(我们照样省略了其他不相关内容):

INVITE 9196@1.2.3.5
Content-Type: application/sdp
c=IN IP4 192.168.0.2
m=audio 50452 RTP/AVP 8 0 101

跟SIP消息类似,FreeSWITCH是无法向这个192.168.0.2发送RTP包的,这个问题可以由客户端通过STUN服务解决,也可以由FreeSWITCH端解决。如FreeSWITCH可以在收到第一个RTP包时得知该RTP流对应的外网IP和端口号(如1.2.3.4:50452),以后所有的RTP流都发送到这个NAT设备的外网地址上,NAT设备会将收到的从FreeSWITCH发送过来的RTP包转发到该内网地址上,话机就能“听”到声音了。

图2、图3 两种拓扑结构下,FreeSWITCH就相当于这里说的SIP话机,它跟外网设备通信时就需要通过STUN之类的服务来获取相应的外网IP和端口号。

NAT 穿越

要解决 NAT穿越问题就要解决内、外网地址映射的问题。也就是说,除了NAT设备自己知道这个映射关系以外,SIP客户端(如话机)和服务器(如FreeSWITCH)都需要知道。

在继续尝试解决NAT穿越问题前,我们先做以下的准备:

  • 了解你的网络环境和拓扑结构。如了解自己的IP地址段、网关IP、外网IP、使用的路由设备厂商和型号、提供接入的服务商或运营商等。另外有一些在线工具可以帮你快速知道自己的外网地址,如你可以直接访问国内的http://ip138.com/或国外的http://ifconfig.me。笔者经常用下列命令获取自己的外网地址:curl ifconfig.me  
  • 禁用路由器的ALG(Application Level Gateway)。某些路由器有ALG功能,它们会修改SIP包中的IP地址,“帮助”你进行NAT穿越。但很遗憾的是,它们实现的往往有Bug,而且该功能默认是打开的。FreeSWITCH不需要ALG。

实际上,NAT问题本来是不需要FreeSWITCH来解决的。在理想的情况下,任何可能用于NAT后端的设备要想与外界通信,都必须能自己解决NAT穿越问题。但事实上,现实世界不是理想的国度,很多设备都无法解决NAT穿越问题。因此FreeSWITCH团队通过深入研究,提供了很多方法来帮助解决NAT的穿越问题。这些方法在默认的安装下一般都能工作得很好,但在不一般的情况下还是需要手工处理的。

SIP 穿越

FreeSWITCH默认使用ACL来判断对方是否处于一个NAT环境中,配置项如下:

<param name="apply-nat-acl" value="nat.auto"/>

其中,nat.auto 是一个ACL,它包含 RFC1918 规定的私网地址,并去掉了本地网络的地址。当SIP客户端向FreeSWITCH注册时,FreeSWITCH会比较SIP消息中的Contact地址是否包含在这个ACL中,如果包含,说明是来自一个NAT背后的设备,那么它就把其Contact地址自动替换为与该设备对应的外网地址(即SIP包的来源地址,已经被NAT设备转换成了外网地址),因而接下来再有人呼叫它时,FreeSWITCH就能正常给它发INVITE请求了。

下列命令列出了一个SIP客户端通过NAT注册的情况(其中Contact中进行了人工换行):

注册成功后,可以在FreeSWITCH中查看其注册情况:

可以看到,其中Contact字段中有fs_nat=yes标志,它表示FreeSWITCH已经“聪明”地知道了该客户端是处于一个NAT后面。上面的显示是经过urlencode的,不直观,我们用urldecode 反编码如下:

从上面的信息可以看到,FreeSWITCH既知道它的内网地址,又知道它的外网地址。以后任何时候FreeSWITCH向这个客户端发消息时,均使用fs_path指定的地址。

RTP 穿越

解决了SIP穿越问题后,我们再来看RTP是如何穿越的。由于客户端的SDP信息中的IP地址是私网地址,因而FreeSWITCH无法直接给它发RTP包。而且,从9.4.1节我们也了解到,很多NAT设备都只有内网的主机曾经向外网主机发过包以后,才允许外面的包进入。因此FreeSWITCH使用了一个名为RTP自动调整的特性,即FreeSWITCH在SIP协商时给对方一个可用的公网RTP地址,然后等待客户端发送RTP包,一旦它收到RTP包以后,就可以根据对方发包的地址给它发RTP包了。当这种情况发生时,可以在Log中看到类似如下的信息:

[INFO] switch_rtp.c:4753 Auto Changing port from 192.168.1.124:50492 to 1.2.3.4:50492

虽然它不能解决所有问题,但总比不解决要好。当然,由于它没有约定使用SIP协商中指定的IP地址,这种解决办法可能有安全性问题,如黑客可以随机猜一些端口并向这些RTP端口发包,FreeSWITCH收到后将远端地址调整到新的黑客的IP和端口,进而RTP数据全跑到黑客那里去了。所以FreeSWITCH为了防止这个问题,规定这种端口调整只能在电话开始的时候进行,一旦调整过,就不能再进行调整了。

这种自动调整是默认开启的,如果用户不需要该功能,可以在Profile中将其禁掉:

<param name="disable-rtp-auto-adjust" value="true"/>

或者只针对个别的呼叫来禁止自动调整,在呼叫时可以通过设置“rtp_auto_adjust=false”通道变量禁止。

其他解决方案

FreeSWITCH在默认的情况下就能很好地应对NAT的情况。但在某些网络环境下或对接某些客户端设备时(我们姑且称为“差”设备),以上手段可能还不够。FreeSWITCH还提供了一些设置,这些设置默认是不开启的,如果开启了,可能这些“差”设备就可以工作了,但会影响“好”设备。

有些设备会自动进行NAT穿越,但却把SIP包里的参数改得乱七八糟。FreeSWITCH提供了一个参数,其可以在Profile设置,以实现对这些SIP包进行“深度”检测,进而决定到底该用哪个IP地址。该参数是:<param name="aggressive-nat-detection" value="true"/>

另外,FreeSWITCH还有一族称为NDLB 的配置参数,可以帮助某些“差”设备,如将aggressive-natdetection参数配置到用户目录中,它会用接收到的SIP包的源地址改写Contact中的IP地址。

下面一个参数需要配置到Profile中:<param name="NDLB-force-rport" value="true"/>

要理解 NDLB-force-rport 这个参数,先来说一下什么是rport。对于支持rport的设备,在发起请求时会在Via字段中带一个空的rport参数,如:

FreeSWITCH在收到该请求后,发现该SIP包的实际来源地址是1.2.3.4:5060,因此它会将响应包发往该地址,并在Via字段中设置rport的值为实际来源端口值,同时增加received参数,指定来源IP地址。它回应的SIP消息如下:

SIP客户端在收到该响应后,“学习”到了自己对应的外网地址,因而在接下来重发的注册信息中,Contact地址就可以填外网的地址了。

有意思的是,某些设备支持rport这一功能但在发送请求的时候在Via字段中又没有相应的rport参数。因而在FreeSWITCH开启NDLB-force-rport参数可以认为所有设备请求都带了rport参数,进而打开这些“差”设备的rport功能。需要指出的是,有些设备确定不支持rport,带了该参数可能会导致这些设备不能用,因而该参数的值也可以设置成safe(其他的取值还有client-only和server-only,读者可以自己尝试一下),即仅打开已知可用设备这一功能。

该参数是作用于整个Profile的。有时候在既要支持好设备又要支持差设备的情况下,是否使用该参数不能两全。解决的办法是可以再建立一个新的Profile,单独让这些差设备注册到新的Profile。

NDLB还有其他的参数,有的跟NAT相关,有的不相关。当然了,笔者希望大家永远都用不到这些参数,因为一旦涉及这些参数就会很麻烦,但如果有些设备必须要用这些参数,可以参考http://wiki.freeswitch.org/wiki/NDLB。

好了,接下来就要进入本节的正题了,让我们具体了解其他NAT穿越问题的解决方案。

  • 客户端解决方案

前面了解到,FreeSWITCH提供了多种手段来帮助客户端设备穿越NAT,并且在前面也提到,这些工作原来应该是在客户端做的。如果客户端能够知道自己将要被映射出去的外网IP地址和端口,那么它就可以直接在SIP/SDP消息中填上外网地址,因而FreeSWITCH就不用再费劲检测客户端是否有NAT了。

在前面讲到可以使用 ifconfig.me之类的服务获取自己的外网IP,但SIP通信中还必须得知道映射的外网端口号。这一工作需要靠STUN 来实现。STUN的原理是:在公网上部署一台STUN服务器,位于NAT后面的客户端设备向它发送一系列的UDP包先在NAT设备上打一个“洞”,STUN 服务器取到 UDP 包的来源地址以后,会回送相关的消息告诉该客户端它被映射的外网地址。

大多数的SIP话机及软电话客户端均可以选择是否启用STUN服务。使用STUN以后,SIP消息的Contact头域中就可以直接填入外网地址,这时在FreeSWITCH端看到的注册信息如下(可以跟FreeSWITCH检测到NAT的情况对比一下):

对比前面容易得出这样的结论,STUN服务对于锥型NAT是有效的,但对于对称型NAT就无能为力了。这是因为锥型NAT对内网的同一个源地址和端口发往任何外网IP地址的数据都映射成同一外网IP和端口,而对称型NAT则对发往不同IP地址的包进行不同的映射,因而在对称NAT的情况下虽然客户端能从STUN服务器学习到外网IP地址和端口号,但是在客户端向真正的SIP服务器发UDP包时,在NAT设备上会映射出另外一个IP及端口号,因而先前从STUN服务器学习到的是无效的。

另外,考虑图3所示拓扑结构,如果互相通信的两个SIP终端分别位于两个对称NAT之后,则它们是无法进行通信的,因为打通一个“洞”需要首先向对方发送数据,而在对称NAT的情况下谁都无法向先向对方发送数据,因而无法实现UDP通信。

TURN 技术主要用来解决对称型NAT的问题的,当然,它对于其他的NAT类型同样有效。使用它需要在公网上部署一台TURN服务器,该服务器作为一个“中间人”对双方的数据进行转发。

FreeSWITCH不支持TURN ,在这里我们就不多讲了,有兴趣的读者可以参阅相关资料。

除了STUN和TURN外,还有RSIP(Realm Specific IP)、symmetric RTP等NAT穿越技术,但这些技术应用于不同的网络拓扑时都会各有利弊,无法在所有情况下都保证完美。所以人们一直在寻求一种足够灵活的方案,使用它可以在各种情况下对NAT穿透提供最优的解决方案。ICE技术就是满足这一条件的解决方案。

ICE 的全称是交互式连通建立(Interactive Connectivity Establishment),它其实不是一个新的协议,而是综合利用现有的STUN和TURN等技术,使之在最合适的情况下工作,以弥补单独使用某种协议所带来的固有的缺陷。

在SIP通信中,ICE通过扩展SDP,为RTP媒体提供多个候选的地址(Candidate),这样两个SIP终端之间就可以尝试多个不同的候选地址找到一个最优的通信路径。比如,如果两个SIP终端处于相同的NAT后面,则它们可以直接用内网地址进行通信;如果位于不同的NAT后面,并且是锥型NAT,则他们可以选择从STUN服务器获得的外网地址进行通信;如果是对称型NAT,则只能通过TURN来进行通信了。

  • FreeSWITCH处于客户端的位置

以上我们讲的大部分是针对FreeSWITCH在公网上作为服务器端的情况。考虑图9-5b所示拓扑结构,当FreeSWITCH需要穿越NAT向外部的网关注册时,它就相当于处在客户端的位置 。我们一直在说客户端如果处于NAT之后,NAT问题要自己解决,因此,FreeSWITCH也有自己的解决方案。

首先,FreeSWITCH支持通过uPnP或NAT-PMP协议在路由器上“打洞”。打洞完成后它就知道了自己将要映射的外网地址了。当然,使用该协议需要路由器支持,在笔者的网络环境下,FreeSWITCH启动时可以看到如下信息,证明FreeSWITCH确实检测到外网的地址了:

也可以在FreeSWITCH中使用如下命令进行验证:

上述命令显示了4个端口映射关系,下列命令可以看到映射后的外网地址:

其中,ExtIP:119.40.26.181就表示FreeSWITCH是通过uPnP学习到的外网地址。

FreeSWITCH检测到NAT以后,会设置Profile的外网SIP和RTP的IP。下列命令显示了external Profile当前的Ext-SIP-IP和Ext-RTP-IP:

其中Ext-SIP-IP和Ext-RTP-IP是在与外网的服务器通信时用的。它在FreeSWITCH中默认的配置(在external.xml中)如下:

<param name="ext-rtp-ip" value="auto-nat"/>
<param name="ext-sip-ip" value="auto-nat"/>

其中,auto-nat说明它试图自动检测到NAT外网的地址。如果路由器不支持uPnP及NAT-PMP协议,那么可以在这里填入一个stun服务器的地址,让它从stun服务器获取,如:

<param name="ext-sip-ip" value="stun:stun.freeswitch.org"/>
<param name="ext-rtp-ip" value="stun:stun.freeswitch.org"/>

注意,stun.freeswitch.org可以做测试用,但它不保证永远可用。如果使用上述STUN服务不能正常获取IP,则可以尝试其他STUN服务器。在网上可以找到好多免费的STUN服务器,当然需要自己运营SIP服务的应该自己搭建STUN服务器。

FreeSWITCH提供了一个stun API命令用于测试STUN服务器是否能正确返回外网的IP及端口号。下列命令检测到服务器是正常的:

freeswitch> stun stun.freeswitch.org
119.180.70.163:50285

下列命令则不能返回正常的结果,因为我们使用的不是一个有效的STUN服务器:

freeswitch > stun www.freeswitch.org
-STUN Failed! [Timeout]

如果不使用STUN,也可以手工找出路由器的外网IP,有以下两种设置方法:

<param name="ext-sip-ip" value="autonat:119.40.26.181"/>
<param name="ext-rtp-ip" value="autonat:119.40.26.181"/>
<param name="ext-sip-ip" value="119.40.26.181"/>
<param name="ext-rtp-ip" value="119.40.26.181"/>

其中,上述两种配置方法是差不多的,只不过带了“autonat:”前缀以后FreeSWITCH会更智能一些,读者可以自己尝试,看看带与不带有什么区别,这里就不多讲了。最后,你也可以设置一个DNS,如host:sip.example.com,通过域名解析获得外网的IP地址。

  • 其他

一般来说,FreeSWITCH足够智能,使用默认的配置就够用了。但FreeSWITCH并不能帮你解决所有NAT的问题,实际使用过程中还得具体问题具体分析。比如你可能为了兼容某些设备改了一些Profile的参数,同时导致另外一些本来好用的设备不好用了。或者,你修改了ext-sip-ip/ext-rtp-ip后外部的用户注册好用了,但NAT内部用户打电话却有问题了。大多数情况下,一个Profile通过设置适当的参数就能解决这些问题。但如果你遇到的不是上述的“大多数”的情况,那么可以通过创建多个Profile来解决,让某些设备注册到其中一个Profile,另一拨人注册到另一个Profile。当然同时使用多个Profile还需要一些设置技巧,我们将在后面的实战章节中深入探讨。

另外,考虑图3 所示拓扑结构,如果FreeSWITCH是企业内部的PBX,员工可能经常会在办公室A或外部B(家里)之间换来换去,那么它可能要在办公室里向内网地址192.168.0.2注册,在外部时就向1.2.3.4注册。每次改来改去比较麻烦。这种情况可以在企业内部部署DNS服务器通过DNS解决;另外有的路由器设备支持所谓“发夹”(Hairpin)的功能,通过这个功能可以解决该问题,有条件的读者也可以尝试一下,因为笔者没有见过这种路由设备,在这里就不多讲了。

FreeSWITCH 权威指南

相关 book:https://www.freeswitch.org.cn/tags.html#Book

FreeSWITCH 权威指南 在线地址:https://www.freeswitch.org.cn/2010/04/30/freeswitch-zhong-wen-wen-dang.html

FreeSWITCH 权威指南 下载:https://www.jb51.net/books/521146.html

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值