第一、工作原理:
呼叫抵达FreeSWITCH时,根据它使用的技术、网络特征(profile),或者根据用户目录的显式配置(针对注册用户发起的呼叫),会着陆到一个特定的拨号方案context。呼叫会从头到尾遍历context,尝试挨个匹配每个extension。在某个extension内部,根据条件匹配结果,会把action或anti-action添加到呼叫的TODO列表。根据condition的"break"参数值,决定是停止处理下一条condition,还是继续判断条件。根据extension的"continue"值,决定是继续处理下一个extension还是跳出。当呼叫结束拨号方案的context遍历时(可能是因为"continue"值结束,也可能是确实遍历所有extensions结束),TODO列表中所存储的所有(anti-)action将被挨个执行。所以拨号盘共有2个阶段,如下:
第一阶段:拨号方案解析(也叫路由ROUTING阶段),这时,呼叫遍历拨号方案,并在TODO列表中堆积action(没有执行);
第二阶段:执行(EXECUTE)阶段,这时所有堆积下来的action将被批量执行。
拨号计划的配置文件在 conf/dialplan 中,在前面的章节中我们讲过,它们是在 freeswitch.xml 中,由 <X-PRE-PROCESS cmd="include" data="dialplan/*.xml"/> 装入的。
拨号计划由多个 Context (上下文/环境)组成。每个 Context 中有多个 Extension (分支,在简单的 PBX 中也可以认为是分机号,但很显然,Extension 涵盖的内容远比分机号多)。所以,Context 就是多个 Extension 的逻辑集合,它相当于一个分组,一个 Context 中的 Extension 与其它 Context 中的 Extension 在逻辑上是隔离的
在新部署的FreeSWITCH所携带的演示配置实例中,定义了三个context(只要你想,可以配置任意多个):
● default: 处理内部信任用户所发起的呼叫
● public:处理其它非系统用户发起的呼叫
● features: 呼叫不会直接发到这里,但其它两个context会从这里“借用”一些特性
第二、拨号计划的介绍:
例一:Channel 的建立过程。
FreeSWITCH 默认的配置提供了 1000 - 1019 共 20 个 SIP 账号,密码都是 1234 。
<extension name="Local_Extension">
<condition field="destination_number" expression="^(10[01][0-9])$">
<action application="set" data="dialed_extension=$1"/>
<action application="export" data="dialed_extension=$1"/>
</condition>
</extension>
这个框架说明,用正则表达式 (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。
补充:set 是将变量设置到当前的 Channel 上,即 a-leg。而 export 则也将变量设置到 b-leg 上。
例二:拨号框架:extension、condition和action讲解。
<extension name="this_extension">
<condition field="destination_number" expression=""^(1234)$">
<!-- action -->
<!-- action -->
</condition>
</extension>
如上所示,在一个拨号方案的"context"(演示配置的"default"或"public")内,我们会看到"extension"的的起始标记,紧接着是condition开始标记,最后是condition结束标记和extension结束标记,在开始标记和结束标记之间,是一系列的action描述。
在上面实例中,condition检查"destination_number"变量值是否与正则表达式"^1234$"匹配。如果拨打的号码是1234,那么,condition起始标记和结束标记间的所有action都会被添加到TODO列表中(结束拨号方案遍历之后,TODO列表中的所有action都会被执行)。
补充说明正则:
1,^:表示“字符串开始”,
2,$:它表示字符串结束;
3,这两个标记符之间,用圆括符括起一些其它字符。圆括符会“抓取”它包含的内容,并把它放在一个“寄存器”里。在本例中,文本“1234”被放在寄存器“$1”中(如果有多对圆括符,那么每二对里的内容放在寄存器“$2”中,以此类推)。
以下是一些正则表达式的释义:
模式 | 含义 |
345 | 匹配包含序列“345”的任何字符串 |
^345 | 匹配以序列"345"打头的任何字符串 |
345$ | 匹配以序列"345"结尾的任何字符串 |
^345$ | 匹配与序列“345”完全相同的字符串 |
\d | 匹配任何单个数字(0-9) |
\d\d | 匹配两个连续数字 |
^\d\d\d$ | 匹配长度正好是三位的数字字符串 |
^\d{7}$ | 匹配长度正好是七位的数字字符串 |
^(\d{7})$ | 匹配长度正好是七位的数字字符串,并把匹配的值存入变量$1 |
^1?(\d{10})$ | 匹配可能以数字"1" 打头,并跟着10位长度的数字字符串,并把后10位数字存入变量$1。(原字符串可能是10位或以1打头的11位数字)。 |
^(3\d\d\d)$ | 匹配以3打头的任何四位长度的数字字符串,并把整个数值存入变量$ |
^1234$ ^ 匹配字符串开头,$ 匹配结尾,所以本表达式严格匹配 1234
^1234|5678$ | 是或的意思,表示匹配 1234 或 5678
^123[0-9]$ [ ] 表式匹配其中的任意一个字符,其中的 - 是省略的方式,表示 0 到 9,它等于 [0123456789]
也就是说它会匹配 1230,1231,1232 ... 1239
^123\d$ 同上,\d 等于 [0-9]
^123\d+$ + 号表示1个或多个它前面的字符,因为 + 前面是 \d,所以它就等于1个或多个数字,实际上,
它匹配任何以123开头的至少4位数的数字串,如1230,12300,12311,123456789等
^123\d*$ *号与+号的不同在于,它匹配0个或多个前面的字符。
所以,它匹配以123开头的至少3位数的数字串,如 123,123789
^123 跟上面一样,由于没有结尾的$,它匹配任何以123开头的数字串,但除此之外,它还匹配后面是字母的情况,如 123abc
123$ 匹配任何以123结尾的字符串
^123\d{5}$ {5}表示精确匹配5位,包含它前面的一个字符。在这里,它匹配以123开头的所有8位的电话号码
^123(\d+)$ ( )在匹配中不起作用,跟^123\d+是相同的,但它对匹配结果有作用,
匹配结果中除123之外的数字都将存储在$1这个变量中,在下一步使用
^123(\d)(\d+)$ 如果用它跟12345678匹配,则匹配成功,结果是 $1 = 4, $2 = 5678
例三:Action与Anti-action的作用
这两个命令都存在拨号盘中,如果条件表达式匹配成功,则执行Action;否则执行Anti-action。
<extension name="TEST">
<condition field="destination_number" expression="^4321$">
<action application="answer"/>
<action application="log" data="YES"/>
<anti-action application="log" data="ERROR"/>
<action application="hangup"/>
</condition>
</extension>
如果拨号4321,控制台则输出YES。执行:answer、hangup指令,但不执行anti-action操作。
如果拨号其他非4321,控制台输出ERROR,其他的指令不在执行。
例四:多condition组合使用
<extension name="that_extension">
<condition field="ani" expression=""^54321$"/>
<condition field="destination_number" expression=""^1234$">
<!-- action -->
<!-- action -->
</condition>
</extension>
它的含义是:首先检查"ani"变量(主叫号码)是不是54321。如果是,接着检查第二个条件:目标号码是不是1234。如果是,则把action添加到TODO列表中
补充说明:
1, ani:主呼叫号码
2, destination_number:呼叫的目标号码
3,condition有一个隐含属性:"break='on-false'"。
例五:Break属性
Break属性的可选值(它的含义是跳出条件判断逻辑,跳出extension):
1,on-false: 这是缺省值,如果表达式值为 false ,不再检查下一个条件
2,on-true: 如果表达式值为 true ,不再检查下一个条件
3,never: 永远不跳出,继续检查后续条件
4,always: 不检查后续条件,直接跳出
<extension name="other_extension">
<condition field="ani" expression=""^5551212$" break="on-true"/>
<condition field="destination_number" expression=""^1234$">
<!-- action -->
<!-- action -->
</condition>
</extension>
在这个示例中,如果主叫号码不是5551212,那么需要判断第二个条件。如果呼叫的是5551212那么就不用再判断第二个条件了。
例六:info属性
<extension name="show_info">
<condition field="destination_number" expression="^10086$">
<action application="answer"/>
<action application="info"/>
<action application="sleep" data="1000"/>
<action application="hangup"/>
</condition>
</extension>
补充说明:
1,检查destination_number变量,是否是10086,
2,如果是呼叫号10086,就应答(answer),
3,执行"info" app,将在控制台上转储每个通道变量和它的值。
4,休眠1秒(sleep)
5,挂机(hangup)
上述简单的例子主要通过info,把通道信息全部列出来,方便将来的调试使用。
例七:通道变量的获取
以"variable_"前缀打头的行,都可以在拨号方案中以${}结构访问。
比如,上面输出中的最后一行:"variable_current_application: [info]",它表示通道变量"${current_application}"当前的值是"info"。
写一个新的extension来示范一下:
<extension name="test">
<condition field="destination_number" expression="^1234$">
<action application="answer"/>
<action application="log" data="current_channel variable is: ${current_application}"/>
<action application="hangup"/>
</condition>
</extension>
例八:自定义通道变量
设置通道变量nice_guy,并获取该值信息
<extension name="Test">
<condition field="destination_number" expression="^1234$">
<action application="answer"/>
<action application="log" data="current_channel variable is: ${current_application}"/>
<action application="log" data="nice_guy is: ${nice_guy}"/>
<action application="set" data="nice_guy=verymuch"/>
<action application="log" data="nice_guy is: ${nice_guy}"/>
<action application="hangup"/>
</condition>
</extension>
补充说明:
第一次打印nice_guy is:
第一次打印nice_guy is:verymuch(通过set已经赋值了,故有第二次输出有内容的)
例九:Set的"inline"属性
如果你在拨号方案中设置一个通道变量,然后在条件匹配中检查这个变量,它将无法如你预期的样子工作!这是因为,设置变量的"action"将在EXECUTE阶段执行,而条件检查是在之前的ROUTING阶段进行的。 为了达到目的,你必须在set时使用"inline"属性,这样能把参数设置提前到ROUTING阶段,如下:
<extension name="Test">
<condition field="destination_number" expression="^1234$">
<action application="answer"/>
<action application="set" data="key=789" inline="true"/>
</condition>
<condition field="${key}" expression="789">
<action application="log" data="YES"/>
<anti-action application="log" data="ERROR"/>
</condition>
</extension>
本例是在条件1下,action阶段设置key=789信道变量,然后再第二条件condition中获取。如果不在set设置时候增加inline=true标记,否则获取不到该值。
第三、外连拨号参数讲解:
1,answer
answer APP向主叫方发摘机信号,并建立媒体流。在SIP术语中,它触发FreeSWITCH发送一个"200 OK"消息,协商音视频编码,并开始RTP流传输。
<action application="answer"/>
2,bridge
bridge APP发起新的呼叫作为B-leg,并把新建的B-leg媒体流与呼入的媒体流(也就是A-leg)桥接。如果拨号串以逗号分隔,那么拨号是同时进行的。如果拨号串以管道符分隔,那么拨号是按次序进行的。第一部摘机的话机将接收话务,同时会取消其它话机的呼叫。
<action application="bridge" data="user/1004"/>
<action application="bridge" data="sofia/gateway/my_gateway_name/$1"/>
3,hangup
hangup APP断开媒体流并结束通话。
<action application="hangup"/>
<action application="hangup" data="USER_BUSY"/>
4,ivr
ivr APP把话务接到一个预定义的IVR。
<action application="ivr" data="ivr_demo"/>
5,play_and_get_digits
play_and_get_digits会向主叫播放一个音频文件,同时监听主叫的DTMF按键,并作出响应。这在编写Freeswitch脚本时尤其有用,它允许完整的应用程序逻辑和控制流。 在拨号方案里,你可以用它实现简单的交互,而不需要调用ivr app和XML IVR语言。
<action application="play_and_get_digits" data="4 6 3 7000
/path/to/sound.wav /path/to/invalid.wav my_keys \d+ "/>
<action application="log" data="User entered these digits: ${my_keys}"/>
参数说明:
- 最少输入两位
- 最多输入五位
- 尝试三次
- 等待8秒没有输入认为输入结束
- 输入井号表示结束
- 搜集输入时播放/path/to/sound.wav
- 非法输入时播放/path/to/invalid.wav
- 用通道变量my_keys存储输入内容
- 以模式匹配\d+检查输入内容
6,playback
playback APP向主叫播放一个音频文件。文件格式可以是FreeSWITCH模块支持的任意格式。FreeSWITCH演示配置中,所有音频文件都是wav格式的。
<action application="playback" data="goodbye.wav"/>
7,sleep
sleep暂停拨号方案执行,参数单位毫秒
<action application="sleep" data="1000"/>
8,transfer
transfer app把话务送回拨号方案。这将触发一个全新的ROUTING阶段和EXECUTION阶段。
<action application="transfer" data="54321"/>
<action application="transfer" data="789 XML custom"/>
9,sofia
基本上Sofia的拨号串有两种格式。
<!-- Correct -->
<action application="bridge" data="sofia/gateway/myway/user">
<!-- 通过internal profile拨号的等效项是这样的-->
<action application="bridge" data="sofia/internal/user@192.168.1.8 /">
10,echo
挂起 听到自己的回声
<action application="echo" data=""/>
11,delay_echo
挂起 延迟 听到自己的回声
<action application="delay_echo" data="5000"/>
12,park
<action application="park"/>
13,log
日志内容输出
<action application="log" data="INFO Hello FreeSwitch,I am stupid."/>
14,info
查看信道所有信息,在 FreeSWITCH 中,每一次呼叫都由一条或多条“腿”(Call Leg)组成,其中的一条腿又称为一个 Channel(信道),每一个 Channel 都有好多属性,用于标识 Channel 的状态,性能等,这些属性称为 Channel Variable(信道变量),简写为 Channel Var 或 Chan Var 或 Var。
<action application="info" data=""/>
15,set
set app设置一个通道变量
<action application="set" data="my_chan_var=example_value"/>
<action application="log" data="INFO my_chan_var contains ${my_chan_var}"/>
16,export
设置一个变量
<action application="set" data="dialed_extension=$1"/>
<action application="export" data="dialed_extension=$1"/>
set 是将变量设置到当前的 Channel 上,即 a-leg。而 export 则也将变量设置到 b-leg 上。当然,这里 b-leg 还不存在。所以在这里它对该 Channel 的影响与 set 其实是一样的。因此,使用 set 完全是多余的。但是除此之外,export 还设置了一个特殊的变量,叫 export_vars,它的值是dialed_extension。
17,bind_meta_app
作用是在该 Channel 是绑定 DTMF
<action application="bind_meta_app" data="1 b s execute_extension::dx XML features"/>
<action application="bind_meta_app" data="2 b s record_session::$${recordings_dir}/${caller_id_number}.${strftime(%Y-%m-%d-%H-%M-%S)}.wav"/>
<action application="bind_meta_app" data="3 b s execute_extension::cf XML features"/>
<action application="bind_meta_app" data="4 b s execute_extension::att_xfer XML features"/>
上面四行分别绑定了 1、2、3、4 四个按键,它们都绑定到了 b-leg上。注意,这时候 b-leg 还不存在。
18,voicemail
语音信箱
<action application="voicemail" data="default ${domain_name} ${dialed_extension}"/>
第四、内连拨号计划 - Inline Dialplan
Inline dialplan 与 Action 中的 inline 参数是不同的。它与 XML Dialplan 不同,它没有 Extension,也没有复杂的 Condition。而只是象 XML Dialplan中那样简单的 Action 的叠加。
它的语法格式:
app1:arg1,app2:arg2,app3:arg3
从语法可以看出,它只是多个 APP 以及参数组成的字符串,APP之间用逗号分隔,而APP与参数之间用冒号分隔。如果参数中有空格,则整个字符串都需要使用单引号引起来。在我们上面的例子 中,你通过拨打 9196 来找到对应的 XML dialplan,在这里,我们可以直接在命令行上写出对应的 inline 形式:
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>]
首先,它的第一个参数是呼叫字符串, 第二个参数可以是 & 加上一个 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。在 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 中的 echo 则是一个 APP,当然你也可以顺序执行多个 APP:
originate user/1000 answer,playback:/tmp/pleace_leave_a_message.wav,record:/tmp/recording.wav inline
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'
当然你也可以用在任何需要 Dialplan 的地方,如(以下两行实为一行)
uuid_transfer 2bde6598-0f1a-48fe-80bc-a457a31b0055
'set:test_var=test_value,info,palyback:/tmp/beep.wav,record:/tmp/recording.wav'
第五、应用场景:
场景一:
freeswitch 调试dialplan,自动接听,播放一段彩铃后开始录音,录音指定时间后自动挂机
<!-- 单腿 录音-->
<extension name="onelegrecord" >
<condition field="destination_number" expression="^(1001)$">
<action application="answer"/>
<action application="playback" data="/home/freeswitch/voice/rengongluzhi.mp3"/>
<action application="record_session" data="/home/freeswitch/record/${strftime(%Y-%m-%d-%H-%M-%S)}_${destination_number}_${caller_id_number}.mp3"/>
<action application="sleep" data="10000"/>
<action application="hangup"/>
<action application="info" data=""/>
</condition>
</extension>
红色背景部分,使用record_session,则会起一个进程单独录音,后面继续执行,如果使用record,则record一致会录下去,除非对方挂机。
<action application="sleep" data="10000"/> 这里指定录音时间是10秒
场景二:
拨打1237(也可以自定义其他号码),就可以开始录制拨打电话这方的声音,可以指定生成文件的位置,挂断后wav文件生成完成。
<extension name="record">
<condition field="destination_number" expression="^1237$">
<action application="answer"/>
<action application="playback" data="tone_stream://%(100,1000,800)"/>
<action application="record" data="/tmp/$1.wav"/>
</condition>
</extension>
其中,/tmp/$1.wav是录音生成文件的路径和文件名。下载到windows本地,然后直接播放,就可以听到自己的声音了。
场景三:
没什么好解释的,如果拨 9196,就能听到自己的回声
<extension name="echo">
<condition field="destination_number" expression="^9196$">
<action application="answer"/>
<action application="echo"/>
</condition>
</extension>
场景四:
延迟回声 与 echo 基本一样,但回声会有一定延迟,5000 是毫秒数。
<extension name="delay_echo">
<condition field="destination_number" expression="^9195$">
<action application="answer"/>
<action application="delay_echo" data="5000"/>
</condition>
</extension>
第六、出局Socket – 让话务主动连接你
拨号方案"socket"向指定IP:端口发起一条出局的TCP连接,这时第三方可以控制呼叫,这和连接mod_event_socket的入局控制完全一样。
你必须先运行一个“服务”应用,它监听指定的IP:端口,并准备接收"socket"拨号方案APP发起的连接。一旦TCP连接建立,命令和输出内容和我们之前所见的完全一样。
当你调用socket时,FreeSWITCH会自动把话务置为parked状态。
在拨号方案中调用socket的语法是:
<ip>:<port> [<keywords>]
下面是几个具体的使用实例:
<action application="socket" data="127.0.0.1:8084"/>
<action application="socket" data="127.0.0.1:8084 async"/>
<action application="socket" data="127.0.0.1:8084 full"/>
<action application="socket" data="127.0.0.1:8084 async full"/>
关键字full 和async 会改行为:
- async:关键字async说明所有命令将立即返回(异步执行),从而可以在执行命令堆栈的同时监视socket的事件。如果没有async关键字,那么每个event socket都会堵塞到执行完毕。
- full:关键字full说明对端将拥有event socket的完整命令集。这和入局event socket连接的命令集是一样的,因此,你可以执行API命令,获取全局事件,等等。如果没有full关键字,那么命令集将被限制为特定呼叫相关的范围。换句话说,如果没有full关键字,那么,发到这个socket的命令只能影响当前正在处理的通道。同样地,这个socket连接将只接收这条通道相关的事件。