在UNIX类系统上,FreeSWITCH默认的安装位置是/usr/local/freeswitch,在Windows上可能是C:\Programming Files\FreeSWITCH。在这两种系统上,FreeSWITCH的目录结构大致相同

FreeSWITCH对应的esl依赖版本 freeswitch框架_配置文件

一般来说,大部分的目录结构都是扁平的,没有什么子目录。比较例外的有以下几个:

sounds:sounds目录中存放了各种声音文件。FreeSWITCH支持多种频率的通话,因而也支持多种频率的声音文件。

配置文件

配置文件由许多XML文件组成。在系统装载时,XML解析器会将所有XML文件组织在一起,并读入内存,组成一个大的XML文档(Document),称为XML注册表。XML文档本身非常适合描述复杂的数据结构,在FreeSWITCH中可以非常灵活地使用这些数据。这种设计的好处是可以帮助实现非常高的可扩展性。并且,XML也是一个国际通用的标准,其他外部应用程序也可以很容易地生成XML,从而很容易地与FreeSWITCH集成。另外,系统还允许在某些XML节点上安装回调函数,当这些节点的数据变化时(如在reloadxml时),系统便自动调用这些回调函数,以更新系统的内部数据或状态等。

使用XML唯一的不足就是手工编辑这些XML比较困难,但正如其作者所言 [1],他绝对不是XML的铁杆粉丝,他认为人们在理解XML的过程中迷失了方向,相反,把一个复杂的对象序列化成文本形式以及用一个易于解析又易于生成的标记语言来描述这正是XML的长处。使用XML的最高境界是让所有人意识不到它的存在。虽然现在还没有谁能做到这一点,但随着XML的发展以及更多的GUI界面和配置程序出现,终有一天会发展到只有高级用户才会去看这些XML,那时其他用户就意识不到XML的存在了。

在继续讨论配置文件之前,我们先来看一下这些配置文件的目录结构。在本书第3章我们已经提到过,配置文件的目录结构如下(其中,带扩展名的是文件,其他的一般都是目录):

├── freeswitch.xml
├── mime.types
├── notify-voicemail.tpl
├── tetris.ttml
├── vars.xml
├── voicemail.tpl
├── web-vm.tpl
├── autoload_configs
├── chatplan
├── dialplan

├── default

├── public

└── skinny-patterns
├── directory

└── default
├── ivr_menus
├── jingle_profiles
├── lang

├── en


├── demo


├── dir


├── ivr


└── vm

├── fr


├── demo


├── dir


└── vm
├── mrcp_profiles
├── sip_profiles

├── external

└── internal
└── skinny_profiles

其中最重要的一个配置文件是freeswitch.xml,它的作用是将所有配置文件“粘”到一起。只要有一点点XML [2]基础知识,都可很容易理解这些配置。其中,X-PRE-PROCESS标签是FreeSWITCH中特有的,它称为预处理指令,用于设置一些变量和装入其他配置文件。

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

完整的XML文档分为以下几个重要的部分:configuration(配置)、dialplan(拨号计划)、chatplan(聊天计划)、directory(用户目录)及phrase(分词),每一部分又有不同的配置。由于完整的XML非常大,而且大的XML文档已经对X-PRE-PROCESS标签进行处理,隐藏了很多配置的细节,因此这里我们还是按单个的文件或目录进行介绍。
freeswitch.xml

freeswitch.xml是所有XML文件的黏合剂,我们先从它讲起。为了便于讲解,下面是一个精简过的freeswitch.xml。

<?xml version="1.0"?>
<document type="freeswitch/xml">
    <!-- #comment 这是一个配置文件,本行是注释 -->
    <X-PRE-PROCESS cmd="include" data="vars.xml"/>
    <section name="configuration" description="Various Configuration">
        <X-PRE-PROCESS cmd="include" data="autoload_configs/*.xml"/>
    </section>
</document>

众所周知,XML文档是一种树型的结构。在这里,它的根是document,在document中,有许多Section,每个Section都对应一部分功能。其中有两个X-PRE-PROCESS预处理指令,它们的作用是将data参数指定的文件内容包含(include)到当前文件中来。

上述freeSwitch.xml文件没有特别的作用,它就是将不同的配置文件“包含”到不同的部分(Section)中,从而生成一个大的XML配置文件。最后大的配置文件将类似下列代码所示:

<?xml version="1.0"?>
<document type="freeswitch/xml">
    <section name="configuration" description="Various Configuration">
        <configuration name="mod1.conf" description="Mod 1"/>
        <configuration name="mod2.conf" description="Mod 2"/>
        <configuration name="mod3.conf" description="Mod 3"/>
    </section>
    <section name="dialplan" description="XML Dialplan ">
        <context name="default " description="default Dialplan"/>
        <context name="public" description="public Dialplan"/>
    </section>
