概述
Erlang 是一門函數(shù)式編程語言。相比其它語言來說,它是一門小眾語言,但它具有輕量級多進程、高并發(fā)、熱代碼替換等地球人不能拒絕的特性,并非常易于創(chuàng)建集群應(yīng)用(Cluster)。更重要的是,它天生就是為了編寫電信應(yīng)用的。
除電信領(lǐng)域外,它在游戲及金融領(lǐng)域也擔(dān)負著重要的作用。就 FreeSWITCH 來講,OpenACD 及 Whistle 已是比較成熟的項目。
FreeSWITCH 有一個原生的模塊叫 mod_erlang_socket,它作為一個隱藏的Erlang節(jié)點(Hidden Node)可以與任何 Erlang 節(jié)點通信。
以下假定讀者有一定 Erlang 基礎(chǔ),其基本原理和語法就不再贅述。
安裝 Erlang 模塊
Erlang 模塊默認是不安裝的,首先請確認你的機器上已經(jīng)安裝 Erlang。我總是通過源代碼安裝(./configure && make && make install),如果通過管理工具安裝的請確認有 erlang-devel 或 erlang-dev 包。
確認安裝好 Erlang 環(huán)境以后,在 FreeSWITCH 源代碼目錄中重新執(zhí)行 ./configure,以產(chǎn)生相關(guān)的 Makefile。
然后執(zhí)行
make mod_erlang_event-install
在 FreeSWITCH 控制臺上執(zhí)行
load mod_erlang_event
把電話控制權(quán)轉(zhuǎn)給 Erlang
在該例子中,Erlang 程序作為一個節(jié)點運行,它類似于 Event Socket 概念中的外連套接字(outbound socket)。
在我們開始之前,先檢查 Erlang 模塊的設(shè)置,確認 erlang_event.conf.xml 中有以下三行(其它的行不動).
<param name="nodename" value="freeswitch@localhost"/>
<param name="encoding" value="binary"/>
<param name="cookie" value="ClueCon"/>
其中第一行的 nodename 是為了強制 FreeSWITCH 節(jié)點的名字,如果不配置的話,F(xiàn)reeSWITCH 會自己的節(jié)點起一個最適合的名字,但我們發(fā)現(xiàn)它自己起的名字并不總是正確,因此在這里為了防止引起任何可能的錯誤,人工指定一個名字。注意這里我們使用了短名字,如果你的 Erlang 程序在另一臺機器上,你應(yīng)該使用長名字。
第二行,我們使用二進制編碼。默認的節(jié)點間通信使用文本編碼,文本編碼比二進制編碼效率要低一些。
第三行,設(shè)置該節(jié)點的 Cookie。注意要與 Erlang 節(jié)點的要相同。
在 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 )是一個”嘀“聲,請輸入一個號碼(INPUT_NUMBER_SOUND)使用兩個"嘀"聲。我們這么做的目的是使讀者能直接編譯代碼而不用依賴于任何聲音文件,當(dāng)然,為了能更好的理解這個例子,你應(yīng)該換成更觀的聲音文件,如“/tmp/welcome.wav”(您好,歡迎,請輸入一個號碼 ....)。
為了使用整個通信過程更加透明化,我沒有使用缺省的 freeswitch.erl 庫,而是自己寫了幾個函數(shù)用于在 Erlang 程序和 FreeSWITCH 間傳遞真正的消息。
把電話發(fā)給 Erlang
在 outbound 模式下,F(xiàn)reeSWITCH (可以看作一個客戶端)會連接到你的 Erlang 程序節(jié)點(可以看作一個服務(wù)器)上,并把進來的電話控制權(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 會首先將當(dāng)前的 Channel 給 Park 起來,并給你的 Erlang 程序(test@localhost 節(jié)點)發(fā)送一個 RPC 調(diào)用,調(diào)用的函數(shù)為:ivr:start(Ref)。其中,ivr:start 是你在上述 XML 中定義的,Ref 則是由 FreeSWITCH 端生成的針對該請求的唯一引用。在 Erlang 端,start/1 函數(shù)應(yīng)該 spawn 一個新的進程,并且并新進程的 PID 返回,F(xiàn)reeSWITCH 收到后會將與該 Channel 相關(guān)的所有事件(Event)送給該進程。
start(Ref) ->
NewPid = spawn(fun() -> serve() end),
{Ref, NewPid}.
上述代碼產(chǎn)生一個新進程,新進程將運行 serve() 函數(shù),同時原來的進程會將新進程的 Pid 返回給 FreeSWITCH。
這是用 Erlang 控制呼叫的最簡單的方法。任何時候來了一個呼叫,F(xiàn)reeSWITCH 發(fā)起一個遠端 RPC 調(diào)用給 Erlang, Erlang 端啟動一個新進程來控制該呼叫。由于 Erlang 的進程都是非常輕量級的,因而這種方式非常優(yōu)雅(當(dāng)然,Erlang 端也不用每次都啟動一個新進程,比如說可以事先啟動一組 Pid,看到進來的請求時選擇一個空閑的進程來為該電話服務(wù)。當(dāng)然,正如我們一再強調(diào)的,Erlang 中的進程是非常輕量級的,產(chǎn)生一個新進程也是相當(dāng)快的,所以,沒有必要用這種傳統(tǒng)軟件中“進程池”那么復(fù)雜方法)。
我們將在后面再討論 serve() 函數(shù)。
7778 法
除了使用 RPC 生成新的進程外,還可以用另外一種方法產(chǎn)生新進程。如上面 7778 所示,與 7777 不同的是,其中的 ivr:start 中的 start 換成了“!”。該方法需要你首先在 Erlang 端啟動一個進程,該進程監(jiān)聽也有進來的請求。當(dāng) FreeSWITCH 把電話路由到 Erlang 節(jié)點時,“!”語法說明,F(xiàn)reeSWITCH 會發(fā)送一個 {getpid, ...} 消息給 Erlang,意思是說,我這里有一個電話,告訴我哪個 Pid 可以處理。這種方法比上一種方法稍微有點復(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é)點啟動后首先執(zhí)行 start/0 以啟動監(jiān)聽。start() 時,將首先注冊一個名字,叫 ivr,然后進入 loop 循環(huán)等待接收消息。一旦它收到 {get_pid, ...} 消息,就 spawn 一個新的進程,并把新進程的 Pid 發(fā)送給 FreeSWITCH,然后再次等待新的消息。在這里,新產(chǎn)生的進程同樣執(zhí)行 serve() 函數(shù)為新進來的 Channel 服務(wù)。
呼叫控制
我們來做這樣一個例子。當(dāng)有電話進入時,它首先播放歡迎音:“您好,歡迎致電 XX 公司 ...”,然后讓用戶輸入一個電話號碼:“請輸入一個分機號”,接下來我們會檢查號碼并轉(zhuǎn)接到該號碼,如果不正確,則重新讓用戶輸入。
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.
到這里,我們再回想一下,F(xiàn)reeSWITCH 將一個新來電置為 Park 狀態(tài),然后向你的 Erlang 程序要一個 Pid,得到后會將所有與該 Channel 相關(guān)的消息發(fā)送給該 Pid(而該 Pid 現(xiàn)在正在運行 serve() 函數(shù)等待新消息)。如果一期如我們所愿,該 Pid 收到的第一個消息永遠是 {call, {event, [UUID | Event]} } 。好了,接下來就看你的了。
首先,為了學(xué)習(xí)方便,你打印了一條 Log 消息。然后,你通過設(shè)置 hangup_after_bridge=true 來確保該 Channel 能正常掛斷(你可以看到,跟在 dialplan 中的設(shè)置是一樣的)。接下來,你播放(playback)了歡迎音。注意,這里的 send_lock_msg() 很關(guān)鍵,它確保 FreeSWITCH 在播放完當(dāng)前文件后再執(zhí)行收到的下一條消息。
后續(xù)的消息都類似 {call_event, {event, [UUID | Event]} }. 所以,如果你收到 CHANNEL_EXECUTE_COMPLETE 消息后,檢測到其 Application 參數(shù)是 read 時,它表示用戶按下了按鍵(或者超時),在上面的代碼中,它就會 bridge 到某一分機(或者在輸入超時的情況下重新詢問號碼).
當(dāng)收到 call_hangup 消息時,意味著電話已經(jīng)掛機了,我們的Erlang進程沒有別的事可做,就輕輕的消亡了。
下面是 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)機實現(xiàn)
事實上,一個 channel 在不同的時刻有不同的狀態(tài),所以呼叫流程控制非常適合用狀態(tài)機實現(xiàn)。Erlang OTP 有一個 gen_fsm behaviour,使用它可以非常簡單的實現(xiàn)狀態(tài)機。
本例中我使用了一個 gen_fsm 的增強版本,叫做 gen_fsfsm。它在原先的基礎(chǔ)上增加了以下幾個函數(shù)。
在收到 FreeSWITCH 側(cè)的大部分消息時回調(diào) Module:StateName(Message, StateData) ,如
wait_bridge({call_event, <<"CHANNEL_EXECUTE_COMPLETE">>, UUID, Event}, State)
在收到 CHANNEL_HANGUP_COMPLETE 及 CHANNEL_DESTROY 類的消息時回調(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
在這時我們使用 gen_fsfsm 來實現(xiàn)上面的例子。不過在這時里我們增加了一個語言提示:”請選擇提示語言,1 為普通話,2 為英語...”,以使用讀者對本例有更直觀的印象。
見下面的代碼。首先我們定義一個記錄(見第XX行)來“記住” FSM 進程中的一些東西。簡單起見,我們只是使用上面提到的7778法來啟動 FSM 進程。
進程啟動時(init),我們簡單的過渡到 welcom 狀態(tài)并等待,收到第一條消息后開始播放歡迎聲音,然后讓主叫用戶輸入一個按鍵以選擇語言,同時進程轉(zhuǎn)換到 wait_lang 狀態(tài)并等待。其中,我們將主叫號碼(caller id)等記在狀態(tài)機的 state 變量中。
當(dāng)用戶有按鍵輸入時,會收到 CHANNEL_EXECUTE_COMPLETE 消息,根據(jù)按鍵它會通過設(shè)置 sound_prefix 信道變量以支持不同語種的聲音。接下來會繼續(xù)讓用戶輸入一個號碼,并進入 wait_nubmer 狀態(tài)。
在 wait_number 可能會收到一個合法的號碼,然后進行呼叫,或者用戶輸入超時,它就會播放聲音提示用戶重新輸入一個號碼。
當(dāng)進程轉(zhuǎn)移到 wait_hangup 狀態(tài)時(表示電話已接通,雙方正在對話),它會把收到的消息統(tǒng)統(tǒng)打印出來,所以你會看到 CHANNEL_BRIDGE, CHANNEL_UNBRIDGE 等消息。當(dāng)然收到這些消息后你可以選擇更新數(shù)據(jù)庫有更有用的事件,這里就不再多說了。
當(dāng)然,你肯定已經(jīng)猜出來了,當(dāng)回調(diào)函數(shù)執(zhí)行到 handle_event({channel_hangup_event, ... }) 時進程會清理現(xiàn)場并終止。
-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}.