Using WebSockets on Heroku with Python

Last Updated: 10 April 2014

flask gevent python websockets

Table of Contents

This quickstart will get you going with a Python application that uses a WebSocket, deployed to Heroku.

Sample code for the demo application is available on GitHub. Edits and enhancements are welcome. Just fork the repository, make your changes and send us a pull request.

Prerequisites

Create WebSocket app

The sample application provides a simple example of using WebSockets with Python. You can clone the sample and follow along with the code as you read.

Option 1. Clone the sample app

If you want to get going more quickly you can just clone the sample app:

$ git clone git@github.com:heroku-examples/python-websockets-chat.git
Cloning into 'python-websockets-chat'...
remote: Counting objects: 31, done.
remote: Compressing objects: 100% (24/24), done.
remote: Total 31 (delta 0), reused 31 (delta 0)
Receiving objects: 100% (31/31), 38.33 KiB | 0 bytes/s, done.

Option 2. Create a new Flask app.

$ mkdir python-websockets-chat
$ cd python-websockets-chat

Functionality

The sample application is a simple chat application that will open a WebSocket to the backend. Any time a chat message is sent from the browser, it’s sent to the server and then broadcasted to each connecting client and displayed on the page.

There are a few key pieces to this whole thing. We’re using Flask-Sockets that provides the standard WebSockets API. This allows us to build a simple Flask application that also responds to WebSocket requests. JavaScript on the browser opens a WebSocket connection to the server and responds to a WebSocket message being received from the server to display the chat message. Let’s take a look at both the backend and frontend in more detail.

Dependencies

First, we need to install our depdencies with Pip. To learn more about Pip on Heroku, see Python Dependencies via Pip.

$ pip install Flask Flask-Sockets Gunicorn redis
...
Cleaning up...

$ pip freeze > requirements.txt

Next, we’ll create a file called chat.py that includes our Flask imports and configurations:

# chat.py
import os
import logging
import redis
import gevent
from flask import Flask, render_template
from flask_sockets import Sockets

REDIS_URL = os.environ['REDISCLOUD_URL']
REDIS_CHAN = 'chat'

app = Flask(__name__)
app.debug = 'DEBUG' in os.environ

sockets = Sockets(app)
redis = redis.from_url(REDIS_URL)

Backend

Next, create a class called ChatBackend to register and push updates to all connected WebSockets clients. We’ll be using Redis to subscribe to updates from all users.

class ChatBackend(object):
    """Interface for registering and updating WebSocket clients."""

    def __init__(self):
        self.clients = list()
        self.pubsub = redis.pubsub()
        self.pubsub.subscribe(REDIS_CHAN)

    def __iter_data(self):
        for message in self.pubsub.listen():
            data = message.get('data')
            if message['type'] == 'message':
                app.logger.info(u'Sending message: {}'.format(data))
                yield data

    def register(self, client):
        """Register a WebSocket connection for Redis updates."""
        self.clients.append(client)

    def send(self, client, data):
        """Send given data to the registered client.
        Automatically discards invalid connections."""
        try:
            client.send(data)
        except Exception:
            self.clients.remove(client)

    def run(self):
        """Listens for new messages in Redis, and sends them to clients."""
        for data in self.__iter_data():
            for client in self.clients:
                gevent.spawn(self.send, client, data)

    def start(self):
        """Maintains Redis subscription in the background."""
        gevent.spawn(self.run)

This class allows us to subscribe once (per process) to incoming messages from Redis in the background, while updating each connected WebSocket client asyncronously:

chats = ChatBackend()
chats.start()

Next, let’s setup a standard URL endpoint:

@app.route('/')
def hello():
    return render_template('index.html')

Endpoints

With Flask-Sockets, we can create WebSocket endpoints, much like standard ones. In this case, we’ll have one endpoint for sending and one for receiving messages.

Flask-Sockets will automatically upgrade incoming requests to /submit and /receive to WebSocket connections. The first parameter to these functions (e.g. ws) is the raw websocket connection. Because we’re using Gevent, each process can have thousands of concurrent connections.

Our first endpoint is /submit. This WebSocket endpoint will be used for submitting new messages to the chat service. Incoming messages are received via ws.receive() as standard Python strings. We’ll take those messages and insert them into our Redis subscription channel, so all connected servers can receive updates.

