> [**Asterisk**](http://www.aosabook.org/en/asterisk.html)
> [Russell Bryant](http://russellbryantnet.wordpress.com/)
Asterisk 1是基于GPLv2協議發布的一款開源電話應用平臺。簡單地說,這是一個服務端程序,用于處理電話的撥出、接入以及自定義流程。
此項目由Mark Spencer于1999年創始。當時Mark有一個自己的公司,叫做Linux支持服務公司,他需要一個電話系統來幫助自己操作業務。但他沒有那么多錢去買這樣一個系統,因此他決定自己做。隨著Asterisk知名度的提升,Linux支持服務公司的業務重點也轉向Asterisk,公司也更名為Digium。
Asterisk得名于Unix通配符:*,該項目的宗旨是能做所有與電話相關的事情。通過對自己宗旨的不懈追求,如今的Asterisk已經支持一系列用于接撥電話的技術。這些技術包括諸多VoIP(Voice over IP,語音IP)協議,與傳統電話網絡的模/數連接性,以及PSTN(Public Swithed Telephone Network,公共交換電話網絡)。對多種不同類型電話的撥出與接入能力是Asterisk的拿手好戲之一。
當Asterisk系統有電話接入或撥出時,系統有很多附加特性可用于電話的自定義處理。有些特性是較大型的預置常用應用,如語音郵件(voicemail);另外還有一些小特性,可配合使用,用于創建自定義應用,如回放音頻文件、讀數字按鍵、語音識別等。
## 1.1 關鍵架構概念
本節討論一些跟Asterisk每一部分息息相關的架構概念。這些思想是Asterisk架構的基礎。
1.1.1 通道
在Asterisk中,通道表示Asterisk系統與某電話端點的一條連接(如圖1)。一個最常見的例子是,一路電話呼叫接入了Asterisk系統,就用通道表示這一連接。在Asterisk代碼中,通道是數據結構ast_channel的實例。圖中這個呼叫場景可以視為呼叫方與某一系統應用(比如語音郵件)的交互。

圖1.1 一個通道表示一條呼叫線路
1.1.2 通道橋接
可能大家更熟悉的一個呼叫場景是兩個電話之間的連接:一個人使用電話A呼叫另一個使用電話B的人。在此場景下,連接到Asterisk系統的有兩個電話終端,因而分配了兩個通道(如圖1.2)。

圖1.2 兩個通道表示兩條呼叫線路
如上圖連接的Asterisk通道稱之為通道橋接。為了達到在通道間傳輸媒體的目的而把通道連接起來,這樣的行為即稱為通道橋接。然而,在電話呼叫過程中也可能有視頻或文本的數據流。即使有多于一種類型的媒體流,也是由Asterisk系統中負責呼叫連接兩端的通道獨立處理。在圖1.2中,兩個通道分別對應電話A和電話B,橋接的作用是將媒體從電話A傳輸到電話B,同理也可從電話A傳輸到電話B。所有的媒體流都是通過Asterisk系統傳輸的。Asterisk不允許傳輸無法識別或不能完全控制的媒體流。這意味著Asterisk可以做如下事情:記錄媒體、處理音頻、在不同技術間進行轉換等。
有兩種方法可以完成兩個通道的橋接:通用橋接和專用橋接。通用橋接時,無論通道使用的什么技術都能夠工作。它通過Asterisk的抽象通道接口傳輸所有的音頻和信號。盡管這是一種最靈活的橋接方式,卻是最低效的,因為完成橋接必須有多層抽象。圖1.2描述的就是通用橋接。
專用橋接是面向特定技術的通道連接方式。如果連接到Asterisk的兩個通道使用相同的媒體傳輸技術,則勢必有一種比通過抽象層更為高效的連接方式,因為抽象層是為使用不同技術的通道之間連接而準備的。例如,如果有這樣一種專業化硬件用于連接電話網絡,那么在硬件上橋接通道就成為了可能,媒體流根本無需通過應用程序。對于某些VoIP協議而言,可能通過端點向對方直接發送媒體流,這時只有呼叫信號的信息是不斷流過服務器的。
在橋接兩個通道的時候,系統通過比較兩通道來決定使用通用橋接還是專用橋接。如果兩通道都標識出支持同一種專用橋接方式,那么就是用專用橋接;反之使用通用方式。判決兩通道是否支持同一種專用橋接方式,通過簡單的比較C函數指針即可做到。此法固然絕非上策,但我們還沒有遇到不適用此法的情況。1.2部分還要討論更多有關專用橋接函數的細節。圖1.3描述的是專用橋接的一個實例。

圖1.3 專用橋接實例
1.1.3 幀
在呼叫過程中,Asterisk代碼內部的通信使用幀--數據結構ast_frame的實例--來實現。幀可以是媒體幀,也可以是信號幀。在一個基本的呼叫過程中,媒體幀的流包含音頻,是通過系統傳輸的。信號幀則用于發送呼叫信號事件相關的消息,如按下數字鍵,掛起電話,掛斷電話等。
可用的幀類型列表是靜態定義的。幀由一個數值編碼類型和一個子類型表示。完整列表可在源碼文件include.asterisk/frame.h中找到。下面舉幾個例子:
* VOICE:這類幀攜帶一部分音頻流。
* VIDEO:這類幀攜帶一部分視頻流。
* MODEM:這類幀的數據部分使用編碼,如用于通過IP協議發送傳真的T.38編碼;其主要用途就是處理傳真。對于這類幀的處理,保證原始數據完好無損是很重要的,這樣信號到另一端才能被成功解碼。AUDIO幀不一樣,因為在轉碼到其他音頻編解碼器的時候,犧牲音頻質量以節省帶寬是可接受的做法。
* CONTROL:這類幀表示呼叫信號消息,用于指示呼叫信號事件,包括電話接通,掛斷、掛起等等。
* DTMF_BEGIN:開始的數字鍵。當呼叫者按下電話上的DTMF鍵時,發送此幀。
* DTMF_END:結束的數字鍵。當呼叫者停止按電話上的DTMF鍵時,發送此幀。
## 1.2 Asterisk組件抽象
Asterisk是一款高度模塊化的軟件。其內核程序可由源碼樹上的main/目錄的源碼構建而成。但是內核程序本身作用不大,因為其主要作用是注冊模塊。系統還有一些代碼負責連接所有抽象接口,使電話呼叫工作起來。這些接口的具體實現是由一些可載入模塊在運行時完成注冊的。
默認狀態下,當主程序啟動時,Asterisk會在文件系統上一個預先指定的模塊目錄下找到所有模塊,并加載之。選擇這種默認方式是出于簡便性的考慮。然而,還有一個可更改的配置文件,可具體指定加載哪些模塊及其加載順序。系統配置變得有點復雜,但是能指定那些不需要的模塊不被加載。這樣做的主要好處就是減少了程序的內存占用,然而還有一些安全性的優點。如果一個模塊可從網絡接受連接,但實際并不需要用它,那么最好還是不要加載它。
模塊被加載后,它將在Asterisk內核程序中注冊它所有組件抽象的實現。可由模塊實現并在Asterisk內核注冊的接口多種多樣。系統允許模塊盡量多的注冊各類接口。通常相似的功能組成一個單獨的模塊。
1.2.1 通道驅動
通道驅動接口是Asterisk提供的最復雜且最重要的接口。Asterisk通道API提供了電話協議抽象層,使得其它所有Asterisk特性能夠獨立于所使用的電話協議而工作。此組件負責在Asterisk通道抽象層與其實現的電話技術細節層面之間的轉換。
Asterisk通道驅動接口定義稱為*ast_channel_tech*接口。它定義了通道驅動必須實現的一組方法。通道驅動須實現的第一個方法是*ast_channel*工廠方法,也是*ast_channel_tech*中的*requester*方法。當Asterisk為一個接入或撥出的電話呼叫建立通道后,該通道類型對應的*ast_channel_tech*的實現方法負責對*ast_channel*進行實例化和初始化。
*ast_channel*實例創建完成后,有一個對其創建者*ast_channel_tech*的引用。還有其他一些針對具體技術的操作需要處理,這些操作必須由*ast_channel*執行,因此對它們的處理需要就推給*ast_channel_tech*的適當方法來完成。圖1.2表示Asterisk中的兩個通道,在圖1.4中進行拓展,表示兩個橋接通道,以及圖中對應的通道技術實現。

圖1.4 通道技術層和抽象通道層
*ast_channel_tech*中最重要的方法有:
* *requester*:回調函數,用于請求某個通道驅動實例化一個ast_channel對象,并針對其類型進行初始化。
* *call*:回調函數,用于從端點(由*ast_channel*表示)發起一個撥出呼叫。
* *answer*:Asterisk決定對接入的呼叫進行應答(與該*ast_channel*關聯)時調用。
* *hangup*:系統決定應該掛斷呼叫時調用。調用后,通道驅動以基于某種協議的方式通知端點:呼叫結束。
* *indicate*:呼叫接通后,有可能產生許多其它的事件,需要給端點發信號。例如,如果電話被掛起,此回調函數將被調用,以通知此事件。通知呼叫掛起事件的方法可以是基于協議的,也可能只是由通道驅動發起反復播放掛起音樂的操作。
* *send_digit_begin*:調用此函數的作用是指示數字按鍵(DTMF)的開始發送至設備。
* *send_digit_end*:調用此函數的作用是指示數字按鍵(DTMF)的結束發送至設備。
* *read*:此函數由Asterisk內核調用,用于從端點讀回*ast_frame*。*ast_frame*是Asterisk的一個用于封裝媒體(如音頻或視頻)以及觸發事件的抽象結構。
* *write*:此函數用于向設備發送*ast_frame*。通道驅動取得數據,按照其實現的電話協議做適當的打包,并傳送至端點。
* *bridge*:針對通道類型的專用橋接回調函數。如前所述,通道驅動使用專用橋接,可以為兩個同類型通道實現更高效的橋接方法,而沒必要讓這兩個通道的信號和媒體流都通過額外的抽象層。從性能原因考慮,這是極為重要的。
呼叫結束后,Asterisk內核中負責抽象通道處理的代碼調用*ast_channel_tech*的*hangup*回調函數,銷毀*ast_channel*對象。
1.2.2 撥號計劃應用
Asterisk管理員使用Asterisk撥號計劃(存于/etc/asterisk/extensions.conf文件)來設置呼叫路由表。撥號計劃是由一系列被稱為擴展規則的呼叫規則組成的。當有一個電話呼叫接入,系統用被叫號碼在撥號計劃中查找擴展規則,用以處理本次呼叫。擴展規則包括一組撥號計劃應用程序,由通道執行。撥號計劃中可用于執行的應用由一個應用注冊表維護。模塊被加載時,在運行期間填充注冊表。
Asterisk內置近200個應用。應用定義非常松散,并可任意使用系統內部API與通道交互。有些應用程序執行單個任務,如回放(用于向呼叫方回放一個音頻文件);還有一些應用程序則復雜得多,要執行大量操作,如語音郵件。
你可以集成諸多使用Asterisk撥號計劃的應用,用于自定義呼叫處理。如果你需要對內置撥號計劃語言的能力做些自定義擴展,系統也有腳本接口,允許使用任意編程語言做自定義呼叫處理。即使通過另一編程語言使用這些腳本接口,也需要調用撥號計劃應用來實現與通道交互。
舉例說明之前,我們先看一個Asterisk撥號計劃的語法,此撥號計劃用于處理對號碼1234的呼叫。注意,這里1234這個號碼系信手拈來。共有3個撥號程序被調用:首先,應答呼叫;其次,回放音頻文件;最后,掛斷呼叫。
~~~
; Define the rules for what happens when someone dials 1234.
;
exten => 1234,1,Answer()
same => n,Playback(demo-congrats)
same => n,Hangup()
~~~
關鍵字*exten*用于定義擴展。在*exten*一行的右側,1234的意思是我們為呼叫1234定義了一組處理規則;緊接著,1的意思是此號碼被撥叫后的第一個處理步驟;最后,*Answer*指示系統應答此呼叫。下面兩行都以關鍵字*same*起始,是為最后一個擴展(此例指1234)指定的規則。*n*是下一步(next)的簡寫;該行的最后一項指定了采取的動作。
下面是一個Asterisk撥號計劃的應用實例。此例做的事情是應答接入的一個呼叫。系統向呼叫方播放蜂鳴音,然后從呼叫方讀入最多4個數字,存入變量DIGITS,接著讀入的數字重復播放給呼叫方,最后結束呼叫。
~~~
exten => 5678,1,Answer()
same => n,Read(DIGITS,beep,4)
same => n,SayDigits(${DIGITS})
same => n,Hangup()
~~~
如前所述,應用定義得非常松散--注冊的回調函數原型非常簡單:
~~~
int (*execute)(struct ast_channel *chan, const char *args);
~~~
然而,應用的實現卻要使用/asterisk/目錄下幾乎所有的API。
1.2.3 撥號計劃函數
大多數撥號計劃應用帶有字符串參數。其中有些值是硬編碼,但在某些地方的行為需要有更多的動態處理,這時應使用變量。下面這個例子是一個撥號計劃的代碼片段,其作用是設置一個變量,并使用*Verbose*應用在Asterisk命令行界面上打印這個變量值。
~~~
exten => 1234,1,Set(MY_VARIABLE=foo)
same => n,Verbose(MY_VARIABLE is ${MY_VARIABLE})
~~~
調用撥號計劃函數的語法與上例相同。Asterisk模塊可注冊撥號計劃函數,取得一些信息并返回給撥號計劃;反之,函數也可以從撥號計劃中取數據并有所動作。一個通用規則是:撥號計劃可設置或獲取通道的元數據,但不發任何信號,也不做任何媒體處理,這些工作留給撥號計劃應用來做。
下面這個例子展示了撥號計劃函數的用法。此函數首先向Asterisk命令行界面打印當前通道的*CallerID*,然后調用*Set*應用更改*CallerID*。此例中,*Verbose*和*Set*是應用,*CALLERID*是函數。
~~~
exten => 1234,1,Verbose(The current CallerID is ${CALLERID(num)})
same => n,Set(CALLERID(num)=<256>555-1212)
~~~
*CallerID*信息存于數據結構*ast_channel*的實例中,但這里需要的是一個撥號計劃函數,而不僅僅是一個變量。撥號計劃函數中的代碼能夠從這些數據結構中存取數據。
還有一個撥號計劃函數的用法示例--在呼叫日志中添加自定義信息,即*CDR*(呼叫詳細記錄)。*CDR*函數允許呼叫詳細記錄信息的獲取以及自定義信息的添加。
~~~
exten => 555,1,Verbose(Time this call started: ${CDR(start)})
same => n,Set(CDR(mycustomfield)=snickerdoodle)
~~~
1.2.4 編解碼譯碼器
在VOIP領域有許多編解碼器用于媒體編碼及跨網絡發送。多種技術選擇為媒體質量、CPU消耗、帶寬需求等方面提供了折中方案。Asterisk支持多種不同的編解碼器,必要時能夠在其中兩者之間進行轉碼。
Asterisk設置完呼叫后,將會嘗試使用公共媒體編解碼器來溝通兩個端點,這樣就不需要轉碼。然而實際上這種情況不太可能發生。即使使用公共編解碼器,也需要轉碼。比如,如果通過配置使Asterisk對流經系統的音頻做信號處理(如增大或減小音量),就需要將音頻信號轉換為未壓縮形式之后,才能執行信號處理。也可以通過配置使Asterisk做呼叫錄音。如果配置的錄音格式與呼叫的音頻格式不同,也需要轉碼。
**編解碼的協調**
使用什么編碼協調方法來處理媒體流,這與連接到Asterisk系統的呼叫所使用的技術有關。對于某些情況,如基于傳統電話網絡(PSTN)的呼叫,其容量和優先級都已明示,通用的編解碼器也已達成一致,因而不需要任何協調機制。
例如,對于SIP(最常用的VOIP協議),從高層視角來看,當呼叫送達Asterisk系統時,編解碼器的協調執行如下:
* 端點向Asterisk發送新的呼叫請求中包含其優先使用的編解碼器列表。
* Asterisk查詢管理員生成的配置文件,配置文件中包含一個支持的編解碼器列表,按優先級排序。隨后Asterisk從配置文件的列表中選擇優先級最高(基于配置文件中的優先級設置)、同時也包含在請求方所支持的列表中的編解碼器,供應答使用。
對于更復雜的編解碼,尤其是視頻方面,Asterisk對此領域處理得還不夠好。在過去十年里,編解碼器的協調選修變得愈加復雜。我們還有更多的工作要做,才能更好的處理最新的視頻編解碼,才能使系統對視頻的支持比現在更好。在Asterisk下一個主要發布版的諸多新開發任務中,這項工作是重中之重。
編解碼轉碼器的模塊提供了*ast_translator*接口的一種或多種實現。轉碼器有原格式和目標格式兩種屬性,還提供了一個回調函數,用于將媒體數據塊從原格式轉換為目標格式。轉碼器不涉及電話呼叫的概念,它只知道如何將一種媒體格式轉換為另一種媒體格式。
轉碼器API更多細節信息,參見include/asterisk/translate.h和main/translate.c文件。轉碼器抽象類的實現存于編解碼器目錄。
## 1.3 線程
Asterisk是重量級的多線程應用程序,使用POSIX線程API來管理線程,并使用了相關服務,如加鎖。Asterisk中所有與線程交互的代碼都會這樣做,但要通過一組用于調試的包裝器。Asterisk的大多數線程可歸類為網絡監視線程或通道線程(有時亦稱為PBX線程,因為其主要目的是在通道運行用戶級交換機PBX)。
1.3.1 網監線程
Asterisk每個主通道的驅動都有網監線程,負責監視本通道連接的任何網絡(IP網絡,或PSTN,等等),以及接入的呼叫或其它類型的請求。這類線程還要處理初始連接的設置步驟,如認證及撥號驗證。呼叫設置完成后,監視線程將創建Asterisk通道(ast_channel)的一個實例,并啟動一個通道線程,用于處理此呼叫生存期的其它操作。
1.3.2 通道線程
前面討論過,通道是Asterisk的基本概念。通道有入站通道和出站通道之分。當有呼叫接入Asterisk系統時,就創建一個入站通道,執行撥號計劃。Asterisk為每個入站通道創建一個線程來執行撥號計劃。這類線程即被稱為通道線程。
撥號計劃應用程序一定是在通道線程的環境中執行。撥號計劃函數幾亦如此。盡管也可能從諸如Asterisk CLI的異步接口讀寫撥號計劃函數,但通常情況下,通道線程仍是ast_channel結構的擁有者,控制著其對象的生存期。
## 1.4 呼叫場景
前兩節介紹了Asterisk組件的重要接口以及線程執行模型。本節將對常用的呼叫場景進行分解,闡述Asterisk組件之間如何合作處理電話呼叫。
1.4.1 檢查語音郵件
有這樣一個呼叫場景的實例:呼叫接入電話系統,檢查語音郵件。此場景涉及的第一個主要組件是通道驅動。通道驅動負責處理接入系統的電話呼叫請求,此動作發生在通道驅動的監視線程中。實現對系統的呼叫依賴于所使用的電話技術,因而可能會要求某種協調來設置呼叫。協調方法通常通過呼叫方撥叫的號碼來指定。然而,在某些情況下并沒有可用的指定號碼,因為實現呼叫所用的技術不支持指定撥叫的號碼。例如模擬電話線路上接入的呼叫。
如果撥號計劃(呼叫路由配置)為撥叫的號碼定義了擴展,而通道驅動也查到了Asterisk配置有這樣的擴展,系統將為分配一個Asterisk通道對象(*ast_channel*),并創建一個通道線程。通道線程主要負責處理呼叫的余下動作。

圖1.5 創建呼叫的順序圖
通道線程的主循環用于處理撥號計劃的執行,按照撥號擴展定義的規則和步驟執行。下面是一個擴展示例,用撥號計劃的語法編寫,存于extension.conf文件。有人撥叫**123*時,此擴展應答呼叫,并執行應用*VoicemailMain*。用戶調用此應用程序就能檢查郵箱里的信息。
~~~
exten => *123,1,Answer()
same => n,VoicemailMain()
~~~
當通道線程執行應用程序Answer時,Asterisk就會應答接入的呼叫。應答呼叫要求面針對技術的處理方法,所以除了一些常規的應答處理,還要調用與*ast_channel_tech*結構關聯的應答回調函數,用于處理對呼叫的應答,會涉及通過IP網絡發送特定數據包、掛斷模擬線路等操作。
下一步就是由通道線程執行應用程序*VoicemailMain*(如圖1.6)。此應用程序是由*app_voicemail*模塊提供的。有一要事需要注意:雖然語音郵件代碼可處理大量的呼叫交互,但它并不知道實現對Asterisk系統的呼叫所使用的技術。Asterisk通道的抽象對語音郵件的實現隱藏了這些細節。
為呼叫方提供對語音郵件的訪問服務涉及很多系統特性。然而,所有這些特性主要都實現為響應呼叫方的輸入(主要是以數字按鍵的形式輸入)讀寫音頻文件。DTMF數字可以多種不同的方式發送給Asterisk系統。同樣,這些實現細節都由通道驅動處理。當讀入一個按鍵輸入時,Asterisk將其轉換為一個通用按鍵事件,并以語音郵件代碼形式傳送。
我們討論過,Asterisk重要接口之一是編解碼轉碼器接口。編解碼的實現對于這類呼叫場景而言非常重要。語音郵件代碼向呼叫方回放一個音頻文件時,Asterisk系統與呼叫方通信使用的音頻格式不一定和該音頻格式相同。如果需要音頻轉碼,系統會生成一個轉碼路徑,從源格式經一個或多個編解碼轉碼器到目標格式。

圖1.6 VoicemailMain的調用
在某一時刻,呼叫者完成與語音郵件系統的交互,掛斷了呼叫。這時通道驅動檢測到此動作的發生,并將其轉換為Asterisk通道的一個通用信號事件。語音郵件代碼接收到這一信號事件后退出,因為呼叫方掛斷后就沒有什么可執行的了。然后通道線程中的控制流程將返回到主循環,繼續執行撥號計劃。
1.4.2 呼叫橋接
Asterisk中還有一個很常用的呼叫場景叫做兩通道間的呼叫橋接。此場景即一方電話通過系統呼叫另一方電話。呼叫的初始設置過程與前例相同。呼叫設置完畢,通道線程開始執行呼叫計劃時,之后的處理流程是不同的。
下面這個撥號計劃是呼叫橋接的一個簡單示例。如果系統使用了此擴展,當一方電話撥叫1234時,撥號計劃執行應用*Dial*,這正是發起出站呼叫的主應用。
~~~
exten => 1234,1,Dial(SIP/bob)
~~~
*Dial*應用的參數*SIP/bob*的含義是,系統應發起一個出站呼叫,發送到設備*SIP/bob*。此參數的*SIP*部分指定了傳送呼叫應使用SIP協議,*bob*部分由實現SIP協議的通道驅動*chan_sip*負責解釋。假設此通道驅動有一個叫做bob的賬戶已經配置正確,那么它就知道如何將呼叫送達Bob的電話。
首先,應用程序*Dial*要求Asterisk內核根據*SIP/bob*標識符分配一個新的Asterisk通道。然后,內核請求SIP通道驅動執行針對所用技術的初始化操作。通道驅動也會發起出站呼叫過程。隨著請求操作的繼續執行,將會有事件傳回給Asterisk內核,并由*Dial*應用程序接收。這些事件包括呼叫響應、目標忙、網絡擁塞、呼叫被拒,或者其它很多可能的響應。理想情況下,呼叫會被應答。然而事實上,被應答的呼叫又被傳回到入站通道上。Asterisk在應答出站呼叫之前,都不會應答這一部分接入系統的呼叫。當兩個通道都有應答時,通道橋接就開始了(如圖1.7)。

圖1.7 普通呼叫橋接的組件圖
通道橋接過程中,音頻和信號事件由一個通道不斷傳送至另一通道,直到發生某些導致橋接終止的事件,如一方呼叫掛斷。圖1.8所示的順序圖闡釋了通過呼叫橋接傳輸音頻幀的執行過程。

圖1.8 橋接中處理音頻幀的順序圖
呼叫完成時,掛斷流程與前例很相似。主要不同之處在于此處涉及兩個通道。通道線程結束之前,兩個通道都要執行針對技術的掛斷處理操作。
## 1.5 結論
迄今為止,Asterisk的架構已有十年以上的歷史。然而,盡管這個行業在不斷發展,Asterisk的一些東西,如通道的基本概念、使用撥號計劃進行靈活的呼叫處理,仍然支持著復雜電話系統的開發。有一個領域Asterisk的架構還沒有處理的太好,即如何使系統在多服務器間可伸縮。Asterisk開發社區正在開發一個叫做Asterisk SCF(可伸縮通信框架)的伙伴項目,目的就是解決可伸縮性的課題。未來幾年,我們期待看到Asterisk以及Asterisk SCF繼續稱雄電話市場,包括更大型的系統安裝項目。
## 腳注
1. http://www.asterisk.org/
2. DTMF表示多頻雙音,即按下一個電話鍵發送的呼叫音。
- 前言(卷一)
- 卷1:第1章 Asterisk
- 卷1:第3章 The Bourne-Again Shell
- 卷1:第5章 CMake
- 卷1:第6章 Eclipse之一
- 卷1:第6章 Eclipse之二
- 卷1:第6章 Eclipse之三
- 卷1:第8章 HDFS——Hadoop分布式文件系統之一
- 卷1:第8章 HDFS——Hadoop分布式文件系統之二
- 卷1:第8章 HDFS——Hadoop分布式文件系統
- 卷1:第12章 Mercurial
- 卷1:第13章 NoSQL生態系統
- 卷1:第14章 Python打包工具
- 卷1:第15章 Riak與Erlang/OTP
- 卷1:第16章 Selenium WebDriver
- 卷1:第18章 SnowFlock
- 卷1:第22章 Violet
- 卷1:第24章 VTK
- 卷1:第25章 韋諾之戰
- 卷2:第1章 可擴展Web架構與分布式系統之一
- 卷2:第1章 可擴展Web架構與分布式系統之二
- 卷2:第2章 Firefox發布工程
- 卷2:第3章 FreeRTOS
- 卷2:第4章 GDB
- 卷2:第5章 Glasgow Haskell編譯器
- 卷2:第6章 Git
- 卷2:第7章 GPSD
- 卷2:第9章 ITK
- 卷2:第11章 matplotlib
- 卷2:第12章 MediaWiki之一
- 卷2:第12章 MediaWiki之二
- 卷2:第13章 Moodle
- 卷2:第14章 NginX
- 卷2:第15章 Open MPI
- 卷2:第18章 Puppet part 1
- 卷2:第18章 Puppet part 2
- 卷2:第19章 PyPy
- 卷2:第20章 SQLAlchemy
- 卷2:第21章 Twisted
- 卷2:第22章 Yesod
- 卷2:第24章 ZeroMQ