Phoenix Channels
Elixir Evening Club 1
Alex Rozumii
Why?
How to?
Generate project
$ phx.new elixirclubchat
* creating elixirclubchat/config/config.exs
* creating elixirclubchat/config/dev.exs
* creating elixirclubchat/config/prod.exs
* creating elixirclubchat/config/prod.secret.exs
.......
Generate channel
$ mix phx.gen.channel lobby
* creating lib/elixirclubchat/web/channels/lobby_channel.ex
* creating test/elixirclubchat/web/channels/lobby_channel_test.exs
Add the channel to your `lib/elixirclubchat/web/channels/user_socket.ex` handler, for example:
channel "lobby:lobby", Elixirclubchat.Web.LobbyChannel
Add simple HTML
<!-- lib/elixirclubchat/web/templates/page/index.html.eex -->
<div id='message-list' class='row'>
</div>
<div class='row form-group'>
<div class='col-md-3'>
<input type='text' id='name' placeholder='Name' />
</div>
<div class='col-md-9'>
<input type='text' id='message' placeholder='Message' />
</div>
</div>
Add channel JS
import {Socket} from "phoenix"
let socket = new Socket("/socket", {params: {}})
socket.connect()
let channel = socket.channel("lobby:lobby", {})
channel.join()
Add posting JS
let messageInput = document.getElementById("message")
let nameInput = document.getElementById("name")
messageInput.addEventListener("keypress", (e) => {
if (e.keyCode == 13 && messageInput.value != "") {
channel.push('shout', {
name: nameInput.value,
text: messageInput.value
});
messageInput.value = ""
}
})
Add polling JS
let messageList = document.getElementById("message-list")
let renderMessage = (message) => {
let messageElement = document.createElement("p")
messageElement.innerHTML = `<b>${message.name}</b> ${message.text}`
messageList.appendChild(messageElement)
}
channel.on("shout", message => renderMessage(message))
Bonus: Add CSS
#message-list {
border: 1px solid #777;
height: 400px;
padding: 10px;
overflow: scroll;
margin-bottom: 50px;
}
That's it!
We need to go deeper
# lib/elixirclubchat/application.ex
defmodule Elixirclubchat.Application do
....
children = [
supervisor(Elixirclubchat.Web.Endpoint, []),
]
opts = [strategy: :one_for_one, name: Elixirclubchat.Supervisor]
Supervisor.start_link(children, opts)
end
end
# lib/elixirclubchat/web/endpoint.ex
defmodule Elixirclubchat.Web.Endpoint do
use Phoenix.Endpoint, otp_app: :elixirclubchat
socket "/socket", Elixirclubchat.Web.UserSocket
plug Plug.Static
plug Plug.Logger
plug Elixirclubchat.Web.Router
end
defmodule Elixirclubchat.Web.UserSocket do
use Phoenix.Socket
## Channels
channel "lobby:lobby", Elixirclubchat.Web.LobbyChannel
## Transports
transport :websocket, Phoenix.Transports.WebSocket
# transport :longpoll, Phoenix.Transports.LongPoll
def connect(_params, socket) do
{:ok, socket}
end
def id(_socket), do: nil
end
defmodule Elixirclubchat.Web.LobbyChannel do
use Elixirclubchat.Web, :channel
def join("lobby:lobby", payload, socket) do
if authorized?(payload) do
{:ok, socket}
else
{:error, %{reason: "unauthorized"}}
end
end
# It is also common to receive messages from the client and
# broadcast to everyone in the current topic (lobby:lobby).
def handle_in("shout", payload, socket) do
broadcast socket, "shout", payload
{:noreply, socket}
end
defp authorized?(_payload) do
true
end
end
Cowboy
# Is called when the websocket connection is initiated
defcallback init
# Is called to handle messages from the client
defcallback websocket_handle
# Is called to handle Erlang/Elixir messages
defcallback websocket_info
# Is called when the socket connection terminates
defcallback websocket_terminate
Transport
# Phoenix.Socket.Transport
## The transport behaviour
The transport requires one function:
* `default_config/0` - returns the default transport configuration
to be merged when the transport is declared in the socket module
(App) Socket
# lib/elixirclubchat/web/channels/user_socket.ex
defmodule Elixirclubchat.Web.UserSocket do
use Phoenix.Socket
## Channels
# channel "room:*", Elixirclubchat.Web.RoomChannel
channel "lobby:lobby", Elixirclubchat.Web.LobbyChannel
## Transports
transport :websocket, Phoenix.Transports.WebSocket
# transport :longpoll, Phoenix.Transports.LongPoll
def channel do
quote do
use Phoenix.Channel
end
end
# deps/phoenix/lib/phoenix/channel.ex
@doc """
Broadcast an event to all subscribers of the socket topic.
"""
def broadcast(socket, event, message) do
%{pubsub_server: pubsub_server, topic: topic} = assert_joined!(socket)
Server.broadcast pubsub_server, topic, event, message
end
# deps/phoenix/lib/phoenix/channel/server.ex
@doc """
Broadcasts on the given pubsub server
"""
def broadcast!(pubsub_server, topic, event, payload) do
PubSub.broadcast! pubsub_server, topic, %Broadcast{
topic: topic,
event: event,
payload: payload
}
end
PubSub
# Configures the endpoint
config :elixirclubchat, Elixirclubchat.Web.Endpoint,
pubsub: [name: Elixirclubchat.PubSub,
adapter: Phoenix.PubSub.PG2]
PubSub
def init({server_name, pool_size}) do
pg2_group = pg2_namespace(server_name)
:ok = :pg2.create(pg2_group)
:ok = :pg2.join(pg2_group, self())
{:ok, %{name: server_name, pool_size: pool_size}}
end
defp get_members(server_name) do
:pg2.get_members(pg2_namespace(server_name))
end
PubSub
defp do_broadcast(pids, fastlane, server_name,
pool_size, from_pid, topic, msg) do
Enum.each(pids, fn
pid when is_pid(pid) and node(pid) == node() ->
Local.broadcast(fastlane, server_name,
pool_size, from_pid, topic, msg)
.....
end)
:ok
end
PubSub
defp do_broadcast(nil, pubsub_server, shard,
from, topic, msg) do
pubsub_server
|> subscribers_with_fastlanes(topic, shard)
|> Enum.each(fn
{pid, _} when pid == from -> :noop
{pid, _} -> send(pid, msg)
end)
end
RePG2
Registry: Elixir 1.4
Thanks!
Ask me some questions!
Phoenix Channels
By Alex Rozumii
Phoenix Channels
- 2,169