asterisk拨号脚本学习

引言:

asterisk的英文翻译是✳,但他实现的却是一个开源IPPBX的功能,也就是电话交换交换机的功能,你可以理解成任何关于通话的功能asterisk都能做,你也可以根据自己的需求更改一切,包括协议栈,以及相应的操作界面,而操作界面配置通话信息的本质就是修改asterisk的拨号脚本,asterisk进程再根据拨号脚本来实现通话。

这句话几乎就是句废话,唯一要记得就是,配置通话,就是改拨号脚本。

基本组成

如果你的电脑已经安装好了asterisk,在/etc/asterisk下可以看到许多配置文件,这些配置文件便是编辑拨号脚本的地方,如果没有,你可能漏掉了某些编译步骤,可以参考我的另一篇文章。

要点:/etc/asterisk下看到的配置文件就是所谓的拨号脚本。

我们的拨号脚本由四个基本概念组成的:上下文(contexts),分机(extensions),优先级(priorities),应用(applications)。

我们可以通过下面这个例-1脚本来解释,asterisk拨号脚本的基本组成
例-1:

[ivrs]
exten = 6500,1,Gotoif($[${LEN(${APIDTMFFLAG})}>0]?2:3)
exten = 6500,2,UserEvent(DtmfEndString,Info: ${EXTEN})
exten = 6500,3,Set(APIDTMFFLAG=1)
exten = 6500,4,UserEvent(UpdateCallee,Callee: 6500)
exten = 6500,5,Gotoif($[${APIDIALSTATUS}==BUSY]?h,1)
exten = 6500,6,Set(YSGROUPEXTEN=6500)
exten = 6500,n,Set(CDR(orgdst)=6500)
exten = 6500,n,ExecIf($[${LEN(${INROUTERNAME})} > 0]?Set(INBOUNDTOIVR=yes))
exten = 6500,n,Goto(ivr_6500,6500,1)
exten = h,1,Hangup()

看不懂?不可能吧亲。
[ivrs]这就是一个context,ivrs是这个context的名字,凡是用[]包起一传字符,那这就是一个context,而后面的一大串,都是context的内容,这个context包含的内容会直到遇到下一个[]。
exten表示分机,6500表示分机号,1,2,…表示优先级,Gotoif表示相关的操作函数。

1.上下文(contexts)

例-1中的 [ivrs] 便是contexts。我们的拨号脚本是由一个个称为 contexts 的部分组成的。contexts 保持了脚本中不同部分作用的相互独立。在一个 contexts 中定义的 extension 和在另一个contexts 中定义的 extension 是完全独立的,除非我们特别允许它们可以相互作用,即通过include指令来实现。举个例子,每个 contexts 相当于一个部门,每个 contexts 中的 extension 都是部门的成员,比如说研发部门的人与测试部门的人是相互独立的,研发部门的人不用去做测试的工作,测试部门的人不用去做研发的工作。一通电话相当于一个三合一迭代,从技术支持部门到产品到研发再到测试,最后发布版本,相当于一通电话走了几个 contexts 之后结束了,这个功能(如内部分机互打、分机打到IVR、打外线…)也就结束了。

以上这句话理解不了就算了,就是想告诉你,不同context之间是独立的,纳尼?什么是独立?就是你打10086会进中国移动,你打10000会进入中国电信,接通后会有不同的语音客服,以及不同的语音操作。

2.分机(extensions)

例-1中的 exten = 6500 便是 extensions。
extensions 的语法是exten,然后紧跟一个等于符号,即:
exten =
再然后跟着的是这个extension的名字或者数字号码。
当 extensions 中保留有相同的部分时,我们还可以使用same = 来简化。即上面的例子,我们还可以简化成:

[ivrs]
exten = 6500,1,Gotoif($[${LEN(${APIDTMFFLAG})}>0]?2:3)
 same = n,UserEvent(DtmfEndString,Info: ${EXTEN})
 same = n,Set(APIDTMFFLAG=1)
 same = n,UserEvent(UpdateCallee,Callee: 6500)
......

