Asterisk权威指南/第六章 拨号计划基础

拨号计划是你的Asterisk系统的心脏。它定义了呼叫是如何流进和流出系统的。拨号计划用一种脚本语言写成的,Asterisk依照其中的指令响应外部触发。和传统电话系统相比,Asterisk的拨号计划是完全可定制的。

本章介绍Asterisk的基本概念。这里讲的内容对你理解拨号计划代码至关重要,同时也是你写任何拨号计划的基础。示例的设计是有前后承接关系的,我们建议你不要逃过本章的太多内容,因为本章对理解Asterisk十分重要。也请你明白本章不可能是对拨号计划的能力的完全阐述;我们的目标是基础知识。后面的章节我们会介绍拨号计划更高级的内容。我们鼓励你做试验。

拨号计划语法

编辑

Asterisk拨号计划在名为extensions.conf的配置文件中定义。

注:extensions.conf文件通常位于/etc/asterisk目录下,但它的位置会视你如何安装Asterisk而不同。这个文件的其他常见位置包括/usr/local/etc/asterisk和/opt/etc/asterisk。

拨号计划由四个主要概念构成:上下文、分机、优先级和应用程序。在解释完这些要素在拨号计划中各自扮演的角色后,我们将让你建立一个基本的但是能工作的拨号计划。

注:示例配置文件。如果你在安装Asterisk的时候安装了示例配置文件,你就很可能已经有了一个现成的extensions.conf文件。不要从示例文件开始,我们建议你从空白开始建立你自己的extensions.conf文件。对于学习如何建立拨号计划来说,从示例文件开始不是最好的方式,也不是最容易的方式。

上下文

编辑

拨号计划被分成不同的部分,称为上下文。上下文使得拨号计划的不同部分之间不会发生交互。在某个上下文中定义的分机是跟任何其他上下文中的分机完全隔离的,除非明确指定有交互。我们会在本章的末尾讲到如何允许上下文之间发生交互。

作为一个简单的例子,想象一下我们有两家公司共享一台Asterisk服务器。如果我们把每家公司的自动接待都放在他们各自的上下文中,他们彼此间就完全分开了。这就允许我们独立地定义什么分机做什么,比如拨分机0的时候:从A公司的语音菜单拨分机0会找到A公司的接待员,而从B公司的语音菜单拨分机0会找到B公司的接待员。(当然,这也表示我们已经告诉了Asterisk拨0的意思就是把呼叫转到接待员。)

上下文通过把名字放到方括号中来定义。名字由字母(大写或小写)、数字、连字符(就是减号)和下划线构成。入局呼叫(incoming calls)的上下文看起来可能像这样:

[incoming]

注:上下文名字的最大长度是79(80个字符减去末尾的1个空字符)。

上下文定义后面的所有指令都是该上下文的一部分,直到下一个上下文定义为止。在拨号计划的开头,有两个特殊的上下文,[general]和[globals]。[general]包含拨号计划的一般设置(你可能永远不必关心这些设置),至于[globals]我们将在“全局变量”一节中讨论。就现在来说,只需要知道这两个名字并不是真正的上下文就可以了。不要用[general],[default]和[globals]作为上下文的名字,其他名字随你挑。

当你定义一个信道(不在extensions.conf中定义,而是在sip.conf,iax.conf和chan_dahdi.conf这一类的文件中)的时候,每个信道都需要的参数之一就是上下文。上下文就是来自信道的连接在拨号计划中开始的地方。信道的上下文设置就是定义你如何把信道插到拨号计划中。

 

注:“插”这个概念对于掌握信道和拨号计划至关重要。一旦你理解了在信道中指定的上下文和拨号计划中相应的上下文的关系,就很容易调试呼叫流程的异常问题了。

上下文一个很重要的用途(也许是最重要的用途)就是提供安全控制。通过正确地使用上下文,你能够控制某些特性(比如打长途电话)只对部分用户开放。如果你设计拨号计划的时候不谨慎,就可能有人不正当地使用你的系统。构建Asterisk系统时一定要留意这一点;Internet上有很多机器人程序专门设计用于识别那些不安全的Asterisk系统,然后盗用它们。

注:https://wiki.asterisk.org/wiki/display/AST/Important+Security+Considerations总结了一些使你的Asterisk系统保持安全的步骤,阅读并理解这篇文章至关重要。如果你忽略这些安全警告,可能会导致任何人都可以用你的系统打长途电话而不用掏钱!如果你不认真地对待Asterisk的系统安全,可能会损失惨重。请确保在系统安全上有足够的投入,以防止盗用。

分机

编辑

在通讯行业,分机这个词一般指的是一串数字,当拨出的时候,可以打通一个电话(或者访问某项系统资源,比如语音信箱,或呼叫队列)。在Asterisk中,分机的概念要强大很多,因为它定义了一个步骤序列(每个步骤包含一个应用程序),Asterisk会按照这些步骤来处理呼叫。

在每个上下文中,我们可以根据需要定义任意数量的分机。当某个分机被触发(通过入局呼叫,或者通过信道上拨出的数字)时,Asterisk会执行分机中定义的步骤。所以,当呼叫流经拨号计划时,是分机决定了系统做什么。分机当然可以用于指定传统意义上的电话号码(比如,分机153可以拨通John的SIP电话机),但在Asterisk中,分机的用途远不止于此。

分机的语法是,一个关键字exten,后面跟一个由等号和大于号构成的箭头,像这样:

exten =>

后面再跟分机的名字(或数字)。当使用传统电话系统时,我们倾向于认为分机就是用于打通另外一部电话的数字号码。在Asterisk中,完全不一样;例如,分机名可以是字母、数字的任意组合。在本章和下一章,数字分机和字母数字分机我们都会用到。