</document>

可以看出,configuration这个Section中有很多configuration,而dialplan这个Section中有很多Context。关于这些配置项的具体含义我们将在后面相关的章节讲解,在此就不多讲了。

由于X-PRE-PROCESS是一个预处理指令,FreeSWITCH在加载阶段只对其进行简单替换,并不进行语法分析,因此对它进行注释是没有效果的,这是一个新手常犯的错误。下面举例说明一下。
假设vars.xml的内容如下,它是一个合法的XML:

<!-- this is vars.xml -->
<vars>
    <var1>xxxxx</var1>
    <var2>xxxxx</var2>
</vars>
<!-- end of vars.xml -->

若在调试阶段想把一条X-PRE-PROCESS指令注释掉:

<!-- <X-PRE-PROCESS cmd="include" data="vars.xml"/> -->

当FreeSWITCH预处理时,还没有到达XML解析阶段,也就是说它还不认识XML注释语法,而仅会机械地将预处理指令替换为如下的vars.xml里的内容:

<!--    <!-- this is vars.xml -->
<vars>
    <var1>xxxxx</var1>
    <var2>xxxxx</var2>
</vars>
<!-- end of vars.xml --> -->

因而,由于XML的注释不能嵌套,这里便产生了错误的XML。解决办法是破坏掉X-PRE-PROCESS的定义,如作者常用下面两种方法:

<xX-PRE-PROCESS cmd="include" data="vars.xml"/>
<XPRE-PROCESS cmd="include" data="vars.xml"/>

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

vars.xml主要通过X-PRE-PROCESS指令定义了一些全局变量,如:

<X-PRE-PROCESS cmd="set" data="domain=$${local_ip_v4}"/>
<X-PRE-PROCESS cmd="set" data="domain_name=$${domain}"/>
<X-PRE-PROCESS cmd="set" data="hold_music=local_stream://moh"/>
<X-PRE-PROCESS cmd="set" data="use_profile=internal"/>

读者可能已经注意到了,该指令使用set表示要设置一个变量。如上述第1行,将domain这一变量的值设置为FreeSWITCH对应的esl依赖版本 freeswitch框架_配置文件_02{local_ip_v4}是什么意思呢?它又是从哪里来的呢?

可以这样讲,在这里使用X-PRE-PROCESS设置的变量都称为全局变量,它们在FreeSWITCH运行期间永远都是有效的。而后面可能还会遇到局部变量,它们通常在拨号计划中,在一个呼叫的生命周期中才有效。如果需要引用这些变量,则全局变量以$FreeSWITCH对应的esl依赖版本 freeswitch框架_XML_03{var}表示。

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

FreeSWITCH对应的esl依赖版本 freeswitch框架_服务器_04

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

freeswitch> global_getvar sound_prefix
/usr/local/freeswitch/sounds/en/us/callie
 freeswitch > global_getvar local_ip_v4
 192.168.7.6

由于这些变量是在vars.xml加载前设置的,因而可以在varx.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.xml”的方式命名(模块名中不包含“mod_”,如sofia.conf.xml)。

各模块的配置文件不尽相同,其中的配置项也都是由不同的模块分别解析的。这里我们以sofia.conf.xml为例简单介绍一下。

<configuration name="sofia.conf" description="sofia Endpoint">
  <global_settings>
    <param name="log-level" value="0"/>
    <!-- <param name="auto-restart" value="false"/> -->
    <param name="debug-presence" value="0"/>
    <!-- <param name="capture-server" value="udp:homer.domain.com:5060"/> -->
  </global_settings>
  <!--
      The rabbit hole goes deep.  This includes all the
      profiles in the sip_profiles directory that is up
      one level from this directory.
  -->
  <profiles>
      <X-PRE-PROCESS cmd="include" data="../sip_profiles/*.xml"/>
  </profiles>
</configuration>

该配置文件比较简单,首先它定义了一个configuration标签,其中name表示名字,这里是sofia.conf,而后面的description是一个简单的描述。

其实,sofia.conf.xml的文件名并不重要,完全可以改成其他的名字,只要扩展名是.xml就可以正常被freeswitch.xml中的X-PRE-PROCESS预处理指令装入。这里重要的是,这个配置文件中的configuration标签的name属性,mod_sofia在启动时会向XML注册表中查找name为“sofia.conf”的configuration,进而访问其下面的配置参数。