我们分析这句语句,它难吗?说实话,一开始看确实挺难的,一大堆字母,但是我们静下心来分析。
[ivrs],中括号里面包字符串,很明显代表一个context,既然他是一个context,那后必定会包含各种操作。
于是乎就有了exten=6500,也就是这个context有6500这个电话,接着是1,1代表优先级,意思是如果你拨打6500优先会走优先级为1的这条,然后依次往下走。至于最后面的Gotoif,就是一个函数,什么,函数都不明白?赶快回炉重造!看名字我们就是知道Gotoif其实就是一个跳转的函数,但是到哪里呢,我们继续往下看。${}是一个取变量值的操作符,就好像取地址的内容使用的*一样,因此${APIDTMFFLAG}的含义就是取APIDTMFFLAG这个变量的内容,至于他在哪里定义的,暂时不关我们的事,而这里的LEN又是一个函数,是取长度的函数,于是乎${LEN(${APIDTMFFLAG})}的含义就变成了取APIDTMFFLAG这个变量对应的字符长度,而$[${LEN(${APIDTMFFLAG})}>0]则是一个判断语句,我们就可以知道$[]是一个判断语句,判断ADPIDTMFFLAG的长度是否大于0,若是的话则gotoif(2),否则gotoif(3)。

3.优先级(priorities)

例-1中的1 2 3 4 5 6 n …就是优先级。priorities 是一个数字序列,从1开始,并且每次执行一个指定的 application 。
为什么要写非数字编号优先级(n)?因为每当asterisk遇到优先级是n的语句时,会自动转换为前一个优先级+1。如果优先级全是写数字,那么想要在中间增加一些东西,所有后续的优先级都需要手工重编号,那就太麻烦了(asterisk是不会执行未编号或错误编号的步骤的)。

这里记住,我们可以用label标签重命名纯数字的优先级。

4.应用(applications)

例-1中的优先级后面的部分便是 applications, 即,实际干活的部分。每个applications都会在当前的channel(通道)上执行一个特定的操作,例如设置通道变量,播放一段声音,挂机等等。

定义分机

pjsip.conf
[1003](endpoint-basic)                  
transport_name=udp                      
context=DLPN_DialPlan1003               
auth=1003-auth                          
outbound_auth=1003-auth                 
......

我们定义分机是定义在pjsip.conf中的(如果你使用的是chan_sip则在sip.conf中),对于每一个分机都必须有的一个参数就是 context,这个 context 参数定义了一个指向extensions.conf 的指针,它定义了这个分机是怎么插入拨号脚本的。例子中是 context=DLPN_DialPlan1003 ,那么它就会对应的去找extensions.conf 配置文件里面对应的 [DLPN_DialPlan1003] context

我们在chan_siip中定义一个分机,但是其具体如何拨号的,是被定义在extensions.conf中。

样式匹配

asterisk提供样式匹配的机制,允许我们在拨号脚本中创建一个能够匹配许多不同号码的extension。样式是从一个下划线_开始的,这告诉asterisk我们正在匹配一个样式,而不是一个精确的extension名字或号码。

很重要,电话号码实在是太多了,如果一个电话号码,我们定义一种拨号规则多累,因此我们需要定一个拨号规则,能给多个分机使用,这样就必须使用通配符的操作。

在下划线之后,我们可以使用一个或多个下列字符:

X 匹配任意09之间的一个数字

Z 匹配任意19之间的一个数字

N 匹配任意29之间的一个数字

[15-7] 匹配指定范围的一个数字,这个样式要求匹配一个1,5,6,7中的任意一个数字

.() 匹配一个或多个字符,不论它们是什么

!(叹号) 匹配0个或多个字符,不论它们是什么

举两个小例子来巩固一下:

_X. 匹配所有数字开头的位数至少为2的字符串

_NXX 匹配任意从200999之间的三个数字号码的extension

_[96-8] 匹配9,6,7,8中的任意一个数字

这段话很重要,X匹配任意0-9之间的一个数字,X可以代表0-9中任何一个数字,为什么用X呢因为X是罗马数字十,他涵盖了0-9之间的十个数字。Z删掉了0,N删掉了0,1,2。

