【AI Shift Advent Calendar 2022】Elixir+PhoenixでWebSocketアプリを作ってみよう

はじめまして、開発チームでサーバサイドを担当している由利(ゆり)です。
こちらの記事は、AI Shift Advent Calendar 11日目の記事になります。

今回はこれまでの記事とはかなり毛色が変わって、WebSocketアプリを作ってみよう!という記事です。

最初にお断りをしておくと、AI Shiftのサーバサイドでは普段Go言語、Pythonなどをメインで利用しており、Elixirは今のところ全く業務では出てまいりません...
筆者の趣味としてご覧ください。

AI ShiftとWebSocket

AI Shiftでは自然言語処理を活用したチャットボットサービスやボイスボットサービスを提供しております。
特にボイスボットではサービスの性質上、内部でWebSocketなどのStreaming技術を多用しています。

WebSocketアプリケーションはもちろん様々な言語で実装できます。
ですが、簡便に高信頼性のアプリケーション構築ができる方法があれば、より良いサービスが提供できるのでは。と思い、日々調査しています。

Elixir・Phoenixとは?

さて、今回のWebSocketアプリケーションで利用する言語・フレームワークについて簡単にご説明しましょう。

ElixirはErlang VM上で動作する関数型のプログラミング言語です。
Erlangは低レイテンシで分散型のフォールトトレラントシステムを実現するために生まれた言語で、Elixirもこのしくみを利用しているのが特徴です。

PhoenixはElixirにおけるデファクトスタンダードなWebアプリケーションフレームワークです。
Erlang VM上で動作するElixirの特徴を活用して、低遅延で高い可用性を要求されるWebアプリケーションを実現できます。

Ruby on Railsを作っていたメンバーによって開発されたフレームワークのため、Railsなどに親しんでいる開発者の方であればとても馴染みやすい設計となっているのが特徴です。

早速作ってみよう

サンプルコード

今回実装したサンプルコードをGitHubに上げておきました。
https://github.com/ayuri-kn/phoenix-chat-sample

後ほどご紹介する追加サンプルも入っております。
実装済みになりますので、手っ取り早く試してみたい方は環境構築後にcloneして、ご確認ください!

環境構築

前提としている環境は下記です。
※Windows環境の方は申し訳ありません。
 こちらで試した際には更新したいと思います。

  • macOS 12.5.1
  • Erlang/OTP 25
  • Elixir 1.14.1
  • Phoenix 1.6.15
  • Docker環境(Docker Desktopなど)

ErlangとElixirをまずインストールしましょう。
最近はバージョン管理ツールとしてasdfが便利なので、筆者はよく利用しています。

# asdfのinstall
$ brew install asdf

# asdfにerlang/elixirのpluginを追加
$ asdf plugin add erlang https://github.com/asdf-vm/asdf-erlang.git
$ asdf plugin add elixir https://github.com/asdf-vm/asdf-elixir.git

# asdf経由でerlang/elixirをinstall
$ asdf install erlang latest
$ asdf global erlang <installed-version>
$ asdf install elixir latest
$ asdf global elixir <installed-version>

# installされているか、確認する
$ elixir -v
Erlang/OTP 25 [erts-13.1.1] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit]

Elixir 1.14.1 (compiled with Erlang/OTP 25)

Elixirがインストールできたら、ElixirのビルドツールMixが使えるようになっているはずです。
これを使ってPhoenixをインストールしましょう。

$ mix local.hex
$ mix archive.install hex phx_new 1.6.15

これで準備は整いました。

最初のWebアプリケーション

まずはインストールしたPhoenixを使ってWebアプリケーションを作ってみましょう。
プロジェクト名「Chat」でアプリケーションを作成してみます。

$ mix phx.new chat

## 下記のように途中で依存関係のinstallを聞かれるので、Yを選択する
> Fetch and install dependencies? [Yn] Y

これで作成されました!(嘘のような本当の話です)
では早速起動してみましょう。

ただ、Phoenixは基本的にDBが存在していることを前提としているので、一手間かけます。
下記ファイルをダウンロードしていただき、ローカルにDB(PostgreSQL)を起動しましょう。
https://github.com/ayuri-kn/phoenix-chat-sample/blob/main/docker-compose.yml

$ docker-compose up -d

DBの起動を確認したら、下記コマンドで起動します。

# 作成したアプリケーションディレクトリに移動
$ cd chat
# RDBのセットアップ
$ mix ecto.create
# 起動
$ mix phx.server

おめでとうございます!起動したはずです。
早速Browserで下記にアクセスしてみましょう。
http://localhost:4000/

これでWebアプリケーションのスケルトンが作成できました。

WebSocketを使ってChatアプリを作ってみよう

