Channels in Phoenix

P. Madhanasekaran

A Channel in Phoenix is an implementation of WebSocket, the protocol laid down by Internet Engineering Task Force (IETF) for real-time apps. Chris McCord the creator of Phoenix was earlier a Rails programmer. He found the existing tool in Rails, the Event-Machine, to be inadequate to handle the demands of soft real-time apps. So he decided to create Channel/ WebSocket implementation in Elixir to handle the challenges of real-time web, as Elixir has the syntax and tools like Ruby but runs on fault-tolerant and distributed Erlang VM. It is said that Phoenix channels can handle two million web-socket connections on a single server. For those who are more interested in learning how to use “channels” in Phoenix, the blog “Building a chat application using Elixir and Phoenix” by Nithin Bekal is suggested and you can download the sample furnished by him in the link given in the blog and run it for a better understanding. This is actually a simplified version of the chat app by the creator of Phoenix, which is available on https://github.com/phoenixframework/phoenix).That can also be downloaded and run. These samples use ES6, the JavaScript standard which is supported in all modern JavaScript frameworks and Browsers. Hence these are more advanced samples. There is another blog by Sheharyar Nasser, “Building a Simple Chat App With Elixir and Phoenix”.  This follows the guide on Channels in the Phoenix home page. The link to a sample is also available in the blog. Unlike the other samples which use ES6, only JavaScript is used here and it requires only minimal changes to the code generated by mix phoenix.new task.

WebSocket:

 The aim of WebSocket is “to provide a two way bi-directional fully duplex” communication channel. WebSocket uses “HTTP” only for initial handshake and thereafter the lower level “TCP” connection is used. In HTTP the server sends the response, only if there is a request from a client On the other hand, the WebSocket connection is fully-duplex as either server or client can push data whenever it is available, without waiting for any request from the other side. Because of this, the size of a message also gets greatly reduced. Web-socket improves scalability; that is more clients can access the application. This is the result of   

1) reduced network traffic for sending a message.
2) lower latency -that is the time taken to send message.  

In short WebSocket is a protocol and a JavaScript API and the protocol is a very low level, full-duplex protocol and messages can be sent in both directions simultaneously. There are server implementations for WebSocket in many languages like Java, Ruby, Python, C#, JavaScript etc but the client has to use only the JavaScript API for coding various events and event-handlers. As they are using a transport layer/ lower level protocol, only streams and messages are passed and no cookies or headers is required.

Web-socket implementations in JavaScript

In earlier articles, we saw the JavaScript implementations of WebSocket- Socket.io and Sockjs with Node.js. In fact that the WebSocket implementations available are so many that a Node.js programmer may find it difficult to choose one from them.  Phoenix implementation appears to be influenced by these libraries. These libraries are actually called “WebSocket emulation” libraries. These libraries use Web-socket in newer Browsers and a fallback mechanism like “Comet”, ”Polling” or “long Polling” in older Browsers, that do not support WebSocket. The goal, of these libraries, is to let applications use a WebSocket API but fall back to non-WebSocket alternatives when necessary at runtime, i.e. without the need to change application code. The emulation libraries have components for both the server and the client. The components will be used to code communication on both the sides. Events emitted on a socket channel on one side will be handled by the corresponding event handler on the other side. Both the sides can send messages or attach handlers to process the incoming messages.

 

When you use Phoenix, the Client request goes to the Endpoint.

  1.  The initial HTTP request goes through the plug Router to Router Module .It is forwarded by a route there, to a mapped controller action. That is here index() action in PageController and it displays index.html.eex template.
  2. Through Index template the client asks for a socket connection, and it goes to the Socket Module specified in socket-path. This socket module has routes which map Channels with “addresses”  to particular Channel Modules.   The client creates a channel specifying an address and calls the join() in it. Then the join() in the Channel Module mapped to that address is invoked . When the client pushes a message-event  in the channel, the handle_in() in the module  handles  the event.

Sample
Create a new phoenix app executing the following command:

mix phoenix.new chat - -no-ecto

#ecto is an ORM like library which we don’t need for this sample In a CRUD app we will see its use later
After creating the necessary files mix will ask “Fetch and install dependencies ? [Yn].If you enter “y” the dependencies will be downloaded. You can execute the commands

cd  chat
mix phoenix.server.

If you see the message “Running Chat.Endpoint with Cowboy using http://localhost:4000 .In the browser, if you navigate to that address you will see a Welcome message. As pointed out in the earlier article, the route ‘ get "/", PageController, :index’ forwarded the request to index action in the PageController and it was the PageView that was responsible for rendering the page from the “/web/templates/page/index.html.eex” and “/web/templates/layout/app.html.eex”. You can see that the environment is working.  Now you can make necessary changes to the generated code.

