これは、Erlang Advent Calendar 2015 の14日目の記事です。
はじめに
私は仕事では主にフロントエンドの開発をしているのですが、時には株価やニュースなどリアルタイム性の高い機能の実装が必要になる事もあります。
先日、WebSocketを使ったリアルタイム性の高いWebアプリを作りたい、と思い調べていたところ、N2Oというフレームワークがあることを知りました。
もともとは別のErlang製WebフレームワークであるChicagoBossを調査していたのですが、プロジェクトのページにComparison of Erlang Web Frameworksという比較表があり、機能が高く速そうだったことから興味を持ちました。
N2Oについて
N2O(H2Oじゃないよ)はウクライナのSynrc Research Centerという会社で開発されたWebアプリケーションフレームワークです。
N2Oは"Nitrogen 2x Optimized"の略で、同じErlangのWebアプリケーションフレームワークであるNitrogenから、主に高速化を目的として分岐したプロダクトとの事です。
従って、Nitrogenを知っていれば、N2Oにもすぐ馴染めるのではないでしょうか。
Nitrogenプロジェクトでは文書が充実しており、それをほぼそのまま参考に出来るのは有利です。
個人的にはNitrogenも使った事がなかったのですが、N2O自体の文書も結構充実しており、特に困ることはありませんでした。
特に、N2Oについての68ページの電子書籍がPDFで公開されているのが大きかったです。
マイナーなフレームワークでもしっかりした文書があると安心感があり、入って行きやすいですね。
また、このN2O Erlang Web Stackという紹介スライドも明快で背中を押されました。
Synrc社の実例紹介ページによると、ウクライナの銀行、PrivatBankのオンラインバンキングシステムに、N2Oを始めとした同社開発のフレームワークが活用されているとの事です。
特徴
N2Oは他のフレームワークと比べて、以下のような特徴があります。
- 高速
- バイナリ形式(BERT)のメッセージ送受信サポート
- テンプレートのサポート (ErlyDTL, Nitrogen, NITRO) ※ErlyDTLサポート ≒ Djangoのテンプレートが使える
- 実装がコンパクト (約1,100行)
私はErlang初心者なので、大きなフレームワークは多分理解不能、また規模が大きいとカスタマイズもしんどそうですが、千行位なら読み込めば何とかなるかも…。
sudo apt-get install -y cloc
~/devel/erlang/n2o-master$ cloc ./src
27 text files.
27 unique files.
1 file ignored.
http://cloc.sourceforge.net v 1.60 T=0.04 s (708.0 files/s, 39046.9 lines/s)
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
Erlang 26 229 63 1142
N2Oでは、HTTPプロトコルのハンドリングにはCowboy, Pub/Subにはgproc、テンプレートの処理にErlyDTLが使われています。
本体の行数が少ないのは、各機能を効果的にライブラリに委譲している事にも由来していそうです。
ところで、気になるのはやはり速度です。
少し前の情報のようですが、プロジェクトのページに以下のような表が掲載されていました。
もう一つ、N2Oのプロジェクトページの比較表から引用しますが、
|Framework| Enabled Components| Speed| Timeouts|
|---------|--------|-----------|---------|----------|
|PHP5 FCGI| Simple script with two | 5K| timeouts|
|ChicagoBoss| No sessions, No DSL, Simple DTL| 500| no|
|Nitrogen| No sessions, No DSL, Simple DTL| 1K| no|
|N2O| All enabled, sessions, Template, heavy DSL| 7K| no|
|N2O| Sessions enabled, template with two variables, no DSL| 10K| no|
|N2O| No sessions, No DSL, only template with two vars| 15K| no|
もしPHPの2倍のスピードが出るのなら素晴らしいですね。これは是非試さねば。
動作確認環境
- サーバサイド
- Ubuntu 14.04 64-bit (AWS EC2 / t2.micro)
- Erlang 18.1 (Erlang Solutionsのビルド)
- N2O v2.11
- クライアントサイド
- Windows 7 64-bit
- Chrome 46
(おことわり)
このエントリは、半年ほど前、N2O v2.4で調査した際のメモを元に作成しましたが、
N2Oの最近のバージョン(多分2.8?)では結構大きな変更が加わったようで、サーバ・クライアント間で送受信するデータの構造やAPIが変わりました。
N2Oの最新バージョンは、2015/12/14現在、v2.11です。
引用したサンプルがv2.11でも動く事は軽く確認しましたが、内容が古い可能性があります(古いAPIを使っている等)。
時間が出来たら調べて内容を更新したいと思っています。
また、整理してからサンプル一式をGithubにソースをコミットする予定でいます。
お勧めのチュートリアル
まずはサンプルないかな? と、ググってみましたが、余りヒットしません。
Synrc社による実装例があるので、こちらを読むのが王道でしょうが、結構本格的なサンプルであるので入門用には重いです。
そんな中、以下のCRASHDUMP.IOによるチュートリアルが参考になりましたのでお勧めです。
4回シリーズで、ゼロから始めてHello World・テンプレートまでと、内容も軽めで分かりやすいです。
(ただし情報は若干古い)
作ってみる
以下を作ることを目標とします。
- 株価のリアルタイムレート画面
- サーバ側: Erlang + N2O
- クライアント側: ブラウザ、JS
開発ツールのインストール
ビルド時に必要になるツールをインストールします。
$ sudo apt-get install git inotify-tools
リアルタイムレートの提供元は?
少し探した範囲では、株価のレートを無料でリアルタイム配信してくれるサービスが見当たらなかったので、ビットコインのレートを使うことにします。
ビットコインの市場は、土日もなく24時間/365日動き続けているので、むしろこちらの方が都合が良いですね。
WebSocketを扱うので、wscat コマンドを環境に含めておくと良い感じです。
socat/netcatでも出来ない訳ではないですが、ちょっと面倒。
wscatのインストール
$ sudo apt-get install nodejs npm
$ sudo update-alternatives --install /usr/bin/node node /usr/bin/nodejs 10
$ sudo npm install -g wscat
$ wscat --version
1.0.1
WebSocketからリアルタイムレート受信 (コマンド編)
BitfinexのパブリックAPIを使うと、Bitcoinのリアルタイムレートが取得出来ますので、こちらを使用します。
$ wscat -c wss://api2.bitfinex.com:3000/ws
connected (press CTRL+C to quit)
< {"event":"info","version":1}
# 以下をペーストし、エンターキーを押す
{"event":"subscribe", "pair":"BTCUSD", "channel":"ticker"}
(応答)
< {"event":"subscribed","channel":"ticker","chanId":5,"pair":"BTCUSD"}
< [5,459.14,0.41,459.7,3,4.16,0.01,459.61,57932.64969181,469.99,440]
< [5,"hb"]
< [5,459.23,13.34,459.57,3,4.12,0.01,459.57,57932.85133057,469.99,440]
< [5,"hb"]
< [5,"hb"]
(以下略)
応答のフォーマットです。
// snapshot
[
"<CHANNEL_ID>",
"<BID>",
"<BID_SIZE>",
"<ASK>",
"<ASK_SIZE>",
"<DAILY_CHANGE>",
"<DAILY_CHANGE_PERC>",
"<LAST_PRICE>",
"<VOLUME>",
"<HIGH>",
"<LOW>"
]
リアルタイムレートが取得できたので、次は、このレートを情報源として接続してきたクライアントに配信するサーバサイドのプログラムを、N2Oで作ってみます。
WebSocketからリアルタイムレート受信 (Erlang編)
N2Oに組み込む前に、まずは単独でレートを受信してみます。
WebSocketのクライアントライブラリが必要ですが、今回はwebsocket_clientを使います。
(他にはGunも便利なライブラリだそうです)
-module(ws_bitfinex).
-behaviour(websocket_client_handler).
-export([
start_link/0,
init/2,
websocket_handle/3,
websocket_info/3,
websocket_terminate/3
]).
start_link() ->
crypto:start(),
ssl:start(),
websocket_client:start_link("wss://api2.bitfinex.com:3000/ws", ?MODULE, []).
init([], _ConnState) ->
websocket_client:cast(self(), {text,
<<"{\"event\":\"subscribe\",\"pair\":\"BTCUSD\",\"channel\":\"ticker\"}">>}),
{ok, 2}.
websocket_handle({text, Msg}, _ConnState, State) ->
io:format("Received msg ~p~n", [Msg]),
{ok, State}.
websocket_info(start, _ConnState, State) ->
{reply, {text, <<"erlang message received">>}, State}.
websocket_terminate(Reason, _ConnState, State) ->
io:format("Websocket closed in state ~p wih reason ~p~n",
[State, Reason]),
ok.
単体で動かしてみます。
$ wget https://github.com/jeremyong/websocket_client/archive/v0.7.tar.gz
$ tar xvfz v0.7.tar.gz
$ cd websocket_client-0.7
$ make
$ vim ws_bitfinex.erl
# ... 上記のws_bitfinex.erlを貼り付けて保存 ...
$ erl -pa ebin
$ c(ws_bitfinex).
{ok,ws_bitfinex}
2> ws_bitfinex:start_link().
{ok,<0.52.0>}
Received msg <<"{\"event\":\"info\",\"version\":1}">>
Received msg <<"{\"event\":\"subscribed\",\"channel\":\"ticker\",\"chanId\":4,\"pair\":\"BTCUSD\"}">>
Received msg <<"[4,456.83,11.78,457,13.52324434,-5.61,-0.01,457,65607.13460856,469.99,440]">>
Received msg <<"[4,\"hb\"]">>
...以下略...
3> q(). またはCtrl + C
ok
OK。Erlangでレートが受信出来ました。
N2Oでリアルタイムレート配信
続いて、N2Oを使った配信サービスの方を考えます。
先ほどのAPIで購読したリアルタイムレートを、接続してきたクライアントに配信(中継)すれば良いので、仕組みはチャットとかなり似ています。
(チャットは誰もが話しますが、レート配信は一人だけが話すイメージ)
N2Oのひな型にサンプルにチャットがありますが、このサンプルを改造すれば良さそうです。
ところで、N2Oには「レビューボード」のサンプルが付属しており、これはコードレビューを行うサンプルアプリで結構凝っています。
それとは別にもっと単純なチャットのサンプルがあるのですが、これはアプリのひな型を作った時に生成されます。
madでビルド・パッケージング・REPL
では、早速やってみましょう。
N2Oでは、rebarではなく、madというツールでビルド等を行います。
madでは、コマンドに、完全な名前ではなく始めの3文字を省略形として使用可能です。
$ curl https://github.com/synrc/n2o/archive/2.11.tar.gz -o n2o-2.11.tar.gz
$ tar xvfz n2o-2.11.tar.gz
$ cd n2o-2.11/samples
# ./mad deps compile plan repl
# 省略形の方がタイピングが楽
$ ./mad dep com pla rep
N2Oアプリのひな型の作成
だいぶ雰囲気がつかめてきたところで、アプリの作成をやってみます。
アプリのひな型も、madで作成します。
mad app <アプリケーション名>
このコマンドを発行すると、デフォルトでは、非常にシンプルなチャットアプリが生成されます。
生成されるものはもしかしたらカスタマイズできるのかも知れませんが、調べていません。
_rate-ticker_というプロジェクト名にします。
~/devel/erlang/n2o-2.11/samples$ ./mad app rate-ticker
Create File: "rate-ticker/apps/rebar.config"
Create File: "rate-ticker/apps/sample/src/sample.app.src"
Create File: "rate-ticker/apps/sample/priv/static/synrc.css"
Create File: "rate-ticker/apps/sample/src/index.erl"
Create File: "rate-ticker/apps/sample/src/routes.erl"
Create File: "rate-ticker/apps/sample/src/sample.erl"
Create File: "rate-ticker/sys.config"
Create File: "rate-ticker/apps/sample/priv/static/spa/index.htm"
Create File: "rate-ticker/apps/sample/rebar.config"
Create File: "rate-ticker/apps/sample/priv/templates/index.html"
Create File: "rate-ticker/rebar.config"
Create File: "rate-ticker/vm.args"
OK
$ cd ./rate-ticker
# 依存関係解消 コンパイル プラン(パッケージング) REPL
$ ../mad deps compile plan repl
最後のコマンドの'repl'は、「REPLを開く」の意なので、シェル上でErlangのREPLが開いているはずです。
当たり前ですがここでコマンド等を試しに入力することも可能です。
設定ファイル
プロジェクトディレクトリ直下に設定ファイルがあるので、必要に応じて調整します。
- rebar.config
- 利用するライブラリの定義
- sys.config
- パラメータの定義
ファイルの内容を見れば一目瞭然なので、説明は省略します。
ブラウザで確認
ここまでで、サーバのポート8001番で最低限のWebアプリケーション(チャット)が動いていますので、
ブラウザでアクセスしてみます。
複数のブラウザで開いてみて、あるブラウザで入力したメッセージが、全てのブラウザに正しく伝わるかも確認してみましょう。
ブラウザ上で何かメッセージを入力してから、CHATボタンを押すと、サーバにはシリアライズされたデータが送信されてきます。
単純なメッセージでも、結構複雑な形式のデータとして送られています。
サーバ側で確認
サーバ側のシェルセッション上にはErlangのREPLが開いており、INFOレベルのログが出力されるので、何が起きているか確認できます。
また、ここでENTERキーを押すと、対話型のシェルに入りますので、Erlangのコードを入力可能です。
試しにコマンドラインからメッセージを送ってみましょう。
wf:send(room,{client,{"system","message!!!"}}).
無事ブラウザに「message!!!」が表示されたでしょうか?
バイナリフレーム送受信の確認
もうちょっと詳しく見てみましょう。
クライアント側にどのようなメッセージが来ているか、ブラウザの開発者ツールで確認してみます。
Chromeで、F12キーを押して開発者ツールを開き、Network - Framesを選択、WSタブをクリック。
オレンジ色で、Binary Frameという列がありますから、確かにバイナリでデータが届けられました。
次に、デバッグプリントを有効にして、コンソール上に詳細情報を出すようにします。
F12キーを押して開発者ツールを開き、Sourceペーンのn2o/protocols/client.jsをクリックします。
これはサーバからのメッセージを読み込む処理の一部です。
client.jsのソースを見るとすぐ分かる通り、debug という変数が真ならデータがコンソールにデバッグ出力されるようになっているので、debug変数を定義しましょう。
debug変数は、別のファイル、n2o.jsに定義されています。
Chrome上でテンポラリでJSを編集しても良いですが、毎回やるのも面倒ですので、サーバ側のJSを編集してしまうのがお勧めです。
// N2O CORE
var active = false,
// debug = false,
debug = true,
関係ないですが、サーバ上でファイルを編集すると、madが自動的にリロードしてくれるため、アプリの手動再起動が必要ありません。
ブラウザの方をリロードするだけで大丈夫です。これは大変便利ですね。
(もしそうならない場合は、inotify-toolsをインストールして下さい)
// JSON formatter
var $client = {};
$client.on = function onclient(evt, callback) {
// 追加
console.log(evt.data);
try { msg = JSON.parse(evt.data);
// 追加
console.log(msg);
if (debug) console.log(JSON.stringify(msg));
if (typeof callback == 'function' && msg) callback(msg);
for (var i=0;i<$bert.protos.length;i++) {
p = $bert.protos[i]; if (p.on(msg, p.do).status == "ok") return { status: "ok"}; }
} catch (ex) { return { status: "error" }; }
return { status: "ok" }; };
ブラウザのコンソールに生っぽいデータが表示されたと思います。
N2Oではデータの送受信をBERTというバイナリ形式で行っています。
クライアント側だと「protocols/bert.js」を読むと雰囲気がつかめると思います。
ブラウザの開発者ツールで、client.jsを開き、適切な場所にブレークポイントを設置すると生のデータが確認できます。
例えば、メッセージはこのような形式です。
{"eval":"","data":[131,104,2,107,0,6,115,121,115,116,101,109,107,0,10,109,101,115,115,97,103,101,33,33,33]}
これはなんでしょうか。
"eval": ""
この例では空なので見てもよく分からないですが、この部分にはJavaScriptの命令文が入って来ます。結構ユニークですね。
ブラウザでevalするだけ で、フォームに値がセットされたり、文字や背景色が変わったりという事に使う事が出来ます。
どうせデータを送ってもパースしてからどこかにセットするわけなので、サーバから送られる時、命令文を組み立てて置けばよい、ということでしょうか。この発想は私には目から鱗でした。NitrogenのPushBack方式、と解説されていましたが、SPAで良くあるやり方なのか、Nitrogenのやり方なのかはよく分かりませんでした。
"data":[131,104,2,107,0,6,115,121,115,116,101,109,107,0,10,109,101,115,115,97,103,101,33,33,33]
dataも、生の値ではなく、BERT形式でエンコードされています。
ブラウザコンソール上ではdec()関数でデコードできます。
dec([131,104,2,107,0,6,115,121,115,116,101,109,107,0,10,109,101,115,115,97,103,101,33,33,33])
試しにコンソールからカスタムメッセージを送ってみましょう。
ただメッセージを送るだけでも、結構複雑なデータ構造を与えないといけません。
トークンの値は、接続時にサーバ側から送られているので、ブラウザの開発者ツールで確認して値を置き換えて下さい。
トークンの値が間違っていると、サーバ側で無視されますので、何も起きません。
// ※トークンの値は上の画像のハイライトの部分を拾って書き換えて下さい
ws.send(enc(
tuple(atom('pickle'),
bin('send'),
// トークン
bin('g2gCaAVkAAJldmQABWluZGV4ZAAEY2hhdGsABHNlbmRkAAVldmVudGgDYgAABapiAAVpO2IACG2w'),
[
tuple(tuple(utf8_toByteArray('send'),bin('detail')),[]),
tuple(atom('message'), utf8_toByteArray('こんにちは世界'))
])));
上手く行くと、以下のように送ったメッセージがそのままサーバから返ってきてブラウザ上に表示されるはずです。
N2Oのアーキテクチャ
リアルタイムレート画面の作成
簡単なサンプルチャットアプリを動かして、自動・手動のメッセージ送受信まで出来ました。
次にリアルタイムレート配信サービスを作成します。
仕組み的にはチャットとレート配信は似ているため、チャットのサンプルを改造すれば良さそうです。
画面用のパーツとしてJSのライブラリ等を使うため、Webアプリの「ルーティング」を調整する必要があります。
また、画面のテンプレートの作成・処理も必要です。
まずは配信部分。単体で動作確認した、リアルタイムレートをAPIから取得するソースを使います。
wf_bitfinex.erlをプロジェクトに組み込み。
$ cp ws_bitfinex.erl n2o-2.11/samples/rate-ticker/apps/sample/src/
ws_bitfinex.erlを調整します。レート受信時に、N2Oのwf:send()を呼び出して、クライアントにブロードキャストします。
% WSのハートビートなどはそのまま送らず省略しないといけません。本当は。
websocket_handle({text, Msg}, _ConnState, State) ->
io:format("Received msg ~p~n", [Msg]),
% 追加
wf:send(room,{client,{"feed",Msg}}),
{ok, State}.
% ws_bitfinex:start_link() 追加
start(_,_) -> supervisor:start_link({local,sample},sample,[]),
ws_bitfinex:start_link().
% 最後の行に追記。前の行末に「,」を付け加えるのをお忘れなく。
{websocket_client,".*",{git,"git://github.com/jeremyong/websocket_client",{tag,"v0.7"} }}
再ビルド・起動します。
$ mad dep com pla rep
リアルタイムレートの確認 (ブラウザ)
画面周りのテンプレート作成
リアルタイムレートがブラウザに配信され生で表示されるところまで来ました。
あとは綺麗に表示すれば完成なのですが・・・
元々作っていたサンプルがN2O v2.11で動かなくなってしまったため調整中です。
すみません!
** TBD **
小ネタ集
マルチクライアント対応 (Pub/Sub)
** ※この手順は昔のバージョンでのみ必要です。N2O v2.11では標準でこのように動作するようになっていました **
標準のチャットのサンプルは、1 対 1の通信をするものとなっていますので、
もっとチャットらしく、1 対 多のPub/Subの通信をするよう変えてみます。
N2Oには、Pub/Sub用APIが用意されていますので、呼び出すだけ使えます。
(Pub/Subの実装はgprocが利用されています)
event(init) -> wf:reg(room);
event(chat) -> wf:send(room,{client,{peer(),message()}});
event({client,{P,M}}) -> wf:insert_bottom(history,#panel{id=history,body=[P,": ",M,#br{}]});
HTTPS化
※N2O v2.4の場合。要更新
init([]) ->
%% http
% cowboy:start_http(http, 28232, [{port, wf:config(n2o,port)}],
% [{env, [{dispatch, rules()}]}]),
%% https
cowboy:start_https(https, 28232, [
{port, wf:config(n2o,port)},
{certfile, "/usr/local/cert/ssl/server.crt"},% 要調整
{keyfile, "/usr/local/cert/ssl/server.key"}, % 要調整
{password,""}],
[{env, [{dispatch, rules()}]}]),
ベンチマーク
** TBD **
(改めて)N2Oが実現している機能
サンプルを動かして気付いたのは以下の機能です。
- WebSocket + XHR fallback ← サーバを落とすと、JS側でXHRによる接続が試行される
- バイナリでのメッセージ送受信 (BERT)
- 簡易暗号化 (pickle)
- セッション
- PING/PONGは自動で4秒毎に発行される
- サーバからJSの式を送りクライアントで評価する仕組み(多分Nitrogen由来の機能)
N2Oを使ってみた感想
N2Oはかなり魅力的なライブラリに感じました。
- シンプル。覚えるべきAPIが少ない (wf.erlを読めば大体分かる)
- バイナリ送受信標準サポート(取り外し可能。JSONに切り替える事も出来るらしい。未検証)
- ハートビート機能が標準で付いてくる (4秒毎のPING/PONG、こちらも取り外し可能)
- 速さ (注: 体感) ※要ベンチマーク
今回紹介した機能を実現するため実装したのは、ソースコードの行数で言うと50行程度。
あまりにあっさり出来るので正直拍子抜けする位でした。
また表現力も高く、SPAのアプリの中にはErlangの一ファイルですっきり実現出来るものもありそうです。
(N2Oのトップページで紹介されているchat.erlのイメージ)
個人的には、N2Oは一見シンプルですがフレームワークの機能は豊富だし奥が深そうに感じています。
一点、N2OではWebSocketが使えないクライアントのために、XHRによるフォールバックのエンドポイントも用意されているのですが、v2.4の頃は安定性に少し問題があり、開発者も修正に積極的ではない様子でした。
実際、チャットのサンプルをWebSocketとXHRで動かした時に、XHRの方にだけメッセージが来ない現象がありました。
しかし、最近のバージョンではn2o_streamというエンドポイントが新規で追加されているので、改善されている可能性がありそうです。
終わりに
N2Oは利用者がまだそれほど多くなく、ライブラリの安定性や高負荷時の挙動については別途評価が必要と思われますが、WebSocketでリアルタイム通信をするようなアプリでは、候補として検討してみてはいかがでしょうか?
今回は、長い割にはN2Oを駆け足で紹介しただけで終わってしまいました。
WebSocketに加えてHTTP/1.1 Chunkedでの配信する、等も試してみたので何かの機会に紹介できたらなと思います。
ErlangとN2Oを使って、将棋ウォーズやlichessのようなリアルタイム対戦サービスを作るのが私の来年の目標です。
それではみなさん、メリークリスマス!
参考資料
- N2O v2.9 作者による解説
- BERT BERT規格
- gproc - Extended process registry for Erlang gprocの解説