@sockets.route('/submit')
def inbox(ws):
    """Receives incoming chat messages, inserts them into Redis."""
    while ws.socket is not None:
        # Sleep to prevent *contstant* context-switches.
        gevent.sleep()
        message = ws.receive()

        if message:
            app.logger.info(u'Inserting message: {}'.format(message))
            redis.publish(REDIS_CHAN, message)

Our second endpoint is /receive. This WebSocket endpoint will be used for receiving new messages from our Redis subscription channel, and sending them to the connected client. Because we only want one Redis connection per process, we’ll send our WebSocket instance to the ChatBackend discussed earlier. It’ll handle things from there, but we need to keep the socket open so it can receive updates. To do this, we’ll just run gevent.sleep() perpetually, to trigger a Gevent context switch.

@sockets.route('/receive')
def outbox(ws):
    """Sends outgoing chat messages, via `ChatBackend`."""
    chats.register(ws)

    while ws.socket is not None:
        # Context switch while `ChatBackend.start` is running in the background.
        gevent.sleep()

Now, when a new user connects to /receive, our ChatBackend instance will sleep, and new messages from our Redis channel will be pushed to the user’s webbrowser.

Because of this architectural model, you can run this application on as many Dynos as you want and all of your users will be able to receive updates.

See the full version of chat.py on GitHub.

Frontend

The second part of this is setting up the client side to actually open the WebSockets connection with the server. We’ll be using that / view we defined earlier.

@app.route('/')
def hello():
    return render_template('index.html')
<!-- templates/index.html -->
gt;
<html>
  <head>
    <title>Python Websockets Chat Demo</title>
    <link href="static/css/bootstrap.min.css" rel="stylesheet" media="screen">
    <link href="static/css/application.css" rel="stylesheet" media="screen">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
  </head>
  <body>
    <div class="container">
      <div class="jumbotron">
        <h1>Python Websockets Chat Demo</h1>
        <p>This is a Python WebSockets Chat Demo. The code lives <a href="https://github.com/heroku-examples/">on GitHub</a>. You can find the full guide <a href="https://devcenter.heroku.com/articles/python-websockets">on Heroku DevCenter</a>.</p>
      </div>
      <form id="input-form" class="form-inline">
        <div class="form-group">
          <input id="input-handle" type="text" class="form-control" placeholder="Enter handle" autofocus />
        </div>
        <div class="form-group">
          <input id="input-text" type="text" class="form-control" placeholder="Enter chat text here!" autofocus />
        </div>
        <button class="btn btn-primary" type="submit">Send</button>
      </form>
      <div class="page-header">
        <h1>Chat</h1>
      </div>
      <div id="chat-text">
      </div>
    </div>


    <script type="text/javascript" src="static/js/jquery-2.0.3.min.js"></script>
    <script type="text/javascript" src="static/js/reconnecting-websocket.min.js"></script>
    <script type="text/javascript" src="static/js/application.js"></script>
  </body>
</html>

The index page uses Bootstrap for CSS and jQuery JavaScript help. We’re storing all of our static assets in the static folder. Flask by default will serve static files in public/. We’ll need to get these files.

$ mkdir -p static static/css static/js
$ curl -o static/js/jquery-2.0.3.min.js http://code.jquery.com/jquery-2.0.3.min.js
$ curl -o static/js/reconnecting-websocket.min.js https://raw.github.com/joewalnes/reconnecting-websocket/master/reconnecting-websocket.min.js
$ curl -o static/css/bootstrap.min.css http://netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css

With a small CSS trick, we can get a scrolling chat window that contains all the messages by using the overflow attribute.

/* static/css/application.css */
#chat-text {
  overflow: auto;
  height: 500px;
}

Now back to WebSockets. Let’s write the static/js/application.js that gets loaded by the main page. First, let’s open our WebSocket connections to the server. We’ll be using reconnecting-websocket, which will automatically reconnect any disrupted connections in the browser.

// static/js/application.js
var inbox = new ReconnectingWebSocket("ws://"+ location.host + "/receive");
var outbox = new ReconnectingWebSocket("ws://"+ location.host + "/submit");

With an open WebSocket, the browser will receive messages and we’ll define a function to handle this. The messages we’re using will be a JSON response with two keys: handle (user’s handle) and text (user’s message). When the message is received, it needs to be parsed by JSON and then inserted as a new entry in the page. We’ll spruce it up with a bit of jQuery animate.

