Phoenix v1.4.0 Phoenix.Socket behaviour View Source

A socket implementation that multiplexes messages over channels.

Phoenix.Socket is used as a module for establishing and maintaining the socket state via the Phoenix.Socket struct.

Once connected to a socket, incoming and outgoing events are routed to channels. The incoming client data is routed to channels via transports. It is the responsibility of the socket to tie transports and channels together.

By default, Phoenix supports both websockets and longpoll when invoking Phoenix.Endpoint.socket/3 in your endpoint:

socket "/socket", MyApp.Socket, websocket: true, longpoll: false

The command above means incoming socket connections can be made via a WebSocket connection. Events are routed by topic to channels:

channel "room:lobby", MyApp.LobbyChannel

See Phoenix.Channel for more information on channels.

Socket Behaviour

Socket handlers are mounted in Endpoints and must define two callbacks:

  • connect/3 - receives the socket params, connection info if any, and authenticates the connection. Must return a Phoenix.Socket struct, often with custom assigns
  • id/1 - receives the socket returned by connect/3 and returns the id of this connection as a string. The id is used to identify socket connections, often to a particular user, allowing us to force disconnections. For sockets requiring no authentication, nil can be returned

Examples

defmodule MyApp.UserSocket do
  use Phoenix.Socket

  channel "room:*", MyApp.RoomChannel

  def connect(params, socket, _connect_info) do
    {:ok, assign(socket, :user_id, params["user_id"])}
  end

  def id(socket), do: "users_socket:#{socket.assigns.user_id}"
end

# Disconnect all user's socket connections and their multiplexed channels
MyApp.Endpoint.broadcast("users_socket:" <> user.id, "disconnect", %{})

Socket fields

  • :id - The string id of the socket
  • :assigns - The map of socket assigns, default: %{}
  • :channel - The current channel module
  • :channel_pid - The channel pid
  • :endpoint - The endpoint module where this socket originated, for example: MyApp.Endpoint
  • :handler - The socket module where this socket originated, for example: MyApp.UserSocket
  • :joined - If the socket has effectively joined the channel
  • :join_ref - The ref sent by the client when joining
  • :ref - The latest ref sent by the client
  • :pubsub_server - The registered name of the socket’s pubsub server
  • :topic - The string topic, for example "room:123"
  • :transport - An identifier for the transport, used for logging
  • :transport_pid - The pid of the socket’s transport process
  • :serializer - The serializer for socket messages

Logging

Logging for socket connections is set via the :log option, for example:

use Phoenix.Socket, log: :debug

Defaults to the :info log level. Pass false to disable logging.

Garbage collection

It’s possible to force garbage collection in the transport process after processing large messages. For example, to trigger such from your channels, run:

send(socket.transport_pid, :garbage_collect)

Client-server communication

The encoding of server data and the decoding of client data is done according to a serializer, defined in Phoenix.Socket.Serializer. By default, JSON encoding is used to broker messages to and from clients with Phoenix.Socket.V2.JSONSerializer.

The serializer decode! function must return a Phoenix.Socket.Message which is forwarded to channels except:

  • "heartbeat" events in the “phoenix” topic - should just emit an OK reply
  • "phx_join" on any topic - should join the topic
  • "phx_leave" on any topic - should leave the topic

Each message also has a ref field which is used to track responses.

The server may send messages or replies back. For messages, the ref uniquely identifies the message. For replies, the ref matches the original message. Both data-types also include a join_ref that uniquely identifes the currently joined channel.

The Phoenix.Socket implementation may also sent special messages and replies:

  • "phx_error" - in case of errors, such as a channel process crashing, or when attempting to join an already joined channel

  • "phx_close" - the channel was gracefully closed

Phoenix ships with a JavaScript implementation of both websocket and long polling that interacts with Phoenix.Socket and can be used as reference for those interested in implementing custom clients.

Custom sockets and transports

See the Phoenix.Socket.Transport documentation for more information on writing your own socket that does not leverage channels or for writing your own transports that interacts with other sockets.

Custom channels

You can list any module as a channel as long as it implements a start_link/1 function that receives a tuple with three elements:

{auth_payload, from, socket}