其余特殊语法

1.include

拨号脚本中,我们可以通过include指令来访问脚本中的不同部分,但要注意包含的顺序。asterisk将首先尝试匹配当前context中的extensions,如果没成功,它再尝试匹配第一个被包含的context 中的extensions(包括这个context中包含的其他context),然后再继续匹配下一个被包含的context。

抓住重点:拨号的时候,会扫描当前context中的全部分机,如果当前context没有分机则会寻找Include所包含的context中包含的分机。

拿我们的 IVR 来举例:

[ivr_6500]
exten = 6500,1,NoOp(6500)
......
exten = 0,1,ExecIf($[${Ivr2outbound} = yes]?Set(CDR(Ivr2outbound)=yes))         //按键0
......
include = conferences
include = queues
include = ivrs
......
include = from-outside2-6500
......
[from-outside2-6500]
exten = _X.,1,GotoIf($[${CALL_PERMISSION(6500,${EXTEN})}=yes]?from-outside,${EXTEN},1:others,${EXTEN},1)

用户拨打6500号码,进入到 [ivr_6500],接着拨打队列6700,由于在[ivr_6500]中匹配不到extension6700,于是asterisk匹配[conferences],如果没匹配到,接着匹配[queues]…如果此时,我们把 include = from-outside2-6500 写在 include = queues前面,请问,会产生什么结果呢?为什么?

这个问题就是废话,那当然先走[from-outside2-6500]了,直接匹配到_X.

1.拨号脚本中include如果存在2个及以上context中包含相同extension匹配规则的情况,拨打的号码会按顺序来匹配。例如:
[DialPlan_1000DLPN]
include = outroute1
include = outroute2
[outroute1]
exten = _91.,1,Noop(... ...)
[outroute2]
exten = _91.,1,Goto(... ...)

此时 1000 拨打 9130,第一条先匹配到,就会走 outroute1 。
做个小修改1:

[outroute1]
exten = _9.,1,Noop(... ...)

将outroute1的匹配样式改为_9.,此时拨打 9130,也是会从 outroute1 出去,而不会去找精确度高的样式。
再做个小修改2(include同名的context):

[Mobile_DLPN_DialPlan1003]
include = outroute1
include = outroute1
[outroute1]
exten = _X,1,Noop(... ...)
[outroute1]
exten = _X.,1,Goto(... ...)

include 的 context 一样,此时拨打个位数号码走第一条,拨打两位及以上号码走第二条,效果与include 不一样的 context 一样。
这里我只想重复一点,可以写同名的context,而且匹配时会寻找正确的那条context。

使用 dialplan show 命令可以看到,它其实是只有一条的:

IPPBX*CLI> dialplan show Mobile_DLPN_DialPlan1003
[ Context 'Mobile_DLPN_DialPlan1003' created by 'pbx_config' ]
  Include =>        'outroute1'                            [pbx_config]

-= 0 extensions (0 priorities) in 1 context. =-
IPPBX*CLI> dialplan show outroute1
[ Context 'outroute1' created by 'pbx_config' ]
  'h' =>            1. Hangup()                                   [pbx_config]
  '_X.' =>          1. Goto(... ...) [pbx_config]
                    ......
  '_X' =>           1. Noop(... ...) [pbx_config]
                    ......
-= 3 extensions (37 priorities) in 1 context. =-

再巩固一下(include同名的context)

[ivr_6500]
......
include = pagegroups                   
include = from-outside2-6500                                  
include = from-outside2-6500           


[from-outside2-6500]             
exten = _2000,1,NoOp(__1____)         // (1)                  


[from-outside2-6500]                                                                          
exten = _X.,1,NoOp(__2____)           // (2)
exten = _X.,2,GotoIf($[${CALL_PERMISSION(6500,${EXTEN})}=yes]?from-outside,${EXTEN},1:others,${EXTEN},1)  // (3)

使用 dialplan show 命令可以看到:

