View
499
Download
0
Category
Preview:
Citation preview
簡単な自己紹介
こんにちは 伊藤雅俊( @masatoshiitoh )です。
Erlang/OTP で作ったもの。 「回線速度測定アプリ」のサーバー側。 ( TCP 、 UDP )
Erlang/OTP で作っているもの。 多人数ゲームサーバー。 (ゆるふわ開発中)
▪ https://github.com/masatoshiitoh/easymmo その他制作物。
10 年ほど前に、 speed.rbbtoday.com という回線速度測定サーバーを Java で作りました。
Erlang/OTP の全部は説明できません
これは、 Erlang/OTP で開発するのは(用途によっては)けっこう簡単かも、という感覚をお伝えする資料です。
Erlang についての記事や本の初見時に「???」となりそうなところを中心にしています。 お薦め参考書&サイト
▪ 「プログラミング Erlang 」(飛行機本)▪ 「すごい Erlang ゆかいに学ぼう!」(すごい E 本)▪ ウェブ版「すごい Erlang ゆかいに学ぼう! 」
▪ http://www.ymotongpoo.com/works/lyse-ja/
▪ リリースコトハジメ▪ https://gist.github.com/voluntas/4243786
細かい文法はざっくり省略しています。
質疑応答
なにか気になることはありますか?
せっかくなので、今日は随時質問で止めていただいて大丈夫です。
今回は、「最初のとっかかり」としての勉強会として、「悪くないんじゃない?」感を感じていただければと思います。
Erlang/OTP の特徴
ネットワークサーバーを書くのに便利な言語です。メッセージへのリアクションを書き連ねる、という構造です。 Websocket から生の TCP 、 UDP まで、同じような、受信パケットのパターン
に応じた処理(返信、状態更新など)で書けます(コードについては後述) 。 OTP によって、サーバープロセスの自動再起動などをテンプレ化した
「 gen_server 」というビヘイビアが用意されているので、初期化コードと機能部分を書けば動かせます。
テキスト・言語処理は不向き。 文字列がなく、すべてリストまたはバイナリとして扱います。 なので、 UI 系アプリとかも不向きだと思います( wx というライブラリは一応
ありますが・・・) 。
コード
さきほどのデモの Websocket サーバーの、Erlang/OTP 側のコード ( 抜粋 ) 。
何が来たら何を返すか、のコード。 now 、という文字(バイナリ)が届いたら、現在
時刻の文字列(バイナリ)を返す、というのが先頭の関数。
その下は、来たデータをそのまま送り返すのが 2つ続いています。
Websocket の読み書きは cowboy ライブラリにまかせているので、アプリはプロトコルの内容に専念しています。
このような「入ってくる値」と処理のペアを列挙するスタイルは、 Erlang/OTP では一般的なものです。
websocket_handle({text, <<"now">>}, Req, State) -> {{Year,Mon,Day},{Hour,Min,Sec}} = erlang:localtime(), TimeStr = io_lib:format("~p/~p/~p ~p:~p:~p", [Year,Mon,Day,Hour,Min,Sec]), BinTimeStr = list_to_binary(TimeStr), {reply, {text, BinTimeStr}, Req, State};
websocket_handle({text, Data}, Req, State) -> {reply, {text, Data}, Req, State};
websocket_handle({binary, Data}, Req, State) -> {reply, {binary, Data}, Req, State};
websocket_handle(_Frame, Req, State) -> {ok, Req, State}.
websocket_info(_Info, Req, State) -> {ok, Req, State}.
websocket_terminate(_Reason, _Req, _State) -> ok.
パターンマッチ
同じ関数名、同じ引数の数の関数をならべて、関数を呼び分けさせることができます。
同じ関数名でずらずら並んで見えますが、 Erlang ではふつうの書き方です。
websocket_handle({text, <<"now">>}, Req, State) -> {{Year,Mon,Day},{Hour,Min,Sec}} = erlang:localtime(), TimeStr = io_lib:format("~p/~p/~p ~p:~p:~p", [Year,Mon,Day,Hour,Min,Sec]), BinTimeStr = list_to_binary(TimeStr), {reply, {text, BinTimeStr}, Req, State};
websocket_handle({text, Data}, Req, State) -> {reply, {text, Data}, Req, State};
websocket_handle({binary, Data}, Req, State) -> {reply, {binary, Data}, Req, State};
websocket_handle(_Frame, Req, State) -> {ok, Req, State}.
websocket_info(_Info, Req, State) -> {ok, Req, State}.
websocket_terminate(_Reason, _Req, _State) -> ok.
メッセージへのリアクション
ネットワークサーバーを書くのに便利な言語 メッセージへのリアクションを書き連ねる構造、とご紹介しました。
Cowboy ウェブサーバーで websocket サーバーを書く場合(デモ例)▪ websocket_handle
Gen_server でサーバーを書く場合▪ handle_info
自前でメッセージを受信する場合▪ receive
TCPソケットのサンプル
TCP を直接扱いたい場合。
初期化まわり。
Listen して、 accept して、ソケットを渡して別プロセスを起動する、という定番の流れ。
start_server(Port) -> {ok, Listen} = gen_tcp:listen(Port, [binary, {active, false}, {packet, raw}, {nodelay, true}, {reuseaddr, true} ]), par_wait(Listen).
par_wait(Listen) -> {ok, Socket} = gen_tcp:accept(Listen), spawn(fun() -> loop(Socket) end), par_wait(Listen).
TCPソケットのサンプル
こちらは accept 後に起動される部分のコード。
gen_tcp:recv で受信データをとり、 case で分岐し、処理しわけています。
loop(Socket) -> top(Socket), gen_tcp:close(Socket).
top(Socket) -> X = gen_tcp:recv(Socket, 0, 2000), case X of {ok, <<1,1,1>>} -> io:format("down_start!~n"), measure_downlink(Socket); {ok, Bin} -> io:format("initialize received ~p~n", [binary_to_list(Bin)]), top(Socket); {error, closed} -> io:format("Server socket closed~n"); %% return from top. _A -> gen_tcp:close(Socket), io:format("Undefined error -~p at ~p - closed.~n", [ _A, ?LINE ]) end.
アクターモデル
Erlang といえばアクターモデル、メッセージパッシング、というのを聞くことがあると思います。
プロセスは「 spawn 」「 spawn_link 」で簡単に生成できます。 戻り値がプロセス IDなので、これがメッセージの宛先になります。
プロセスに向けてメッセージを送信する文法。 プロセス ID ! メッセージ
メッセージ受信側は、 receive ループで待ち受けます。
簡単な文法でプロセス生成とプロセス間(もしくは VM間)通信、すごい!!
gen_server などが抽象化してくれているので、あまり使いませんが・・・
プロセスとメッセージ送受信(一方通行版)
プロセス作ってメッセージを送る、のサンプル。
あまり直接書くことはありませんが、すごくシンプルなコードでメッセージの送受信ができるのがわかります。
メッセージを受け取る側は、 receive で受信したあと、自分自身を呼ぶことで、末尾再帰になっています。
受信するコードPid = spawn(fun() -> loop() end).loop() -> receive {add, X, Y} -> io:format(“~p + ~p = ~p ~n”, [X, Y, X+Y]); {sub, X, Y} -> io:format(“~p - ~p = ~p ~n”, [X, Y, X-Y]) end, loop().
送信するコード
Pid ! {add, 1, 2}.
プロセスとメッセージ送受信(双方向版)
メッセージへの結果を受け取りたいことはよくあります。
!記号でメッセージを投げ合う場合は、メッセージに返送の宛先となるプロセス ID を含めます。
ただ、結果を戻したい場合は、gen_server の handle_call がおすすめです。
受信するコードPid = spawn(fun() -> loop() end).loop() -> receive {CPid, add, X, Y} -> CPid ! X+Y; %% 計算結果を CPid宛に返送 {CPid, sub, X, Y} -> CPid ! X-Y; end, loop().
送信するコードdo_add(Pid, X, Y) -> Pid ! {self(), add, X, Y}, %% 送信 receive %% 結果を受信する Result -> Result end.
Gen_server のサンプル
gen_server を使う場合はこんな感じです。 gssample というモジュールが別プロセ
ス(ここでは <0.35.0> )で待ち受けています。
コード。モジュールの宣言部。
add と sub は、 gen_server 呼び出しをラップし、 API として外部に見せるためのものです。
下の 4 行、 start_link~handle_call が gen_server の必須コードです。
-module(gssample).-behaviour(gen_server).
-export([add/2, sub/2]).
-export([start_link/1]).-export([terminate/2]).-export([init/1]).-export([handle_call/3]).
1> gssample:start_link(0).{ok,<0.35.0>}2> gssample:add(1,2).33> gssample:sub(1,2).-14>
Gen_server のサンプル
これは gen_server 呼び出しをラップしている部分です。
gen_server:call は以下の2つの引数をとりますです。 モジュール名 送りつけたいタプル
成功時の戻り値は {ok, 値 }なので、必要に応じて値だけを戻します
add(A,B) -> Reply = gen_server:call(?MODULE, {add, A, B}), {ok, V} = Reply, V.
sub(A,B) -> Reply = gen_server:call(?MODULE, {sub, A, B}), {ok, V} = Reply, V.
Gen_server のサンプル
こちらは「呼ばれる側」。
gen_server で「なにかのサービスを提供するプロセス」を書くときのコード。
ここでは、 add と sub の 2種類の命令を受け取っています。
「 ! 」「 receive 」を直接使うよりも、何をやるコードか見えやすいです。
start_link(_Opts) -> Args = [], gen_server:start_link({local, ?MODULE}, ?MODULE, Args, []).
init(_Args) -> NewState = [], {ok, NewState}.
terminate(_Reason, State) -> ok.
handle_call({add, A, B}, From, State) -> {reply, {ok, A+B}, State};
handle_call({sub, A, B}, From, State) -> {reply, {ok, A-B}, State}.
Erlang 、他の言語と違うなー感がある部分
アトム、変数。 大文字小文字に文法上の意味がある。 大文字アルファベットまたはアンダースコア始まりは「変数」 。 小文字アルファベット始まりは「アトム」 定数値とか名前。生成数に上限がある。
変数への代入(束縛)は1回だけ。 > A = 3. %%代入。 > A = 3. %%パターンマッチ成功。 > A = 2. ←エラー! 3!=2、だから。 1回目の“=”は「代入」。2回目以降は「パターンマッチ」 。
関数型というよりはパターンマッチ&ガードによる「条件分岐」がErlangのポイントです。 C++でいう「オーバーロード」のように、これが引数に来たときはこれが実行される、というのを羅列できます。値や、タプルの形もパターンに書けます。
おかげで、関数の中で ifを使うことはあまりありません。
Erlang/OTP っぽい?ところ
プロセスを spawn で好きに作れる。 OS プロセスじゃないので作成できる数が多い ( 数千~数万 ) 。 定番の書き方は、末尾再帰でプロセスを生かし続ける形式です。 たとえば通信するプロセスなら、エンドポイント1つを1プロセスで、といった
ことが可能です。
タプルで戻り値が自由自在( map型、 dict型なども使える) 。 1 “foo” {a, 1, “foo”}
ほかの言語だと、構造体とかクラスインスタンスを返すところでも、必要なデータをまとめてタプルを組めば OK 。とても楽。
Erlang/OTP っぽい?ところ 2
タプルとパターンマッチ D = {ok, “foo”}. {ok, E } = D.
▪ E には、 “ foo” が入る。
これ、受信したデータのペイロードだけ受け取るときとかに、よく出てくる代入方法です。
右は、 TCP 受信サンプルの再掲です。
loop(Socket) -> top(Socket), gen_tcp:close(Socket).
top(Socket) -> X = gen_tcp:recv(Socket, 0, 2000), case X of {ok, <<1,1,1>>} -> io:format("down_start!~n"), measure_downlink(Socket); {ok, Bin} -> io:format("initialize received ~p~n", [binary_to_list(Bin)]), top(Socket); {error, closed} -> io:format("Server socket closed~n"); %% return from top. _A -> gen_tcp:close(Socket), io:format("Undefined error -~p at ~p - closed.~n", [ _A, ?LINE ]) end.
他の言語と似ているところ:
関数は好きな名前、好きな数を作れます。 さきほどの gen_server サンプルの add や sub など。
中の処理は標準的な演算子とか処理モジュールで組み合わせていける。 数値の足し算は「 + 」ですし。
いわゆる「 main 」はありませんが、 OTP で自動起動させる時の定番の書き方はあります。
公開されているライブラリが便利
Erlang/OTP では、特に通信まわりでいろいろなライブラリが公開されていて、非常に便利です。
今回は http & Websocket サーバーとして cowboy を使いました。
Rebar というビルドツールを使うと、外部リポジトリからの取込や更新が簡単に定義できます。
状態を保持する方法
「関数型は状態を保持しない、ステートレスだ。 」 でも、文字通りのステートレスで押し切るのは無理ですよね。
どうしてるか?
どこかが持ってます。 gen_server などの「 State 」引数。
▪ 再帰のアキュムレータに似てる。 再帰するときに、現在の状態を引き回す。
▪ State変数と同様。 プロセス辞書。
▪ 環境変数とかグローバル変数に似てる。 Ets/Dets/Mnesia など DB 。
▪ 変数的な高頻度更新には重い。
Gen_server の state引数
右は、さきほどのgen_serverのサンプル。 handle_call/handle_info の最後に引数。 handle_call/handle_info の戻り値の最後の要素。 「次に実行されるとき」の値が引き回されてい
る。 gen_serverはメッセージボックスによってシリ
アライズしているので、呼び出しは順番に。 メッセージで呼び出される。 メッセージはキューに入っている。
いっときに1つの関数が実行されるだけ Stateの値は、順次更新されれば良い。
ちなみにstateは、何をセットしてもいい。 タプル。 RecordやDictやMapでもいい。 もっともシンプルなものとしては「数値1つ」でも
いい。 空にしたいときは「 []」として、空の配列で良い。
start_link(_Opts) -> Args = [], gen_server:start_link({local, ?MODULE}, ?MODULE, Args, []).
init(_Args) -> NewState = [], {ok, NewState}.
terminate(_Reason, State) -> ok.
handle_call({add, A, B}, From, State) -> {reply, {ok, A+B}, State};
handle_call({sub, A, B}, From, State) -> {reply, {ok, A-B}, State}.
プロセス辞書
各プロセスが保持していて、モジュールや関数が違っても読み書きできる、グローバル変数というか連想配列みたいなもの。 put(Key, Value) get(Key) erase(Key) get_keys(Value) 値からキーを探す。 erase() 全消し。
プロセス辞書に依存するコードは、テストやデバッグには不向きなコードになる。(プロセス辞書の状態を揃えないとテストが安定しない、プロセス辞書の状態を見ないとデバッグできない)
冒頭のデモを修正するデモ
State 引数を更新しながら Websocket の相手をする。 http://ninenines.eu/docs/en/cowboy/HEAD/guide/
ws_handlers/
ビルドからデプロイについて
Rebar が便利です。
開発 Basho Technologies ( Riak の開発元です)
Rebar については、こちらの Gist がわかりやすいです 「 Erlang リリース コトハジメ」( voluntas さん)
▪ https://gist.github.com/voluntas/4243786
Rebarによるプロジェクト初期生成
rebar create
rebar create appid=hoge rebar create-node nodeid=hogehoge rebar generate
実行は、 ./rel/ノード ID/bin/ アプリ名 start
アプリケーションの中で自動起動させるには foo_sup.erl に書く。
gssample の起動方法がこれです。スーパバイザの init の中で定義します。▪ http://www.ymotongpoo.com/works/lyse-ja/ja/21_bawo.html▪ が参考になります。
foo.app の依存アプリケーションの一覧に追加する。 websocket サンプルの cowboy がこれです。
▪ http://www.ymotongpoo.com/works/lyse-ja/ja/22_boa.html▪ の 22.3.8 が参考になります。
自分で書くときは前者が簡単。 外部アプリケーションを利用するときなどに、後者のことがあります。ド
キュメントに起動の仕方が指定されているので、それに従います。
Recommended