Skip to content

Instantly share code, notes, and snippets.

@josephan
Last active November 2, 2018 20:56
Show Gist options
  • Save josephan/dc5981ed31b5759bce3aa49e1467b3d6 to your computer and use it in GitHub Desktop.
Save josephan/dc5981ed31b5759bce3aa49e1467b3d6 to your computer and use it in GitHub Desktop.
Phoenix WebSocket Notes

Phoenix WebSocket Notes

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

  1. Zero dependencies
  2. Robust fault-tolerance
  3. Supports massive load

Channels

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 topic
  • handle_in/3, handles incoming messages from client
  • handle_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 channel
  • broadcast_from/3, sends message to other subs
  • push/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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment