ヽ(´・肉・`)ノログ

How do we fighting without fighting?

ElixirとPhoenixとMithrilのFFスタックでChatアプリを作った

./ff_stack_chat_example.gif

  • Elixir という言語
  • Phoenix という Elixir で書かれた Web フレームワーク
  • Mithril という JavaScript フレームワーク

で Chat アプリケーション ( の原型 ) が書けたので記録する.

「FF スタック」という名称は rebuild.fm の Fullstack Final Fantasy Framework のタイトルから借りた.

クライアント側の実装は Mithril 本の 11 章に書いてある Chat アプリケーションを元にしている.

を見比べると

  • ES6 に書き換えた
  • ChatSocket を socket.io から phoenix 対応に書き換えた

くらいの違いしかないことがわかるだろう.

PhoenixFramework へ js ライブラリを加える

PhoenixFramework のクライアント側でサードパーティ製の JavaScript ライブラリ, 今回だと Mithril.js を使いたい場合にどうするか?

フロントエンドに慣れていない僕のような人は Bower を使い,依存関係を設定するだけで済ませるのがよさそうだった.

なぜなら,JavaScript や CSS などを管理するため,PhoenixFramework には Brunch というツールが組み込まれている. その Brunch は Bower を統合した状態での動作をサポートしている (See How to use Bower? ) ためである.

実際に Bower を使って Mithril を利用するには

  1. bower.json を作る
  2. bower.json に Mithril の依存関係を足す

の 2 つを行えばよい.

1 を達成するために bower init コマンドでも bower.json を生成できる.しかし様々な設定が書きこまれている. bower.json に最低限必要なのは name だけなので,今回は手動でファイルを作成した.

2 は,bower.json がある状態で bower install mithril --save とコマンドを打つと, Mithril パッケージを手元へダウンロードするのと,bower.json へ依存関係の記述を同時に行ってくれる.

この 2 つを行うだけで Phoenix の app.js で Mithril が使えるようになる.

ユーザーの接続状態を管理する

通常のチャットでは自分が誰であるか,最初に id とパスワードを入力して認証するだろう.

今回作るチャットでは,簡易な認証機構として「現在接続しているユーザーと同じ名前の人は接続できない」ということにした.

これを達成するためには

  1. 接続者一覧の準備
  2. 接続時に名前を入力する
  3. 接続している接続者一覧と比較する
    • 存在していれば接続拒否
    • 存在していなければ接続者一覧への名前追加と接続許可
  4. 切断時に接続者一覧からの名前削除

を行わなければならない.

一覧の取り扱い

今回は大がかりなことをやらないので,接続者一覧には単なるリストを使うことにした. 接続者一覧には一覧の取得,一覧への名前追加,一覧からの名前削除を行いたい.

Elixir で状態を管理するのには Agent というモジュールを使うと便利なので,これを利用する.

また,接続者一覧にはプログラムのどこからでもアクセスしたい. 言い換えると,関数の引数に接続者一覧へアクセスできる情報を持っていなくても,接続者一覧へアクセスしたい.

そこで今回はモジュールの名前 ( Chat.LoginUser ) 経由でアクセスできるようにした. Chat.LoginUser.login(pid, name)Chat.LoginUser.logout(pid) といったようなものだ.

複数プロセスから触れる,さらに状態の更新があると聞くとレースコンディションについて不安になるだろう. マルチスレッドプログラミングでよくある,前の行でチェックした内容が,他のスレッドにより更新されてしまうため,次の行では保証されているとは限らないというやつだ.

しかし ErlangVM では,関数の始まりから終わりまでの間は他のスレッド(プロセス)からその関数内の内容を書き換えられることがない. そのためレースコンディションはおこしにくい.ErlangVM では全ての関数が Java や Ruby でいうところの syncronized で実行されているようなものだと考えるとイメージしやすいだろうか.

横道にそれてしまった.つまり,短く言うと大丈夫だ. (と考えているが,間違っていたら教えてほしい)

さて,この一覧の初期化はどこで行えばよいだろうか? PhoenixFramework では初期化処理が lib/[プロジェクト名].ex で行われているので, 今回であれば lib/chat.ex一覧の初期化処理を追加する.

接続/切断時の処理をどこに書くか

Phoenix では Socket というもので低レイヤーの接続を確立して,その 1 つの Socket の上で複数の Channel という高レイヤーのものを取り扱っている.

Socket 層で接続を確立するときに認可の仕組みを入れることもできるが,今回は Channel 層での認可を取り扱う. もし Socket 層での認証/認可を行いたい場合は Phoenix.Socket の Socket Behaviour の項をみるとよい.

Channel には 4 つのコールバックが用意されている.それぞれ以下のイベントと対応付いている

join/3
接続時
terminate/2
切断時
handle_in/3
メッセージが送られてきたとき
handle_out/3
メッセージを接続先に送るとき(接続先が興味のない内容を送らないようにフィルタできる)

つまり join/3 に接続開始時の処理, terminate/2 に切断時の処理を書けばよい.

接続の拒否をするには

さて既に接続一覧に同じ名前が含まれており,接続を拒否したい場合はどうしたらよいだろう.

その場合は join/3 の返り値を {:error, reply} にすると, クライアントへエラーが伝わり,かつ reply で送ったものをエラーの内容として取得できる.

この場合 Channel への接続は確立されない.接続を試みる前と同じ状態にある.

クライアントからサーバーへ送られてきた内容をブロードキャストする

通常のチャットでは,クライアントが発言をサーバーへ送ると,その他のクライアントの発言一覧も更新される.

これを達成するためには

  1. クライアントからサーバーへ送られてきた内容を取得する
  2. サーバーから全てのクライアントへ内容を送る

を行わなければならない.

クライアントからサーバーへ送られてきた内容は handle_in/3 コールバックで取得できる. 3 引数には 「イベント名」,「内容」,「ソケット」 が渡されてくる,大抵の場合は「イベント名」と「内容」にだけ興味があるだろう.

サーバーから,接続している全てのクライアントへ内容を送るのは broadcast!/3 でできる.

Channel へ接続したときにこれまでの情報を取得する

通常のチャットでは,接続すると過去の発言も見られるようになっている.

これを達成するには接続 -> (認可OK) -> 過去発言取得 と,クライアントからサーバーへ 2 回問合せを行ってもよい.

Phoenix では,クライアントからサーバーへ接続したときに認証結果の他に任意のデータを返すことができる. これを利用すると,接続 -> (認可OKと過去発言データ) と 1 回のやりとりで認可と過去発言の取得できるため通信回数を削減できる.

まとめ

PhoenixFramework のクライアントサイドで外部ライブラリを簡単に利用する方法を書いた.

また,Chat に必要な要素である

  • 認証/認可
  • 接続状態の管理
  • サーバーからクライアントへのブロードキャスト
  • 接続時のデータ読み込み

を PhoenixFramework のどこで行うかを書いた. Chat に限らず,ソフトリアルタイムな通信を行う Web アプリケーションで利用できる普遍的な内容になっているはずだ.