It has been a really long while since my last Elixir post. I have been doing a lot of research and learning for my upcoming book.
Introducing Phoenix & Websockets example application
We’re going to learn how to create an application that uses websockets. This is how it looks like:
This is taken off Cowboy’s Websocket Example. This example application demonstrates bi-directional communication between the server (the phoenix application that you would be building later) and the client (your browser).
The source for the application can be found on github.
Misadventures with Websockets
Note: You can skip all the complaining and head to the next section, “Enter the Phoenix”. You would however miss the effort it took for me to get a workable websockets example.
I needed websockets as part of the example application for the book. Unfortunately, I hit into several stumbling blocks.
Here is a non-exhaustive list of the difficulties I ran into:
1. Dynamo
This was the first web framework I turned to, since I spent the most time with. There is a websocket example provided.
Unfortunately, as I realized later, simply copying and pasting the code in a dynamo project (that is, one created with mix dynamo my_project
) wasn’t going to cut it.
Some helpful folks on #elixir-lang
pointed out that I had to add some other settings. However by then I was to fed up to try any further. It took me too long to realise that a single file Dynamo (the one reflected in the examples) worked differently from a Dynamo project.
2. Cowboy
This time, I tried to go au naturel, and tried using Cowboy. Playing with :dispatch
was too much for me. Gave up after a couple of hours.
3. Online Resources
Either my google-fu took a vacation, or there was a serious lack of documentation, or even source code examples. This obviously should not come as a surprise, but I was really grumpy for not being able to find any working code.
Enter the Phoenix
Enough complaining. Let’s make sure we’re on the same Elixir version:
% elixir -v
Elixir 0.12.4-dev
Installation
Head over to https://github.com/phoenixframework/phoenix and install Phoenix.
Or just copy paste:
git clone https://github.com/phoenixframework/phoenix.git
cd phoenix
mix do deps.get, compile
Now that you are in the phoenix
directory, go ahead and create the example application:
Note: Create the application outside the phoenix
directory.
I’ll just create an app on my ~/Desktop
:
% mix phoenix.new so_much_websockets ~/Desktop
Navigate to the so_much_sockets
directory:
% cd ~/Desktop/so_much_sockets
Fetch the required dependencies:
% mix deps.get
Folder Structure
Whenever I play around with web frameworks, I like to see the folder structure, just to get a feel:
% tree
.
├── README.md
├── lib
│ ├── so_much_websockets
│ │ ├── controllers
│ │ │ └── pages.ex
│ │ ├── router.ex
│ │ └── supervisor.ex
│ └── so_much_websockets.ex
├── mix.exs
└── test
├── so_much_websockets_test.exs
└── test_helper.exs
4 directories, 8 files
At this point of writing, Phoenix has some slight incompatiblities with one of the dependencies.
Open up mix.exs
and have deps/0
look like this:
defp deps do
[
{ :phoenix, github: "chrismccord/phoenix" },
{ :plug, git: "https://github.com/elixir-lang/plug.git", tag: "v0.2.0", override: true },
]
end
After that, run mix deps.get
again.
Hello, Phoenix!
Let’s run our app:
% iex -S mix
iex(1)> SoMuchWebsockets.Router.start
Running Elixir.SoMuchWebsockets.Router with Cowboy with [port: 4000]
Then navigate to http://localhost:4000 to see the universal programmer greeting.
Give yourself a pat on the back.
Where did our response come from?
1. router.ex
Examine the router:
defmodule SoMuchWebsockets.Router do
use Phoenix.Router, port: 4000
get "/", SoMuchWebsockets.Controllers.Pages, :index, as: :page
end
Should be self explantory. Not let’s follow the trail and look at what’s in
SoMuchWebsockets.Controllers.Pages
.
2. SoMuchWebsockets.Controllers.Pages
defmodule SoMuchWebsockets.Controllers.Pages do
use Phoenix.Controller
def index(conn) do
text conn, "Hello world"
end
end
Bingo! Let’s replace Hello world
with some html content. Because I’m extremely lazy, let’s just do this:
defmodule SoMuchWebsockets.Controllers.Pages do
use Phoenix.Controller
def index(conn) do
html conn, """
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Websocket client</title>
<script src="/javascript/jquery.min.js"></script>
<script type="text/javascript">
var websocket;
$(document).ready(init);
function init() {
if(!("WebSocket" in window)){
$('#status').append('<p><span style="color: red;">websockets are not supported </span></p>');
$("#navigation").hide();
} else {
$('#status').append('<p><span style="color: green;">websockets are supported </span></p>');
connect();
};
$("#connected").hide();
$("#content").hide();
};
function connect()
{
wsHost = $("#server").val()
websocket = new WebSocket(wsHost);
showScreen('<b>Connecting to: ' + wsHost + '</b>');
websocket.onopen = function(evt) { onOpen(evt) };
websocket.onclose = function(evt) { onClose(evt) };
websocket.onmessage = function(evt) { onMessage(evt) };
websocket.onerror = function(evt) { onError(evt) };
};
function disconnect() {
websocket.close();
};
function toggle_connection(){
if(websocket.readyState == websocket.OPEN){
disconnect();
} else {
connect();
};
};
function sendTxt() {
if(websocket.readyState == websocket.OPEN){
txt = $("#send_txt").val();
websocket.send(txt);
showScreen('sending: ' + txt);
} else {
showScreen('websocket is not connected');
};
};
function onOpen(evt) {
showScreen('<span style="color: green;">CONNECTED </span>');
$("#connected").fadeIn('slow');
$("#content").fadeIn('slow');
};
function onClose(evt) {
showScreen('<span style="color: red;">DISCONNECTED </span>');
};
function onMessage(evt) {
showScreen('<span style="color: blue;">RESPONSE: ' + evt.data+ '</span>');
};
function onError(evt) {
showScreen('<span style="color: red;">ERROR: ' + evt.data+ '</span>');
};
function showScreen(txt) {
$('#output').prepend('<p>' + txt + '</p>');
};
function clearScreen()
{
$('#output').html("");
};
</script>
</head>
<body>
<div id="header">
<h1>Websocket client</h1>
<div id="status"></div>
</div>
<div id="navigation">
<p id="connecting">
<input type='text' id="server" value="ws://localhost:4000/ws"></input>
<button type="button" onclick="toggle_connection()">connection</button>
</p>
<div id="connected">
<p>
<input type='text' id="send_txt" value=></input>
<button type="button" onclick="sendTxt();">send</button>
</p>
</div>
<div id="content">
<button id="clear" onclick="clearScreen()" >Clear text</button>
<div id="output"></div>
</div>
</div>
</body>
</html>
"""
end
end
You need to then exit the console, and run SoMuchWebsockets.Router.start
again. (I’m pretty sure there’s a much better way to do this, but bear with me first.) When you refresh, you should see something similar to the screenshot at the very beginning of this post.
Obviously, this will not work yet, because we have yet to implement the main meat of our application - The Websocket Handler.
Implementing the Websocket Handler
1. Create websocket_handler
in lib/so_much_websockets
:
defmodule SoMuchWebsockets.WebSocketHandler do
@behaviour :cowboy_websocket_handler
def init({:tcp, :http}, _req, _opts) do
{:upgrade, :protocol, :cowboy_websocket}
end
def websocket_init(_transport_name, req, _opts) do
:erlang.start_timer(1000, self, "Hello!")
{:ok, req, :undefined_state}
end
def websocket_handle({:text, msg}, req, state) do
{:reply, {:text, "That's what she said! #{msg}"}, req, state}
end
def websocket_info({:timeout, _ref, msg}, req, state) do
:erlang.start_timer(1000, self, "How' you doin'?")
{:reply, {:text, msg}, req, state}
end
def websocket_info(_info, req, state) do
{:ok, req, state}
end
def websocket_terminate(_reason, _req, _state) do
:ok
end
end
I’ll leave you to find out what websocket_handle
and websocket_info
do.
TL;DR: The former handles messages from the browser, and the latter handles messages to the server.
You can experiment with this once the application is completed.
2. Add routes to handle the websockets
Open router.ex
and make it look like this:
defmodule SoMuchWebsockets.Router do
use Phoenix.Router,
port: 4000,
dispatch: [
{ :_, [
{"/stylesheets/[...]", :cowboy_static, {:dir, "priv/static/stylesheets"}},
{"/javascript/[...]", :cowboy_static, {:dir, "priv/static/javascript"}},
{"/ws", SoMuchWebsockets.WebSocketHandler, [] },
{:_, Plug.Adapters.Cowboy.Handler, { __MODULE__, [] }}
]}
]
get "/", SoMuchWebsockets.Controllers.Pages, :index, as: :page
end
3. Add jQuery
First, create a priv
directory, and create a static
folder. Now in that static
folder, create 2 more directories: javascript
and stylesheets
.
In the javascript
folder, download jquery.min.js
. You can get a copy from here.
This is how your directory structure should look like:
├── lib
│ ├── so_much_websockets
│ │ ├── controllers
│ │ │ └── pages.ex
│ │ ├── router.ex
│ │ ├── supervisor.ex
│ │ └── websocket_handler.ex
│ └── so_much_websockets.ex
├── mix.exs
├── mix.lock
├── priv
│ └── static
│ ├── javascript
│ │ └── jquery.min.js
│ └── stylesheets
└── test
├── so_much_websockets_test.exs
└── test_helper.exs
Paydirt!
Go to the console, and restart the router again, and refresh your browser. You should finally see some text scrolling by.
Give yourself another pat in the back. You totally deserve this.
I’m just not that smart
Please do not take what I said above as slamming Dynamo or Cowboy. Far from it.
The point is I wasn’t too smart in figuring out how to get websockets working on both Dynamo / Cowboy, and just somehow managed to get it working on Phoenix after some tinkering and help from its creators.
Hopefully, this helps some poor soul struggling with Websockets.
I feel you.