Phoenix1.6ではWebSocketの準備にmix phx.gen.socketを使う
Phoenixの時期バージョンである1.6系で、プロジェクト開始時のWebSocket周りについて変更があったので、メモしておきます。
結論、Phoenix1.6からは、mix phx.new
タスクでWebSocket周りの設定とファイルが生成されなくなったので、別途mix phx.gen.socket
を実行する必要があります。
本記事の背景
本記事を書いている時点では、Phoenix 1.6.0-rc.0 Released - Phoenix Blogにある通り、Phoenixの時期バージョンである1.6系の本リリースに向けての取り組みが進んでいます。このバージョンに関する変更はphoenix/CHANGELOG.md at master · phoenixframework/phoenixにまとまっています。
1.6.0-rc.0 (2021-08-26)の変更リストの中に、
[mix phx.new] No longer generate a socket file by default, instead one can run mix phx.gen.socket
とあるのが、今回の話題です。
Phoenixプロジェクトを新規作成する
まずは、現在リリースされている、RC版のPhoenixをインストールします。
$ mix archive.install hex phx_new 1.6.0-rc.0
そして、いつものようにmix phx.new
タスクで、プロジェクトを作成します。
$ mix phx.new phx_16 --no-ecto
WebSocketを準備する
1.5系まででは、WebSocket周りのファイルがあれこれ作られていましたが、1.6系からはmix phx.gen.socket
タスクを実行する必要があります。
mix phx.gen.socket
タスクを、モジュール名を引数に渡して実行します。
$ mix phx.gen.socket User
* creating lib/phx_16_web/channels/user_socket.ex
* creating assets/js/user_socket.js
Add the socket handler to your `lib/phx_16_web/endpoint.ex`, for example:
socket "/socket", Phx16Web.UserSocket,
websocket: true,
longpoll: false
For the front-end integration, you need to import the `user_socket.js`
in your `assets/js/app.js` file:
import "./user_socket.js"
1.5系まででは、上記のファイルや設定は、mix phx.new
した時点で、UserSocket
というモジュール込みで生成されていたのですが、上記のタスク実行によって生成されるようになりました。ただし、assets/js/app.js
でWebSocketを扱うJavaScriptのファイルのimport
を追加する必要があるのは同じですね。
生成されたファイルの差分を見てみると、こんな感じ。
diff --git a/assets/js/user_socket.js b/assets/js/user_socket.js
new file mode 100644
index 0000000..7a22631
--- /dev/null
+++ b/assets/js/user_socket.js
@@ -0,0 +1,64 @@
+// NOTE: The contents of this file will only be executed if
+// you uncomment its entry in "assets/js/app.js".
+
+// Bring in Phoenix channels client library:
+import {Socket} from "phoenix"
+
+// And connect to the path in "lib/phx_16_web/endpoint.ex". We pass the
+// token for authentication. Read below how it should be used.
+let socket = new Socket("/socket", {params: {token: window.userToken}})
+
+// When you connect, you'll often need to authenticate the client.
+// For example, imagine you have an authentication plug, `MyAuth`,
+// which authenticates the session and assigns a `:current_user`.
+// If the current user exists you can assign the user's token in
+// the connection for use in the layout.
+//
+// In your "lib/phx_16_web/router.ex":
+//
+// pipeline :browser do
+// ...
+// plug MyAuth
+// plug :put_user_token
+// end
+//
+// defp put_user_token(conn, _) do
+// if current_user = conn.assigns[:current_user] do
+// token = Phoenix.Token.sign(conn, "user socket", current_user.id)
+// assign(conn, :user_token, token)
+// else
+// conn
+// end
+// end
+//
+// Now you need to pass this token to JavaScript. You can do so
+// inside a script tag in "lib/phx_16_web/templates/layout/app.html.heex":
+//
+// <script>window.userToken = "<%= assigns[:user_token] %>";</script>
+//
+// You will need to verify the user token in the "connect/3" function
+// in "lib/phx_16_web/channels/user_socket.ex":
+//
+// def connect(%{"token" => token}, socket, _connect_info) do
+// # max_age: 1209600 is equivalent to two weeks in seconds
+// case Phoenix.Token.verify(socket, "user socket", token, max_age: 1_209_600) do
+// {:ok, user_id} ->
+// {:ok, assign(socket, :user, user_id)}
+//
+// {:error, reason} ->
+// :error
+// end
+// end
+//
+// Finally, connect to the socket:
+socket.connect()
+
+// Now that you are connected, you can join channels with a topic.
+// Let's assume you have a channel with a topic named `room` and the
+// subtopic is its id - in this case 42:
+let channel = socket.channel("room:42", {})
+channel.join()
+ .receive("ok", resp => { console.log("Joined successfully", resp) })
+ .receive("error", resp => { console.log("Unable to join", resp) })
+
+export default socket
diff --git a/lib/phx_16_web/channels/user_socket.ex b/lib/phx_16_web/channels/user_socket.ex
new file mode 100644
index 0000000..354337e
--- /dev/null
+++ b/lib/phx_16_web/channels/user_socket.ex
@@ -0,0 +1,51 @@
+defmodule Phx16Web.UserSocket do
+ use Phoenix.Socket
+
+ # A Socket handler
+ #
+ # It's possible to control the websocket connection and
+ # assign values that can be accessed by your channel topics.
+
+ ## Channels
+ # Uncomment the following line to define a "room:*" topic
+ # pointing to the `Phx16Web.RoomChannel`:
+ #
+ # channel "room:*", Phx16Web.RoomChannel
+ #
+ # To create a channel file, use the mix task:
+ #
+ # mix phx.gen.channel Room
+ #
+ # See the [`Channels guide`](https://hexdocs.pm/phoenix/channels.html)
+ # for futher details.
+
+
+ # Socket params are passed from the client and can
+ # be used to verify and authenticate a user. After
+ # verification, you can put default assigns into
+ # the socket that will be set for all channels, ie
+ #
+ # {:ok, assign(socket, :user_id, verified_user_id)}
+ #
+ # To deny connection, return `:error`.
+ #
+ # See `Phoenix.Token` documentation for examples in
+ # performing token verification on connect.
+ @impl true
+ def connect(_params, socket, _connect_info) do
+ {:ok, socket}
+ end
+
+ # Socket id's are topics that allow you to identify all sockets for a given user:
+ #
+ # def id(socket), do: "user_socket:#{socket.assigns.user_id}"
+ #
+ # Would allow you to broadcast a "disconnect" event and terminate
+ # all active sockets and channels for a given user:
+ #
+ # Elixir.Phx16Web.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
+ #
+ # Returning `nil` makes this socket anonymous.
+ @impl true
+ def id(_socket), do: nil
+end
WebSocketを動かしてみる
設定の追加
まずは、mix phx.gen.socket
を実行した後に出てきた以下の指示をそれぞれ実行します。
Add the socket handler to your `lib/phx_16_web/endpoint.ex`, for example:
socket "/socket", Phx16Web.UserSocket,
websocket: true,
longpoll: false
For the front-end integration, you need to import the `user_socket.js`
in your `assets/js/app.js` file:
import "./user_socket.js"
変更後のdiffは以下の通りです。
diff --git a/assets/js/app.js b/assets/js/app.js
index 9eabcff..0e163db 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -4,7 +4,7 @@ import "../css/app.css"
// If you want to use Phoenix channels, run `mix help phx.gen.channel`
// to get started and then uncomment the line below.
-// import "./user_socket.js"
+import "./user_socket.js"
// You can include dependencies in two ways.
//
diff --git a/lib/phx_16_web/endpoint.ex b/lib/phx_16_web/endpoint.ex
index 03ae366..966bd7c 100644
--- a/lib/phx_16_web/endpoint.ex
+++ b/lib/phx_16_web/endpoint.ex
@@ -10,7 +10,12 @@ defmodule Phx16Web.Endpoint do
signing_salt: "tB4UeyNi"
]
- socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
+ socket("/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]])
+
+ socket("/socket", Phx16Web.UserSocket,
+ websocket: true,
+ longpoll: false
+ )
# Serve at "/" the static files from "priv/static" directory.
#
@@ -25,7 +30,7 @@ defmodule Phx16Web.Endpoint do
# Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.
if code_reloading? do
- socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
+ socket("/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket)
plug Phoenix.LiveReloader
plug Phoenix.CodeReloader
end
チャネルの作成
次は、WebSocketのチャネルを作りましょう。
lib/phx_16_web/channels/user_socket.ex
にこんなコメントが生成されています。
## Channels
# Uncomment the following line to define a "room:*" topic
# pointing to the `Phx16Web.RoomChannel`:
#
# channel "room:*", Phx16Web.RoomChannel
#
# To create a channel file, use the mix task:
#
# mix phx.gen.channel Room
#
# See the [`Channels guide`](https://hexdocs.pm/phoenix/channels.html)
# for futher details.
というわけで、channel "room:*", Phx16Web.RoomChannel
の部分をコメントアウトして、mix phx.gen.channel Room
を実行します。
$ mix phx.gen.channel Room
* creating lib/phx_16_web/channels/room_channel.ex
* creating test/phx_16_web/channels/room_channel_test.exs
Add the channel to your `lib/phx_16_web/channels/user_socket.ex` handler, for example:
channel "room:lobby", Phx16Web.RoomChannel
上記の最後に指示のあるハンドラは、すでに前述の通り設定してあるのでOKです。チャネルはlib/phx_16_web/channels/room_channel.ex
に、以下の通り生成されます。
diff --git a/lib/phx_16_web/channels/room_channel.ex b/lib/phx_16_web/channels/room_channel.ex
new file mode 100644
index 0000000..99427ce
--- /dev/null
+++ b/lib/phx_16_web/channels/room_channel.ex
@@ -0,0 +1,32 @@
+defmodule Phx16Web.RoomChannel do
+ use Phx16Web, :channel
+
+ @impl true
+ def join("room:lobby", payload, socket) do
+ if authorized?(payload) do
+ {:ok, socket}
+ else
+ {:error, %{reason: "unauthorized"}}
+ end
+ end
+
+ # Channels can be used in a request/response fashion
+ # by sending replies to requests from the client
+ @impl true
+ def handle_in("ping", payload, socket) do
+ {:reply, {:ok, payload}, socket}
+ end
+
+ # It is also common to receive messages from the client and
+ # broadcast to everyone in the current topic (room:lobby).
+ @impl true
+ def handle_in("shout", payload, socket) do
+ broadcast socket, "shout", payload
+ {:noreply, socket}
+ end
+
+ # Add authorization logic here as required.
+ defp authorized?(_payload) do
+ true
+ end
+end
WebSocketクライアントによる接続
準備ができたので、CLIからWebSocketに接続します。wscatを使いましょう。
まずは、WebSocketサーバーを立ち上げます。
$ iex -S mix phx.server
そして、別のターミナルでwscatを起動します。
$ wscat -c ws://localhost:4000/socket/websocket
Connected (press CTRL+C to quit)
>
まずはトピックにjoinして見ましょう。
> {"topic":"room:lobby","ref":1, "payload":{},"event":"phx_join"}
< {"event":"phx_reply","payload":{"response":{},"status":"ok"},"ref":1,"topic":"room:lobby"}
WebSocketサーバ側では、こんなログが出ています。
[info] JOINED room:lobby in 32µs
Parameters: %{}
では、メッセージを送ってみましょう。ちなみに、Roomチャンネルのping
イベントのハンドラは以下のような実装になっています。
def handle_in("ping", payload, socket) do
{:reply, {:ok, payload}, socket}
end
以下のようにして、ping
イベントに適当なペイロードをわたして送ってやります。
> {"topic":"room:lobby","ref":1, "payload":{"body": {"message": "Hello, WebSocket!"}},"event":"ping"}
< {"event":"phx_reply","payload":{"response":{"body":{"message":"Hello, WebSocket!"}},"status":"ok"},"ref":1,"topic":"room:lobby"}
うまく動いているようですね。WebSocketサーバ側では、こんなログが出ています。
[debug] HANDLED ping INCOMING ON room:lobby (Phx16Web.RoomChannel) in 50µs
Parameters: %{"body" => %{"message" => "Hello, WebSocket!"}}
おわりに
Phoenix1.6系からは、mix phx.new
タスクではWebSocket関連の設定やファイルが生成されないようになりました。しかし、mix phx.gen.socket
によって1.5系同様のことはすぐにできるということが確認できました。
Discussion