Templates:
To suit our needs the generated template files have to be modified.
The “web/templates/index.html.eex”  may be modified to get input from the user as under:

/web/templates/page/index.html.eex

<div    id="message-list"></div>
      <br/>
      <div class="col-md-2 form-group">
      <label>Username</label>
      <input id="username"    type="text" class="form-control" />
      </div> 
      <div    class="col-md-6 form-group">
      <label>Message</label>
      <input id="message"    type="text" class="form-control" />
    </div> 

As mentioned earlier, the client has to use WebSocket JavaScript API to make necessary actions like opening and closing socket connections, sending and receiving messages. To get input from the use for the message to be sent or to display the message received from the server, we can use JQuery, the library we have seen earlier. So in the /web/templates/layout/app.html.eex we can  add

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>

before
<script src="<%= static_path(@conn, "/js/app.js") %>"></script>.
Another option is to download the JQuery file and put it in the /web/static/vendor folder. Any other client-side JavaScript framework or plain JavaScript can be used in place of JQuery.

Client code:
The generated project structure has the following files for writing the client code in JavaScript in the directory /web/static/js. 1)app.js 2) socket.js

The code generated for /web/static/js/app.js  has only an “import” statement with a number of comments.

import "phoenix_html"


Uncomment the line
import socket from "./socket" # to enable use of socket exported from the file socket.js which we will see next

/web/static/js/socket.js
// NOTE: The contents of this file will only be executed if
// you uncomment its entry in "web/static/js/app.js".
// To use Phoenix channels, the first step is to import Socket // and connect at the socket path in "lib/chat/endpoint.ex":

import {Socket} from "phoenix" let socket = new Socket("/socket", {params: {}}) //-------1) socket.connect()
// Now that you are connected, you can join channels with a topic:
let channel = socket.channel("room:lobby", {}); //-----2) let list    = $('#message-list'); //’message-list’ –id put in a variable let message = $('#message');//’message’-id  put in another variable let username    = $('#username');
//  event listener for ‘keypress’ event and  on hitting
enter key input received is pushed into the channel in the message body as “new:message” event

message.on('keypress', event => { if (event.keyCode == 13) {  console.log(event) channel.push('new:message', { username: username.val(), message: message.val() });
message.val('');
} });

// event-listener for ‘new_message ‘ event. on() event-listener  
triggered when the server broadcasts ‘new_message’ event