A custom channel implementation MUST invoke GenServer.reply(from, reply_payload) during its initialization with a custom reply_payload that will be sent as a reply to the client. Failing to do so will block the socket forever.

A custom channel receives Phoenix.Socket.Message structs as regular messages from the transport. Replies to those messages and custom messages can be sent to the socket at any moment by building an appropriate Phoenix.Socket.Reply and Phoenix.Socket.Message structs, encoding them with the serializer and dispatching the serialized result to the transport.

For example, to handle “phx_leave” messages, which is recommended to be handled by all channel implementations, one may do:

def handle_info(
      %Message{topic: topic, event: "phx_leave"} = message,
      %{topic: topic, serializer: serializer, transport_pid: transport_pid} = socket
    ) do
  send transport_pid, serializer.encode!(build_leave_reply(message))
  {:stop, {:shutdown, :left}, socket}
end

We also recommend all channels to monitor the transport_pid on init and exit if the transport exits. We also advise to rewrite :normal exit reasons (usually due to the socket being closed) to the {:shutdown, :closed} to guarantee links are broken on the channel exit (as a :normal exit does not break links):

def handle_info({:DOWN, _, _, transport_pid, reason}, %{transport_pid: transport_pid} = socket) do
  reason = if reason == :normal, do: {:shutdown, :closed}, else: reason
  {:stop, reason, socket}
end

Any process exit is treated as an error by the socket layer unless a {:socket_close, pid, reason} message is sent to the socket before shutdown.

Custom channel implementations cannot be tested with Phoenix.ChannelTest and are currently considered experimental. The underlying API may be changed at any moment.

Note: in future Phoenix versions we will require custom channels to provide a custom child_spec/1 function instead of start_link/1. Since the default behaviour of child_spec/1 is to invoke start_link/1, this behaviour should be backwards compatible in almost all cases.

Link to this section Summary

Functions

Adds a key/value pair to socket assigns

Defines a channel matching the given topic and transports

Callbacks

Receives the socket params and authenticates the connection

Identifies the socket connection

Link to this section Types

Link to this type t() View Source
t() :: %Phoenix.Socket{
  assigns: map(),
  channel: atom(),
  channel_pid: pid(),
  endpoint: atom(),
  handler: atom(),
  id: nil,
  join_ref: term(),
  joined: boolean(),
  private: %{},
  pubsub_server: atom(),
  ref: term(),
  serializer: atom(),
  topic: String.t(),
  transport: atom(),
  transport_pid: pid()
}

Link to this section Functions

Link to this function assign(socket, key, value) View Source

Adds a key/value pair to socket assigns.

Examples

iex> socket.assigns[:token]
nil
iex> socket = assign(socket, :token, "bar")
iex> socket.assigns[:token]
"bar"
Link to this macro channel(topic_pattern, module, opts \\ []) View Source (macro)

Defines a channel matching the given topic and transports.

  • topic_pattern - The string pattern, for example "room:*", "users:*", or "system"
  • module - The channel module handler, for example MyApp.RoomChannel
  • opts - The optional list of options, see below

Options

  • :assigns - the map of socket assigns to merge into the socket on join

Examples

channel "topic1:*", MyChannel

Topic Patterns

The channel macro accepts topic patterns in two flavors. A splat (the * character) argument can be provided as the last character to indicate a "topic:subtopic" match. If a plain string is provided, only that topic will match the channel handler. Most use-cases will use the "topic:*" pattern to allow more versatile topic scoping.

See Phoenix.Channel for more information

Link to this section Callbacks

Link to this callback connect(params, arg1) View Source (optional)
connect(params :: map(), Phoenix.Socket.t()) ::
  {:ok, Phoenix.Socket.t()} | :error

Receives the socket params and authenticates the connection.

Socket params and assigns

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.

Link to this callback connect(params, arg1, connect_info) View Source (optional)
connect(params :: map(), Phoenix.Socket.t(), connect_info :: map()) ::
  {:ok, Phoenix.Socket.t()} | :error

Identifies the socket connection.

Socket IDs 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:

MyApp.Endpoint.broadcast("users_socket:" <> user.id, "disconnect", %{})

Returning nil makes this socket anonymous.