gen_serverは前の節で取り扱ったサーバのビヘイビアの役割を持っています。
前節のように自分でビヘイビアを作成するのではなく、Erlangより提供されているビヘイビアである gen_server を利用する目的は 次のようなものです。
Javaで例えるのは少し間違っているような気がするのですが、利用者の感覚としては 「高機能のスーパークラスを継承して、自分の目的にあったサブクラスを作成する」というような感覚に 近いと思います。
今までの例より少し長いのですが、サンプルを掲載します。このサンプルは簡単なチャットシステムとなっています。
クライアントがサーバーにメッセージを送信すると、ログインしている全てのユーザに対してサーバーがメッセージを転送します
-module(gserv1).
-export([start/0]).
-export([command/1]).
-export([init/1, handle_call/3, handle_cast/2]).
%start server
start() ->
gen_server:start_link({local, l_server}, ?MODULE, [], []).
%client command
command(Who) ->
receive
login ->
io:format("~p~n",[login(Who)]);
logout ->
io:format("~p~n",[logout(Who)]);
restart ->
io:format("~p~n",[restart()]);
{send, Message} ->
send_message(Who, Message);
{message, Sender, Message} ->
io:format("Rcver: ~p~nSender: ~p~nMessage: ~p~n"
,[Who, Sender, Message])
end,
command(Who).
login(Who) ->
gen_server:call(l_server, {login, Who}).
logout(Who) ->
gen_server:call(l_server, {logout, Who}).
send_message(Who, Message) ->
gen_server:call(l_server, {send, Who, Message}).
restart() ->
gen_server:cast(l_server, restart).
%for gen_server
init(_Args) ->
{ok,{[],[]}}.
handle_call({login, Who}, {From,_Ref}, {PidList, LoginList}) ->
case lists:member(From, PidList) of
false ->
NewPidList = [From | PidList],
NewLoginList = [{Who, From} | LoginList],
Message = log_in;
true ->
Message = already_login,
NewPidList = PidList,
NewLoginList = LoginList
end,
{reply, Message, {NewPidList, NewLoginList}};
handle_call({logout, Who}, {From,_Ref}, {PidList, LoginList}) ->
case lists:member(From, PidList) of
true ->
NewPidList = PidList -- [From],
NewLoginList = LoginList -- [{Who, From}],
Message = logout;
false ->
NewPidList = PidList,
NewLoginList = LoginList,
Message = not_loggin
end,
{reply, Message, {NewPidList, NewLoginList}};
handle_call({send, Sender, Message}, _From, {PidList, LoginList}) ->
multicast(Sender, Message, PidList),
{reply, "send", {PidList,LoginList}}.
handle_cast(restart, _From) ->
{noreply, {[],[]}}.
%users function
multicast(_Sender, _Message, []) ->
done;
multicast(Who, Message, [ Pid | PidList]) ->
Pid ! {message, Who, Message},
multicast(Who, Message, PidList).
実行結果は以下の通りになります。
1> gserv1:start().
{ok,<0.33.0>}
2> Mario = spawn(gserv1, command, [mario]).
<0.35.0>
3> Mario ! login.
log_in
login
4> Mario ! login.
login
already_login
5> Luigi = spawn(gserv1, command, [luigi]).
<0.39.0>
6> Luigi ! login.
log_in
login
7> Luigi ! {send, hello}.
Rcver: mario
Sender: luigi
Message: hello
Rcver: luigi
Sender: luigi
Message: hello
{send,hello}
8> Luigi ! logout.
logout
logout
9> Luigi ! logout.
not_loggin
logout
10> Mario ! {send, good}.
Rcver: mario
Sender: mario
Message: good
{send,good}
11> Mario ! restart.
ok
restart
12> Mario ! logout.
not_loggin
logout
この節は、gen_serverの主要な部分のみを扱います。いくらか関数が抜けていますが、それは次節で扱います。
gen_serverはビヘイビアとしての機能を突き詰めたものです。通常のBIFとは異なり、 ユーザは gen_server により定められた関数を作成することが求められます。
頭にいれておいて欲しいことは、サーバは状態(State)を持っているということです。
それでは具体的に見ていきましょう。
サーバーを起動する gen_server の関数である start_link() は以下のものになります。
start() ->
gen_server:start_link({local, l_server}, ?MODULE, [], []).
この関数を実行すると、サーバーが作成されます。サーバ作成の際に、指定されたモジュールの init/1を利用します。この init/1 はサーバを起動する際に、ユーザが必要とする操作を行ないます。 この操作にはサーバの状態の作成も含まれます。
init(_Args) ->
{ok,{[],[]}}.
今回の例では特に処理はせず、サーバの状態として {[],[]}
を設定しています。
ここまでの実行結果は以下の通りです。
1> gserv1:start().
{ok,<0.33.0>}
次にサーバに対して、処理の依頼をしてみましょう。まずはログインの依頼について見ていきます。
command(Who) ->
receive
login ->
io:format("~p~n",[login(Who)]);
logout ->
io:format("~p~n",[logout(Who)]);
restart ->
io:format("~p~n",[restart()]);
{send, Message} ->
send_message(Who, Message);
{message, Sender, Message} ->
io:format("Rcver: ~p~nSender: ~p~nMessage: ~p~n"
,[Who, Sender, Message])
end,
command(Who).
これがクライアントプロセスとなる関数です。このプロセスがサーバに対して呼び出しを行ないます。
2> Mario = spawn(gserv1, command, [mario]).
<0.35.0>
3> Mario ! login.
このように、プロセスに対してメッセージを送ると、以下の関数が実行されます。
login(Who) ->
gen_server:call(l_server, {login, Who}).
この関数の中で gen_server:call/2 が使われています。 gen_server:call/2はサーバに処理を依頼し、結果を得るための関数です。
このモジュールは start_link() と同じようにビヘイビアを必要とします。
そのビヘイビアは以下のような書式となっています。
ログインを管理するビヘイビアはコードの以下の部分です。多少長いですが、動作は単純です。
すでに Pid が登録されていれば"登録済み"とし、登録されていなければ登録します。
handle_call({login, Who}, {From,_Ref}, {PidList, LoginList}) ->
case lists:member(From, PidList) of
false ->
NewPidList = [From | PidList],
NewLoginList = [{Who, From} | LoginList],
Message = log_in;
true ->
Message = already_login,
NewPidList = PidList,
NewLoginList = LoginList
end,
{reply, Message, {NewPidList, NewLoginList}};
返り値は、 gen_server:call により規定されている形にする必要があります。
gen_server:callを呼び出した式に返される値は、 Message のみです。
ここまでの実行結果を見てみましょう。
3> Mario ! login.
log_in
login
最後に gen_server:cast() を扱います。この関数は call とは違い、クライアントはサーバの処理結果を必要としていません。
callが send -> receive としていた(擬似的に)のに対し、castは send だけであると見なせます。
この関数を呼び出しているのは以下の部分です。
restart() ->
gen_server:cast(l_server, restart).
この関数も、以下の関数をコールバックとして実装する必要があります。
ここまでの処理結果は以下の通りです。
11> Mario ! restart.
ok
restart
gen_serverには関係ない部分のコードの説明をします。
ログアウトに関するコードは以下の部分です。これも、gen_server:call()を使っていますが、 gen_server:cast()でも作成することが可能です。
handle_call({logout, Who}, {From,_Ref}, {PidList, LoginList}) ->
case lists:member(From, PidList) of
true ->
NewPidList = PidList -- [From],
NewLoginList = LoginList -- [{Who, From}],
Message = logout;
false ->
NewPidList = PidList,
NewLoginList = LoginList,
Message = not_loggin
end,
{reply, Message, {NewPidList, NewLoginList}};
もしログアウトするPidが登録されていれば、lists:member(From, PidList)
がtrueになります。
登録されている場合は、差分リストを用いてリストより削除しています。
マルチキャストに関する部分は以下の部分です。
handle_call({send, Sender, Message}, _From, {PidList, LoginList}) ->
multicast(Sender, Message, PidList),
{reply, "send", {PidList,LoginList}}.
multicast(_Sender, _Message, []) ->
done;
multicast(Who, Message, [ Pid | PidList]) ->
Pid ! {message, Who, Message},
multicast(Who, Message, PidList).
これは、再帰関数を使っています。PidListに登録されている全てのPidに対してメッセージを送信しています。
送信リストが空になったら、この関数は終了して呼び出し元に帰ります。