实际通话时是(1) -> (3) 的;如果步骤(2)(3)的优先级分别改成2,3,那么实际通话就是(1) -> (2) -> (3)
意思就是,优先走能拨通的。

2.对于include指令,它不止能用来包含contexts,还能用来包含其他的配置文件,比如我们的extensions.conf中开头就包含了其他的配置文件:
#include extensions_countrycode.conf
[general]
static = yes
writeprotect = no
autofallthrough = yes
......

那么,在我们的 extensions.conf 里面就可以使用(include)到 extensions_countrycode.conf 里面的 contexts了。

重点:学习c的引用头文件的方式可以引用别的配置文件,以此来引用别的context。

2.常用的应用及参数

首先来看一个例-2:

exten = 345,1,Set(TEST=1)
 same = n,GotoIf($[${TEST} = 1]?weasels:lguanss)
 same = n(weasels),Playback(weasels)    //此处会播放一个名为weasels.wav的声音文件
 same = n,Hangup()
 same = n(lguanss),Playback(lguanss)     //此处会播放一个名为lguanss.wav的声音文件
 same = n,Hangup()

Set(): 给某个变量赋值,此处是给TEST赋值为1
GotoIf(): 条件跳转。${TEST}是取TEST的值,即1。$[${TEST} = 1]? weasels:lguanss,意思是判断TEST的值是否为1,若为1,表达式结果为真,则跳转到weasels,否则跳转到lguanss (${}是取值符,$[]是运算符)
n(weasels): 括号里的称为lable(标签),它存在的意义是为了方便大家理解呼叫从一个地方跳转到标签的地方去。
Playback(): 用于在某个channel上播放一个预先记录下来的声音文件,在Playback()的操作过程中,用户的输入是会被忽略的
Hangup(): 挂机,不需要额外的信息(参数)就能完成操作,这个叫无条件挂机;当然,里面也可以有一些参数,比如USER_BUSY,用于说明挂机的原因,有参数的情况比较少见,我们这里就不多介绍了

当然,我们还可以使它跳转到一个独立的extensions中而不是跳转到一个priority labels。于是,上面这个例-2也可以改写成:

exten = 345,1,Set(TEST=1)
exten = 345,n,GotoIf($[${TEST} = 1]?weasels,1:lguanss,1)
exten = weasels,1,Playback(weasels)
exten = weasels,n,Hangup()
exten = lguanss,1,Playback(lguanss)
exten = weasels,n,Hangup()

我们还可以略去GotoIf()中的跳转地址,将例-2改写成:

exten = 345,1,Set(TEST=1)
 same = n,GotoIf($[${TEST} = 1]?:lguanss)           // exten = 345,2,GotoIf($[${TEST} = 1]?:lguanss)
 same = n,Playback(weasels)    //此处会播放一个名为weasels.wav的声音文件
 same = n,Hangup()
 same = n(lguanss),Playback(lguanss)     //此处会播放一个名为lguanss.wav的声音文件
 same = n,Hangup()

其实就是把GotoIf()的第一个跳转地址略去,把标签(weasels)也去掉,此时如果TEST的值为1,它会直接执行下一步。
结论:条件跳转两个目的地中的任意一个都可以被忽略,但是不能两个都被忽略。当执行到被忽略的目的地时,asterisk将简单的执行当前extension中的下一个priority。

再来看一个例-3:

[ivr_6500]
exten = 6500,1,NoOp(6500)
exten = 6500,n,Set(TIMEOUT(digit)=3)
exten = 6500,n,Background(record/ls_builtin_default)
exten = 6500,n,UserEvent(PlayPromptEnd,Ivrid: ${EXTEN})
exten = 6500,n,WaitExten(3)
exten = 6500,n,ExecIf($[${Ivr2outbound} = yes]?Set(CDR(INGORELOCAL1CDR)=yes))
exten = 6500,n,Goto(from-outside,2000,1)

[from-outside]
exten = 1000,1,Gotoif($[${APIDTMFFLAG}==1]?2:4)
......
exten = 2000,1,Gotoif($[${APIDTMFFLAG}==1]?2:4)
......
exten = 3000,1,Gotoif($[${APIDTMFFLAG}==1]?2:4)
......