channel.on('new_message', msg => {    //---3) list.append(`<b>${msg.username || 'Anonymous'}:</b> ${msg.message}<br>`); list.prop({scrollTop: list.prop("scrollHeight")}); });
channel.join() .receive("ok", resp => { console.log("Joined successfully", resp) }) //----4) .receive("error", resp => { console.log("Unable to join", resp) }) export default socket
  1. Connects to the server using the `Socket` class .A single connection is established to the server and channels are mulitplexed over the connection
  2. Topics are string identifiers - names that the various layers use in order to make sure messages end up in the right place. Topics can use wildcards. This allows for a useful "topic:subtopic" convention.
  3. The client listens for the "new_msg" event using channel.on, and then append the message body to the DOM.
  4. For each channel, you can bind to ‘ok’ and ‘error’  events  to monitor whether the channel join has succeeded or not.

 Next we can handle the incoming and outgoing events on the server

Server-side code

The extract from Endpoint module is given below

Extract from /lib/Chat/endpoint.ex
defmodule Chat.Endpoint do
 
use Phoenix.Endpoint, otp_app: :chat

 
socket "/socket", Chat.UserSocket //------1)
Specifies the socket-handler which handles the socket connection. 
The socket from the client in /web/static/js/socket.js has to connect to this.

A single socket connection can be used by a number of channels and the routes to the channels have to be defined in the socket-handler.

In the code generated for /web/channels/user_socket.ex,  the socket- handler, you can see the following comment.

# channel "room:*", Chat.RoomChannel.

Uncomment it.
It is actually a channel route. Unlike the HTTP routes which are defined in /web/routes.ex, the channel routes are defined in the socket-handler. A channel route matches on the “topic” string and dispatches matching requests to the given Channel module. The star character * acts as a wildcard matcher. The generated code for the socket handler may look as under:

/web/channels/user_socket.ex
defmodule Chat.UserSocket do
 
use Phoenix.Socket

  channel "room:*", Chat.RoomChannel #---1)
  transport :websocket, Phoenix.Transports.WebSocket #---2)
# transport :longpoll, Phoenix.Transports.LongPoll  #----3)
  .......

  def connect(_params, socket) do
   
{:ok, socket}  //------4)
  end

 
def id(_socket), do: nil
end
  1. Channel route, that makes sure messages are get routed to the correct channel. Whenever a client sends a message whose topic starts with "rooms:", it will be routed to our RoomChannel.
  2. Look at the transport protocols used.
  3. This should be un-commented, for older browsers that do not support web-socket.
  4. The connect() function returns the socket  in a tuple with Ok message. In other words, to authorize the socket to join a topic, we return {:ok, socket}

Channel Module  

HTTP requests are handled in a controller action ; similarly  a channel message is handled in a Channel Module event-handler. Channels are isolated, concurrent processes on the server that subscribe to topics and broker events between the client and server. To join a channel, you must provide the topic. The channel is joined with “ok”. Then the server channel process listens for "new:msg" events and handles it.

We can code a new ex file for the Channel Module for managing our chat room messages as under:

/web/channels/room_channel.ex
defmodule Chat.RoomChannel do
use Phoenix.Channel

 

  def join("room:lobby", message, socket) do #------1) {:ok, socket} end
  def handle_in("new:message", msg, socket) do #--------2) broadcast! socket, "new_message", msg  #---3) & 4) {:noreply, socket} end end 

Each Channel has to implement one or more clauses of each of the call-back functions - join/3, terminate/2, handle_in/3, and handle_out/3.

  1. The channel. join() in web/static/js/socket.js, we saw earlier, triggers join() in the  Channel Module, where the channel address matches. With our channel in place, we can get the client and server talking. We just need to set our room name to "rooms:lobby. In other words, for our chat app, we'll allow anyone to join the "rooms:lobby" topic. ". The socket is returned by the server with “ok” message.
  2. This event-handler handles the message, if the event-name matches.
  3. The message is broadcast in the socket. The ‘broadcast’ enables more than one connected client to receive the message.
  4. Actually the message received is broadcast as ‘msg’ with the event-name new_message. This triggers channel.on() event in the joined client channels. The client listens for new_ message and appends it to the message-list container.

       In short, we handle incoming events with handle_in/3. We pattern match on the event name  "new:msg", and then grab the payload that the client passed over the channel. For our chat application, we simply notify all other rooms:lobby subscribers of the new message with broadcast!/3.

Run the app
Execute

mix phoenix.server

You can see the message “Running Chat.Endpoint with Cowboy using http://localhost:4000”.Click F12 and click on Console. You can see the message “Joined successfully “   from line 76 of the file “socket.js”. Our client and server are now talking over a persistent connection. You can open more than one browser and more than one tab in a browser and navigate to the address. Enter some data and may see the result as in Figures- 1, 2 & 3.You can have a look at the JavaScript console also for the events generated.

Summing up:

Have a look at the following diagram. It depicts the data-flow from client to server when there is socket connection. Both the client library and the server use similar methods/functions.

Both the client and the socket-handler in the server use the “socket” provided by phoenix-socket library. The Client uses socket methods connect() and channel() respectively to connect to socket-handler and obtain a channel object from the connection. Once connection is established, the connect() in user socket module sends back the socket with :Ok atom. The Client can obtain different channels passing different string parameters (topic:sub-topic pattern)  as arguments to socket.channel(). The channel route in the socket handler maps the channels to ChannelModule. Thereafter if the client invokes any channel- event, a corresponding event-handler in the ChannelModule will handle the same. For example if the client invokes channel.join() , the join() in the Channel Module sends back the socket with :OK atom, if the join succeeds. When the client invokes channel.push() to send a named message-event in the joined Channel, it will be handled by the handle_in() event-handler in the Channel Module, if the name of the message- event  sent by  the push() and handled by handle_in() in the Channel Module is the same. The handle_in()  event-handler will broadcast the message received and it  can trigger on() event-handler  in all the joined channels-  of-course the event-names should match.

Conclusion:

Web-socket is meant for real-time web apps. HTTP protocol is used to establish initial connection between a client and server. The TCP connection used by HTTP to establish the initial handshake becomes permanent and either the client or server can push messages in it. Though the web-socket implementations on the server can be in different languages, on the client, only JavaScript API is used by different client libraries. The implementation in Phoenix is fault-tolerant and scalable and performing well,   as it runs Erlang VM. Though all WebSocket implementations can handle more connections because of small payload and lesser latency, Phoenix channels handle about 2 million web-socket connections on a single machine. But the disadvantage is that both the language Elixir and the framework Phoenix are relatively a new and seeing new releases in short intervals. Phoenix uses brunch/Node.js for asset management. Sometimes, there may be some difficulties in finding out the correct version of Node.js for a particular phoenix version and setting up the development environment. But the plus point is that the number of companies using Phoenix is on the rise and the programmers who adopt Phoenix early may be benefitted from this. If you invest your time and efforts in Phoenix – a combination of productive tools, as in Rails, and the scalability, concurrency and the fault-tolerance provided by time-tested Erlang VM, it will yield you good returns.

Figure-1

Figure-2

Figure 3









}