One of the regular questions we get at Fly is “Do you support WebSocket connections?”. The answer is “Yes”, but before you head off let us tell you a bit more and show you an example.
WebSockets are powerful things for creating interactive applications. Example Zero for WebSocket examples is the chat application. This leverages WebSockets’ ability to keep a connection alive over a long period of time while bidirectionally passing messages over it that ideally should be something conversational.
If you haven’t got an example app like that, we’ve got one here for you - flychat-ws in fly-examples. It’s been put together to use only raw WebSockets (and express for serving up pages) and no other libraries. (A shoutout to other libraries that build on WebSockets like socket.io).
Let’s get this application up on Fly first. Clone the repository and run:
npm install
to fill out the node_modules directory.
Assuming you have installed flyctl and signed up with Fly (head to the hands-on if you haven’t), the next step is to create an application:
flyctl init
Hit return to autogenerate a name and accept all the defaults. Now run:
flyctl deploy
And watch your new chat app deploy onto the Fly platform. When it’s done run:
flyctl open
And your browser will open with your new chat window. You’ll notice we did no special configuration or changes to the application to make it deploy. So let’s dive in and see what’s in there.
Down the WebSocket
The source for this application isn’t extraordinary. The only thing that should stand out is the fly.toml
file which was created when we ran fly apps create
:
Dockerfile fly.toml public
LICENSE node_modules server.js
README.md package-lock.json package.json
The fly.toml
file contains all the configuration information about how this app should be deployed; it looks like this:
app = "flychatting-ws"
[[services]]
internal_port = 8080
protocol = "tcp"
[services.concurrency]
hard_limit = 25
soft_limit = 20
[[services.ports]]
handlers = ["http"]
port = "80"
[[services.ports]]
handlers = ["tls", "http"]
port = "443"
[[services.tcp_checks]]
interval = 10000
timeout = 2000
And to paraphrase the contents, it says
- The application will take tcp connections on port 8080.
- An http service will be available to the outside would on port 80 (which will be sent to port 8080).
- An https service will also be available to the world on port 443 (and again traffic will be sent to port 8080).
- There’s a 25 connection hard limit before a new instance of the application is spun up.
- The 8080 internal port will be checked every 10 seconds (with a 2 second time out) for connectivity - if the app doesn’t respond it’ll be restarted.
This is all out-of-the-box Fly configuration.
Into the Server
The server.js
file is a whole 17 lines long but it does plenty in 17 lines:
const { createServer } = require('http');
const express = require('express');
const WebSocket=require('ws');
First it pulls in the packages needed, express
and ws
the WebSockets library.
Then it configures express to serve static files from the public
directory:
const app=express();
app.use(express.json({ extended: false}));
app.use(express.static('public'));
The public directory contains the web page and JavaScript for the chat application. Now we move on to starting up the servers. There are two to start up: the WebSocket Server and the Express server. But they need to know where to listen, so we’ll grab the port from the environment - or default to port 3000:
var port = process.env.PORT || 3000;
Now we can start the servers:
const server=new WebSocket.Server({ server:app.listen(port) });
Reading from the inside out, it starts the Express server with it listening on our selected port and then hands that server over to create a new WebSocket.Server
.
Now all we have to do is tell the code what to do with incoming connections:
server.on('connection', (socket) => {
socket.on('message', (msg) => {
server.clients.forEach( client => {
client.send(msg);
})
});
});
When a client connects, it’ll generate a connection event on the server. We grab the socket that connection came in and add an event handler for incoming messages to it. This handler takes any incoming message and sends it out to any connected client. We don’t even have to track which clients are connected in our simple chat. The WebSocket server maintains a list of connected clients so we can walk through that list.
And that’s the end of the server. Yes, there isn’t a lot there but it all works. It would be remiss of us at this point not to mention that we use a Dockerfile to assemble the image that’s run on Fly; here it is:
FROM node:current-alpine
WORKDIR /app
COPY package.json .
COPY package-lock.json .
RUN npm install --production
COPY . .
ENV PORT=8080
CMD [ "npm","start" ]
I say remiss because this is where we set the port number in the environment to match up with the port in the fly.toml
file from earlier. Oh, and we use npm start
as the command to start the server up because in package.json
we’ve made sure we remembered to set a script up:
"scripts": {
"start": "node server.js"
}
So, you can run npm start
to run the server locally (by default on port 3000), or you can build and run it locally using Docker:
docker build -t test-chat .
docker run -p 8080:8080 test-chat
Of course, in this case you’ve already deployed it to Fly with a single command. Let’s move on to the user-facing side of things.
Now for the Client
The client code is all in the public
directory. One HTML file lays out a simple form and calls some JavaScript on loading. That JavaScript is all in client.js
and that’s what we are going to look at now:
'use strict'
var socket = null;
function connect() {
var serverUrl;
var scheme = 'ws';
var location = document.location;
if (location.protocol === 'https:') {
scheme += 's';
}
serverUrl = `${scheme}://${location.hostname}:${location.port}`;
There’s a global socket because we only need one to connect to the server. This gets initialised in our connect call, and it’s here that the code touches on the fact it’ll be running on Fly. It looks up the URL it has been served from, and the port, and if served from an https: URL uses secure WebSockets (wss:
). If not, it’ll use ordinary WebSockets (ws:
).
Fly’s default configuration is to serve up internal port 8080 on external port 80 unsecured and 443 with TLS. That TLS traffic is terminated at the network edge so from the application’s point of view, it’s all traffic on one port and no need to do anything special to handle TLS. Pow, less code to write and manage. All you have to do is make sure you don’t try to do anything special for these connections.
Once we have the URL, we open the WebSocket saying we want to work with a “json” protocol. With the socket opened, let’s wire it up to receive messages:
socket.onmessage = event => {
const msg = JSON.parse(event.data)
$('#messages').append($('<li>').text(msg.name + ':' + msg.message))
window.scrollTo(0, document.body.scrollHeight);
}
This simply decodes the JSON into a message and pops it into our chat display. Most of the code is about doing the page manipulation. The last part of the connect process wires up the submit on a form where you type messages:
$('form').submit(sendMessage);
}
The last part of this is that sendMessage
function:
function sendMessage() {
name = $('#n').val();
if (name == '') {
return;
}
$('#n').prop('disabled', true);
$('#n').css('background', 'grey');
$('#n').css('color', 'white');
const msg = { type: 'message', name: name, message: $('#m').val() };
socket.send(JSON.stringify(msg));
$('#m').val('');
return false;
}
Which is mostly CSS manipulation and reading the name and message fields, and who wants to spend time on that? The important part for the sockets side of things is these two lines:
const msg = { type: 'message', name: name, message: $('#m').val() }
socket.send(JSON.stringify(msg))
Where a message is composed as a JSON object and then that JSON object is turned into a string and sent. The message will return soon enough as our server broadcasts to every client, including the one that originated the message. That means there’s really no need to update our messages view when we send. Score one for lazy coding.
Ready to Fly
So what’s this walk through the code shown us? Obviously that it’s incredibly easy to deploy an app to Fly, but also that Fly takes care of TLS connections so there’s less code for you to make. That it’s simple to make an Express app that also services sockets. That you can quickly test locally both as a native app and as a docker image. And to go remote it takes just one command to move it all onto Fly’s global infrastructure. That WebSockets simply work on Fly is just part of what Fly brings to the developers’ table.