NoOp()打印操作,当通话进行时,会把6500这个分机所占用的通道显示出来。
Set设置变量,如:TIMEOUT(digit)=3就等于设置超时时间。
Background播放背景音乐,WaitExten等待用户输入,参数为超时时间。
ExecIf为条件执行

NoOp(): 可将数值打印到控制台来工作;假设这里是拨打6500会走这个脚本,那么NoOp(${EXTEN}6500)最终打印出来会出现:
"PJSIP/1003-00000001"是对应的通道(哪个通道走的这个脚本)

Background(): 可以播放一个预先录制好的声音文件,跟Playback()不同的是,当用户按下电话上的按键时,它会中断播放的声音,并根据用户输入的数字把这个呼叫跳转到对应的extension中去。大多数IVR便是用的这个。

UserEvent(): 发送专用事件给管理接口,这里是发送PlayPromptEnd给ami,别的用户可能会去监听这个事件,这个对拨号脚本没有什么特别的操作,这里就不多说了。

WaitExten(): 一般是跟在Background()之后使用,作用是等待用户的输入,例-3中即等待3s

ExecIf(): 条件执行,当Ivr2outbound值为yes时,表达式
[${Ivr2outbound} = yes]结果为真,执行Set(CDR(INGORELOCAL1CDR)=yes),否则执行下一步

Goto(): 用于将一个呼叫跳转到拨号脚本的另一个部分,需要将context,extension,priority作为参数传递给它,语法是exten = n,Goto(context,extension,priority)

例子中的意思是:跳转到 context 为 from-outside 中的 2000extension 的优先级为1的地方。
Goto()这个应用还可以只有两个参数,或者只有一个参数,或者直接为 lable。

两个参数:

[macro-realstexten]
......
exten = s,3,Goto(s-BUSY,4)              //两个参数的情况下,指的是该context中的extension和priority
exten = s,4,NoOp(......)
......
exten = s-BUSY,4,Gotoif($[${APIDIALSTATUS}==BUSY]?4)
......

两个参数第一个参数可以跳转到别的分机号

一个参数:

[macro-realstexten]
......
exten = s,3,Goto(4)              //一个参数的情况下,指的是该context中与自己相同的extension的priority
exten = s,4,NoOp(......)
......
exten = s-BUSY,4,Gotoif($[${APIDIALSTATUS}==BUSY]?4)
......

一个参数就默认是当前分机了,但是要跳转的优先级一定要自己写

lable:

[outrouter_AAAOUT]
......
 same = n,Goto(a1)                //lable也是一个参数,跳到对应的lable中去  
......
 same = n(a1),NoOp(NO ADJUST)
......

次数的ai是对应的标签名字,其对应n的值

补充:
如果你想在 extensions.conf 打印东西来调试,保存文件dialplan reload ,一系列操作正确后,在 asterisk 后台却又看不到你打印的东西,此时你可以使用dialplan show + tab键来看看你修改的 context 里是否有加载进 asterisk 里面。

IPPBX*CLI> dialplan show 
add_diversion                  add_pai                        add_ppi                        add_rpid                       alert_playback                 
all_outrouters                 always-Hangup                  asterisk_guitools              blacklist-handle               blacklist-inbound              
blacklist-internal             blacklist-internal-admin       blacklist-outbound-exten       blacklist-outbound-global      callback_trunk_123             
callback_trunk_148             callback_trunk_1499            callback_trunk_161             callback_trunk_25              callback_trunk_zhanghao149     
......
3.常见变量
全局变量:

对于所有的channel在任何时间都是可见的。它声明在我们的extensions.conf开始的[globals]context中。截取一部分,如下:

[globals]
AUTOCLIP_SWITCH = no
TOUCH_MONITOR_FORMAT = wav
......
ABSOLUTE_TIMEOUT = 10800
......

全局变量的好处在于,整个脚本都可以使用该变量,而且只需修改一行代码就可以作用到脚本中所有用到这个变量的地方。

