iTranslated by AI
Using mix phx.gen.socket for WebSocket Setup in Phoenix 1.6
In Phoenix version 1.6, the upcoming version of Phoenix, there have been changes regarding the WebSocket setup when starting a project, so I will document them here.
In conclusion, from Phoenix 1.6 onwards, the mix phx.new task no longer generates WebSocket-related settings and files, so it is necessary to run mix phx.gen.socket separately.
Background of this article
At the time of writing this article, efforts are underway toward the official release of Phoenix 1.6, as mentioned in Phoenix 1.6.0-rc.0 Released - Phoenix Blog. Changes for this version are summarized in phoenix/CHANGELOG.md at master · phoenixframework/phoenix.
In the list of changes for 1.6.0-rc.0 (2021-08-26), there is the following item, which is the subject of this post:
[mix phx.new] No longer generate a socket file by default, instead one can run mix phx.gen.socket
Creating a new Phoenix project
First, install the currently released RC version of Phoenix.
$ mix archive.install hex phx_new 1.6.0-rc.0
Then, create a project with the mix phx.new task as usual.
$ mix phx.new phx_16 --no-ecto
Preparing WebSockets
Up until the 1.5 series, various files related to WebSockets were created automatically, but from the 1.6 series onwards, you need to run the mix phx.gen.socket task.
Execute the mix phx.gen.socket task by passing the module name as an argument.
$ 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"
In versions up to 1.5, the files and configurations above were generated at the time of mix phx.new, including the UserSocket module. Now, they are generated by running the task mentioned above. However, the requirement to add the import for the JavaScript file handling WebSockets in assets/js/app.js remains the same.
Looking at the diff of the generated files, it looks like this:
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
Trying out WebSockets
Adding configuration
First, carry out each of the instructions that appeared after running 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"
The diff after the changes is as follows:
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
Creating channels
Next, let's create a WebSocket channel.
The following comments are generated in 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.
So, uncomment the channel "room:*", Phx16Web.RoomChannel part and run 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
The handler mentioned at the end is already configured as described earlier, so we're good to go. The channel is generated in lib/phx_16_web/channels/room_channel.ex as follows:
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
Connecting with a WebSocket client
Now that everything is ready, let's connect to the WebSocket from the CLI. We'll use wscat.
First, start the WebSocket server.
$ iex -S mix phx.server
Then, launch wscat in another terminal.
$ wscat -c ws://localhost:4000/socket/websocket
Connected (press CTRL+C to quit)
>
First, let's try joining a topic.
> {"topic":"room:lobby","ref":1, "payload":{},"event":"phx_join"}
< {"event":"phx_reply","payload":{"response":{},"status":"ok"},"ref":1,"topic":"room:lobby"}
On the WebSocket server side, you will see a log like this:
[info] JOINED room:lobby in 32µs
Parameters: %{}
Now, let's try sending a message. By the way, the handler for the ping event in the Room channel is implemented as follows:
def handle_in("ping", payload, socket) do
{:reply, {:ok, payload}, socket}
end
Send the ping event with an appropriate payload as follows:
> {"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"}
It seems to be working correctly. On the WebSocket server side, you will see a log like this:
[debug] HANDLED ping INCOMING ON room:lobby (Phx16Web.RoomChannel) in 50µs
Parameters: %{"body" => %{"message" => "Hello, WebSocket!"}}
Conclusion
From Phoenix 1.6, the mix phx.new task no longer generates WebSocket-related settings and files. However, we have confirmed that the same results as in the 1.5 series can be quickly achieved using mix phx.gen.socket.
Discussion