Erlang World


top > otp > gen_server

gen_server

gen_serverについて

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によるサーバの起動

gen_serverはビヘイビアとしての機能を突き詰めたものです。通常のBIFとは異なり、 ユーザは gen_server により定められた関数を作成することが求められます。

頭にいれておいて欲しいことは、サーバは状態(State)を持っているということです。

それでは具体的に見ていきましょう。
サーバーを起動する gen_server の関数である start_link() は以下のものになります。

start() ->
    gen_server:start_link({local, l_server}, ?MODULE, [], []).
start_link(Module, Args, Options) -> Result
start_link(ServerName, Module, Args, Options) -> Result
ServerName = {local,Name} | {global,GlobalName}
Name = atom()
GlobalName = term()
Module = atom()
Args = term()
Options = [Option]
Option = {debug,Dbgs} | {timeout,Time} | {spawn_opt,SOpts}
Dbgs = [Dbg]
Dbg = trace | log | statistics | {log_to_file,FileName} | {install,{Func,FuncState}}
SOpts = [term()]
Result = {ok,Pid} | ignore | {error,Error}
Pid = pid()
Error = {already_started,Pid} | term()
ServerNameが登録される名前、Moduleがコールバックを実装しているモジュール。
Argsが init/1 に渡される引数。Optionsがオプションです。

この関数を実行すると、サーバーが作成されます。サーバ作成の際に、指定されたモジュールの init/1を利用します。この init/1 はサーバを起動する際に、ユーザが必要とする操作を行ないます。 この操作にはサーバの状態の作成も含まれます。

init(_Args) ->
    {ok,{[],[]}}.
Module:init(Args) -> Result
Args = term()
Result = {ok,State} | {stop,Reason}
State = term()
Reason = term()
Moduleは gen_server:start_linkの第2引数にあるモジュールとなります。 つまり、第二引数で指定されたモジュールの中に init/1 を作成しておく必要があります。
返り値は {ok, State} と一般的には設定します。

今回の例では特に処理はせず、サーバの状態として {[],[]} を設定しています。
ここまでの実行結果は以下の通りです。

1> gserv1:start().
{ok,<0.33.0>}

gen_serverのサーバに対する処理の依頼

次にサーバに対して、処理の依頼をしてみましょう。まずはログインの依頼について見ていきます。

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はサーバに処理を依頼し、結果を得るための関数です。

call(ServerRef, Request) -> Reply
call(ServerRef, Request, Timeout) -> Reply
ServerRef = Name | {Name,Node} | {global,GlobalName} | pid()
Node = atom()
GlobalName = term()
Request = term()
Timeout = int()>0 | infinity
Reply = term()
指定されたサーバ ServerRef にたいして、処理を依頼します。
Requestはサーバに渡すパラメータです。

このモジュールは start_link() と同じようにビヘイビアを必要とします。
そのビヘイビアは以下のような書式となっています。

Module:handle_call(Request, From, State) -> Result
Request = term()
From = {pid(),Tag}
State = term()
Result = {reply,Reply,NewState} | {reply,Reply,NewState,Timeout}
Moduleは 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のサーバへの処理の投げ渡し

最後に gen_server:cast() を扱います。この関数は call とは違い、クライアントはサーバの処理結果を必要としていません。
callが send -> receive としていた(擬似的に)のに対し、castは send だけであると見なせます。

この関数を呼び出しているのは以下の部分です。

restart() ->
    gen_server:cast(l_server, restart).
cast(ServerRef, Request) -> ok
ServerRef = Name | {Name,Node} | {global,GlobalName} | pid()
Node = atom()
GlobalName = term()
Request = term()
指定されたサーバ ServerRef に対して、リクエスト Request を送ります。 この関数は実行結果を待たないので、即時実行されます。

この関数も、以下の関数をコールバックとして実装する必要があります。

Module:handle_cast(Request, State) -> Result
Request = term()
State = term()
Result = {noreply,NewState}
引数が handle_call と異なって、呼び出し元の情報がありません。
必要があればリクエストの中に含める必要があります。

ここまでの処理結果は以下の通りです。

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に対してメッセージを送信しています。
送信リストが空になったら、この関数は終了して呼び出し元に帰ります。


Yuichi ITO. All rights reserved.
mail to : ad
inserted by FC2 system