The Websocket protocol allows for ongoing, persistent connections between a client and a server, allowing for real-time two-way communication.
In a Phoenix app, it starts in the endpoint.
defmodule AppWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :app
socket "/socket", AppWeb.UserSocket
# ...
end
That exposes endpoint for socket connections.
"ws://localhost:4000/socket" # => App.UserSocket
Which directs connections to the module below. (Phoenix starts a process for each socket connection).
defmodule AppWeb.UserSocket do
use Phoenix.Socket
def connect(_params, socket) do
{:ok, socket}
end
def id(_socket), do: nil
end
# adding auth
def connect(%{token: token}, socket) do
case find_user(token) do
{:ok, user} ->
socket = assign(socket, :user, user)
{:ok, socket}
_other ->
:error
end
end
def connect(_other, _socket), do: :error
Connecting from JavaScript
import { Socket } from 'phoenix';
let socket = new Socket('/socket', {
logger: (kind, msg, data) => { console.log(`${kind} ${msg} ${data}`) },
params: {
token: window.userToken
}
})
socket.onError(() => console.log('There was an error!'))
socket.onClose(() => console.log('The connectino dropped!'))
socket.connect()
Generated code (mix phx.new app
):
lib/app_web/channels/user_socket.ex
assets/js/socket/js
Phoenix WebSocket Advantages
- Zero dependencies
- Robust fault-tolerance
- Supports massive load
Channels are a means of splitting up websocket messages into separate topics. Clients can subscribe to many channels over one socket connection.
+------------+
| Client |
+-----+------+
|
v
+------------+
| Socket |
+-----+------+
|
+-----+------+
| |
v v
+-----------+ +-----------+
| Channel | | Channel |
+-----------+ +-----------+
Channels are set up inside AppWeb.UserSocket
module. Naming conventions are topic:subtopic
. Use topic:*
for wildcards.
defmodule AppWeb.UserSocket do
use Phoenix.Socket
channel "chat:*", AppWeb.ChatChannel
# ...
end
Channel API: 4 important callbacks:
join/3
, determines if client is able to connect to topichandle_in/3
, handles incoming messages from clienthandle_out/3
, intercepts message before going out to client (to enrich with data)terminate/2
, called if clients leaves/disconnects
Joining a Channel
Client side JavaScript:
import { Socket } from 'phoenix';
let socket = new Socket('/socket', {});
socket.connect();
let channel = socket.channel('chat:lobby', {});
channel.join()
.receive('ok', (resp) => {
console.log('joined!');
})
.receive('error', (resp) => {
console.log('could not join!');
});
Server side:
defmodule AppWeb.ChatChannel do
use AppWeb, :channel
def join("chat:lobby", _params, socket) do
{:ok, socket}
end
def join("chat:secret", _params, socket) do
{:error, %{reason: "You don't have permission!"}}
end
end
It is common to use the join/3
callback to catch up the client on what has been going on with a payload of data.
Elixir:
def join("chat:lobby", _params, socket) do
messages = load_messages()
{:ok, %{messages: messages}, socket}
end
JavaScript:
channel.join()
.receive('ok', ({message}) => {
console.log('catching up', messages);
});
Receiving Messages (client to server)
JavaScript:
channel.push('mew_msg', {body: 'Hello World!'});
Elixir:
def handle_in("new_msg", payload, socket) do
broadcast! socket, "new_msg", payload
{:noreply, socket}
end
Sending Messages (server to client)
Elixir:
AppWeb.Endpoint.Broadcast!("chat:lobby", "new_msg", %{body: "Welcome!"})
JavaScript:
channel.on('new_msg', (payload) => {
alert(payload.body);
});
3 functions to send messages within Channel module:
broadcast/3
, sends message to all subs, including current channelbroadcast_from/3
, sends message to other subspush/3
, sends message to a single channel
Customizing Messages
intercept ["new_msg"]
def handle_out("new_msg", payload, socket) do
# Customize payload
push socket, "new_msg", payload
{:noreply, socket}
end
Termination
def terminate({:shutdown, :left}, socket) do
# called if clients disconnects from channel
# clean up anything
end
Generator
$ mix phoenix.gen.channel ChatChannel