global_settings标签定义了一些全局参数,具体参数的意义我们在此就不多解释了。

profiles标签可通过X-PRE-PROCESS指令装入其他的配置文件,这些配置文件一般都是每个文件描述了一个profile(见默认的conf/sip_profiles/internal.xml),关于这些profile我们将在第8章中详细讲解。在此我们仅看一下profile装入后的效果,具体如下:

<profiles>
         <profile name="profile1"></profile>
         <profile name="profile2"></profile>
</profiles>

另外,autoload_configs目录中有一个特殊的modules.conf.xml,其决定了FreeSWITCH启动时自动加载哪些模块。如下面的配置片断,如果需要在FreeSWITCH启动时自动加载某个模块,就在这里添加一行,如果不需要,就注释掉或直接删除。下面列出了该配置文件的前几行:

<configuration name="modules.conf" description="Modules">
  <modules>
    <!-- Loggers (I'd load these first) -->
    <load module="mod_console"/>
    <load module="mod_logfile"/>
<!-- <load module="mod_syslog"/> -->

最后,autoload_configs目录中还有一个post_load_modules.conf.xml文件,其格式和用法与modules.conf.xml差不多,不同的是其中定义的模块加载时间比较晚。
其他

上面我们大体了解了配置文件的目录结构和简单的配置格式。其他配置文件的配置方法也大同小异。至于具体的配置项的含义只有到了具体的应用时才有意义,因此,我们后面用到某些项时,会详细进行说明。不过,在结束本节之前,我们还需简单介绍以下几个目录,因为我们在后面的章节中可能会经常提到它们:

dialplan目录:该目录下的文件定义了XML拨号计划(Dialplan)。拨号计划是Free-SWITCH中很重要的一部分,它用于对进电话进行路由。我们将会在第6章专门讲解拨号计划。
ivr_menues目录:该目录下存放了默认的一些IVR菜单的例子。
directory目录:该目录中的配置文件决定了当FreeSWITCH作为注册服务器时,哪些用户可以注册,即用于配置本地用户,其中的配置信息称为用户目录。FreeSWITCH的用户目录支持多个域(Domain),每个域可以写到一个XML文件中。默认的配置包括一个default.xml,其中定义了1000~1019一共20个用户(实际上是装入了下一级子目录中的20个配置文件)。

[1] Trust me.I am far from XML’s biggest fan,but I do feel it’s actual uses are lost in a sea of contrived forced solutions that give it a bad name.To serialize complex objects to and from a text format and to make markup that is easily parsed and generated are the real strengths of XML.The best used XML is XML nobody ever realizes is even there.Certainly that is not true completely with FreeSWITCH today but as we scale and more GUI’s and config apps are made,the curve will be in our favor and most people besides only the truly advanced users will ever have to see the XML again.—Anthony Minessale 全文见:http://www.freeswitch.org/node/123。

[2] XML由标签(Tag)和属性构成。和组成一对标签,如果该标签有相关属性,则以形式指定。有些标签无须配对,但必须以“/>”关闭该标签定义,如<other_tag attr=“value”/>。
XML用户目录

我们在5.3.4节提到XML用户目录决定了哪些用户可以注册到FreeSWITCH上。在此,我们再深入分析一下。

SIP并不要求一定要注册才可以打电话,但是通话前的用户认证参数仍需要在用户目录中进行配置。

用户目录的默认配置文件在conf/directory/下,系统自带的配置文件为default.xml,其实现代码如下:

<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})}"/>
  </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>
</domain>

该配置文件决定了哪些用户能注册到FreeSWITCH中。一般来说,所有用户都应该属于同一个domain(除非想使用多个domain,后面我们会有例子)。这里的$${domain}这个全局变量是在vars.xml中设置的,它默认是主机的IP地址,可以修改为使用一个域名。params中定义了该Domain中所有用户的公共参数。在这里只定义了一个dial-string,这是一个至关重要的参数。在使用user/username或sofia/internal/username@domain这样的呼叫字符串时,FreeSWITCH会根据username(以及domain)找到该dial-string,并最终扩展成用户实际的SIP地址 [1]。其中sofia_contact是一个API命令,它会根据用户的注册地址扩展成相应的呼叫字符串。

variables则定义了一些公共变量,在用户主叫或被叫时,这些变量会绑定到相应的Channel上形成Channel Variable。

在domain中还定义了许多组(group),组里面包含很多用户(user)。

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

在这里,组名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="Alice"/>
    <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。

当然,用户目录还有一些更高级的设置,我们留待以后研究。

