33
Erlang/OTP へへへへへ Websocket へへへへへへへへ へへへへへへへへへへへへへへへへへへへへ へへへへ ( @masatoshiitoh )

Erlangご紹介 websocket編

Embed Size (px)

Citation preview

Erlang/OTP へようこそ

Websocket サーバーを例題に言語からデプロイまでウォークスルーします

伊藤雅俊 ( @masatoshiitoh )

簡単な自己紹介

こんにちは 伊藤雅俊( @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 サーバー デモ

Chrome の websocket client を使います。

接続して、メッセージの送受信をします。

デモ画面。

コード

さきほどのデモの 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/OTP の「 OTP 」は、 Ruby on Rails の「 Rails 」のようなもの。

言語+フレームワーク。

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 というビルドツールを使うと、外部リポジトリからの取込や更新が簡単に定義できます。

おまけ:

バイナリ、ビット単位のパターンマッチ低レベルプロトコルの実装に便利。必要になったらどうぞ。

ユニットテストEunit

Dialyzer 静的解析ツールもあります。

状態を保持する方法

「関数型は状態を保持しない、ステートレスだ。 」 でも、文字通りのステートレスで押し切るのは無理ですよね。

どうしてるか?

どこかが持ってます。 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によるプロジェクト初期生成

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 が参考になります。

自分で書くときは前者が簡単。 外部アプリケーションを利用するときなどに、後者のことがあります。ド

キュメントに起動の仕方が指定されているので、それに従います。

質疑応答

なにか気になることはありますか?