通道变量

通道变量是一种只与特定呼叫关联的变量。不同于全局变量,通道变量只存在于呼叫发生期间,并且只对参与呼叫的通道有效。通道变量的设置通过Set()应用来实现。并且在通话过程中,可以使用core show channel …命令来查看。如下图所示:

IPPBX*CLI> core show channel PJSIP/1003-00000008 
 -- General --
           Name: PJSIP/1003-00000008
......
 --   PBX   --
        Context: ivr_6500
......
      Variables:            //通道变量
BACKGROUNDSTATUS=SUCCESS
YSGROUPEXTEN=6500
APIDTMFFLAG=1
......
  CDR Variables:            //CDR变量
......
level 1: SCHEDULEPAGE=yes
level 1: clid="zyh" <1003>
level 1: src=1003
level 1: dst=6500
......

比如说,我们想让预约广播组的CDR特殊处理,那么我就可以设置一个CDR变量SCHEDULEPAGE=yes,然后我在代码中去判断有没有SCHEDULEPAGE这个CDR变量,这个变量的是否有值,如果有就对CDR做你想要的处理。如果这通电话不是预约广播,那么就没有这个CDR变量,也就不会走预约广播对CDR想要的特殊处理了。

PJSIP中的conf信息

例4:

[transport-udp]
type=transport
protocol=udp
bind=0.0.0.0

;Templates for the necessary config sections

[endpoint_internal](!)
type=endpoint
context=from-internal
disallow=all
allow=ulaw

[auth_userpass](!)
type=auth
auth_type=userpass

[aor_dynamic](!)
type=aor
max_contacts=1

;Definitions for our phones, using the templates above

[demo-alice](endpoint_internal)
auth=demo-alice
aors=demo-alice
[demo-alice](auth_userpass)
password=unsecurepassword ; put a strong, unique password here instead
username=demo-alice
[demo-alice](aor_dynamic) 

[demo-bob](endpoint_internal)
auth=demo-bob
aors=demo-bob
[demo-bob](auth_userpass)
password=unsecurepassword ; put a strong, unique password here instead
username=demo-bob
[demo-bob](aor_dynamic) 
在 transport  类型部分我们的配置如下:

type=transport; 这里定义了传输方式,其他配置类型可以使用其参数设置,例如配置中的 objects/sections(例如, endpoints)。

protocol=udp; 这里,我们使用 UDP 协议传输。当然这里无非就是udp和tcp

bind=0.0.0.0; 我们想和所有接口进行通信,无定义其具体IP地址。
注意这里的核心就是type=transport,transport就是传输类型。


在 endpoint 模板中的 endpoint_internal:

type=endpoint; 定义了一个一个 endpoint;用户可以认为这里是一个SIP连接属性设置。

context=from-internal; Asterisk收到从此SIP电话进行的呼叫,具体拨打号码的路由可以查看拨号规则中的context,拨号规则配置文件在/etc/asterisk/extensions.conf中。

disallow=all; 不允许所有的语音编码,仅允许在 'allow'设置的编码。

allow=ulaw; 仅支持 ulaw 语音编码。
此两处设置语音编码


在认证部分的模板中,设置 auth_userpass:

type=auth; 此部分定义认证方式和类型。

auth_type=userpass; 通知Asterisk使用'username''password' 选项来对用户进行认证。一些子认证模板会继承这些设置选项。

在 aor 模板中的aor_dynamic:

type=aor; 定义AOR部分内容。Asterisk 就知道此哪里来发送呼叫到此endpoint。

max_contacts=1; 允许此AOR支持的最大注册数。这里,我们仅允许每个AOR注册一个电话终端。

在 AOR 类型设置部分(查看 type=aor),我们有意没有定义任何的contacts,而是设置了 "max_contacts=1",这样的方式允许我们注册电话到AORs配置文件中,此AORs配置针对每个endpoint。这样的处理方式和旧SIP驱动chan_sip文件的 "host=dynamic" 类似。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

举世无双勇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值