Easy WebSockets with Flask and Gevent

Posted by
on under

This weekend I decided to take a short vacation from my book writing effort and spend time on a project I wanted to work on for a long time. The result of this effort is a brand new Flask extension that I think is pretty cool.

I'm happy to introduce Flask-SocketIO, a very easy to use extension that enables WebSocket communications in Flask applications.

What is WebSocket?

WebSocket is a new communication protocol introduced with HTML5, mainly to be implemented by web clients and servers, though it can also be implemented outside of the web.

Unlike HTTP connections, a WebSocket connection is a permanent, bi-directional communication channel between a client and the server, where either one can initiate an exchange. Once established, the connection remains available until one of the parties disconnects from it.

WebSocket connections are useful for games or web sites that need to display live information with very low latency. Before this protocol existed there were other much less efficient approaches to achieve the same result such as Comet.

The following web browsers support the WebSocket protocol:

  • Chrome 14
  • Safari 6
  • Firefox 6
  • Internet Explorer 10

What is SocketIO?

SocketIO is a cross-browser Javascript library that abstracts the client application from the actual transport protocol. For modern browsers the WebSocket protocol is used, but for older browsers that don't have WebSocket SocketIO emulates the connection using one of the older solutions, the best one available for each given client.

The important fact is that in all cases the application uses the same interface, the different transport mechanisms are abstracted behind a common API, so using SocketIO you can be pretty much sure that any browser out there will be able to connect to your application, and that for every browser the most efficient method available will be used.

What about Flask-Sockets?

A while ago Kenneth Reitz published Flask-Sockets, another extension for Flask that makes the use of WebSocket accessible to Flask applications.

The main difference between Flask-Sockets and Flask-SocketIO is that the former wraps the native WebSocket protocol (through the use of the gevent-websocket project), so it can only be used by the most modern browsers that have native support. Flask-SocketIO transparently downgrades itself for older browsers.

Another difference is that Flask-SocketIO implements the message passing protocol exposed by the SocketIO Javascript library. Flask-Sockets just implements the communication channel, what is sent on it is entirely up to the application.

Flask-SocketIO also creates an environment for event handlers that is close to that of regular view functions, including the creation of application and request contexts. There are some important exceptions to this explained in the documentation, however.

A Flask-SocketIO Server

Installation of Flask-SocketIO is very simple:

$ pip install flask-socketio

Below is an example Flask application that implements Flask-SocketIO:

from flask import Flask, render_template
from flask_socketio import SocketIO, emit

app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
socketio = SocketIO(app)

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

@socketio.on('my event')
def test_message(message):
    emit('my response', {'data': message['data']})

@socketio.on('my broadcast event')
def test_message(message):
    emit('my response', {'data': message['data']}, broadcast=True)

@socketio.on('connect')
def test_connect():
    emit('my response', {'data': 'Connected'})

@socketio.on('disconnect')
def test_disconnect():
    print('Client disconnected')

if __name__ == '__main__':
    socketio.run(app)

The extension is initialized in the usual way, but to simplify the start up of the server a custom run() method is used instead of flask run or app.run(). This method starts the eventlet or gevent servers if they are installed. Using gunicorn with the eventlet or gevent workers should also work. The run() method takes optional host and port arguments, but by default it will listen on localhost:5000 like Flask's development web server.

The only traditional route in this application is /, which serves index.html, a web document that contains the client implementation of this example.

To receive WebSocket messages from the client the application defines event handlers using the socketio.on decorator.

The first argument to the decorator is the event name. Event names 'connect', 'disconnect', 'message' and 'json' are special events generated by SocketIO. Any other event names are considered custom events.

The 'connect' and 'disconnect' events are self-explanatory. The 'message' event delivers a payload of type string, and the 'json' and custom events deliver a JSON payload, in the form of a Python dictionary.

To send events a Flask server can use the send() and emit() functions provided by Flask-SocketIO. The send() function sends a standard message of string or JSON type to the client. The emit() function sends a message under a custom application-defined event name.

Messages are sent to the connected client by default, but when including the broadcast=True optional argument all clients connected to the namespace receive the message.

A SocketIO Client

Ready to try your hand at some Javascript? The index.html page used by the example server contains a little client application that uses jQuery and SocketIO. The relevant code is shown below:

