【Phoenix Channel】(1) WebSocketとChannel - Qiita
【Phoenix Channel】(2) PhoenixClient - Qiita
Elixir/PhoenixのChannelの基本をまとめていきたいと思います。
Phoenixドキュメント - Channel
Phoenixソース
1 環境構築
まずは環境づくりです。プロジェクトを構築し、動作させてみましょう。
mix phx.new my_app --no-ecto
cd my_app
mix phx.server
以下のURLで画面が表示されれば、動作が確認できたことになります。
http://localhost:4000/
次にWebSocketがコメントアウトされているので、以下のように外しておきましょう。
import socket from "./socket"
2 Phoenix.Socket モジュール
ブラウザからのWebSocket通信を管理するOTP process は、 Phoenix.Socket を実装するモジュールに処理を委譲します。例えば、Phoenix.Socket モジュールとして、デフォルトでは以下のuser_socket.ex のようなコードが提供されています。
defmodule MyAppWeb.UserSocket do
use Phoenix.Socket
## Channels
channel "room:*", MyAppWeb.RoomChannel
@impl true
def connect(_params, socket, _connect_info) do
{:ok, socket}
end
@impl true
def id(_socket), do: nil
end
現状、上のuser_socket.exで重要なのは、Channel topic "room:*" にアクセスしてきた通信は、MyAppWeb.RoomChannel にルーティングされることです。ここでは更にSocket接続を許可したり拒否したりする処理を追加することができます。
3 Phoenix.Channelモジュール
MyAppWeb.RoomChannel は Phoenix.Channelを実装するモジュールです。Phoenix.Channelモジュールは、Channel topic 毎に別プロセスとして起動されます。ですから、異なるtopicの処理は全く違うプロセスで行われます。
MyAppWeb.RoomChannel は自分で作成します。現状では以下のようにシンプルなもので大丈夫です。
繰り返しになりますが、ここにくるアクセスは topic "room:*" 経由のものだけです。その上で、join/3 は任意のtopic( i.e. _topic)を受け入れています。つまり "room:1" でも "room:abc" でも良いということです。実際には後で"room:3"でjoinしてみます。また handle_in/3 で event の "event5"を受け取っています。
defmodule MyAppWeb.RoomChannel do
use Phoenix.Channel
require Logger
def join(_topic, _payload, socket) do
Logger.info("*** topic = #{_topic}")
{:ok, socket}
end
def handle_in("event5", %{"error" => true}, socket) do
{:reply, {:error, %{reason: "error flag for event5 request is true"}}, socket}
end
def handle_in("event5", _payload, socket) do
{:reply, {:ok, %{event5: "pong"}}, socket}
end
end
4 クライアントWebSocketコード
Phoenix Channelではクライアント側のWebSocket API(JavaScriptコード)も一式提供されています。
今回は以下のようなテストコードを書いてみます。
- topic "room:3"にJoinする
- topic "room:3"にevent "event5" を2度pushする
import {Socket} from "phoenix"
let socket = new Socket("/socket", {params: {token: window.userToken}})
socket.connect()
let channel = socket.channel("room:3", {})
channel.join()
.receive("ok", resp => { console.log("Joined successfully", resp) })
.receive("error", resp => { console.log("Unable to join", resp) })
channel.push("event5", { error: true })
.receive("error", (resp) => console.error("event5 error:", resp))
channel.push("event5", { error: false, hobbies: ["fishing", "eating"] })
.receive("ok", (resp) => console.log("event5 ok:", resp))
export default socket
5 実行結果
Chrome DevToolsでは以下のような出力が確認できます
コンソールでは以下のような出力が確認できます
[info] *** topic = room:3
[info] JOINED room:3 in 0ツオs
Parameters: %{}
[debug] HANDLED event5 INCOMING ON room:3 (MyAppWeb.RoomChannel) in 0ツオs
Parameters: %{"error" => true}
[debug] HANDLED event5 INCOMING ON room:3 (MyAppWeb.RoomChannel) in 0ツオs
Parameters: %{"error" => false, "hobbies" => ["fishing", "eating"]}
6. Endpointのbroadcast/3
上ではクライアントからChannelにメッセージを送信しました。次は逆を行います。
クライアントのsocket.jsにsend_ping event に対応するコードを追加します。
import {Socket} from "phoenix"
let socket = new Socket("/socket", {params: {token: window.userToken}})
socket.connect()
let channel = socket.channel("room:3", {})
channel.join()
.receive("ok", resp => { console.log("Joined successfully", resp) })
.receive("error", resp => { console.log("Unable to join", resp) })
channel.push("event5", { error: true })
.receive("error", (resp) => console.error("event5 error:", resp))
channel.push("event5", { error: false, hobbies: ["fishing", "eating"] })
.receive("ok", (resp) => console.log("event5 ok:", resp))
channel.on("send_ping", (payload) => {
console.log("ping requested !!!", payload)
channel.push("event5", { error: false, hobbies: ["reading", "walking"] })
.receive("ok", (resp) => console.log("ping:", resp.event5))
})
export default socket
サーバ側のElixirコードは変更しないけど、一応再掲載しておきます。
defmodule MyAppWeb.RoomChannel do
use Phoenix.Channel
require Logger
def join(_topic, _payload, socket) do
Logger.info("*** topic = #{_topic}")
{:ok, socket}
end
def handle_in("event5", %{"error" => true}, socket) do
{:reply, {:error, %{reason: "error flag for event5 request is true"}}, socket}
end
def handle_in("event5", _payload, socket) do
{:reply, {:ok, %{event5: "pong"}}, socket}
end
end
Endpointのbroadcast/3を使えば、Channelに直接messageを送ることができます。そしてChannelのtopicに送られたmessageは、接続されているClientに直接ブロードキャストされます。 以下に実験をしましょう。
Endpointのbroadcast/3でsend_ping eventのmessageを送る
iex(2)> MyAppWeb.Endpoint.broadcast("room:3", "send_ping", %{data: "test"})
:ok
クライアントはend_ping eventを受け、callbackで「ping requested !!!」ログを吐き、event5を送る
サーバはevent5を受け取り、ログを吐く
iex(3)> [debug] HANDLED event5 INCOMING ON room:3 (MyAppWeb.RoomChannel) in 0ツオs
Parameters: %{"error" => false, "hobbies" => ["reading", "walking"]}
今回は以上です。