Phoenix.Channel behaviour
Defines a Phoenix Channel.
Channels provide a means for bidirectional communication from clients that
integrate with the Phoenix.PubSub
layer for soft-realtime functionality.
Topics & Callbacks
When clients join a channel, they do so by subscribing to a topic.
Topics are string identifiers in the Phoenix.PubSub
layer that allow
multiple processes to subscribe and broadcast messages about a given topic.
Everytime you join a Channel, you need to choose which particular topic you
want to listen to. The topic is just an identifier, but by convention it is
often made of two parts: "topic:subtopic"
. Using the "topic:subtopic"
approach pairs nicely with the Phoenix.Router.channel/3
macro to match
topic patterns in your router to your channel handlers:
socket "/ws", MyApp do
channel "rooms:*", RoomChannel
end
Any topic coming into the router with the "rooms:"
prefix, would dispatch
to MyApp.RoomChannel
in the above example. Topics can also be pattern
matched in your channels’ join/3
callback to pluck out the scoped pattern:
# handles the special `"lobby"` subtopic
def join("rooms:lobby", _auth_message, socket) do
{:ok, socket}
end
# handles any other subtopic as the room ID, ie `"rooms:12"`, `"rooms:34"`
def join("rooms:" <> room_id, auth_message, socket) do
{:ok, socket}
end
Authorization
Clients must join a channel to send and receive PubSub events on that channel.
Your channels must implement a join/3
callback that authorizes the socket
for the given topic. It is common for clients to send up authorization data,
such as HMAC’d tokens for this purpose.
To authorize a socket in join/3
, return {:ok, socket}
.
To refuse authorization in join/3, return
:ignore`.
Incoming Events
After a client has successfully joined a channel, incoming events from the
client are routed through the channel’s handle_in/3
callbacks. Within these
callbacks, you can perform any action. Typically you’ll either forward a
message to all listeners with Phoenix.Channel.broadcast!/3
, or push a message
directly down the socket with Phoenix.Channel.push/3
.
Incoming callbacks must return the socket
to maintain ephemeral state.
Here’s an example of receiving an incoming "new_msg"
event from one client,
and broadcasting the message to all topic subscribers for this socket.
def handle_in("new_msg", %{"uid" => uid, "body" => body}, socket) do
broadcast! socket, "new_msg", %{uid: uid, body: body}
{:noreply, socket}
end
You can also push a message directly down the socket:
# client asks for their current rank, push sent directly as a new event.
def handle_in("current:rank", socket) do
push socket, "current:rank", %{val: Game.get_rank(socket.assigns[:user])}
{:noreply, socket}
end
Synchronous Replies
In addition to pushing messages out when you receive a handle_in
event,
you can also reply directly to a client event for request/response style
messaging. This is useful when a client must know the result of an operation
or to simply ack messages.
For example, imagine creating a resource and replying with the created record:
def handle_in("create:post", attrs, socket) do
changeset = Post.changeset(%Post{}, attrs)
if changeset.valid? do
Repo.insert(changeset)
{:reply, {:ok, changeset}, socket}
else
{:reply, {:error, changeset.errors}, socket}
end
end
Alternatively, you may just want to ack the status of the operation:
def handle_in("create:post", attrs, socket) do
changeset = Post.changeset(%Post{}, attrs)
if changeset.valid? do
Repo.insert(changeset)
{:reply, :ok, socket}
else
{:reply, :error, socket}
end
end
Outgoing Events
When an event is broadcasted with Phoenix.Channel.broadcast/3
, each channel
subscriber’s handle_out/3
callback is triggered where the event can be
relayed as is, or customized on a socket by socket basis to append extra
information, or conditionally filter the message from being delivered.
def handle_in("new_msg", %{"uid" => uid, "body" => body}, socket) do
broadcast! socket, "new_msg", %{uid: uid, body: body}
{:noreply, socket}
end
# for every socket subscribing to this topic, append an `is_editable`
# value for client metadata.
def handle_out("new_msg", msg, socket) do
push socket, "new_msg", Dict.merge(msg,
is_editable: User.can_edit_message?(socket.assigns[:user], msg)
)
{:noreply, socket}
end
# do not send broadcasted `"user:joined"` events if this socket's user
# is ignoring the user who joined.
def handle_out("user:joined", msg, socket) do
unless User.ignoring?(socket.assigns[:user], msg.user_id) do
push socket, "user:joined", msg
end
{:noreply, socket}
end
By default, unhandled outgoing events are forwarded to each client as a push,
but you’ll need to define the catch-all clause yourself once you define an
handle_out/3
clause.
Broadcasting to an external topic
In some cases, you will want to broadcast messages without the context of a socket
.
This could be for broadcasting from within your channel to an external topic, or
broadcasting from elsewhere in your application like a Controller or GenServer.
For these cases, you can broadcast from your Endpoint. Its configured PubSub
server will be used:
# within channel
def handle_in("new_msg", %{"uid" => uid, "body" => body}, socket) do
broadcast! socket, "new_msg", %{uid: uid, body: body}
MyApp.Endpoint.broadcast! "rooms:superadmin", "new_msg", %{uid: uid, body: body}
{:noreply, socket}
end
# within controller
def create(conn, params) do
...
MyApp.Endpoint.broadcast! "rooms:" <> rid, "new_msg", %{uid: uid, body: body}
MyApp.Endpoint.broadcast! "rooms:superadmin", "new_msg", %{uid: uid, body: body}
redirect conn, to: "/"
end
Summary↑
broadcast!(socket, event, msg) | Same as |
broadcast!(server, topic, event, message) | |
broadcast(socket, event, msg) | Broadcast event, serializable as JSON to a channel |
broadcast(server, topic, event, message) | |
broadcast_from!(socket, event, msg) | Same as |
broadcast_from!(pubsub_server, socket, event, message) | |
broadcast_from!(pubsub_server, from, topic, event, message) | |
broadcast_from(socket, event, msg) | Broadcast event from pid, serializable as JSON to channel.
The broadcasting socket |
broadcast_from(pubsub_server, socket, event, message) | |
broadcast_from(pubsub_server, from, topic, event, message) | |
push(socket, event, message) | Sends Dict, JSON serializable message to socket |
Functions
Broadcast event, serializable as JSON to a channel.
Examples
iex> Channel.broadcast "rooms:global", "new_message", %{id: 1, content: "hello"}
:ok
iex> Channel.broadcast socket, "new_message", %{id: 1, content: "hello"}
:ok
Same as Phoenix.Channel.broadcast/4
, but
raises Phoenix.PubSub.BroadcastError
if broadcast fails.
Broadcast event from pid, serializable as JSON to channel.
The broadcasting socket from
, does not receive the published message.
The event’s message must be a map serializable as JSON.
Examples
iex> Channel.broadcast_from self, "rooms:global", "new_message", %{id: 1, content: "hello"}
:ok
Same as Phoenix.Channel.broadcast_from/4
, but
raises Phoenix.PubSub.BroadcastError
if broadcast fails.
Sends Dict, JSON serializable message to socket.
Callbacks
Specs:
- handle_in(event :: String.t, msg :: %{}, Phoenix.Socket.t) :: {:noreply, Phoenix.Socket.t} | {:reply, {status :: atom, response :: %{}}, Phoenix.Socket.t} | {:reply, status :: atom, Phoenix.Socket.t} | {:stop, reason :: term, Phoenix.Socket.t} | {:stop, reason :: term, reply :: {status :: atom, response :: %{}}, Phoenix.Socket.t} | {:stop, reason :: term, reply :: status :: atom, Phoenix.Socket.t}
Specs:
- handle_out(event :: String.t, msg :: %{}, Phoenix.Socket.t) :: {:ok, Phoenix.Socket.t} | {:noreply, Phoenix.Socket.t} | {:error, reason :: term, Phoenix.Socket.t} | {:stop, reason :: term, Phoenix.Socket.t}
Specs:
- join(topic :: binary, auth_msg :: %{}, Phoenix.Socket.t) :: {:ok, Phoenix.Socket.t} | :ignore
Specs:
- terminate(msg :: %{}, Phoenix.Socket.t) :: :ok | {:error, reason :: term}