We had requirement of building realtime notification system. So whenever a user mentions n number of users we have to broadcast a message to all the users in real time and thereby update the count of each notification and push the message on their respective client. To implement this we need to understand Channels in phoenix.
Channels:
Phoenix framework has a built-in facility to manage two way communication between web clients and the server. Sender broadcast a message about topics and receiver subscribes the topics so they can get those messages. Also sender can become receiver and receiver can become sender at anytime during communication.
We have to create a channel file web/channels/notificaiton_channel.ex
. This channel has main goal is to authorize the client with given topic, in our case topic is notifications
. We have to show a notification only to its corresponding user. So for example user with id 21 can only joined channel with notifications:21
.
Channel has a join/3
method which takes three arguments topic, payload and socket connection object. This method returns a tuple {:ok, message, socket}
when it is authorised other wise return a tuple with {:error, reason}
. It has handle_in/3
which takes event name, payload coming from client and socket object. We can pattern match the event name new_notification
in our case. This method broadcast the message to other channel instances joined the same topic.
defmodule SampleApp.NotificationChannel do
use Phoenix.Channel
def join("notifications:" <> user_id, _payload, socket) do
{:ok, "Joined Notification:#{user_id}", socket}
end
def handle_in("new_notification", %{"body" => body}, socket) do
broadcast! socket, "new_notification", %{body: body}
{:noreply, socket}
end
end
Channel Client
Phoenix officially provide client library phoenix.js to establish a socket connection. This library establishes a web socket connection to the url ws://localhost:4000/socket
or wss://localhost:4000/socket
for a secure connection. To establish websocket connection we need to import Socket
function from phoenix-js file provided by framework. We have to pass the socket url and params. Create a channel instance by calling socket.channel method and it takes the topic as first argument. In our case we just want to connect it to channel with user id 123. Everytime a new notification comes it triggers to the new_notification
event on the channel and it updates the notification array and inserts a new notification object.
import { Socket } from "phoenix-js";
const token = 'randomtoken';
const socket = new Socket("ws://localhost:4000/socket", { params: { token: token } });
socket.connect();
let channel = socket.channel(`notifications:123`, {}); // notification will be recieved by user with id 123
channel.join()
.receive("ok", resp => { console.log("Joined successfully", resp) })
.receive("error", resp => { console.log("Unable to join", resp) })
// we will update with new notification as soon as we get it from web socket connection.
channel.on('new_notification', payload => {
this.notifications.replace([
payload['notification'],
...this.notifications,
]);
});
To make the notification channel work we also need to add it to web/channels/user_socket.ex
module.Phoenix holds a single connection to the server and multiplexes your channel sockets over that one connection. It authenticate and identify a socket connection. We have added topic as notifications:* because topic notification can be connect to any notification channel user id. You can add all your channels in here:
defmodule SampleApp.UserSocket do
use Phoenix.Socket
## Channels
channel "notifications:*", SampleApp.NotificationChannel
channel "user_approval_stats:*", SampleApp.UserApprovalStatsChannel
## Transports
transport :websocket, Phoenix.Transports.WebSocket
# transport :longpoll, Phoenix.Transports.LongPoll
# 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.
def connect(_params, socket) do
{:ok, socket}
end
# Socket id's are topics that allow you to identify all sockets for a given user:
#
# def id(socket), do: "users_socket:#{socket.assigns.user_id}"
#
# Would allow you to broadcast a "disconnect" event and terminate
# all active sockets and channels for a given user:
#
# SampleApp.Endpoint.broadcast("users_socket:#{user.id}", "disconnect", %{})
#
# Returning `nil` makes this socket anonymous.
def id(_socket), do: nil
end
** Endpoint **
The endpoint is the boundary where all requests to your web application start. By default a phoenix application has UserSocket already added into it.
defmodule SampleApp.Endpoint do
use Phoenix.Endpoint, otp_app: :sample_app
socket "/socket", SampleApp.UserSocket
...
end
We want to broadcast notifications to there respective users whenever someone mentioned so for this we use the broadcast method which triggers the new_notification
event on the client channel instance. Here is an example
SampleApp.Endpoint.broadcast(
"notifications:#{notification.user_id}", "new_notification", payload
)
Here payload is nothing but the data that client would receive on the channel instance with new_notificaiton
event.
References:
https://hexdocs.pm/phoenix/channels.html
http://blog.techdominator.com/article/elixir-phoenix-so-far-channels.html