Cowboy(Erlang)でWebSocket
前回に引き続き、cowboyを試しています。
今回はWebSocket部分について簡単にメモしておきます。
環境
前回に引き続きErlang R15B02で試しています。
cowboyとjiffyのインストール
WebSocketによる通信のみであればcowboy単体で問題ないのですが、個人的事情によりJSONでの通信を試したかったのでjiffyというJSON parserも利用します。
上記事情により、今回のrebar.configは以下のようにしました。
{deps, [{cowboy, ".*", {git, "git://github.com/extend/cowboy.git", {branch, "master"}}}, {jiffy, ".*", {git, "git://github.com/davisp/jiffy.git", {branch, "master"}}} ]}.
rebarのテンプレート機能を使う
これは前回と変わりません(アプリケーション名は変更していますが)。
$ ./rebar create-app appid=sample_websocket $ ./rebar create template=simplemod modid=sample_websocket
構成
前回はすべてのアクセスをdefault_handlerに送りましたが、今回はwebsocketというURIPathへの接続のみwebsocket_handlerに送るようにしてみましょう。
-module(sample_websocket_cowboy). -export([start/0]). start() -> application:start(ranch), application:start(cowboy), application:start(jiffy), Dispatch = [ {'_', [ {[<<"websocket">>], websocket_handler, []} ]} ], cowboy:start_http(sample_websocket_listener, 100, [{port, 19860}], [{dispatch, Dispatch}] ), ok.
今回実装したwebsocket_handlerでは、受信したxとyという二つの数値をStateとして保持しておき、現在のStateを定期的にクライアントへ投げ返します。
-module(websocket_handler). -behaviour(cowboy_websocket_handler). -export([init/3]). -export([websocket_init/3, websocket_handle/3, websocket_info/3, websocket_terminate/3]). init({tcp, http}, _Req, _Opts) -> {upgrade, protocol, cowboy_websocket}. websocket_init(_Any, Req, _Opts) -> timer:send_interval(1000, tick), Req2 = cowboy_req:compact(Req), {ok, Req2, {{0, 0}, []}, hibernate}. websocket_handle({text, Msg}, Req, _State) -> {[{_X, X}, {_Y, Y}]} = jiffy:decode(Msg), {ok, Req, {X,Y}, hibernate}; websocket_handle(_Any, Req, State) -> {ok, Req, State}. websocket_info(tick, Req, {X, Y}) -> Data = jiffy:encode({[{x, X}, {y, Y}]}), {reply, {text, Data}, Req, {X, Y}, hibernate}; websocket_info(_Info, Req, State) -> {ok, Req, State, hibernate}. websocket_terminate(_Reason, _Req, _State) -> ok.
http経由でinitが呼び出される(ハンドシェイクが要求される?)とprotorolをwebsocketへと切り替えます。ここではcowboy_websocketによろしくやってもらう設定にします。
websocket_initではsend_intervalで定期的にメッセージを送信してもらうようにしています。あと、cowboy_req:compactでRequestデータ内のうち不必要なデータを削っています。
websocket_handleでクライアントから送られてきたデータを受信します。上記コードではtextのみ処理をしていますが、binaryやping、pongも指定できます。今回はきっとjosn文字列が送られてくるだろうという想定でdecodeして新しいStateにしています。
websocket_infoでプロセスから送信されたデータを受け取ります。なので、timer:send_intervalで送ったメッセージもこの関数で受け取ることになります。今回はStateをjsonデータにencodeしてクライアント側に送信しています。
3関数共通ですが、hibernateアトムを追加することで、特定のタイミングでプロセスが休止状態になるみたいです。メモリ節約とCPU使用率のためのようですね。
sample_websocket.erlはapplication:start(sample_websocket)を実行するだけのコードになります。
-module(sample_websocket). -export([start/0]). start() -> application:start(sample_websocket).
sample_websocket_app.erlにはsample_websokcet_cowboy:startの呼び出しを追加します。
-module(sample_websocket_app). -behaviour(application). -export([start/2, stop/1]). start(_StartType, _StartArgs) -> ok = sample_websocket_cowboy:start(), sample_sup:start_link(). stop(_State) -> ok.
起動
$ ./rebar get-deps compile $ erl -pa ebin deps/*/ebin -s sample_websocket
これでws://localhost:19860にアクセスするクライアントコードを書くと、ひたすらxとyというデータを投げ合います。
ちなみにWindowsでjiffyをコンパイルしたところ、実行時に見事こけました。何が問題なのかは要調査ということにしておきます。
所感
案外あっさりとかけるものですね。他の言語でもこんなに簡単なのでしょうか?何しろWebSocketのコードを書いたのはこれがはじめてなのでよくわかってません。
今のところ、今回使用した程度のデータサイズであれば特に通信速度は気になりませんね。もう少し負荷をかけてみるべきかも。
ちょっとわからないこと
websocket_handleなどで持ちまわししているStateを別のhandlerから取得できないのかな…と。
gen_server使えばどうにかできるのかもしれないですがよくわかってません…。いやそもそも、そんなつくりにした時点でひどい設計なのかもしれないのかな?