では、本題のWebSocketアプリを作ってみましょう。
今回はBrowser上で入力したTextが別Browser上でも表示されるような、簡単なChatアプリを作成します。

ServerSide実装

まずはServerSideのコードを作成していきます。
先程のアプリケーションディレクトリで下記コマンドを実行してみましょう。
これはsocket handlerを追加するコマンドになります。

$ mix phx.gen.socket User

実行後、lib/chat_web/channels/user_socket.ex ファイルが追加されました。
このファイルの中に下記の記述があるので、このコメントアウトを外しておきましょう。

channel "room:*", ChatWeb.RoomChannel

またリクエストを受け取るためにエンドポイントの定義が必要です。
lib/chat_web/endpoint.ex ファイルに下記を追加します。
socket "/live" ... の指定の上あたりに追加してみましょう。

    socket "/socket", ChatWeb.UserSocket,
      websocket: true,
      longpoll: false

次に接続したsocketに流されてくるリクエストを処理するため、Channelを追加しましょう。
Channelとは受信/送信両方向のイベントを処理するリアルタイム通信コンポーネントです。
これもコマンドで追加します。

$ mix phx.gen.channel Room

実行後追加されたファイル lib/chat_web/channels/room_channel.ex を参照してみましょう。
例えばhandle_in といった記述がありますが、これが受け取ったrequestを処理する部分になります。
このファイルに下記を追加してみましょう。

  def handle_in("new_msg", %{"body" => body}, socket) do
    broadcast!(socket, "new_msg", %{body: body})
    {:noreply, socket}
  end

ServerSideの実装はこれで完了です!

Client実装

次はfront側の実装です。
まず、socket通信を利用するための設定をしておきます。
assets/js/app.jsファイルを開くと user_socket.js のimport部がコメントアウトされていますので、このコメントを外しておきましょう。

// 下記のコメントを外す
// import "./user_socket.js"

では次にChatの入力欄を追加しましょう。
lib/chat_web/templates/page/index.html.heexファイルに下記の2行を適当に加えてみましょう。
これはお察しの通り、Server側でWebページをRenderingする際のtemplateファイルになります。

<div id="messages" role="log" aria-live="polite"></div>
<input id="chat-input" type="text">

次にassets/js/user_socket.jsファイルを開きます。
中にはsocket通信に使う定義sampleが含まれています。
まず、defaultで設定されているchannel定義は一旦コメントアウトしておきます。

// 下記一行をコメントアウトする
let channel = socket.channel("room:42", {})

それでは、コメントアウト行のすぐ下に下記を追加しましょう。

let channel           = socket.channel("room:lobby", {})
let chatInput         = document.querySelector("#chat-input")
let messagesContainer = document.querySelector("#messages")

chatInput.addEventListener("keypress", event => {
  if(event.key === 'Enter'){
    channel.push("new_msg", {body: chatInput.value})
    chatInput.value = ""
  }
})

// 画面表示テスト用
channel.on('new_msg', (payload) => {
  const msgItem = document.createElement('li');
  msgItem.innerText = `[${Date()}] ${payload.body}`;
  messagesContainer.appendChild(msgItem);
});

これでClient側の準備も整いました!

それでは下記コマンドにて再度起動しましょう。

$ mix phx.server

Browser(tab)を2つ起動後 http://localhost:4000/ にアクセスしてみてください。
textフィールドに文字を入力してEnterを押すと、別Browserに配信されているのがわかると思います。

さらに遊んでみる

これだけだとあまり面白みがないのでは...と思いましたので、GitHub側にもう一つサンプルを追加しておきました。
https://github.com/ayuri-kn/phoenix-chat-sample
D3のCollisionサンプルの座標情報をWebSocketで送信し、別Browserに同期しています。

下記をご覧ください。
コードを数箇所追加しただけで、実現することができました。

おわりに

今回の記事では、Elixir+Phoenixで開発するWebSocketアプリケーションをご紹介しました。
コードジェネレータが優秀なので、あまりコードを記述せずに実現できることがお分かりいただけたのではないかと思います。
ぜひ試してみてください!

弊社開発チームでは今利用している技術はもちろんですが、それ以外の技術情報にもアンテナをはりつつ、開発を進めております。
ご興味がありましたら今後の発信にもご期待ください。

明日はAIチームの二宮から第13回対話システムシンポジウムについての記事が公開される予定です。こちらもご覧いただけると幸いです。

最後まで読んでいただきありがとうございました!

参考

公式ドキュメントは英語のみなのでとっつきにくさはありますが、やはり頼れる存在です。
https://hexdocs.pm/phoenix/overview.html

D3のCollisionサンプルは下記のQiita記事を参考にさせていただきました。
Phoenix で WebSocket 通信をする
https://qiita.com/mserizawa/items/2c67031e794964f3a740

PICK UP

TAG