FreeSWITCH 與 Asterisk

FreeSWITCH vs Asterisk FreeSWITCH 與 Asterisk 比較

Anthony Minssale/文 Seven/譯

VoIP通信,與傳統的電話技術相比,不僅僅在於絕對的資費優勢,更重要的是很容易地通過開發相應的軟件,使其與企業的業務邏輯緊密集成。Asterisk作爲開源VoIP軟件的代表,以其強大的功能及相對低廉的建設成本,受到了全世界開發者的青睞。而FreeSWITCH作爲VoIP領域的新秀,在性能、穩定性及可伸縮性等方面則更勝一籌。本文原文在http://www.freeswitch.org/node/117, 發表於2008年4月,相對日新月異的技術來講,似乎有點過時。但本文作爲FreeSWITCH背後的故事,仍很有翻譯的必要。因此,本人不揣鄙陋,希望與大家共讀此文,請不吝批評指正。 --譯者注

FreeSWITCH 與 Asterisk 兩者有何不同?爲什麼又重新開發一個新的應用程序呢?最近,我聽到很多這樣的疑問。 爲此,我想對所有在該問題上有疑問的電話專家和愛好者們解釋一下。我曾有大約三年的時間用在開發 Asterisk 上,並最終成爲了 FreeSWITCH 的作者。因此,我對兩者都有相當豐富的經驗。首先,我想先講一點歷史以及我在 Asterisk 上的經驗;然後,再來解釋我開發FreeSWITCH的動機以及我是如何以另一種方式實現的。

我從2003年開始接觸 Asterisk,當時它還不到1.0版。那時對我來講,VoIP還是很新的東西。我下載並安裝了它,幾分鐘後,從插在我電腦後面的電話機裏傳出了電話撥號音,這令我非常興奮。接下來,我花了幾天的時間研究撥號計劃,絞盡腦汁的想能否能在連接到我的Linux PC上的電話上實現一些好玩的東西。由於做過許多Web開發,因此我積累了好多新鮮的點子,比如說根據來電顯示號碼與客戶電話號碼的對應關係來猜想他們爲什麼事情打電話等。我也想根據模式匹配來做我的撥號計劃,並着手編寫我的第一個模塊。最初,我做的第一個模塊是app_perl,現在叫做res_perl,當時曾用它在Asterisk中嵌入了一個Perl5的解釋器。現在我已經把它從我的系統中去掉了。

後來我開始開發一個Asterisk驅動的系統架構,用於管理我們的呼入電話隊列。我用app_queue和現在叫做AMI(大寫字母總是看起來比較酷)的管理接口開發了一個原型。它確實非常強大。你可以從一個T1線路的PSTN號碼呼入,並進入一個呼叫隊列,坐席代表也呼入該隊列,從而可以對客戶進行服務。非常酷!我一邊想一邊看着我的可愛的Web頁顯示着所有的隊列以及他們的登錄情況。並且它還能週期性的自動刷新。令人奇怪的是,有一次我瀏覽器一角上的小圖標在過了好長時間後仍在旋轉。那是我第一次聽說一個詞,一個令我永遠無法忘記的詞 -- 死鎖。