注:为分机指定名字看起来是一个革命性的概念,但是当你意识到很多VoIP运营商支持(甚至鼓励)用名字或者email地址拨号时,这种方式是很有意义的。这是使得Asterisk灵活和强大的特性之一。

分机中的每个步骤都是由三个组件构成的:

  • 分机的名字(或号码)
  • 优先级(分机包含很多步骤;步骤的序号被称为“优先级”)
  • 应用程序(或者说命令),该步骤要执行的动作

这三个组件用逗号隔开,像这样:

exten => name,priority,application()

作为一个简单的例子,真实的分机看起来可能会像这样:

exten => 123,1,Answer()

在这个例子中,分机名是123,优先级是1,应用程序是Answer()。

优先级

编辑

每个分机能够有多个步骤,叫做优先级。优先级按顺序编号,从1开始,并执行一个指定的应用程序。看下面这个例子,分机123接听电话(优先级1),然后挂断(优先级2):

exten => 123,1,Answer()
exten => 123,2,Hangup()

很明显,这段代码没有做什么有意义的事情。这里要说明的问题是,在一个分机中,Asterisk按顺序执行优先级。这种风格的语法还会不时地出现,尽管新代码已经不再这么写了(你很快就会看到了):

exten => 123,1,Answer()
exten => 123,2,do something
exten => 123,3,do something else
exten => 123,4,do one last thing
exten => 123,5,Hangup()

不编号的优先级

编辑

在老版本的Asterisk中,给优先级编号引起了不少问题。想象一下,某个分机有15个优先级,后来需要在第2步插入什么东西:后面所有的优先级都需要重新编号。Asterisk不能处理漏掉的步骤,也不能处理编错了号的优先级,调试这种问题会很没有头绪,也很没意思。

从1.2开始,Asterisk解决了这个问题:它引入了优先级n的使用,n表示next。只要Asterisk碰到了优先级n,就把上一个优先级拿过来加1。这就使得修改拨号计划容易了,因为你不在需要为所有步骤编号了。例如,你的拨号计划看起来会像这样:

exten => 123,1,Answer()
exten => 123,n,do something
exten => 123,n,do something else
exten => 123,n,do one last thing
exten => 123,n,Hangup()

在内部,Asterisk碰到n时会计算优先级编号。需要注意的是,你必须指定优先级编号1。如果你不小心为第一个优先级指定了n而不是1,你会发现重新装载拨号计划后该分机不存在。

“same =>”操作符

编辑

在简化编码工作的持续努力下,一个新的结构出现了,使得分机的编写和维护更容易。只要分机不变,就不再需要每行都写分机名了,只需要写same =>,后面跟优先级和应用程序:

exten => 123,1,Answer()
   same => n,do something
   same => n,do something else
   same => n,do one last thing
   same => n,Hangup()

缩进不是必须的,但它便于阅读。这种风格的拨号计划,使得它更容易在分机之间拷贝代码。我们自己很喜欢这种风格,也强烈建议它。

优先级标号

编辑

优先级标号允许你为分机中的优先级指定一个名字。这使得你可以避免用数字引用某个优先级(而且你也知道,优先级是可以不编号的)。能够引用优先级之所以重要是因为,你会经常把呼叫从拨号计划的一个部分转移到某个分机的某个优先级。后面我们会详细谈到这个问题。要为优先级指定文字标号,简单地把标号放到优先级后面的括号里就可以了,像这样:

exten => 123,n(label),application()

后面我们会讲到如何根据拨号计划的逻辑,在不同的的优先级之间实现跳转。你会看到很多优先级标号,你也会在自己的拨号计划中用到它们。