[1] 这个呼叫字符串比较复杂,需要许多知识才能理解它,因此我们留到本书的第二部分来详细研究。
呼叫相关概念

本节我们再来复习一下第3章的内容,FreeSWITCH是一个B2BUA,我们还是以图3-2为例。
如果Bob与Alice通话,典型的呼叫流程主要有以下两种:
Bob向FreeSWITCH发起呼叫,FreeSWITCH接着启动另一个UA呼叫Alice,两者通话。
FreeSWITCH同时呼叫Bob和Alice,两者接电话后FreeSWITCH将a-leg和b-leg桥接(bridge)到一起,两者通话。

其中第二种流程还有一种变种。例如,市场上有人利用上、下行通话的不对称性卖电话回拨卡获取利润。比方说,他们可以把FreeSWITCH作为服务器,把回拨卡卖给Bob。Bob按回拨卡上的号码呼叫FreeSWITCH,FreeSWITCH不应答,而是在获得Bob的主叫号码后直接挂机。然后FreeSWITCH回拨Bob,Bob接听后FreeSWITCH启动一个IVR程序指示Bob输入Alice的号码。最后FreeSWITCH再呼叫Alice [1]。

在实际应用中,由于涉及回铃音、呼叫失败等,情况可能要复杂得多。在此我们就不深入讨论了,而是针对简单的呼叫流程介绍一下与呼叫相关的基本概念。
来去话、Session、Channel与Call

在前面所说的第一种呼叫流程中,Bob到FreeSWITCH的通话称为来话(注意,在这里我们都是针对FreeSWITCH来说的),而FreeSWITCH作为一个B2BUA再去呼叫Alice时,就称为去话。显而易见,在第二种呼叫流程中两路通话(两个leg)都是去话。

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

在通话中,媒体(音频或视频)数据流在RTP包中传送。一般来说,Channel是双向的,因此媒体流会有发送(Send/Write)和接收(Receive/Read)两个方向。
回铃音与Early Media

为了便于说明,我们假定A与B不在同一台交换机(服务器)上(如在PSTN通话中可能不在同一座城市),中间需要经过两台交换机中转

FreeSWITCH对应的esl依赖版本 freeswitch框架_配置文件_05

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

理论上讲,B接听电话后交换机b可以一直不向交换机a发送应答消息,而是将真正的话音数据伪装成Early Media,以实现“免费通话”。但这种应用是有限制的,大多数交换机允许Early Media的时间不会太长,如1分钟,以防止不守规则的人进行免费通话。
全局变量与局部变量

在5.3.2节我们讲到,可以使用X-PRE-PROCESS在FreeSWITCH中设置一些变量(包括自动生成的变量),在后续使用时可以用KaTeX parse error: Can't use function '$' in math mode at position 163: …e,即通道变量。通道变量可以以$̲{var}的形式引用。全局变量…{domain}或FreeSWITCH对应的esl依赖版本 freeswitch框架_XML_06{var}只在加载时求值一次 [3],而${var}则在每次执行时都求值(如一个新电话进来时)。

在呼叫过程中,某些变量可以改变Channel的行为。另外,也可以使用自定义的通道变量来存储随路数据等。

在实际使用中会发现,有些变量在显示时(可以使用dp_tools中的info App显示,后面会讲到)是以variable_开头的,但在实际引用时要去掉这些开头的variable_。如variable_username,引用时要使用${username}。我们将在下一章深入讨论通道变量。

[1] 在整个过程中,Bob和Alice的电话都不会被扣费,费用将在回拨卡账号上扣除,通常会比Bob直接呼叫Alice便宜。这种方法比较复杂,用户体验也不好,故我们不赞成这种做法。在此只是举一个例子,说明FreeSWITCH可以这样用。

[2] 彩铃称为CRBT,即Color Ring Back Tone。

[3] 如果更改XML配置文件中的全局变量,一般需要重新启动才能重新加载。在UNIX类系统上,也可以向FreeSWITCH发送HUP信号让它重新加载和解析全局的配置文件,实现命令是kill-HUP<FreeSWITCH的pid>。
小结

FreeSWITCH是由一个稳定的核心及外围的可加载模块组成的。核心代码经过精心设计及严格测试,保证了它的稳定性。外围的模块则保证了它的可扩展性。核心与模块间通过Public API通信,而模块间通过事件系统(Event)通信,事件机制消除了模块间的耦合,进一步增加了其可伸缩性。

与第4章类似,本章也介绍了一些基本概念,如Session、Channel、Channel Variable等,这里介绍的基本概念后面会多次涉及,读者在后面遇到时也不妨回来复习一下,以加深对它们的理解。