那是第一次,但決不是最後一次。那一天,我幾乎學到了所有關於GNU調試器的東西,而那只是許多問題的開始。隊列程序的死鎖,管理器的死鎖。控制檯的死鎖開始還比較少,後來卻成了一個永無休止的過程。現在,我非常熟悉“段錯誤(Segmentation Fault)”這個詞,它真是一個計算機開發者的玩笑。經過一年的辛勤排錯,我發現我已出乎意料的非常精通C語言並且有絕地戰士般的調試技巧。我有了一個分佈於七臺服務器、運行於DS3 TDM信道的服務平臺。與此同時,我也爲這一項目貢獻了大量的代碼,其中有好多是我具有明確版權的完整文件(http://www.cluecon.com/anthm.html)。

到了2005年,我已經儼然成了非常有名的Asterisk開發者。他們甚至在CREDITS文件以及《Asterisk,電話未來之路》這本書中感謝我。在Asterisk代碼樹中我不僅有大量的程序,而且還有一些他們不需要或者不想要的代碼,我把它們收集到了我的網站上。(至今仍在 http://www.freeswitch.org/node/50)

Asterisk 使用模塊化的設計方式。一箇中央核心調入稱爲模塊的共享目標文件以擴展功能。模塊用於實現特定的協議(如SIP)、程序(如個性化的IVR)和其它外部接口(如管理接口)等。 Asterisk的核心是多線程的,但它非常保守。僅僅用於初始化的信道以及執行一個程序的信道纔有線程。任何呼叫的B端都與A端都處於同一線程。當某些事件發生時(如一次轉移呼叫必須首先轉移到一個稱作僞信道的線程模式),該操作把一個信道所有內部數據從一個動態內存對象中分離出來,放入另一個信道中。它的實現在代碼註釋中被註明是“骯髒的”[1]。反向操作也是如此,當銷燬一個信道時,需要先克隆一個新信道,才能掛斷原信道。同時也需要修改CDR的結構以避免將它視爲一個新的呼叫。因此,對於一個呼叫,在呼叫轉移時經常會看到3或4個信道同時存在。

這種操作成了從另一個線程中取出一個信道事實上的方法,同時它也正是開發者許許多多頭痛的源頭。這種不確定的線程模式是我決定着手重寫這一應用程序的原因之一。

Asterisk使用線性鏈表管理活動的信道。鏈表通過一種結構體將一系列動態內存串在一起,這種結構體本身就是鏈表中的一個成員,並有一個指針指向它自己,以使它能鏈接無限的對象並能隨時訪問它們。這確實是一項非常有用的編程技術,但是,在多線程應用中它非常難於管理。在線程中必須使用一個信號量(互斥體,一種類似交通燈的東西)來確保在同一時刻只有一個線程可以對鏈表進行寫操作,否則當一個線程遍歷鏈表時,另一個線程可能會將元素移出。甚至還有比這更嚴重的問題 ─ 當一個線程正在銷燬或監聽一個信道的同時,若有另外一個線程訪問該鏈表時,會出現“段錯誤”。“段錯誤”在程序裏是一種非常嚴重的錯誤,它會造成進程立即終止,這就意味着在絕大多數情況下會中斷所有通話。我們所有人都看到過“防止初始死鎖”[2]這樣一個不太爲人所知的信息,它試圖鎖定一個信道,在10次不成功之後,就會繼續往下執行。

管理接口(或AMI)有一個概念,它將用於連接客戶端的套接字(socket)傳給程序,從而使你的模塊可以直接訪問它。或者說,更重要的是你可以寫入任何你想寫入的東西,只要你所寫入的東西符合Manager Events所規定的格式(協議)。但遺憾的是,這種格式沒有很好的結構,因而很難解析。

Asterisk的核心與某些模塊有密切的聯繫。由於核心使用了一些模塊中的二進制代碼,當它所依賴的某個模塊出現問題,Asterisk就根本無法啓動。如果你想打一個電話,至少在 Asterisk 1.2中,除使用app_dial和res_features外你別無選擇,這是因爲建立一個呼叫的代碼和邏輯實際上是在app_dial中,而不是在覈心裏。同時,橋接語音的頂層函數實際上包含在res_features中。

Asterisk的API沒有保護,大多數的函數和數據結構都是公有的,極易導致誤用或被繞過。其核心非常混亂,它假設每個信道都必須有一個文件描述符,儘管實際上某些情況下並不需要。許多看起來是一模一樣的操作,卻使用不同的算法和傑然不同的方式來實現,這種重複在代碼中隨處可見。

這僅僅是我在Asterisk中遇到的最多的問題一個簡要的概括。作爲一個程序員,我貢獻了大量的時間,並貢獻了我的服務器來作爲CVS代碼倉庫和Bug跟蹤管理服務器。我曾負責組織每週電話會議來計劃下一步的發展,並試圖解決我在上面提到過的問題。問題是,當你對着長長的問題列表,思考着需要花多少時間和精力來刪除或重寫多少代碼時,解決這些問題的動力就漸漸的沒有了。值得一提的是,沒有幾個人同意我的提議並願意同我一道做一個2.0的分支來重寫這些代碼。所以在2005年夏天我決定自己來。

在開始寫FreeSWITCH時,我主要專注於一個核心繫統,它包含所有的通用函數,即受到保護又能提供給高層的應用。像Asterisk一樣,我從Apache Web服務器上得到很多啓發,並選擇了一種模塊化的設計。第一天,我做的最基本的工作就是讓每一個信道有自己的線程,而不管它要做什麼。該線程會通過一個狀態機與核心交互。這種設計能保證每一個信道都有同樣的、可預測的路徑和狀態鉤子,同時可以通過覆蓋向系統增加重要的功能。這一點也類似其它面向對象的語言中的類繼承。

做到這點其實不容易,容我慢慢講。在開發FreeSWITCH的過程中我也遇到了段錯誤和死鎖(在前面遇到的多,後來就少了)。但是,我從核心開始做起,並從中走了出來。由於所有信道都有它們自己的線程,有時候你需要與它們進行交互。我通過使用一個讀、寫鎖,使得可以從一個散列表(哈希)中查找信道而不必遍歷一個線性鏈表,並且能絕對保證當一個外部線程引用到它時,一個信道無法被訪問也不能消失。這就保證了它的穩定,也不需要像Asterisk中“Channel Masquerades”之類的東西了。

FreeSWITCH核心提供的的大多數函數和對象都是有保護的,這通過強制它們按照設計的方式運行來實現。任何可擴展的或者由一個模塊來提供方法或函數都有一個特定的接口,從而避免了核心對模塊的依賴性。

整個系統採用清晰分層的結構,最核心的函數在最底層,其它函數分佈在各層並隨着層數和功能的增加而逐漸減少。

例如,我們可以寫一個大的函數,打開一個任意格式的聲音文件向一個信道中播放聲音。而其上層的API只需用一個簡單的函數向一個信道中播放文件,這樣就可以將其作爲一個精減的應用接口函數擴展到撥號計劃模塊。因此,你可以從你的撥號計劃中,也可以在你個性化的C程序中執行同樣的playback函數,甚至你也可以自己寫一個模塊,手工打開文件,並使用模塊的文件格式類服務而無需關注它的代碼。

FreeSWITCH由幾個模塊接口組成,列表如下:

撥號計劃(Dialplan): 實現呼叫狀態,獲取呼叫數據並進行路由。

終點(Endpoint): 爲不同協議實現的接口,如SIP,TDM等。

自動語音識別/文本語音轉換(ASR/TTS): 語音識別及合成。

目錄服務(Directory): LDAP類型的數據庫查詢。

事件(Events): 模塊可以觸發核心事件,也可以註冊自己的個性事件。這些事件可以在以後由事件消費者解析。

事件句柄(Event handlers): 遠程訪問事件和CDR。

格式(Formats): 文件模式如wav。

日誌(Loggers): 控制檯或文件日誌。

語言(Languages): 嵌入式語言,如Python和JavaScript。

語音(Say): 從聲音文件中組織話語的特定的語言模塊。

計時器(Timers): 可靠的計時器,用於間隔計時。

應用(Applications): 可以在一次呼叫中執行的程序,如語音信箱(Voicemail)。

FSAPI(FreeSWITCH 應用程序接口) 命令行程序,XML RPC函數,CGI類型的函數,帶輸入輸出原型的撥號計劃函數變量。

XML 到核心XML的鉤子可用於實時地查詢和創建基於XML的CDR。

所有的FreeSWITCH模塊都協同工作並僅僅通過核心API或內部事件相互通信。我們非常小心地實現它以保證它能正常工作,並避免其它外部模塊引起不期望的問題。

FreeSWITCH的事件系統用於記錄儘可能多的信息。在設計時,我假設大多數的用戶會通過一個個性化的模塊遠程接入FreeSWITCH來收集數據。所以,在FreeSWITCH中發生的每一個重要事情都會觸發一個事件。事件的格式非常類似於一個電子郵件,它具有一個事件頭和一個事件主體。事件可被序列化爲一個標準的Text格式或XML格式。任何數量的模塊均可以連接到事件系統上接收在線狀態,呼叫狀態及失敗等事件。事件樹內部的mod_event_socket可提供一個TCP連接,事件可以通過它被消費或記入日誌。另外,還可以通過此接口發送呼叫控制命令及雙向的音頻流。該套接字可以通過一個正在進行的呼叫進行向外連接(Outbound)或從一個遠程機器進行向內(Inbound)連接。

FreeSWITCH中另一個重要的概念是中心化的XML註冊表。當FreeSWITCH裝載時,它打開一個最高層的XML文件,並將其送入一個預處理器。預處理器可以解析特殊的指令來包含其它小的XML文件以及設置全局變量等。在此處設置的全局變量可以在後續的配置文件中引用。

如,你可以這樣用預處理指令設置全局變量:

<X-PRE-PROCESS cmd="set" data="moh_uri=local_stream://moh"/>

現在,在文件中的下一行開始你就可以使用 $$(moh_uri},它將在後續的輸出中被替換爲 local_stream://moh。處理完成後XML註冊表將裝入內存,以供其它模塊及核心訪問。它有以下幾個重要部分:

配置文件: 配置數據用於控制程序的行爲。

撥號計劃: 一個撥號計劃的XML表示可以用於 mod_dialplan_xml,用以路由呼叫和執行程序。

分詞: 可標記的IVR分詞是一些可以“說”多種語言的宏。

目錄: 域及用戶的集合,用於註冊及賬戶管理。

通過使用XML鉤子模塊,你可以綁定你的模塊來實時地查詢XML註冊表,收集必要的信息,以及返回到呼叫者的靜態文件中。這樣你可以像一個WEB瀏覽器和一個CGI程序一樣,通過同一個模型來控制動態的SIP註冊,動態語音郵件及動態配置集羣。

通過使用嵌入式語言,如Javascript, Java, Python和Perl等,可以使用一個簡單的高級接口來控制底層的應用。

FreeSWITCH工程的第一步是建立一個穩定的核心,在其上可以建立可擴展性的應用。我很高興的告訴大家在2008年5月26日將完成FreeSWITCH 1.0 PHOENIX版。有兩位敢吃螃蟹的人已經把還沒到1.0版的FreeSWITCH 用於他們的生產系統。根據他們的使用情況來看,我們在同樣的配置下能提供Asterisk 10倍的性能。

我希望這些解釋能足夠概括FreeSWICH和Asterisk的不同之處以及我爲何決定開始FreeSWITCH項目。我將永遠是一個Asterisk開發者,因爲我已深深的投入進去。並且,我也希望他們在以後的Asterisk開發方面有新的突破。我甚至還收集了很多過去曾經以爲已經丟失的代碼,放到我個人的網站上供大家使用, 也算是作爲我對引導我進入電話領域的這一工程的感激和美好祝願吧。

Asterisk是一個開源的PBX,而FreeSWITCH則是一個開源的軟交換機。與其它偉大的軟件如 Call Weaver、Bayonne、sipX、OpenSER以及更多其它開源電話程序相比,兩者還有很大發展空間。我每年都期望能參加在芝加哥召開的 ClueCon大會,並向其它開發者展示和交流這些項目(http://www.cluecon.com)。

我們所有人都可以相互激發和鼓勵以推進電話系統的發展。你可以問的最重要的問題是:“它是完成該功能的最合適的工具嗎?”

[1] / XXX This is a seriously wacked out operation. We're essentially putting the guts of the clone channel into the original channel. Start by killing off the original channel's backend. I'm not sure we're going to keep this function, because while the features are nice, the cost is very high in terms of pure nastiness. XXX /

[2] Avoiding initial deadlock

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章