このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

Erlang ICのマッピング規則とプロトコル・スタック

遠隔呼び出しのメカニズムもプロトコルも非常にシンプルで、きれいなスタック構造になっています。

昨日書きましたが、いやっ、ほんとに単純明快な構造なんですよ。http://www.erlang.org/doc/pdf/ic.pdf の引き写しに過ぎないけど、遠隔呼び出しについて、例と共にメモしておきます。

例として使うIDL定義

例として、次のわざとらしい(OMG IDLによる)インターフェース定義を使います。


// File: greeting.idl

interface greeting {
void say(in string msg);
readonly attribute string name;
oneway void smile();
};

interface greeting を実装したプロセスを「greetingサーバー」と呼ぶことにすると、このインターフェース定義から次のようなことが読み取れます。

greetingサーバーは、例えば遠隔呼び出しsay("Hello")を実行します。属性(attribute)とは、外部からアクセスできる変数みたいなものです。ここで出ている文字列属性nameは、readonlyが付いているので、取得(get)はできるが設定(set)はできない属性ですね。smileも手続き(関数)の形をしてますが、onewayが付いているので一方向(クラアント→サーバー)の“通知”または“指令”です。onewayが付いてなければ、戻り値の到着を待つ同期呼び出しになります。sayはonewayが付いてないので、戻り値がvoidであっても同期呼び出しです。

ICのマッピング規則

Erlang/OTPに付属のIDLコンパイラはICという素っ気ない名前で呼ばれています。ICは、OMG IDLファイルを入力としてErlang、C、Javaのファイルを生成できますが、ここではErlangのケースを考えます。IDLを、各種のプログラミング言語にどう対応付けるか、その規則をマッピング(あるいはバインディング)規則といいます。

IDL→Erlangマッピング規則の基本方針は:

  • データ型は適当に(いや、適切に)対応付ける。
  • インターフェースはErlangモジュールに対応付ける。
  • オペレーション(関数/手続き)は、Erlang関数に対応付ける。
  • 属性は、get関数/set関数の組に対応付ける。readonlyならget関数だけ。
  • onewayかどうか(非同期/同期の別)は、メッセージ通信の方式として区別する。

OMG IDLはオブジェクト指向を背景に設計されているので、サーバーオブジェクトの状態を認めます。このオブジェクト状態(this、selfに相当)は、実装関数の第1引数となります。

というわけなので、greetingサーバーの実装モジュール(greeting_impl)は、次のErlang関数を提供(export)することになります*1

  1. say(OE_State, Msg) 戻り値はvoid型(アトムokを返す)
  2. '_get_name'(OE_State) 戻り値はstring型
  3. smile(OE_State) 戻り値は無視される

実装関数の書き方は完全にプレーンなわけではなくて、次のように書きます。


say(OE_State, Msg) ->
io:fwrite("~s\n", [Msg]), {reply, ok, OE_State}.

'_get_name'(OE_State) -> % 変数とみなされないために引用符が必要
{reply, "Erlang", OE_State}.

smile(OE_State) ->
io:fwrite("(^_^)\n"), {noreply, OE_State}.

この約束がどこから来てるのか -- それが今日の本題なんです。

Erlang ICのプロトコル・スタック

クライアントからの遠隔呼び出し(同期)/遠隔通知(非同期)は、データとしてネットワークに押し出され、サーバーに向かって旅をしますが、そのときデータ形式は、“階層化された約束”に従って符号化/複合化されます。その“階層化された約束”がプロトコル・スタックですね。絵(?)に描いておくと:


--------------------------------
IC Protocol : 呼び出し/戻り値 → Erlangデータ
--------------------------------
Gen_server Protocol : Erlangデータ → 種別/送信元などを付加したデータ
--------------------------------
Erlang Distribution Protocol : Erlangデータ → バイト列(メタデータ付加)
--------------------------------
TCP/IP : バイト列の運搬
--------------------------------

say("Hello") という呼び出しは、次のように符号化されます。

  1. IC Protocolにより、say("Hello") → {say, "Hello"}
  2. Gen_server Protocolにより、{say, "Hello"} → {'$gen_call', {送信元PID, 一意識別子}, {say, "Hello"}}
  3. Erlang Distribution Protocolにより、 {'$gen_call', {送信元PID, 一意識別子}, {say, "Hello"}} → 対応するバイト列
  4. TCP/IPにより、バイト列 → バイト列をデータ部(ペイロード)として含む通信パケット

属性アクセスに関しては、ICのマッピング規則で「name属性 → '_get_name'関数」と変換されているので、IC ProtocolとGen_server Protocolにより、{'$gen_call', {送信元PID, 一意識別子}, '_get_name'} に変換されます。引数なし呼び出しは、関数名(アトム)だけに符号化されることに注意してください。

非同期通知である smile() はというと、返信不要なので、{'$gen_cast', smile} という単純なデータとなります。

gen_server:call/2,3、gen_server:cast/2を使うには、ICマッピング規則とIC Protocolを知っている必要があります。(Gen_server Protocolは内部で処理してくれる。)ローカル(シングルノード)で実験してみると:


15> gen_server:call(Svr, {say, "Hello"}).
Hello
ok
16> gen_server:call(Svr, '_get_name').
"Erlang"
17> gen_server:cast(Svr, smile).
(^_^)
ok
18>

サーバー側の処理

サーバープロセスに、{'$gen_call', {送信元PID, 一意識別子}, {say, "Hello"}} とか {'$gen_cast', smile} がメッセージとして届くと、サーバープロセスは対応するハンドラを選んで実行します。そのハンドラを実装モジュールgreeting_impl内に書いたわけですね。そして、ハンドラに関するお約束が:

  1. 同期呼び出し(call)のハンドラなら、{reply, 戻り値, 新状態} を返す
  2. 非同期通知(cast)のハンドラなら、{noreply, 新状態} を返す

だったのです(より詳しくは、http://www.erlang.org/doc/man/gen_server.html)。

ハンドラからの値を受け取ったサーバープロセスは、自分の状態を更新し、必要なら戻り値をネットワークに送り出します。サーバーからクライアントに返信される戻り値は、IC Protocolでは特に変換されず、Gen_server Protocolにより {一意識別子, 戻り値} と加工されるだけです。

以上のことを理解していれば、新しいマッピング規則を考えたり、プロトコル・スタックの一部を差し替えたりすることもできるでしょう。

*1:他に、サーバープロセスのライフサイクル管理関数が必要です。