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!

498 comments
  • #476 Peter said

    Hi,
    I don't know if you look at this any more but there goes>

    I'm try to write a garage door automation on a RPI Zero but I am have problems with the websocket part that returns the state of the door (open/Closed). I keep get the following when I start the server.
    (venv) pi@garage-pi:~/HomeAuto/garage-pi-r $ flask run --host=0.0.0.0 --debugger
    * Environment: production
    WARNING: Do not use the development server in a production environment.
    Use a production WSGI server instead.
    * Debug mode: off

    MAIN
    run the Socket
    WebSocket transport not available. Install eventlet or gevent and gevent-websocket for improved performance.
    /home/pi/HomeAuto/garage-pi/venv/lib/python3.7/site-packages/flask_socketio/init.py:553: Warning: Silently ignoring app.run() because the application is run from the flask command line executable. Consider putting app.run() behind an if name == "main" guard to silence this warning.
    use_reloader=use_reloader, **kwargs)
    * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

    pip freeze show that the required libraries are installed( to the best of my knowledge)
    pip freeze
    alembic==1.4.3
    click==7.1.2
    colorzero==1.1
    dnspython==1.16.0
    eventlet==0.30.2
    Flask==1.0.2
    Flask-Cors==3.0.9
    Flask-Login==0.4.1
    Flask-Migrate==2.5.3
    Flask-Session==0.3.1
    Flask-SocketIO==4.3.1
    Flask-SQLAlchemy==2.4.4
    Flask-WTF==0.14.3
    gevent==21.1.2
    gevent-websocket==0.10.1
    gpiozero==1.5.1
    greenlet==1.0.0
    gunicorn==20.0.4
    itsdangerous==1.1.0
    Jinja2==2.10
    Mako==1.1.3
    MarkupSafe==1.1.0
    pkg-resources==0.0.0
    python-dateutil==2.8.1
    python-editor==1.0.4
    python-engineio==3.13.2
    python-socketio==4.6.0
    six==1.11.0

    The python code is as follows;
    from flask import render_template, request, jsonify, Flask, redirect, url_for, flash
    from flask_socketio import SocketIO, emit
    from flask_cors import CORS
    from flask_login import login_required, logout_user, current_user, login_user
    from app.models import User
    import sys
    import time
    from gpiozero import Button, LED
    from subprocess import check_call
    from signal import pause
    from app import app
    from app import db
    from app.forms import LoginForm, RegistrationForm, AddUserForm

    door_1_closed = Button(20)
    door_1_opened = Button(21)

    door_2_closed = Button(23)
    door_2_opened = Button(24)

    door_1_toggle = LED(5, True, True)
    door_2_toggle = LED(6, True, True)

    app.config['SECRET_KEY'] = 'secret!'
    CORS(app)
    socketio = SocketIO(app)
    thread = None
    print('MAIN')

    def shutdown():
    check_call(['sudo', 'poweroff'])

    doors = [{'door_name': 'door_one', 'door_state': 'Open'},
    {'door_name': 'door_two', 'door_state': 'Open'}]

    def background_thread():
    door = 1
    while True:
    print('Background Thread')
    # if door_1_closed.value:
    # door = 0
    # doors[door]['door_state'] = 'Closed'

    # if door_1_opened.value:
    # print('Door One is FULLY Open')
    # door = 0
    # doors[door]['door_state'] = 'Open'

    # if not door_1_opened.value and not door_1_closed.value:
    # door = 0
    # doors[door]['door_state'] = 'Moving'

    # if door_2_closed.value:
    # door = 1
    # doors[door]['door_state'] = 'Closed'

    # if door_2_opened.value:
    # door = 1
    # doors[door]['door_state'] = 'Open'

    # if not door_2_opened.value and not door_2_closed.value:
    # door = 1
    # doors[door]['door_state'] = 'Moving'
    # socketio.emit('message', doors[door])
    emit('message', 'This Is Message.')

        time.sleep(5)
    

    @socketio.on('connect')
    def connect():
    global thread
    if thread is None:
    thread = socketio.start_background_task(target=background_thread)

    <h1>if name == 'main':</h1>

    if name == 'app.main':
    print ('run the Socket')
    socketio.run(app, debug=True)
    else:
    print('not running socket')

    Any advice you can give would be much appreciated has I have hit the limits of my knowledge.
    Cheers Peter

  • #477 Miguel Grinberg said

    @Peter: you are using flask run to start your Socket.IO server. That's the problem. See this article or the Flask-SocketIO documentation to learn how to start the server properly.

  • #478 Sergio Valenzuela said

    Hi Miguel !!!

    How I can send audio in your web application Flask socketio ?

    P.D.: Excuse my english rustic.

  • #479 Miguel Grinberg said

  • #480 KSK said

    Hi, I am trying to run the flask-socketio app on localhost. But the app runs on '127.0.0.1' host only. I want it to run on 'localhost:5000' instead. How can I do that? I have even set host as 'localhost' still it runs on 127.0.0.0' only. Please help:/

  • #481 Miguel Grinberg said

    @KSK: localhost and 127.0.0.1 are the same thing. One is a hostname, the other is an IP address, but they refer to the same internet location.

  • #482 carkod said

    How do you do this with multiple websockets? If you have synchronous code, it will be blocked by the websocket

  • #483 Miguel Grinberg said

    @carkod: you have to use a web server that supports some form of concurrency. Both threads and green threads (greenlets) are supported. And if you are willing to drop Flask, then you can also do it with asyncio.

  • #484 Michael K said

    I have installed eventlet and the example app.py works fine.

    If I want to use gunicorn, I use the following command and the app does not run (Async mode shows as eventlet and transport shows as websocket, however it appears the app.py never runs):
    gunicorn --worker-class eventlet -w 1 -b 0.0.0.0:5000 wsgi:app

    using your app.py example, I'm wondering if my entrypoint is incorrect?

    wsgi.py:
    from app import app
    from flask_socketio import SocketIO

    socketio = SocketIO(app)

    if name == "main":
    socketio.run(app)

  • #485 Miguel Grinberg said

    @Michael: What do you mean by "Async mode shows as eventlet and transport shows as websocket"? Where do you see this information if you claim that app.py is not working?

  • #486 Michael K said

    @Miguel: This is from your own example. Run the example app.py from your 'example' directory of your github repo - launch a browser against the server and you can see which async mode and which transport is being used.

  • #487 Miguel Grinberg said

    @Michael: Of course. My confusion comes from the fact that you said that app.py wasn't working. If you are seeing this information that the application shows, then wouldn't that mean that the application is working? You need to provide more details with regards to how the application isn't working, because to me it looks like it is working.

  • #488 Ulisses Leitão said

    Dear Michael
    I follow and I am a great enthusiast of his work. Congratulations!
    I know this post is very old, but I have noticed a bug when I run the script with Kubuntu 21.10. If I use Python 3.9 I get dependencies problems for the eventlet module. I had to install Python 3.8, create a Virtual environment, to be able to install all dependencies of requirements.txt.

    In my project I'm trying to insert a Chat as an iframe in a physical measurements application (RLab - a Remote School Lab for Physics Experiments using Arduino). The site is based on your microBlog and I'm not succeeding to get it working together, microBlog + Chat app. One of the reasons is this version incompatibility. The other is that I don't know how to manage the events. So, when I update the RLab, the Chat restarts. I believe it tries to read "username" and generates an error. Any guidance from You would be appreciated. Thanks.

  • #489 Miguel Grinberg said

    @Ulisses: Have you consulted the documentation? Eventlet isn't the only option, if that does not work for you, then use something else, either gevent or regular threads.

  • #490 Ko Chang said

    Dear Guru-Miguel,

    Is there a way to broadcast to all connected clients every X seconds, i.e. via a async function without an event? I am currently running a .py script that is getting called via an Ajax fetch request, however for each session new API calls are made. Feels like WebSockets is what I need to do this. Something like this would make sense to me. Can you give me a head-up in the right direction?

    @socketio.on('broadcast')
    async def ws_broadcast(message):

    await asyncio.sleep(5)
    data = cmc_api() # < --- cmc_api() = .py that makes the calls and return { x : {}, etc }
    emit('broadcasting', jsonify(data))
    
  • #491 Miguel Grinberg said

    @Ko: you have too many conditions. You can broadcast to all connected clients. If you need to do it every X seconds, then use a for-loop in your function, with a sleep(5) inside. Why without an event? Socket.IO uses events, that's the basic unit that is exchanged between clients and the server.

  • #492 Ko Chang said

    @Miguel, thanks for the response. I can't get a grip on this. On server start / clients i see that they are connected to the websocket. Also I see disconnects (pretty soon tho, but ok). After 3 days of coffee and immense loads on docs, I feel non-einstein by still not figuring this out. Do you offer your service, or would you be willing to help out for some $?

    I tried many variations on the below, including (as written in the docs) :

    def some_function():
    socketio.emit('some event', {'data': 42}) #If i am correct this will broadcast to all connected clients.

    --- this is what I currently have -- No errors, but no data, messages, prints at server side, nor at the client side..

    @socketio.on('broadcast')
    async def ws_broadcast():

    while True:
        time.sleep(5)
        print('broadcasting')
        data = cmc_api() #fetching new data from API's
        emit('broadcast', data, broadcast=True)
    
    <hr />

    On the client side, I need to receive the data and replace elements in the current DOM. Below just the socket .js

    var socket = io();

    socket.on('my response', function(msg) {
        console.log('something is happening here...', msg)
    });
    
    socket.on('broadcast', function(data){
        console.log('broadcasted data', data)
     });
    
    <hr /> <h1></h1>

    .then((object) => {

        console.log('refreshing data')
    
        for (const [key, value] of Object.entries(object['data'])) {
            var searchterm = value['symbol']
    
            try {
                //price - cap - vol
                var price = document.getElementById(searchterm + '-price')
                price.innerHTML = '$ ' + value['price']
    
            }
    
            catch (error) {
                console.error(error);
            }
    
        }
    
    })
    
  • #493 Miguel Grinberg said

    @Ko: I think it would make more sense to work with one of the examples that I provide on my Socket.IO repos. The code that you are showing me has mismatched event names, at least that's what I see. And I don't see how your emit loop gets to run. You have it inside a "broadcast" event, but who calls that event?

  • #494 David said

    Hi Miguel, is there any way to scale how many client connected is possible to have with asyncio? since i have the idea that is going to be a limit on the numbers of threads, and this code seems to disallow the option of multiple-workers. (each worker is going to have a different chat story to share). Thanks and best regards, DAvid

  • #495 Miguel Grinberg said

    @David: First of all, the solution described in this article does not use asyncio, it uses a different async framework called gevent. The number of clients can be configured, I believe a commonly used maximum in gevent web servers is 1000 clients.

  • #496 Ashok said

    Hi ,
    I have used flask-socketIO , it works well with socket.io js by using browser .
    I am unable to connect same by using "ws://127.0.0.1:5000" or by using some embedded devices such ESP 32 controller to our flask based application ? any suggestion.
    I wanted to communicate Flask web app with embedded device (ESP 32).
    Thanks!

  • #497 Miguel Grinberg said

    @Ashok: I don't think there are any Socket.IO clients for microcontrollers, either C or MicroPython, at least I'm not aware of any.

  • #498 Ko-Lin Chang said

    Hi Miguel,

    Some months/year ago I asked some questions related to websockets. You were pretty patient with my level of knowledge at that time. We figured our getting our chat blueprint prod. ready thanks to your guidance on the internet and some help of chat GPT ;). Just wanted to say thanks to you.

    Ciao

Leave a Comment