$(document).ready(function(){
    var socket = io();
    socket.on('my response', function(msg) {
        $('#log').append('<p>Received: ' + msg.data + '</p>');
    });
    $('form#emit').submit(function(event) {
        socket.emit('my event', {data: $('#emit_data').val()});
        return false;
    });
    $('form#broadcast').submit(function(event) {
        socket.emit('my broadcast event', {data: $('#broadcast_data').val()});
        return false;
    });
});

The socket variable is initialized with a SocketIO connection to the server. If the Socket.IO server is hosted at a different URL than the HTTP server, then you can pass a connection URL as an argument to io().

The socket.on() syntax is used in the client side to define an event handler. In this example a custom event with name 'my response' is handled by adding the data attribute of the message payload to the contents of a page element with id log. This element is defined in the HTML portion of the page.

The next two blocks override the behavior of two form submit buttons so that instead of submitting a form over HTTP they trigger the execution of a callback function.

For the form with id emit the submit handler emits a message to the server with name 'my event' that includes a JSON payload with a data attribute set to the value of the text field in that form.

The second form, with id broadcast does the same thing, but sends the data under a different event name called 'my broadcast event'.

If you now go back to the server code you can review the handlers for these two custom events. For 'my event' the server just echoes the payload back to the client in a message sent under event name 'my response', which is handled by showing the payload in the page. The event named 'my broadcast event' does something similar, but instead of echoing back to the client alone it broadcasts the message to all connected clients, also under the 'my response' event.

You can view the complete HTML document in the GitHub repository.

Running the Example

To run this example you first need to download the code from GitHub. For this you have two options:

The example application is in the example directory, so cd to it to begin.

To keep your global Python interpreter clean it is a good idea to make a virtual environment:

$ virtualenv venv
$ . venv/bin/activate

Then you need to install the dependencies:

(venv) $ pip install -r requirements.txt

Finally you can run the application:

(venv) $ python app.py

Now open your web browser and navigate to http://localhost:5000 and you will get a page with two forms as shown in the following screenshot:

Any text you submit from any of the two text fields will be sent to the server over the SocketIO connection, and the server will echo it back to the client, which will append the message to the "Receive" part of the page, where you can already see the message sent by the 'connect' event handler from the server.

Things get much more interesting if you connect a second browser to the application. In my case I'm testing this with Firefox and Chrome, but any two browsers that you run on your machine will do. If you prefer to access the server from multiple machines you can do that too, but you first need to change the start up command to socketio.run(app, host='0.0.0.0') so that the server listens on the public network interface.

With two or more clients when you submit a text from the form on the left only the client that submitted the message gets the echoed response. If you submit from the form on the right the server broadcasts the message to all connected clients, so all get the reply.

If a client disconnects (for example if you close the browser window) the server will detect it a few seconds later and send a disconnect event to the application. The console will print a message to that effect.

Final Words

For a more complete description of this extension please read the documentation. If you want to make improvements to it feel free to fork it and then submit a pull request.

I hope you make cool applications with this extension. I can tell you that I had a lot of fun implementing this extension.

If you make something with it feel free to post links in the comments below.

Miguel

Become a Patron!

Hello, and thank you for visiting my blog! If you enjoyed this article, please consider supporting my work on this blog on Patreon!