一个常见的错误就是,写标号的时候在n和(之间插入一个逗号,像这样:

exten => 123,n,(label),application() ;<-- THIS IS NOT GOING TO WORK

这个错误将导致该部分拨号计划无法工作,并且会有错误提示说“应用程序找不到”。

应用程序

编辑

应用程序是拨号计划的工作部件。每个应用程序在当前信道上执行一个特定的动作,比如播放一段声音,接收按键输入,在数据库中查找什么东西,拨打一个信道,挂断电话,诸如此类。在上面的例子中,你看到了两个简单的应用程序:Answer()和Hangup()。你很快就会学习他们是如何工作的。

一些应用程序,像Answer()和Hangup(),不需要更多的信息就能完成工作了。然而,大多数应用程序都需要额外的信息。这些额外的信息被称为参数,被传递给应用程序以影响它们如何完成工作。要给应用程序传递参数,把它们放到应用程序名后面的括号里,用逗号隔开。

注:有时候你也会看到用管道符(|)分隔参数,而不是逗号。从Asterisk 1.6.0开始,就不再支持用管道符作为分隔符了。

Answer(),Playback()和Hangup()应用程序

编辑

Answer()应用程序用于应答一个正在响铃的信道。它会完成信道的初始设置,以便接收来电。正如我们之前提到的,Answer()没有参数。Answer()不是必须的(在有些情况下甚至不用它更好),但它能保证在执行进一步的动作之前信道已经被连接了。

注:Progress()应用程序。有时候在应答一个呼叫之前向网络传回一些信息是很有用的。Progress()应用程序尝试向来电信道提供呼叫进度信息。有些运营商需要这个,所以有时候你可以通过插入一个Progress()来解决一些奇怪的信号问题。

Playback()应用程序用于在信道上播放一个事先录制的声音文件。用户输入被忽略了,这意味着你不能在自动接待中使用Playback(),除非你不想在其间接收用户输入。

注:Asterisk有很多专业录制的声音文件,位于缺省的声音目录下(一般是/var/lib/asterisk/sounds)。编译Asterisk的时候,你可以选择安装各种不同语言、不同格式的声音文件。我们将在很多例子中使用这些文件。例子中的有些文件来自附加声音包,所以请安装它(见“安装Asterisk”)。你也可以访问http://www.theivrvoice.com/,以同样的语音录制你自己的声音提示。本书的后面,我们还会谈到如何用电话和拨号计划建立和管理你自己的声音提示。

使用Playback(),需要指定一个文件名(不带扩展名)作为参数。例如,Playback(filename)将播放名为filename.wav的声音文件,假设它位于缺省的声音目录下。你也可使用文件的完整路径,像这样:

Playback(/home/john/sounds/filename)

这个例子将播放/home/john/sounds/目录下的filename.wav文件。使用相对路径也可以,比如:

Playback(custom/filename)

这个例子将播放缺省声音目录的子目录custom/下的filename.wav文件(可能是/var/lib/asterisk/sounds/custom/filename.wav)。如果指定的路径下存在多个同名但扩展名不同的文件,Asterisk将自己选择一个(根据转码的代价)。

Hangup()做得事情就和它的名字一样:它挂断当前的活动信道。你想结束当前呼叫的时候应该使用它,以防止在你没有意识到的情况下当前呼叫还被保持在拨号计划的某处。Hangup()应用程序不需要参数,但你可以给它传一个ISDN码,如果你希望的话。

随着本书的展开,我们将会向你介绍更多的Asterisk应用程序。

一个简单的拨号计划

编辑

好了,我们已经学习了足够的理论。现在,请打开文件 /etc/asterisk/extensions.conf , 让我们看看你的第一个 dialplan(还记的吗?我们在学习第 5 章时创建的它)。我们将继续在 其上增加一些东西。

Hello World

编辑

按照许多技术书籍的典型的作法(尤其是计算机编程类书籍),我们的第一个例子称为 “Hello World!” 在 extension 中的第一步,我们应答了这个呼叫。在第二步,我们播放了一个名为 hello‐world 的声音文件。然后第三步,我们挂断了这个呼叫。在这个例子中,对应的代码就 是:

 exten => 200, 1, Answer()
   same => n, Playback(hello-world)
   same => n, Hangup()

如果你已经完成了第 5 章的例子,那么你应该已经配置好了两个 IP 电话机,同时你也 已经有了一个包含上述代码的 dialplan。如果你还没有实现第 5 章的例子,那么你需要将下 述代码输入到 /etc/asterisk/ 目录下的 extensions.conf 文件中。

 [LocalSets]
 exten => 100,1,Dial(SIP/0000FFFF0001) ; replace 0000FFFF0001 with your device name
 exten => 101,1,Dial(SIP/0000FFFF0002) ; replace 0000FFFF0002 with your device name
 exten => 102,1,Dial(DAHDI/1)
 exten => 103,1,Dial(DAHDI/2)
 exten => 104,1,Dial(DAHDI/3)
 exten => 105,1,Dial(DAHDI/4)
 exten =>200,1,Answer()
   same => n, Playback(hello-world)
   same => n, Hangup()

如果你还没有配置任何 channels,那么现在是做这件事的时候了。当你从 头开始创建了一个 Asterisk 的 dialplan 并且利用它打通第一个电话时,满 足感会油然而生。当人们意识到他刚刚创建了一个电话系统时,都会觉得 非常有趣而开怀大笑。这些快乐当然也属于你,所以,请首先让这个最简 单的 dialplan 工作起来。如果你遇到问题,那么请返回第 5 章并完成那里 的例子。 如果你刚刚添加了这些 dialplan 代码,你需要通过 Asterisk CLI 相关命令重新加载这个 dialplan:

 CLI > dialplan reload

或者在 Linux Shell 下输入命令:

 sudo asterisk -rx "dialplan reload"

然后从你已经配置好的任何一部 IP 电话机拨打分机 200,都会听到 Allison Smith 的声音“Hello World”。如果你没有听到,那么请通过 Asterisk CLI 检查错误信息,并确保你使用的 channel 被指定到 LocalSets 上了。

我们不建议你继续本书,直到你确认已经完成下述事宜:

1. 分机 100 和 101 之间呼叫正常;

2. 呼叫 200 可以听到“Hello World”;

尽管这个例子非常短也非常简单,但它依然强调了 contexts,extensions,priority,和 applications 这些核心概念。现在,你已经具备了创建 dialplan 的基础知识了。

构建一个交互式拨号计划

编辑

我们刚才创建的 dialplan 是静态的,它总是对与每个呼叫执行相同的操作。许多 dialplan 也 需要实现根据用户的不同输入执行不同操作的业务逻辑,现在让我们看看如何做到这一点。

Goto(),Background(),和WaitExten()应用程序

编辑

如同名字暗示的那样,Goto()用于将一个呼叫跳转到dialplan的另一个部分。Goto()的语法需 要将目的 context,extension,和 priority 作为参数传递给它,像这样:

 same => n, Goto(context, extension, priority)

我们将创建一个名为 TestMenu 的新的 context,并且在 LocalSets context 中创建一个新的 extension,这个 extension 将利用 Goto()跳转到 TestMenu:

在[LocalSets]段增加

 exten => 201,1 Goto(TestMenu,start,1)
 [TestMenu]
 exten=>start,1,Answer()

现在,每当一个设备进入 LocalSets context 并且拨叫 201,这个呼叫就会被传递给 TestMenu context 中的 start extension(它现在还没有做任何有意义的事,因为我们还有更多的代码需 要添加)。

我们在这个例子中使用 start 作为 extension 的名字,它实际上可以用任何 名字代替,不管是数字的还是字母的。我们更倾向于用不能直接拨号的字 母作为 extension 的名字,是因为这可以提高可读性。重点是,我们可以 用 123 或者 xyz123,或者 99luftballons,或者任何你想解的字符串代替 start。这个“start”在 dialplan 中没有任何意义,它只是代表另一个 extension。

在交互式 dialplan 中最有用的一个 application 是 Background()注 7。像 Playback()一样, Background()可以播放一个预先录制好的声音文件,但是,当用户按下电话上的按键时,它 会中断播放的声音,并根据用户输入的数字把这个呼叫跳转到对应的 extension 去。例如, 当用户按下数字 5 时,Asterisk 会停止播放语音提示,并将呼叫跳转到 extension 5 的第一步 (假设存在 extension 5 )。 Background()最常见的应用是创建语音菜单(一般称作 auto attendants 注 8 或 phone trees)。很多公司通过语音菜单将来电引导到合适的分机上,从而将前台秘书从不得不接听 每个电话中解脱出来。 Background()采用和 Playback()相同的语法:

 [TestMenu]
 exten=>start,1,Answer()
   same=> n, Background(main-menu)

如果你希望 Asterisk 在播放完语音提示后继续等待用户输入一段时间,你可以使用 WaitExten()。WaitExten()一般跟在 Background()之后使用,其作用是等待用户的 DTMF 输入:

 [TestMenu]
 exten=>start,1,Answer()
   same=> n, Background(main-menu)  
   same=> n, WaitExten()

如果你希望为 WaitExten()指定等待用户响应的时间(以取代默认的超时时间注 9),只需要简 单的将代表秒数的数字代入 WaitExten(),像这样:

 same => n,WaitExten(5); We recommend always passing a time argument to WaitExten()

Background()和 WaitExten()都允许用户输入 DTMF 数字。然后 Asterisk 会尝试在当前 context 中寻找与这个数字匹配的 extension。如果寻找到了,Asterisk 就会将呼叫传递给这个 extension。 让我们通过在我们的示例 dialplan 中增加几行来说明这一点:

 [TestMenu]
 exten=>start,1,Answer()
   same=> n, Background(main-menu)
   same=> n, WaitExten(5)
 exten=> 1,1,Playback(digits/1)
 exten=> 2,1,Playback(digits/2)

做完这些修改后,保存并重载你的 dialplan:

 CLI> dialplan reload

如果你呼叫分机 201,你会听到一个声音提示“main menu”。然后 Asterisk 会等待 5 秒来接 收你输入的数字。如果你按下的数字是 1 或 2 ,Asterisk 就会去匹配相应的 extension,然后语音报出你按下的数字。由于我们没有再提供进一步的指示,所以再然后你的呼叫会被挂 断。你也会发现,如果你按下不同的数字(例如 3),这个 dialplan 将无法处理。 让我们再做点改进。我们将利用 Goto()使这个 dialplan 能够在播报按下的数字音后重复 播放问候语:

 [TestMenu]
 exten=>start,1,Answer()
   same=> n, Background(main-menu)
   same=> n, WaitExten(5)
 
 exten=> 1,1,Playback(digits/1)
   same=> n, Goto(TestMenu,start,1)
 exten=> 2,1,Playback(digits/2)
   same=>n, Goto(TestMenu,start,1)

新增的这几行会在播报完按下的数字后把这个呼叫跳转回 start,这比直接挂断友好的多了。 如果你仔细查看了 Goto() 的说明,会发现实际上你输入一个、两个、或 三个参数给 Goto()都是可以的。如果你只输入一个参数,Asterisk 将假设 这个参数是同一个 extension 中的 priority。如果你输入两个参数,Asterisk 将把它们处理为同一个 context 下的 extension 和 priority。 在这个例子中,我们使用了三个参数是为了清晰的缘故。如果只输入 extension 和 priority 也是相同的效果,因为目的 context 和源 context 是相 同的。

处理无效入口和超时

编辑

现在,我们的第一个语音菜单已经工作起来了,让我们再增加一些特殊的 extensions。 首先,我们需要一个 extension 来处理错误的输入。在 Asterisk 中,如果一个 context 收到了 一个针对不存在的 extension 的请求(例如,在我们上面的例子中输入 9),呼叫会被转给 i extension 处理。我们还需要一个 extension 来处理当用户在给定的时间(默认的超时时间是 10 秒)内没有按下任何按键的情况。如果用户在 WaitExten()被调用后太长时间没有按下按 键,呼叫会被传递给 t extension。下面是增加了这两个 extension 后的 dialplan:

 [TestMenu]
 exten=>start,1,Answer()
   same=> n, Background(main-menu)
   same=> n, WaitExten(5)
 
 exten=> 1,1,Playback(digits/1)
   same=> n, Goto(TestMenu,start,1)
 exten=> 2,1,Playback(digits/2)
   same=>n, Goto(TestMenu,start,1)
 exten=> i,1,Playback(pbx-invalid)
   same=>n, Goto(TestMenu,start,1)
 exten=> t,1,Playback(vm-goodbye)
   same=>n, Hangup()

增加了 i 和 t extension 使得我们的菜单更加可靠也更友好。当然还得说,它仍然非常简单, 因为到目前为止,外线呼叫仍然没有办法联系到一个内线用户。为了做到这一点,我们需要 学习另一个 application,称作 Dial()。

使用Dial()应用程序

编辑

Asterisk 最有价值的特性之一,就是它将不同的用户相互连接起来的能力。当不同的用 户使用不同的通讯方式时,这一特性尤其有用。举例来说,用户 A 可能使用 PSTN 通话,而 用户 B 则可能是在世界另一端的咖啡馆里使用 IP 电话机通话。幸运的是,Asterisk 已经完成 了大部分在完全不同的网络之间连接和转化的艰苦工作。你需要做的全部工作就是学习如何 使用 Dial() application。 Dial()的语法要比我们之前遇到的其它 application 复杂的多,但是别让困难把你吓跑了。 Dial()使用了四个参数,下面让我们来看一看。

参数一:目标

编辑

这第一个参数是呼叫目的地,它由呼叫采用的技术(或通道)和远端分机或资源的地址 组成,中间用斜线隔开。常见的技术类型包括 DAHDI(模拟电话接口和 T1/E1/J1 接口等), SIP,和 IAX2。 举例来说,假设你希望呼叫标识为 DAHDI/1 的 DAHDI 分机,这是一个 FXS 接口,可以 连接一部普通模拟话机。DAHDI/1 的意思是采用的技术类型是 DAHDI,资源(或称信道标识) 是 1。类似的,呼叫一个 SIP 分机(定义在 sip.conf)的 destination 可以表示为 SIP/0004F2001122, 呼叫一个 IXA 分机(定义在 iax.conf)的 destination 可以表示为 IAX2/Softphone。注 10 如果当dialplan 中的 extension 105 被执行时,我们希望 Asterisk 使 DADHI/1 channel 振铃,那么应该 加入如下一行:

 exten => 105,1,Dial(DAHDI/1)

我们也可以同时呼叫多个目标,不同的 destinations 之间用“&”隔开,像这样:

 exten => 105,1,Dial(DAHDI/1&SIP/0004F2001122&IAX2/Softphone)

Dial()可以同时振铃所有的指定 channel,并且接通第一个应答的 channel(所有其它 channel 立即停止振铃)。如果 Dial()无法联系上任何一个 destinations,Asterisk 会将它无法完成呼叫 的原因代码写进变量 DIALSTATUS,并且继续执行这个 extension 的下一个 priority。注 11 Dial()也可以用来连接在 channel 配置文件中没有定义的远端 VoIP 分机。完整的表达式 如下例:

 Dial(technology/user[:password]@remote_host[:port][/remote_extension]) 

作为一个例子,你可以用下面的 extension 呼叫 Digium 的演示服务:

 exten => 500,1,Dial(IAX2/guest@misery.digium.com/s) 

Dial()的语法用于 DAHDI channel 时略有不同:

 Dial(DAHDI/[gGrR]channel_or_group[/remote_extension]) 

下面的例子是通过 DAHDI 的通道 4 注 12 拨打 1‐800‐555‐1212:

 exten => 501,1,Dial(DAHDI/4/18005551212)

====参数二:超时===Dial()的第二个参数是 timeout,以秒为单位。如果指定了 timeout,Dial()会尝试呼叫 destination(s)指定的秒数,超时后就会放弃呼叫而继续执行 extension 的下一条。如果没有指 定 timeout,Dial()就会一直尝试呼叫被叫 channel(s),直到被叫应答或主叫挂机。指定 10 秒 超时的例子如下:

 exten => 201,1,Dial(DAHDI/1,10)

如果呼叫在超时前被应答,channels 之间的连接就会建立,dialplan 完成。如果 destination 一直不应答,占线或者不可用,超时后 Asterisk 会设置变量 DIALSTATUS 并继续执行这个 extension 的下一条。

 让我们把刚刚学习的这些用到下面这个例子中:
 exten=>201,1,Dial(DAHDI/1,10)
   same=> n, Playback(vm-nobodyavail)
   same=>n, Hangup()

如你所见,在这个例子中,如果呼叫无应答,Asterisk 会播放 vm‐nobodyavail.gsm。

参数三:选项

编辑

Dial()的第三个参数是 option 字符串。它可能包含一个或多个可以影响 Dial()行为的字符。 所有可能的 option 太多了以至于我们无法都在本书讨论,我们只讨论一个最流行的 option, m。如果你将 m 作为 Dial()的第三个参数,主叫听到的回铃音会被 hold music 取代(译者注: 就是咱们的“彩铃”功能了)。举例如下:

 exten=>201,1,Dial(DAHDI/1,10,m)
   same=> n, Playback(vm-nobodyavail)
   same=>n, Hangup()

参数四:URI

编辑

Dial()的第四个参数是 URI。如果被叫 channel 支持在呼叫时接收 URI,这个参数指定的 URI 就会被发送(例如,如果你的 IP 电话机支持接收 URI,它就会现实在 IP 电话机的显示屏 上;同样地,如果你在使用软件电话,这个 URI 可能会弹出在你的计算机屏幕上)。这个参 数非常少被用到。

很少(如果有的话)有电话支持 URI。如果你在寻找一些类似弹屏的应用, 你可以参考第 18 章,“Using XMPP(Jabber) with Asterisk”一节。

更新拨号计划

编辑

让我们在前面语音菜单的例子中使用 Dial():

 [TestMenu]
 exten=>start,1,Answer()
   same=> n, Background(main-menu)
   same=> n, WaitExten(5)
 
 exten=> 1,1,Dial(SIP/0000FFFF0001,10)
   same=> n, Goto(TestMenu,start,1)
 exten=> 2,1,Dial(SIP/0000FFFF0002,10,m)
   same=>n, Goto(TestMenu,start,1)
 exten=> i,1,Playback(pbx-invalid)
   same=>n, Goto(TestMenu,start,1)
 exten=> t,1,Playback(vm-goodbye)
   same=>n, Hangup()

空白参数

编辑

请注意第二、第三、第四个参数都可以不用,只有第一个参数是必须的。举例来说,如 果你想指定一个 option,但是并不想指定 timeout,你只要简单的将 timeout 参数留空就可 以了,像这样:

 exten => 1,1,Dial(DAHDI/1,,m)

使用变量

编辑

在 Asterisk 中可以通过使用变量(Variables)来帮助我们减少输入,提高清晰度,和增 加逻辑。如果你具有计算机编程经验的话,你应该已经理解变量是什么了。如果你没有编程 经验,那么我们来简单解释下变量是什么以及怎么使用变量。变量是 Asterisk 中极其重要的 一个概念。 变量是一个可存储数值的容器。变量的优点是它的值可以改变,但维持名字不变。这就 意味着你可以在代码中引用变量名而不必关心值是什么。因此,举例来说,我们可以创建一 个变量命名为 JOHN 并且指定它的值是 DAHDI/1。这样,我们可以在书写 dialplan 时通过 John 的名字来引用他的 channel,而不需要记住 John 使用的 channel 是 DAHDI/1。如果将来 我们更改了 John 使用的 channel,我们不需要修改任何引用了变量 JOHN 的代码,我们只需 要修改变量 JOHN 的值就可以了。 有两种方法引用变量。引用变量名时,简单的输入变量的名字就可以了,例如 LEIF。而 如果你希望引用变量的值,你就必须输入 $ 字符,左大括弧,变量名,右大括弧(以 LEIF 为例,我们通过${LEIF}来引用它的取值)。下例说明如何在 Dial()中使用变量:

 exten => 301,1,Set(LEIF=SIP/0000FFFF0001)
   same => n,Dial(${LEIF})

在 dialplan 中,每当遇到${LEIF},Asterisk 都会自动用我们指定给变量 LEIF 的值来代替它。 注意,变量名是大小写敏感的。命名为 LEIF 的变量和命名为 Leif 的变量 是不同的变量。出于易读性考虑,所有本书例子中的变量名都是大写。你 可能也知道 Asterisk 设置的所有变量也是大写。某些变量,例如 CHANNEL 和 EXTEN ,是 Asterisk 保留的。你不应尝试设置这些变量。流行的作法 是将全局变量(global variables)写作大写,而 channel 变量写作 Pascal/Camel 这样单词首字母大写的形式。

在 dialplan 中我们可以使用三种类型的变量:全局变量,channel 变量,和环境变量。 让我们花一点时间来看看每种类型。

全局变量

编辑

如同名字暗示的那样,全局变量对于所有 channel 在任何时间都是可见的。全局变量是 非常有用的,它可以用在 dialplan 的任何地方以提高可读性和管理性。假设你有个很大的 dialplan包含了数百条对SIP/0000FFFF0001 channel的引用。现在,想象一下你不得不遍历 整个 dialplan 并把所有的这个引用都换成 SIP/0000FFFF0002。这将是一个非常长而且很容易 出错的过程。 在另一方面,如果你在 dialplan 一开始已经定义好了值为 SIP/0000FFFF0001 的全局变量, 并且代码中只是引用这个变量。那么你只需要修改一行代码就可以作用到 dialplan 中所有用 到这个 channel 的地方。 全局变量可以被声明在 extensions.conf 开始的[globals] context 中。作为一个例子,我们 创建了一个名为 LEIF 的全局变量,它的值为 SIP/0000FFFF0001。这个变量会被 Asterisk 在解 析 dialplan 时设置。

 [globals]
 LEIF = SIP/0000FFFF0001

信道变量

编辑

Channel 变量是一种只与特定呼叫关联的变量。不同于全局变量,channel 变量只存在于 呼叫发生期间,并且只对参与呼叫的 channel 有效。 Asterisk 的默认 dialplan 中有许多预先定义好的 channel 变量,这些变量的说明可以在 Asterisk 的维基百科 https://wiki.asterisk.org/wiki/display/AST/Channel+Variables 中找到。 Channel 变量的设置通过 Set() application 来实现:

 exten => 202,1,Set(MagicNumber=42)
 same => n,SayNumber(${MagicNumber})

环境变量

编辑

Asterisk 环境变量是一种在 Asterisk 中访问 Unix 环境变量的方法。它通过在 dialplan 中 使用ENV() dialplan function来实现注13。语法看起来像${ENV(var)},其中var是你想引用的 Unix环境变量。环境变量在Asterisk dialplan中并不常用,但是如果你需要的话,它是可以 使用的。

为拨号计划添加变量

编辑

现在我们已经学习了变量,让我们把它们增加到我们的 dialplan 例子中。我们将增加三 个与 channel 名相关的全局变量:

 [globals]
 LEIF=SIP/0000FFFF0001
 JIM=SIP/0000FFFF0002
 RUSSELL=SIP/0000FFFF0003

 [LocalSets]
 exten => 100,1,Dial(${LEIF})
 exten => leif,1,Dial(${LEIF})

 exten => 101,1,Dial(${JIM})
 exten => jim,1,Dial(${JIM})
 
 exten => 102,1,Dial(${RUSSELL})
 exten => russell,1,Dial(${RUSSELL})
 
 [TestMenu]
 exten => 201,1,Answer
   same => n,Background(enter‐ext‐of‐person)
   same => n,WaitExten()
 exten => 1,1,Dial(DAHDI/1,10)
   same => n,Playback(vm‐nobodyavail)
   same => n,Hangup()
 exten => 2,1,Dial(SIP/Jane,10)
   same => n,Playback(vm‐nobodyavail)
   same => n,Hangup()
 exten => i,1,Playback(pbx‐invalid)
   same => n,Goto(incoming,123,1)
 
 exten => t,1,Playback(vm‐goodby)
   same => n,hangup()

你可能注意到我们给每个 extension 号码都增加了一个别名。在 6.1.2 分机(Extensions)一 节,我们解释过 Asterisk 并不关心你对 extension 的命名方式。在这个例子中,我们简单的 给每个分机都增加了字符的和数字号码的两个名字。extension 100 和 leif 都可以定位到 SIP/0000FFFF0001,extension 101和jim都可以定位到SIP/0000FFFF0002,而102和russell 也都可以定位到 SIP/0000FFFF0003。这些设备通过全局变量${LEIF},${JIM},和${RUSSELL} 来标识,通过 Dial()来实现呼叫。 在我们的测试菜单中,我们简单的随便选择了被叫分机,例如 DAHDI/1 和 SIP/Jane。你 可以用任意分机替代它们。我们建立 TestMenu context 只是想给你一些 Asterisk dialplan 是什 么样的概念。

模式匹配

编辑

如果我们希望允许人们通过 Asterisk 拨打电话并且利用 Asterisk 连接外部资源,我们就 需要一个方法能匹配所有可能被拨打的号码。为处理这类问题,Asterisk 提供了样式匹配 (pattern matching)的机制。样式匹配机制可以允许你在你的 dialplan 中创建一个能够匹配 许多不同号码的 extension。这个功能非常有用!

模式匹配语法

编辑

当我们使用样式匹配是,特定的字母和符合代表了我们希望匹配的东西。样式总是从一 个下划线(_)开始。这告诉 Asterisk 我们正在匹配一个样式,而不是匹配一个精确的 extension 名字。 如果你忘记了样式开始的下划线,Asterisk 会认为这只是一个 extension 的 名字,而不会做一个样式匹配。这是人们刚开始学习 Asterisk 时最容易犯 的一个错误。 在下划线之后,你可以使用一个或多个下列字符:

X 匹配任意 0 到 9 之间的一个数字

Z 匹配任意 1 到 9 之间的一个数字

N 匹配任意 2 到 9 之间的一个数字

[15‐7] 匹配指定范围的一个数字。在这个例子中的样式要求匹配一个 1,以及 5,6,7 中的任意一个数字

.(点) 通配符;匹配一个或多个字符,不论它们是什么. 如果你不够小心,通配符匹配可能让你的 dialplan 做一些你没想到的事情 (例如匹配了内建的 extensions 如 i 和 h)。你应该仅当你已经尽可能的 匹配了尽量多的数字后才使用通配符。例如,下面的样式应该永远不要使 用: _. 事实上,当你这么使用时 Asterisk 会提示你一个告警。如果你真的需要一 个匹配所有输入的样式,你也应该采用如下列用法,匹配所有数字开头的 字符串: _X. 或者如下例用法,匹配任意字符串: _[0‐0a‐zA‐Z].

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

如果想在你的 dialplan 中使用样式匹配,只要简单的把样式输入到 extension 名字(或 号码)的位置就可以了:

 exten => _NXX,1,Playback(silence/1&auth‐thankyou)

在这个例子里,这个样式匹配任意从 200 到 999 之间的三个数字号码的 extension(N 代表任意 2 到 9 之间的数字,每个 X 代表一个 0 到 9 之间的数字)。也就是说,如果用户拨 打这个 context 中的任意 200 到 999 之间的三位数字的分机号码,他都会听到一个声音文件 auth‐thankyou.gsm。

关于 Asterisk 样式匹配的另一个需要知道的重要规则是,如果 Asterisk 发现一个样式可 以匹配多个 extension,它将使用最精确的那个(从左到右)。比如说你已经定义了下面两个 样式,并且有用户拨打 555‐1212 :

 exten => _555XXXX,1,Playback(silence/1&digits/1)
 exten => _55512XX,1,Playback(silence/1&digits/2)

在这个例子中,第二个 extension 会被选中,因为它更加精确。

模式匹配范例

编辑

下面这个例子匹配 7 位数字号码,并且首个数字大于或等于 2:

_NXXXXXX

这个样式可以兼容 NANP(北美编号计划)的本地 7 位号码。 当采用 10 位号码拨号时,这个样式看起来会是: _NXXNXXXXXX 注意,这两个样式都不能处理长途号码。我们马上会讲到长途号码的处理。

让我们来试试另一个样式: _1NXXNXXXXXX 这个样式可以匹配数字 1,跟着一个在 200 到 999 之间的地区代码,然后是任意的 7 位号码。 在 NANP 地区,你可以用这个样式匹配任意长途号码。注 14 最后是这个样式: _011. 注意最后的点。这个样式匹配 011 开始的,并且至少还有一位数字的任意号码。在 NANP 中, 这代表一个国际电话号码(我们将在下一节使用这个样式来给我们的 dialplan 增加外呼能力)。

使用${EXTEN}信道变量

编辑

如果你使用了样式匹配,但又需要知道到底拨打的是什么号码时,可以怎么办呢?可以 利用${EXTEN} channel 变量。每当你拨叫一个分机时,Asterisk 会设置${EXTEN} channel 变量 为实际拨打的号码。我们可以用一个名为 SayDigits()的 application 来测试一下:

 exten => _XXX,1,Answer()
   same => n,SayDigits(${EXTEN})

在这个例子中,SayDigits()会回读你拨打的三位分机号码。 通常,从${EXTEN}的前面去掉几位数的操作是有用的。这可以利用表达式${EXTEN:X}来 实现,其中 X 是你希望去掉的位数,方向是从左到右。例如,如果${EXTEN}的值是 95551212, ${EXTEN:1} 就等于 5551212。让我们再看另一个例子:

 exten => _XXX,1,Answer()
   same => n,SayDigtis(${ExTEN:1})

在这个例子中,SayDigits()将从第二个数字开始,这样将只回读被叫分机的后两位数。

更高级的数字操作 ${EXTEN}变量还有一种表达式${EXTEN:x:y},其中 x 是起始位置,y 是返回的数字个数。给定 下列字符串: 94169671111 我们可以利用${EXTEN:x:y}抽取下列数字:

${EXTEN:1:3} 得到 416
${EXTEN:4:7} 得到 9671111
${EXTEN:‐4:4} 将从倒数第 4 个数字开始,得到 1111
${EXTEN:2:‐4} 将从跳过 2 个数字开始,并不包括最后的 4 个数字,得到 16967
${EXTEN:‐6:‐4} 将倒数第 6 个数字开始,并不包括最后 4 个数字,得到 67
${EXTEN:1} 将返回第 1 个数字之后的全部数字,得到 4169671111(如果返回的数字个数为空的话,它就返回

剩余的全部数字) 这是一个非常强大的表达式,但是大部分这些变化并不常用。在大多数情况下,你将 使用${EXTEN}(或者${EXTEN:1},如果你需要去掉外线识别码)。

包含

编辑

Asterisk 的一个重要特性是允许在一个 context 中定义的 extension 可以在另一个 context 中使用。这是通过 include 指令实现的。通过 include 指令我们可以访问 dialplan 的不同部 分。 Include 的语法如下所示,其中 context 是被包含的 context 的名字。

 include => context

在一个 context 中包含另一个 context,将允许被包含 context 中的 extension 可以在当前 context 下拨打。 当我们在当前 context 下包含其它 contexts 时,我们一定要留意包含的顺序。Asterisk 将首先尝试匹配当前 context 中的 extensions。如果没成功,它再尝试匹配第一个被包含的 context 中的 extensions(包括这个 context 中包含的其它 context),然后再继续匹配下一个 被包含的 context。 我们将在第 7 章进一步讨论 include 。

结束语

编辑

现在你已经得到了一个基本但是具有一定功能的 dialplan。虽然仍然有许多东西我们没 有讲到,但是你应该已经学习到了所有的基础知识。在后续的章节中,我们将在此基础上进 一步展开讨论。 如果 dialplan 中的有些部分你还没有理解,你应该在进入下一章前重读一两遍。理解这 些原理及如何应用它们是极其重要的,因为这是理解下一章的基础。



注释:

注 1:这是一个非常重要的考虑。对于传统 PBX 来说,一般都有对于前台秘书接听的默认设置。这就意味 着即使你忘记配置了,它也可能能正常工作。但在 Asterisk 中,正好与之相反。如果你没有告诉 Asterisk 如 何处理某个状态,Asterisk 就不会做任何处理,然后这个呼叫会被挂断。我们后续会通过一些实例来讨论如 何避免这种情况发生。请参阅“处理无效的输入和超时”一节。

注 2:请注意“空格”是明显的非法字符。千万不要把“空格”用在 context 名字中——你不会喜欢那结果 的。

注3:Asterisk允许在priority中包含简单的算式,例如n+200,以及priority s(same的意思),但是由于 priority label 的关系我们并不赞成这么使用。需要注意的是 extension s 和 priority s 是截然不同的两个概念。

注 4:除了 voicemail.conf 的某些部分以外。

注 5:Asterisk 中还有一个 application 叫做 Background(),它与 Playback()非常相似,不过 Background()可以 接受主叫的输入。你将在第 15 章和第 17 章学习到更多有关 Background()的知识。

注 6:Asterisk 是基于文件格式转化代价最低的原则来选择最合适的文件的——这意思是说,它会选择解码 为可播放的语音时 CPU 消耗最低的格式。当你启动 Asterisk 时,它会计算不同音频格式之间转换编码的代 价(这经常随系统不同而不同)。你可以通过在Asterisk CLI下输入命令show translation看到不同编码之间 转换的代价。其中的数字表示 Asterisk 转换 1 秒声音需要用多少微秒(milliseconds)。我们将在后续章节讨 论更多关于编码格式(称为 codecs)的内容。

注 7:需要注意的是,可能是由于 Background()名字的原因,有些人想当然的以为它的作用是“背景音乐”, 即在继续执行 dialplan 后续步骤的同时声音一直播放。实际上,我们用 background 这个名字只是想说明它 是在后台播放声音的同时,在前台等待 DTMF 输入。

注 8:更多关于自动应答(auto attendants)的信息可以在第 15 章找到。

注 9:参见 dialplan function TIMEOUT() 了解如何修改默认超时时间。参见第 10 章了解什么是 dialplan functions。

注 10:如果是在实际产品中使用,这实在不是一个好的设备名字。想像一下如果你的系统上使用了多于一 个软件电话(Softphone),或者你未来增加了一部软件电话,你怎么区分它们?

注 11:我们将在下一小节“变量”讨论有关变量的内容。在后续章节,我们也将讨论如何使你的 dialplan 基于 DIALSTATUS 的值做出决定;

注 12:请记住这里假定这个通道(DAHDI/4)可以接通外部号码(1‐800‐555‐1212)。

注 13:我们将在稍后讨论 dialplan functions,你无需过于担心环境变量,它们对理解 dialplan 无关紧要。

注 14:如果你是在美国长大的,你可能以为在拨打长途电话前拨的数字 1 是“长途代码”。这个认识是不 对的。数字 1 是 NANP 的国家代码。当你要把你的电话告诉其它国家的人时请一定记住这一点。接受者可 能不知道你的国家代码,这样他就无法在只知道你的地区代码和电话号码的情况下呼叫你。你的带有国家 代码的完整号码是 +1 NPA NXX XXXX(其中 NPA 是你的地区代码),例如,+1 416 555 1212 译者注 1,(2012.4.13)初稿成于 2011.12.2,其中 context 的翻译,觉得很是纠结。从意义上我们理解 context 其实指的是一个“作用域”,每个 context 在 dialplan 中就是一个独立小王国。但这个翻译还真不容易,初 版我翻译为字段,今天看看,实在太容易理解成 field,非常不妥。说不得,还是先老老实实的直译为“上 下文”吧。