【Phoenix Channel】(1) WebSocketとChannel

[【Phoenix Channel】(1) WebSocketとChannel - Qiita](https://qiita.com/sand/items/886ace4dcdff5953e673)
[【Phoenix Channel】(2) PhoenixClient - Qiita](https://qiita.com/sand/items/8b3733366e85d0cf9082)



Elixir/PhoenixのChannelの基本をまとめていきたいと思います。

[Phoenixドキュメント - Channel](https://hexdocs.pm/phoenix/Phoenix.Channel.html#content)
[Phoenixソース](https://github.com/phoenixframework/phoenix)


# 1 環境構築


まずは環境づくりです。プロジェクトを構築し、動作させてみましょう。

```
mix phx.new my_app --no-ecto
cd my_app
mix phx.server
```

以下のURLで画面が表示されれば、動作が確認できたことになります。
http://localhost:4000/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/961e1436-af4d-3776-fad3-ba61320646d6.png)



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

```JavaScript:my_app/assets/js/app.js
import socket from "./socket"
```


# 2 Phoenix.Socket モジュール

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

```elixir: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/3** で **event** の "event5"を受け取っています。

```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
```

# 4 クライアントWebSocketコード

Phoenix Channelではクライアント側のWebSocket API(JavaScriptコード)も一式提供されています。
今回は以下のようなテストコードを書いてみます。

* topic "room:3"にJoinする
* topic "room:3"にevent "event5" を2度pushする

```JavaScript: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/4d65c455-bca7-0f3c-c318-4b763994a90a.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 に対応するコードを追加します。

```elixir: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コードは変更しないけど、一応再掲載しておきます。

```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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/b73685ea-d991-616e-1eb7-8e55e739b5b1.png)


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

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




今回は以上です。