Memento memo.

Today I Learned.

PhoenixのChannelを使う

www.phoenixframework.org

Phoenixのガイドを眺めていて一番気になったのがChannelだったので、上記の公式ガイドに沿ってChannelを使ってみました。結構端折っています。

プログラミングElixir

プログラミングElixir

概要をつかむために公式ガイドの一番上のところだけ訳してみます。

Channels are a really exciting and powerful part of Phoenix that allow us to easily add soft-realtime features to our applications. Channels are based on a simple idea - sending and receiving messages. Senders broadcast messages about topics. Receivers subscribe to topics so that they can get those messages. Senders and receivers can switch roles on the same topic at any time.

Since Elixir is based on message passing, you may wonder why we need this extra mechanism to send and receive messages. With Channels, neither senders nor receivers have to be Elixir processes. They can be anything that we can teach to communicate over a Channel - a JavaScript client, an iOS app, another Phoenix application, our watch. Also, messages broadcast over a Channel may have many receivers. Elixir processes communicate one to one.

The word "Channel" is really shorthand for a layered system with a number of components. Let's take a quick look at them now so we can see the big picture a little better.

適当訳:

チャネルはPhoenixの中でも本当に面白くて強力なところであり、簡単にソフトリアルタイム性をアプリケーションに持たせることができます。チャネルはメッセージの送受信という、単純なアイデアに基いています。senderはtopicについてブロードキャストし、receiverはtopicを購読することによってメッセージを受け取ることができます。sender, receiverはいつでも役割を交代することができます

ElixirはMessage Passingに基いているため、どうしてメッセージの送受信に他の仕組みを利用するのか疑問に思うことでしょう。チャネルでは、senderもreceiverもElixirのプロセスである必要はありません。チャネルと通信するものはJavaScriptiOSでも何だってよいのです。また、Elixirのプロセスが1対1で通信するのに対し、チャネルにおけるメッセージブロードキャスティングはreceiverが複数になり得ます。

"Channel"という言葉は多くのコンポーネットをもった多層システムを簡略に表したにすぎません。 全体像をもっとよくつかめるようになるために、もう少し覗いてみましょう。

以下、各partsの説明(省略)

  • Socket Handlers
  • Channel Routes
  • Channels
  • PubSub
  • Messages
  • Topics
  • Transports
  • Transport Adapters
  • Client Libraries

手を動かすのが一番早い理解につながるので、 例の如く適当にプロジェクトとDBを作成します

プロジェクト作成

$ mix phoenix.new channel_sample
$ cd channel_sample
$ mix ecto.create

SocketとChannelの設定

lib/hello_phoenix/endpoint.ex の4行目付近ですでにsocketが定義されています。

defmodule ChannelSample.Endpoint do
  use Phoenix.Endpoint, otp_app: :channel_sample

  socket "/socket", ChannelSample.UserSocket
...

ChannelSample.UserSocket 自体は web/channels/user_socket.ex で定義されています。5行目のコメントアウトを外して、channelの設定をします。

defmodule ChannelSample.UserSocket do
  use Phoenix.Socket

  ## Channels
  channel "room:*", ChannelSample.RoomChannel

Channelモジュールの実装

HelloPhoenix.RoomChannel モジュールはまだ存在しないので、web/channels/room_channel.ex ファイルを作成し、以下の内容でモジュールを定義します。

defmodule ChannelSample.RoomChannel do
  use Phoenix.Channel

  def join("room:lobby", _message, socket) do
    {:ok, socket}
  end

  def join("room:" <> _private_room_id, _params, _socket) do
    {:error, %{reason: "unauthorized"}}
  end
end

<> は文字列結合をしています)

認可のために、join/3 関数を定義する必要があります。 今回は"room:lobby" topicだけは誰でも入れるようにし、private_roomのことは考えないこととします。

クライアント側の設定

web/static/js/socket.js に最低限の実装が最初からあるので、中身を確認します。

// web/static/js/socket.js
...
socket.connect()

// Now that you are connected, you can join channels with a topic:
let channel = socket.channel("room:lobby", {})
channel.join()
  .receive("ok", resp => { console.log("Joined successfully", resp) })
  .receive("error", resp => { console.log("Unable to join", resp) })

export default socket

上記のように57行目付近を let channel = socket.channel("room:lobby", {}) に変更します。

また、web/static/js/app.js 末尾行のコメントアウトを外してsocket.jsを有効にします。

import socket from "./socket"

Phoenixがライブリロードされ、ブラウザコンソールに以下のように出力され、 socket通信が確立されていることがわかります。 (Phoenixを起動していない場合は $ mix phoenix.server します)


f:id:shotat_jp:20160924160548p:plain


web/templates/page/index.html.eex を修正して、入力フォームとメッセージ表示用のコンテナを作成します。

<div id="messages"></div>
<input id="chat-input" type="text"></input>

socket.js ファイルを修正して、socket通信でメッセージの受送信ができるようにします。

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

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

channel.on("new_msg", payload => {
  let messageItem = document.createElement("li");
  messageItem.innerText = `[${Date()}] ${payload.body}`
  messagesContainer.appendChild(messageItem)
})

// let channel = socket.channel("topic:subtopic", {})
channel.join()
  .receive("ok", resp => { console.log("Joined successfully", resp) })
  .receive("error", resp => { console.log("Unable to join", resp) })

export default socket

RoomChannelモジュールを修正します。 socketでメッセージが飛んできた際のフック関数として handle_in を実装すれば大丈夫です。

broadcast! で接続中のクライアント全員にメッセージを送ります。

handle_out ではクライアントごとにフィルタリング処理を行ったり、Interceptor的な役割をもっています。今回は何もしていません。

defmodule ChannelSample.RoomChannel do
  use Phoenix.Channel

  def join("room:lobby", _message, socket) do
    {:ok, socket}
  end

  def join("room:" <> _private_room_id, _params, _socket) do
    {:error, %{reason: "unauthorized"}}
  end

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

  def handle_out("new_msg", payload, socket) do
    push socket, "new_msg", payload
    {:noreply, socket}
  end
end

これでChatができるようになりました。


f:id:shotat_jp:20160924165910p:plain

Sample

こちらにChat Appのサンプルがあるようです。

http://phoenixchat.herokuapp.com/

Heroku上で動いています。逆に対応してるPaaSはHerokuだけかもしれないです。

Programming Phoenix: Productive, Reliable, Fast

Programming Phoenix: Productive, Reliable, Fast

まとめ

Channelが手軽につかえていい感じです。 当然普通にRails的な使い方もできるのでなかなか汎用性高いのでは?と思いました(小並感)