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,非常不妥。說不得,還是先老老實實的直譯為「上 下文」吧。