LoginSignup
6
4

More than 3 years have passed since last update.

【Phoenix Channel】(1) WebSocketとChannel

Last updated at Posted at 2020-08-08

【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/

image.png

次にWebSocketがコメントアウトされているので、以下のように外しておきましょう。

my_app/assets/js/app.js
import socket from "./socket"

2 Phoenix.Socket モジュール

ブラウザからのWebSocket通信を管理するOTP process は、 Phoenix.Socket を実装するモジュールに処理を委譲します。例えば、Phoenix.Socket モジュールとして、デフォルトでは以下のuser_socket.ex のようなコードが提供されています。

my_app/lib/my_app_web/channels/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/3event の "event5"を受け取っています。

my_app/lib/my_app_web/channels/room_channel.ex
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する
my_app/assets/js/socket.js
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では以下のような出力が確認できます
image.png

コンソールでは以下のような出力が確認できます

[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 に対応するコードを追加します。

my_app/assets/js/socket.js
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コードは変更しないけど、一応再掲載しておきます。

my_app/lib/my_app_web/channels/room_channel.ex
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を送る

image.png

サーバはevent5を受け取り、ログを吐く

iex(3)> [debug] HANDLED event5 INCOMING ON room:3 (MyAppWeb.RoomChannel) in 0ツオs
  Parameters: %{"error" => false, "hobbies" => ["reading", "walking"]}

今回は以上です。

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4