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"}}}
  ]}.

送受信時に利用するJSONデータ構造

x座標とy座標をもつJSONデータを使います。

{
  "x" : 0,
  "y" : 0
}

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使えばどうにかできるのかもしれないですがよくわかってません…。いやそもそも、そんなつくりにした時点でひどい設計なのかもしれないのかな?

追記:クライアント側をF#で書いてみた

WebSharper(F#)でWebSocket - pocketberserkerの爆走