inbox.onmessage = function(message) {
  var data = JSON.parse(message.data);
  $("#chat-text").append("<div class='panel panel-default'><div class='panel-heading'>" + $('<span/>').text(data.handle).html() + "</div><div class='panel-body'>" + $('<span/>').text(data.text).html() + "</div></div>");
  $("#chat-text").stop().animate({
    scrollTop: $('#chat-text')[0].scrollHeight
  }, 800);
};

The now that the page can receive messages from the server, we need a way to actually send messages. We’re going to override how the submit button works in our input form. We’ll be using event.preventDefault() to stop the form from actually sending a POST to the form. Instead, we want to grab the the values from the form and send them as a JSON message over the WebSocket to the server.

$("#input-form").on("submit", function(event) {
  event.preventDefault();
  var handle = $("#input-handle")[0].value;
  var text   = $("#input-text")[0].value;
  outbox.send(JSON.stringify({ handle: handle, text: text }));
  $("#input-text")[0].value = "";
});

See the full version of application.js on GitHub.

Run locally

Next, we’ll create a Procfile, which declares our ‘web’ process:

web: gunicorn -k flask_sockets.worker chat:app

The Heroku Toolbelt comes with the foreman tool to assist us. We’ll want to specify a port to use.

Let’s launch the web app locally.

$ foreman start
11:33:12 web.1  | started with pid 3419
11:33:13 web.1  | 2013-10-04 11:33:13 [3419] [INFO] Starting gunicorn 18.0
11:33:13 web.1  | 2013-10-04 11:33:13 [3419] [INFO] Listening at: http://0.0.0.0:5000 (3419)
11:33:13 web.1  | 2013-10-04 11:33:13 [3419] [INFO] Using worker: flask_sockets.worker
11:33:13 web.1  | 2013-10-04 11:33:13 [3422] [INFO] Booting worker with pid: 3422

Deploy

It’s time to deploy your app to Heroku. If you haven’t done so already put your application into a git repository:

$ git init
$ git add .
$ git commit -m "Ready to deploy"

Create the Heroku app to deploy to:

$ heroku create
Creating limitless-ocean-5046... done, stack is cedar
http://limitless-ocean-5046.herokuapp.com/ | git@heroku.com:limitless-ocean-5046.git
Git remote heroku added

Add a Redis add-on service:

$ heroku addons:add rediscloud:20
Adding rediscloud:20 on limitless-ocean-5046... v3 (free)

While in beta, WebSocket functionality must be enabled via the Heroku Labs:

$ heroku labs:enable websockets
Enabling websockets for limitless-ocean-5046... done
WARNING: This feature is experimental and may change or be removed without notice.
For more information see: https://devcenter.heroku.com/articles/heroku-labs-websockets

Deploy your code with git push.

$ git push heroku master
Counting objects: 113, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (109/109), done.
Writing objects: 100% (113/113), 156.22 KiB, done.
Total 113 (delta 60), reused 0 (delta 0)

-----> Python app detected
-----> No runtime.txt provided; assuming python-2.7.4.
-----> Preparing Python runtime (python-2.7.4)
-----> Installing Distribute (0.6.36)
-----> Installing Pip (1.3.1)
-----> Installing dependencies using Pip (1.3.1)
       ...
       Successfully installed Flask Flask-Sockets Jinja2 MarkupSafe Werkzeug distribute gevent gevent-websocket greenlet gunicorn itsdangerous redis
       Cleaning up...
-----> Discovering process types
       Procfile declares types -> web

-----> Compiled slug size: 29.1MB
-----> Launching... done, v3
       http://intense-brook-6616.herokuapp.com deployed to Heroku

Congratulations! Your web app should now be up and running on Heroku.

Next Steps

Now that you have deployed your app and understand the very basics we’ll want to expand the app to be a bit more robust. This won’t encompass everything you’d want to do before going public, but it will get you along the a path where you’ll be thinking about the right things.

Using Redis locally, add to .env

To connect to Redis locally, add a .env file with your environment variables, including REDISCLOUD_URL. You can easily copy all of Heroku’s variables to a local .env file:

$ heroku config -s > .env

Foreman will automatically include these values when it is run.

Security

Remember, this is only a demo application and is likely vulnerable to various attacks. Please refer to WebSockets Security for more general guidelines on securing your WebSocket application.

WebSockets + TLS = wss://

The first easy thing we can do here is ensure that all of our production traffic is going through the wss:// protocol to encrypt our traffic similiar to https://. To do this, you can simply change the ws:// strings in to wss:// in application.js for secure requests.