概述
Erlang 是一門(mén)函數(shù)式編程語(yǔ)言。相比其它語(yǔ)言來(lái)說(shuō),它是一門(mén)小眾語(yǔ)言,但它具有輕量級(jí)多進(jìn)程、高并發(fā)、熱代碼替換等地球人不能拒絕的特性,并非常易于創(chuàng)建集群應(yīng)用(Cluster)。更重要的是,它天生就是為了編寫(xiě)電信應(yīng)用的。
除電信領(lǐng)域外,它在游戲及金融領(lǐng)域也擔(dān)負(fù)著重要的作用。就 FreeSWITCH 來(lái)講,OpenACD 及 Whistle 已是比較成熟的項(xiàng)目。
FreeSWITCH 有一個(gè)原生的模塊叫 mod_erlang_socket,它作為一個(gè)隱藏的Erlang節(jié)點(diǎn)(Hidden Node)可以與任何 Erlang 節(jié)點(diǎn)通信。
以下假定讀者有一定 Erlang 基礎(chǔ),其基本原理和語(yǔ)法就不再贅述。
安裝 Erlang 模塊
Erlang 模塊默認(rèn)是不安裝的,首先請(qǐng)確認(rèn)你的機(jī)器上已經(jīng)安裝 Erlang。我總是通過(guò)源代碼安裝(./configure && make && make install),如果通過(guò)管理工具安裝的請(qǐng)確認(rèn)有 erlang-devel 或 erlang-dev 包。
確認(rèn)安裝好 Erlang 環(huán)境以后,在 FreeSWITCH 源代碼目錄中重新執(zhí)行 ./configure,以產(chǎn)生相關(guān)的 Makefile。
然后執(zhí)行
make mod_erlang_event-install
在 FreeSWITCH 控制臺(tái)上執(zhí)行
load mod_erlang_event
把電話控制權(quán)轉(zhuǎn)給 Erlang
在該例子中,Erlang 程序作為一個(gè)節(jié)點(diǎn)運(yùn)行,它類似于 Event Socket 概念中的外連套接字(outbound socket)。
在我們開(kāi)始之前,先檢查 Erlang 模塊的設(shè)置,確認(rèn) erlang_event.conf.xml 中有以下三行(其它的行不動(dòng)).
<param name="nodename" value="freeswitch@localhost"/>
<param name="encoding" value="binary"/>
<param name="cookie" value="ClueCon"/>
其中第一行的 nodename 是為了強(qiáng)制 FreeSWITCH 節(jié)點(diǎn)的名字,如果不配置的話,F(xiàn)reeSWITCH 會(huì)自己的節(jié)點(diǎn)起一個(gè)最適合的名字,但我們發(fā)現(xiàn)它自己起的名字并不總是正確,因此在這里為了防止引起任何可能的錯(cuò)誤,人工指定一個(gè)名字。注意這里我們使用了短名字,如果你的 Erlang 程序在另一臺(tái)機(jī)器上,你應(yīng)該使用長(zhǎng)名字。
第二行,我們使用二進(jìn)制編碼。默認(rèn)的節(jié)點(diǎn)間通信使用文本編碼,文本編碼比二進(jìn)制編碼效率要低一些。
第三行,設(shè)置該節(jié)點(diǎn)的 Cookie。注意要與 Erlang 節(jié)點(diǎn)的要相同。
在 Erlang 代碼里,我們先定義一些宏:
-define(FS_NODE, 'freeswitch@localhost').
-define(WELCOME_SOUND, "tone_stream://%(100,1000,800);loops=1").
-define(INPUT_NUMBER_SOUND, "tone_stream://%(100,1000,800);loops=2").
在這里,歡迎音(WELCOME_SOUND )是一個(gè)”嘀“聲,請(qǐng)輸入一個(gè)號(hào)碼(INPUT_NUMBER_SOUND)使用兩個(gè)"嘀"聲。我們這么做的目的是使讀者能直接編譯代碼而不用依賴于任何聲音文件,當(dāng)然,為了能更好的理解這個(gè)例子,你應(yīng)該換成更觀的聲音文件,如“/tmp/welcome.wav”(您好,歡迎,請(qǐng)輸入一個(gè)號(hào)碼 ....)。
為了使用整個(gè)通信過(guò)程更加透明化,我沒(méi)有使用缺省的 freeswitch.erl 庫(kù),而是自己寫(xiě)了幾個(gè)函數(shù)用于在 Erlang 程序和 FreeSWITCH 間傳遞真正的消息。
把電話發(fā)給 Erlang
在 outbound 模式下,F(xiàn)reeSWITCH (可以看作一個(gè)客戶端)會(huì)連接到你的 Erlang 程序節(jié)點(diǎn)(可以看作一個(gè)服務(wù)器)上,并把進(jìn)來(lái)的電話控制權(quán)送給它。
在 Dialplan 中有兩種設(shè)置方法:
<extension name="test">
<condition field="destination_number" expression="^7777$">
<action application="erlang" data ="ivr:start test@localhost"/>
</condition>
</extension>
<extension name="test">
<condition field="destination_number" expression="^7778$">
<action application="erlang" data ="ivr:! test@localhost"/>
</condition>
</extension>
7777 法
7777 法是我起的名字。在這種方法里,如果你呼叫 7777, FreeSWITCH 會(huì)首先將當(dāng)前的 Channel 給 Park 起來(lái),并給你的 Erlang 程序(test@localhost 節(jié)點(diǎn))發(fā)送一個(gè) RPC 調(diào)用,調(diào)用的函數(shù)為:ivr:start(Ref)。其中,ivr:start 是你在上述 XML 中定義的,Ref 則是由 FreeSWITCH 端生成的針對(duì)該請(qǐng)求的唯一引用。在 Erlang 端,start/1 函數(shù)應(yīng)該 spawn 一個(gè)新的進(jìn)程,并且并新進(jìn)程的 PID 返回,F(xiàn)reeSWITCH 收到后會(huì)將與該 Channel 相關(guān)的所有事件(Event)送給該進(jìn)程。
start(Ref) ->
NewPid = spawn(fun() -> serve() end),
{Ref, NewPid}.
上述代碼產(chǎn)生一個(gè)新進(jìn)程,新進(jìn)程將運(yùn)行 serve() 函數(shù),同時(shí)原來(lái)的進(jìn)程會(huì)將新進(jìn)程的 Pid 返回給 FreeSWITCH。
這是用 Erlang 控制呼叫的最簡(jiǎn)單的方法。任何時(shí)候來(lái)了一個(gè)呼叫,F(xiàn)reeSWITCH 發(fā)起一個(gè)遠(yuǎn)端 RPC 調(diào)用給 Erlang, Erlang 端啟動(dòng)一個(gè)新進(jìn)程來(lái)控制該呼叫。由于 Erlang 的進(jìn)程都是非常輕量級(jí)的,因而這種方式非常優(yōu)雅(當(dāng)然,Erlang 端也不用每次都啟動(dòng)一個(gè)新進(jìn)程,比如說(shuō)可以事先啟動(dòng)一組 Pid,看到進(jìn)來(lái)的請(qǐng)求時(shí)選擇一個(gè)空閑的進(jìn)程來(lái)為該電話服務(wù)。當(dāng)然,正如我們一再?gòu)?qiáng)調(diào)的,Erlang 中的進(jìn)程是非常輕量級(jí)的,產(chǎn)生一個(gè)新進(jìn)程也是相當(dāng)快的,所以,沒(méi)有必要用這種傳統(tǒng)軟件中“進(jìn)程池”那么復(fù)雜方法)。
我們將在后面再討論 serve() 函數(shù)。
7778 法
除了使用 RPC 生成新的進(jìn)程外,還可以用另外一種方法產(chǎn)生新進(jìn)程。如上面 7778 所示,與 7777 不同的是,其中的 ivr:start 中的 start 換成了“!”。該方法需要你首先在 Erlang 端啟動(dòng)一個(gè)進(jìn)程,該進(jìn)程監(jiān)聽(tīng)也有進(jìn)來(lái)的請(qǐng)求。當(dāng) FreeSWITCH 把電話路由到 Erlang 節(jié)點(diǎn)時(shí),“!”語(yǔ)法說(shuō)明,F(xiàn)reeSWITCH 會(huì)發(fā)送一個(gè) {getpid, ...} 消息給 Erlang,意思是說(shuō),我這里有一個(gè)電話,告訴我哪個(gè) Pid 可以處理。這種方法比上一種方法稍微有點(diǎn)復(fù)雜,但程序有更大的自由度。
start() ->
register(ivr, self()),
loop().
loop() ->
receive
{get_pid, UUID, Ref, FSPid} ->
NewPid = spawn(fun() -> serve() end),
FSPid ! {Ref, NewPid},
?LOG("Main process ~p spawned new child process ~p~n", [self(), NewPid]),
loop();
_X ->
loop()
end.
上面的代碼中,用戶應(yīng)在 Erlang 節(jié)點(diǎn)啟動(dòng)后首先執(zhí)行 start/0 以啟動(dòng)監(jiān)聽(tīng)。start() 時(shí),將首先注冊(cè)一個(gè)名字,叫 ivr,然后進(jìn)入 loop 循環(huán)等待接收消息。一旦它收到 {get_pid, ...} 消息,就 spawn 一個(gè)新的進(jìn)程,并把新進(jìn)程的 Pid 發(fā)送給 FreeSWITCH,然后再次等待新的消息。在這里,新產(chǎn)生的進(jìn)程同樣執(zhí)行 serve() 函數(shù)為新進(jìn)來(lái)的 Channel 服務(wù)。
呼叫控制
我們來(lái)做這樣一個(gè)例子。當(dāng)有電話進(jìn)入時(shí),它首先播放歡迎音:“您好,歡迎致電 XX 公司 ...”,然后讓用戶輸入一個(gè)電話號(hào)碼:“請(qǐng)輸入一個(gè)分機(jī)號(hào)”,接下來(lái)我們會(huì)檢查號(hào)碼并轉(zhuǎn)接到該號(hào)碼,如果不正確,則重新讓用戶輸入。
serve() ->
receive
{call, {event, [UUID | Event]} } ->
?LOG("New call ~p~n", [UUID]),
send_msg(UUID, set, "hangup_after_bridge=true"),
send_lock_msg(UUID, playback, ?WELCOME_SOUND),
send_msg(UUID, read, "1 4 " ?INPUT_NUMBER_SOUND " erl_dst_number 5000 #"),
serve();
{call_event, {event, [UUID | Event]} } ->
Name = proplists:get_value(<<"Event-Name">>, Event),
App = proplists:get_value(<<"Application">>, Event),
?LOG("Event: ~p, App: ~p~n", [Name, App]),
case Name of
<<"CHANNEL_EXECUTE_COMPLETE">> when App =:= <<"read">> ->
case proplists:get_value(<<"variable_erl_dst_number">>, Event) of
undefined ->
send_msg(UUID, read, "1 4 " ?INPUT_NUMBER_SOUND " erl_dst_number 5000 #");
Dest ->
send_msg(UUID, bridge, "user/" ++ binary_to_list(Dest))
end;
_ -> ok
end,
serve();
call_hangup ->
?LOG("Call hangup~n", []);
_X ->
?LOG("ignoring message ~p~n", [_X]),
serve()
end.
到這里,我們?cè)倩叵胍幌,F(xiàn)reeSWITCH 將一個(gè)新來(lái)電置為 Park 狀態(tài),然后向你的 Erlang 程序要一個(gè) Pid,得到后會(huì)將所有與該 Channel 相關(guān)的消息發(fā)送給該 Pid(而該 Pid 現(xiàn)在正在運(yùn)行 serve() 函數(shù)等待新消息)。如果一期如我們所愿,該 Pid 收到的第一個(gè)消息永遠(yuǎn)是 {call, {event, [UUID | Event]} } 。好了,接下來(lái)就看你的了。
首先,為了學(xué)習(xí)方便,你打印了一條 Log 消息。然后,你通過(guò)設(shè)置 hangup_after_bridge=true 來(lái)確保該 Channel 能正常掛斷(你可以看到,跟在 dialplan 中的設(shè)置是一樣的)。接下來(lái),你播放(playback)了歡迎音。注意,這里的 send_lock_msg() 很關(guān)鍵,它確保 FreeSWITCH 在播放完當(dāng)前文件后再執(zhí)行收到的下一條消息。
后續(xù)的消息都類似 {call_event, {event, [UUID | Event]} }. 所以,如果你收到 CHANNEL_EXECUTE_COMPLETE 消息后,檢測(cè)到其 Application 參數(shù)是 read 時(shí),它表示用戶按下了按鍵(或者超時(shí)),在上面的代碼中,它就會(huì) bridge 到某一分機(jī)(或者在輸入超時(shí)的情況下重新詢問(wèn)號(hào)碼).
當(dāng)收到 call_hangup 消息時(shí),意味著電話已經(jīng)掛機(jī)了,我們的Erlang進(jìn)程沒(méi)有別的事可做,就輕輕的消亡了。
下面是 send_msg 和 send_lock_msg 函數(shù):
send_msg(UUID, App, Args) ->
Headers = [{"call-command", "execute"},
{"execute-app-name", atom_to_list(App)}, {"execute-app-arg", Args}],
send_msg(UUID, Headers).
send_lock_msg(UUID, App, Args) ->
Headers = [{"call-command", "execute"}, {"event-lock", "true"},
{"execute-app-name", atom_to_list(App)}, {"execute-app-arg", Args}],
send_msg(UUID, Headers).
send_msg(UUID, Headers) -> {sendmsg, ?FS_NODE} ! {sendmsg, UUID, Headers}.
使用狀態(tài)機(jī)實(shí)現(xiàn)
事實(shí)上,一個(gè) channel 在不同的時(shí)刻有不同的狀態(tài),所以呼叫流程控制非常適合用狀態(tài)機(jī)實(shí)現(xiàn)。Erlang OTP 有一個(gè) gen_fsm behaviour,使用它可以非常簡(jiǎn)單的實(shí)現(xiàn)狀態(tài)機(jī)。
本例中我使用了一個(gè) gen_fsm 的增強(qiáng)版本,叫做 gen_fsfsm。它在原先的基礎(chǔ)上增加了以下幾個(gè)函數(shù)。
在收到 FreeSWITCH 側(cè)的大部分消息時(shí)回調(diào) Module:StateName(Message, StateData) ,如
wait_bridge({call_event, <<"CHANNEL_EXECUTE_COMPLETE">>, UUID, Event}, State)
在收到 CHANNEL_HANGUP_COMPLETE 及 CHANNEL_DESTROY 類的消息時(shí)回調(diào)以下函數(shù)
Module:handle_event({channel_hangup_event, UUID, Event}, StateName, State) on
Module:handle_event({channel_destroy_event, UUID, Event}, StateName, State) on CHANNEL_DESTROY
在這時(shí)我們使用 gen_fsfsm 來(lái)實(shí)現(xiàn)上面的例子。不過(guò)在這時(shí)里我們?cè)黾恿艘粋(gè)語(yǔ)言提示:”請(qǐng)選擇提示語(yǔ)言,1 為普通話,2 為英語(yǔ)...”,以使用讀者對(duì)本例有更直觀的印象。
見(jiàn)下面的代碼。首先我們定義一個(gè)記錄(見(jiàn)第XX行)來(lái)“記住” FSM 進(jìn)程中的一些東西。簡(jiǎn)單起見(jiàn),我們只是使用上面提到的7778法來(lái)啟動(dòng) FSM 進(jìn)程。
進(jìn)程啟動(dòng)時(shí)(init),我們簡(jiǎn)單的過(guò)渡到 welcom 狀態(tài)并等待,收到第一條消息后開(kāi)始播放歡迎聲音,然后讓主叫用戶輸入一個(gè)按鍵以選擇語(yǔ)言,同時(shí)進(jìn)程轉(zhuǎn)換到 wait_lang 狀態(tài)并等待。其中,我們將主叫號(hào)碼(caller id)等記在狀態(tài)機(jī)的 state 變量中。
當(dāng)用戶有按鍵輸入時(shí),會(huì)收到 CHANNEL_EXECUTE_COMPLETE 消息,根據(jù)按鍵它會(huì)通過(guò)設(shè)置 sound_prefix 信道變量以支持不同語(yǔ)種的聲音。接下來(lái)會(huì)繼續(xù)讓用戶輸入一個(gè)號(hào)碼,并進(jìn)入 wait_nubmer 狀態(tài)。
在 wait_number 可能會(huì)收到一個(gè)合法的號(hào)碼,然后進(jìn)行呼叫,或者用戶輸入超時(shí),它就會(huì)播放聲音提示用戶重新輸入一個(gè)號(hào)碼。
當(dāng)進(jìn)程轉(zhuǎn)移到 wait_hangup 狀態(tài)時(shí)(表示電話已接通,雙方正在對(duì)話),它會(huì)把收到的消息統(tǒng)統(tǒng)打印出來(lái),所以你會(huì)看到 CHANNEL_BRIDGE, CHANNEL_UNBRIDGE 等消息。當(dāng)然收到這些消息后你可以選擇更新數(shù)據(jù)庫(kù)有更有用的事件,這里就不再多說(shuō)了。
當(dāng)然,你肯定已經(jīng)猜出來(lái)了,當(dāng)回調(diào)函數(shù)執(zhí)行到 handle_event({channel_hangup_event, ... }) 時(shí)進(jìn)程會(huì)清理現(xiàn)場(chǎng)并終止。
-module(fsm_ivr). -behaviour(gen_fsfsm).
-export([start/1, init/1, handle_info/3, handle_event/3, terminate/3]). -export([welcome/2, wait_lang/2, wait_number/2, wait_hangup/2]).
-define(FS_NODE, 'freeswitch@localhost'). -define(WELCOME_SOUND, "tone_stream://%(100,1000,800);loops=1"). -define(INPUT_NUMBER_SOUND, "tone_stream://%(100,1000,800);loops=2"). -define(SELECT_LANG_SOUND, "tone_stream://%(100,1000,800);loops=3"). -define(LOG(Fmt, Args), io:format("~b " ++ Fmt, [?LINE | Args])).
-record(state, {
fsnode :: atom(), % freeswitch node name
uuid :: undefined | string(), % channel uuid
cid_number :: undefined | string(), % caller id
dest_number :: undefined | string() % called number
}).
start(Ref) ->
{ok, NewPid} = gen_fsfsm:start(?MODULE, [], []),
{Ref, NewPid}.
init([]) ->
State = #state{fsnode = ?FS_NODE},
{ok, welcome, State}.
%% The state machine welcome({call, _Name, UUID, Event}, State) ->
CidNumber = proplists:get_value(<<"Caller-Caller-ID-Number">>, Event),
DestNumber = proplists:get_value(<<"Caller-Caller-Destination-Number">>, Event),
?LOG("welcome ~p", [CidNumber]),
send_lock_msg(UUID, playback, ?WELCOME_SOUND),
case u_utils:get_env(dtmf_type) of
{ok, inband} ->
send_msg(UUID, start_dtmf, "");
_ -> ok
end,
send_msg(UUID, read, "1 1 " ?SELECT_LANG_SOUND " erl_lang 5000 #"),
{next_state, wait_lang, State#state{uuid = UUID, cid_number = CidNumber, dest_number = DestNumber}}.
wait_lang({call_event, <<"CHANNEL_EXECUTE_COMPLETE">>, UUID, Event}, State) ->
case proplists:get_value(<<"Application">>, Event) of
<<"read">> ->
DTMF = proplists:get_value(<<"variable_erl_lang">>, Event),
LANG = case DTMF of
<<"1">> -> "cn";
<<"2">> -> "fr";
_ -> "en"
end,
send_msg(UUID, set, "sound_prefix=/usr/local/freeswitch/sounds/" ++ LANG);
_ -> ok
end,
send_msg(UUID, read, "1 4 " ?INPUT_NUMBER_SOUND " erl_dst_number 5000 #"),
{next_state, wait_number, State};
wait_lang(_Any, State) -> {next_state, wait_lang, State}. % ignore any other messages
wait_number({call_event, <<"CHANNEL_EXECUTE_COMPLETE">>, UUID, Event}, State) ->
case proplists:get_value(<<"Application">>, Event) of
<<"read">> ->
case proplists:get_value(<<"variable_erl_dst_number">>, Event) of
undefined ->
send_msg(UUID, read, "1 4 " ?INPUT_NUMBER_SOUND " erl_dst_number 5000 #"),
{next_state, wait_number, State};
Dest ->
send_msg(UUID, bridge, "user/" ++ binary_to_list(Dest)),
{next_state, wait_hangup, State}
end;
_ -> {next_state, wait_number, State}
end;
wait_number(_Any, State) -> {next_state, wait_number, State}. % ignore any other messages
wait_hangup({call_event, Name, _UUID, Event}, State) ->
?LOG("Event: ~p~n", [Name]),
{next_state, wait_hangup, State};
wait_hangup(_Any, State) -> {next_state, wait_hangup, State}. % ignore any other messages
handle_info(call_hangup, StateName, State) ->
?LOG("Call hangup on ~p~n", [StateName]),
{stop, normal, State};
handle_info(_Info, StateName, State) -> {next_state, StateName, State}.
handle_event({channel_hangup_event, UUID, Event}, StateName, State) ->
%% perhaps do bill here
HangupCause = proplists:get_value(<<"Channel-Hangup-Cause">>, Event),
?LOG("Hangup Cause: ~p~n", [HangupCause]),
{stop, normal, State}.
terminate(normal, StateName, State) -> ok; terminate(Reason, StateName, State) ->
% do some clean up here
send_msg(State#state.uuid, hangup, ""),
ok.
%% private functions send_msg(UUID, App, Args) ->
Headers = [{"call-command", "execute"},
{"execute-app-name", atom_to_list(App)}, {"execute-app-arg", Args}],
send_msg(UUID, Headers).
send_lock_msg(UUID, App, Args) ->
Headers = [{"call-command", "execute"}, {"event-lock", "true"},
{"execute-app-name", atom_to_list(App)}, {"execute-app-arg", Args}],
send_msg(UUID, Headers).
send_msg(UUID, Headers) -> {sendmsg, ?FS_NODE} ! {sendmsg, UUID, Headers}.