513 comments
  • #451 Paul Chaffee said

    Man you respond fast. Thanks!

    if I change the log.append line in the html file to
    $('#log').append('
    ' + $('<div/>').text('Received #' + msg.count + ': ' + msg.data).html());

    I see in the browser:
    [object Object]

  • #452 Paul Chaffee said

    Further checks: changing the tuples to lists results in same browser console output.

    List of lists works (can access the nested elements fine) but I really want to use dicts as the outer structure rather than lists.

  • #453 Miguel Grinberg said

    @Paul: use console.log() to dump the objects to the console and be able to inspect them.

  • #454 Paul Chaffee said

    Ah, ok. In the javascript console it shows that the content of msg.data is indeed the python dict that I'm sending from main.py (but no quotes around "player1" and "player2" i.e., they're not recognized as strings:
    [Log] msg.data is: – {player1: ["p1_card1.jpg", "p1_card2.jpg"], player2: ["p3_card1.jpg", "p2_card2.jpg"]}

    If I do this:
    console.log(msg.data['player1'])

    within the javascript function I described, it just breaks (doesn't do anything). So I just don't know what to do with this object on the js side to get ahold of the values.

  • #455 Paul Chaffee said

    Hi Again, what I've found is that within the js environment, an object that was sent from the server is for some reason different from one created within the js environment. I tried the following code. msg.data is the payload from the server and has the same structure as the dictionary item shown being created in the client. The js variable is called data_local.

    Entering this on the client side:
    socket.on('my_response', function(msg, cb) {
    $('#log').append('
    ' + $('<div/>').text('Received #' + msg.count + ': ' + msg.data).html());
    console.log('msg.data: ', msg.data);
    console.log('msg.data[\"player1\"]: ', msg.data["player1"]);
    console.log('msg.data.player1: ', msg.data.player1);
    //console.log('msg.data.player1[0]: ', msg.data.player1[0]);
    console.log('typeof msg.data: ', typeof msg.data)

                var data_local = {"player1": ["p1_card1.jpg", "p1_card2.jpg"], "player2": ["p2_card1.jpg", "p2_card2.jpg"]}
                console.log('data_local: ', data_local);
                console.log('data_local[\"player1\"]: ', data_local["player1"]);
                console.log('data_local.player1: ', data_local.player1);
                console.log('data_local.player1[0].cards: ', data_local.player1[0]);
                console.log('typeof data_local: ', typeof data_local)
    

    .,,});

    yields this in the browser console:

    [Log] msg.data: – {player1: ["p1_card1.jpg", "p1_card2.jpg"], player2: ["p2_card1.jpg", "p2_card2.jpg"]}
    [Log] msg.data["player1"]: – ["p1_card1.jpg", "p1_card2.jpg"] (2)
    [Log] msg.data.player1: – ["p1_card1.jpg", "p1_card2.jpg"] (2)
    [Log] typeof msg.data: – "object"
    [Log] data_local: – {player1: ["p1_card1.jpg", "p1_card2.jpg"], player2: ["p2_card1.jpg", "p2_card2.jpg"]}
    [Log] data_local["player1"]: – ["p1_card1.jpg", "p1_card2.jpg"] (2)
    [Log] data_local.player1: – ["p1_card1.jpg", "p1_card2.jpg"] (2)
    [Log] data_local.player1[0].cards: – "p1_card1.jpg"
    [Log] typeof data_local: – "object"

    The client side @socket.on decorator emits the same data structure from python as what I created in the js code for "data_local."

    So according to the log, js registers both objects (the local one and the one emitted by the server) as of type "object."

    Notice that the js script has no trouble accessing an array element from an array that is a value in a key-value pair.

    However! when I un-comment this line from the js script: //console.log('msg.data.player1[0]: ', msg.data.player1[0]);
    the function stops working. It doesn't seem to be able to recognize the received msg object as having an array as a value in a json/dictionary object sent from the server. It can clearly recognize unstructured string objects as values in key-value pairs, but I cannot access an element when the value is an array.

    What to do?

  • #456 Paul Chaffee said

    I'm so sorry, Miguel. The issue was that the js script was trying to parse other messages sent during the connecting process as dict or JSON objects. Sending a dict with a list as a value to the client works perfectly fine, i.e., just as it should. No problem at all accessing the elements of the inner list. 100% user error. Sorry for wasting your time, and thanks for the intial feedback. Using the console logs did allow me to solve it in the end.
    Cheers,
    Paul

  • #457 Miguel Grinberg said

    @Paul: The keys do not have quotes in JavaScript, but they are strings. Also what Python calls a dict is called an object in JS.

    You should be able to do msg.data.player1 or msg.data['player1'] unless you have some hidden character like maybe a leading or trailing space. What I would do in your case is to log msg or msg.data to the console, then once you have it printed there right-click it and select "Store as global variable". Now you have a variable that you can play with in the console to figure out how to get the data that you need. I honestly don't see a difference between the two objects, except for this data_local.player1[0].cards which seems wrong to me, since there is no cards attribute.

  • #458 Paul Chaffee said

    No, you're absolutely right. There is no difference at all. The whole issue was not related to the data type/structure I was sending from the server from that decorator. It was because the js function that was handling the server messages was also handling other messages that were just strings, and that was what was causing the error, and stopping execution. You're right: there is no difference between the local created object and the one sent by the server.

    Thanks for answering, and sorry for wasting your time on this. You helped me solve it. You're a saint for answering everybody's questions. Cheers.

  • #459 Payero said

    Miguel,
    First, thanks for such a great explanation and demonstration on how to use flask socketio. I am having errors when the server takes a long time to send a response the connection gets reset. The error message is the same as the ones described in Issue 248 (https://github.com/miguelgrinberg/Flask-SocketIO/issues/248). The easiest way to reproduce it is by taking this example code and add a time.sleep(30) before sending a response back to the client:

    @socketio.on('my event', namespace='/test')
    def test_message(message):
    time.sleep(30)
    emit('my response', {'data': message['data']})

    Even though your code uses ping/pong and has a background thread sending a message every 10 seconds I still get a disconnection and the last message I send after a long pause is never received by the client. Is there a way to prevent that?

    Thanks,

  • #460 Miguel Grinberg said

    @Payero: are you using eventlet or gevent? The time.sleep() function is incompatible with these frameworks. Use socketio.sleep() instead and then your server will not block.

  • #461 Chirag Soni said

    Hello, Miguel

    I used FlaskSocketIo to implement a Realtime Shopping Cart for all sessions(users) like if a person makes a change then it will be updated to all the active sessions. it works for a few requests(updates) but when I sending multiple requests in a small interval of time this will be fluctuating on requests.

    So can you please tell me is there a way to make a queue of requests and handle them one by one.

  • #462 Miguel Grinberg said

    @Chirag: what do you mean by "fluctuating on requests"?

  • #463 Shahrukh said

    Hi Miguel,

    How do I run socketio from Application Factory pattern like the one in Mega Tutorial? I tried declaring it in the create_app function but like socketio.init_app(app) but couldn't establish a connection.

  • #464 Shahrukh said

    Ignore my last question – I was able to figure it out. My next question is that though this is a WebSocket & not HTTP, but in my Terminal, I still see GET, POST and HTTP/1.1 200 response – any idea how can one know if it's Websocket vs. long HTTP polling? Thanks!

  • #465 Shahrukh said

    Following up on my old question – I loaded the page in Firefox, and tried to see WebSocket connection in "WS" but it was empty – https://hacks.mozilla.org/2019/10/firefoxs-new-websocket-inspector/
    Thoughts?

  • #466 Miguel Grinberg said

    @Shahrukh: Socket.IO uses both HTTP and WebSocket. If you didn't install any web server that supports WebSocket, then your application will only work over HTTP long-polling. Check out the docs for more info.

  • #467 Shahrukh said

    Thanks Miguel. Is there a way I can stimulate that in local environment using Flask alone? Or does it have to be installed on a real web server? I'm trying to learn and play with Web sockets but unsure about the setup. Thanks again!

  • #468 Matias said

    Hello Miguel, first of all thanks for such a good project, it works wonderful. I have a query I do not know what is the best way to do it and your insurance will guide me. I have the following code:

    init.py

    async_mode = "gevent"
    socketio = SocketIO()
    
    def create_app(main=True):
        app = Flask(__name__)
        app.config.from_object("config.ProductionConfig")
    
        var1 = app.config['VAR1']
    
        if main:
            socketio.init_app(app, async_mode=async_mode)
        else:
            socketio.init_app(None, async_mode=async_mode)
    
        from .public import public_bp
        app.register_blueprint(public_bp)
    
        return app
    

    And when I want to access in public.routes it is generated:
    NameError: name 'var1' is not defined

    The only way it works is if I use in create_app ( app.var1 = app.config['VAR1'] ), and in public/routes use current_app.var1 but I don't think it is a correct way to do it, using flask-session works for this case? What other possibilities do I have? I use gunicorn gevent with the websocket class. Thank you!

  • #469 Miguel Grinberg said

    @Shahrukh: Did you review the docs? You just need to install one of the websocket web servers that are supported.

  • #470 Miguel Grinberg said

    @Matias: I'm confused. In your example, var1 is a local variable. How is it supposed to work outside of the create_app() function? Use current_app.config['VAR1'] to get this value.

  • #471 Matias said

    Sorry for my mistake, I was trying to get the value of var1 out of a function and an error was generated since I used the factory pattern. Everything works fine now. Thanks Miguel!

  • #472 yasmin said

    is there any way to use this to solve 30 seconds timeout( due to a simpe sqlalchemy query ) on heroku , on flask project based no your mega tutorial?

  • #473 Miguel Grinberg said

    @yasmin: I don't see how this can be related to your database and/or timeout issues, no.

  • #474 Nirmal said

    Hi Miguel,

    Is there a way to have 1-to-1 communication with each client? For example, the conversation between a client and server should not be visible to other clients. How can I achieve this?

    Thanks in advance!

  • #475 Miguel Grinberg said

    @Nirmal: this is shown in the article. If you don't say otherwise, your emit() call is directed to the originating user only.

Leave a Comment