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
  • #301 Miguel Grinberg said

    @Vipul: Upgrade your python-engineio to version 0.8.7 to remove the messages printed to the console, they were left there by mistake. The new versions of the socket.io protocol have two layers, a low-level layer called engine.io and a high-level layer called socket.io. The two packages implement the two layers independently of each other. Flask-SocketIO depends on python-socketio, which in turn depends on python-engineio.

  • #302 Easyrider said

    Hi Miguel!

    I am having troubles using the flask-socketio and I was hoping you could help me with this.

    When I try to send an JSON object with an empty field like the following:
    data = {"param1" : "value", "data" : {}}
    I get an typeerror "Reduce(9 of empty sequence with no initial value"
    Sending an empty dictionary results in the same error.

    socketio.emit("response", {}, room......)

    I found that if I convert the dict/JSON to a string before i send it, everything is OK, but I think that this is the wrong solution.

    It is only at serverside I have the problem.

    Is there a solution for this?

    Thanks!

  • #303 Easyrider said

    Hi again. I updated flask-socketio (used 1.0b1) and the problem is done!

  • #304 Vammi said

    Hello Miguel,

    I am working on an exam webapp and it needs to cater 'online exams' to registered students. I am using flask-socketio to reload the question part of the page and i am able to reload the questions like in ajax call. I want to use 'eventlet' as async server and in my eventlet.spawn(), i am calling a function which updates the database with answer choice and queries for the next/preivous question and while trying to execute the queries(sqlalchemy models), i am getting the following error message and here is the stack trace:

    Traceback (most recent call last):
    File "/home/blue/iLearnings/2015/candle/candle_01/venv/lib/python3.4/site-packages/sqlalchemy/util/_collections.py", line 986, in call
    return self.registry[key]
    KeyError: <eventlet.greenthread.GreenThread object at 0xb6045d9c>

    During handling of the above exception, another exception occurred:

    Traceback (most recent call last):
    File "/home/blue/iLearnings/2015/candle/candle_01/venv/lib/python3.4/site-packages/eventlet/hubs/hub.py", line 457, in fire_timers
    timer()
    File "/home/blue/iLearnings/2015/candle/candle_01/venv/lib/python3.4/site-packages/eventlet/hubs/timer.py", line 58, in call
    cb(args, kw)
    File "/home/blue/iLearnings/2015/candle/candle_01/venv/lib/python3.4/site-packages/eventlet/greenthread.py", line 217, in main
    self._resolve_links()
    File "/home/blue/iLearnings/2015/candle/candle_01/venv/lib/python3.4/site-packages/eventlet/greenthread.py", line 232, in _resolve_links
    f(self,
    ca, ckw)
    File "/home/blue/iLearnings/2015/candle/candle_01/candleapp/main/views.py", line 1203, in physics_sample_test_provider_callback
    reply_physics_json = gt.wait()
    File "/home/blue/iLearnings/2015/candle/candle_01/venv/lib/python3.4/site-packages/eventlet/greenthread.py", line 175, in wait
    return self._exit_event.wait()
    File "/home/blue/iLearnings/2015/candle/candle_01/venv/lib/python3.4/site-packages/eventlet/event.py", line 125, in wait
    current.throw(self._exc)
    File "/home/blue/iLearnings/2015/candle/candle_01/venv/lib/python3.4/site-packages/eventlet/greenthread.py", line 214, in main
    result = function(
    args,
    kwargs)
    File "/home/blue/iLearnings/2015/candle/candle_01/candleapp/main/views.py", line 1084, in physics_sample_test_provider
    visitor = Visitor_Sample_Test_User_Pen.query.filter_by(sid=sid).first()
    File "/home/blue/iLearnings/2015/candle/candle_01/venv/lib/python3.4/site-packages/flask_sqlalchemy/init.py", line 454, in get
    return type.query_class(mapper, session=self.sa.session())
    File "/home/blue/iLearnings/2015/candle/candle_01/venv/lib/python3.4/site-packages/sqlalchemy/orm/scoping.py", line 71, in call
    return self.registry()
    File "/home/blue/iLearnings/2015/candle/candle_01/venv/lib/python3.4/site-packages/sqlalchemy/util/_collections.py", line 988, in call
    return self.registry.setdefault(key, self.createfunc())
    File "/home/blue/iLearnings/2015/candle/candle_01/venv/lib/python3.4/site-packages/flask_sqlalchemy/init.py", line 704, in create_session
    return SignallingSession(self, **options)
    File "/home/blue/iLearnings/2015/candle/candle_01/venv/lib/python3.4/site-packages/flask_sqlalchemy/init.py", line 149, in init
    self.app = db.get_app()
    File "/home/blue/iLearnings/2015/candle/candle_01/venv/lib/python3.4/site-packages/flask_sqlalchemy/init.py", line 845, in get_app
    raise RuntimeError('application not registered on db '
    RuntimeError: application not registered on db instance and no application bound to current context

    I am using the following:
    alembic==0.7.7
    amqp==1.4.8
    anyjson==0.3.3
    billiard==3.3.0.22
    blinker==1.4
    celery==3.1.19
    eventlet==0.18.3
    Flask==0.10.1
    Flask-Bootstrap==3.3.5.2
    Flask-Cache==0.13.1
    Flask-DebugToolbar==0.10.0
    Flask-Login==0.2.11
    Flask-Mail==0.9.1
    Flask-Migrate==1.4.0
    Flask-Moment==0.5.0
    flask-paginate==0.4.1
    Flask-Principal==0.4.0
    Flask-Script==2.0.5
    Flask-SocketIO==1.0b1
    Flask-SQLAlchemy==2.0
    Flask-WTF==0.12
    greenlet==0.4.9
    itsdangerous==0.24
    Jinja2==2.8
    kombu==3.0.30
    Mako==1.0.1
    Markdown==2.6.5
    MarkupSafe==0.23
    PyMySQL==0.6.6
    python-engineio==0.6.7
    python-socketio==0.5.2
    pytz==2015.7
    redis==2.10.5
    requests==2.9.1
    six==1.9.0
    SQLAlchemy==1.0.6
    websockets==2.5
    Werkzeug==0.10.4
    wheel==0.24.0
    WTForms==2.0.2

    I did monkey patching as follows in manage.py as follows:
    def create_app(config_name):
    ''' This creates the application '''
    app = Flask(name)
    app.config.from_object(config[config_name])
    config[config_name].init_app(app)

    eventlet.monkey_patch()
    

    views.py :
    greenth = eventlet.spawn(physics_sample_test_provider,json,request.sid)
    print("Printing the greenthread ", greenth)
    greenth.link(physics_sample_test_provider_callback)

    Could you please help me, how we can access/update the database (preferably using sqlalchemy models) in eventlet.spawn methods.

    Thanks,
    vammi

  • #305 Miguel Grinberg said

    @Vammi: if you are running database operations in a thread, you have to push an app context in that thread. This is unrelated to Flask-SocketIO, Flask-SQLalchemy needs to have access to an app instance to grab database configuration from it. In Flask each thread needs to have its own app context.

  • #306 Vammi said

    Thank you Miguel, i have pushed the app context to the thread and able to access the db. After gogling for a while, i am planning to drop the idea of using the server side implementation of catering one question for each click (using socketio) as it needs to create/pass the context for each question and pushing/popping the context to thread seems to be very expensive.

    For the last week, i am working with socketio and it is a wonderful library and only thing i struggled with it is while making it eventlet compatible as 'eventlet is the preferred engine' for production applications and i am stuck with the db operations in the eventlet.spawn().

    Just one more clarification, if we want to serve hundreds of connections through socketio simultaneously, do we need to implement the eventlet.spawn() (what is the advantage of implementing the eventlet.spawn) or does the eventlet.monkey_patch() takes care of spawning multiple threads including db operations (blocking IO).

    Thank you very much for the socketio and everything.

    Thanks,
    Vammi

  • #307 Miguel Grinberg said

    @Vammi: Note that Flask does a context push/pop for every request. Unless your event rate is really high, I don't see how this can be a performance issue.

    I'm not sure I understand your question regardign eventlet. The spawn() function is used in eventlet to start a thread (or greenlet to be accurate). It has nothing to do with databases. If the database driver that you use is not compatible with eventlet, then you need to monkey patch the standard library, so that all the I/O functions are routed through eventlet, and then in most cases you should be able to use your database in a eventlet friendly way.

  • #308 Vammi said

    Hi Miguel,

    Thanks for the clarification about the Flask context push/pops for every request.

    And Sorry for the confusion regarding the eventlet. i have re-phrased the question as follows, hopefully it is clear.

    During the development, i have initialized the socketio as below and used it without invoking eventlet or explicilty used eventlet in the code.
    socketio.init_app(app)
    # This was earlier version before specifying the eventlet usage explicitly
    # as per the documentation, socketio uses eventlet/gevent/Flask dev server sequentially which ever is installed
    # At this stage, I could able to get the individual socketio communication from app while using two different browsers without any issues simultaneously
    # Once I am comfortable with the above, I have implemented the eventlet as below and converted the code to eventlet.spawn() and subsequent callback function. At this stage, i have got issues with the db operations inside the greenlet thread which got resolved after pushing app.app_context(please note during this entire exercise, I have already installed eventlet in development virtualenv)

       socketio.init_app(app, engineio_logger=True, async_mode='eventlet')
       # I have explicitly used eventlet - async_mode 
       # implemneted the eventlet.spawn()`  1
    

    I am confused about the following paragraph under the "Requirements section in socketio documentation" (http://flask-socketio.readthedocs.org/en/latest/)

    The extension automatically detects which asynchronous framework to use based on what is installed. Preference is given to eventlet followed by gevent. If neither one is installed, then the Flask development server is used.
    

    Does the above statement means socketio.run() uses eventlet even if i do not mention the async_mode as 'eventlet' while initializing the socketio & does not implement eventlet.spawn() in application?

    If socketio uses the eventlet for async services in the above case, what is advantage of implementing the eventlet.spawn() in code?

    Thanks,
    Vammi

  • #309 Miguel Grinberg said

    @Vammi: this isn't a matter of having an advantage. The Python standard library is incompatible with the asynchronous model used by eventlet, so when eventlet is used, you have to use it in all places. So for example, if you use threads, they need to be eventlet threads (or greenlets), regular threads cannot be used, as they break the eventlet model. The easiest way to ensure you use eventlet compatible classes is to monkey patch the standard library. This replaces the incompatible classes with eventlet friendly equivalents.

  • #310 Hélio Mendonça said

    Dear Miguel
    I already had success using your Flask-SocketIO in other small projects.
    I was trying now to use it in a larger project using the “Large Application Structure” you described in your book “Flask Web Development” (that is using a script manager, blueprint, views, etc.).
    If I use the @socketio.on() and socketio.emit() in the same file I declare the socketio (manage.py) all works great, but if I try to use them inside the views.py they do not work (probably due to the way I invoque socketio using: from manage import socketio).
    Can you check this in my simplified Github Project (https://github.com/hsmptg/sokio) that illustrate this problem and give me some tips on how I can solve this?
    Best regards
    Helio

  • #311 Miguel Grinberg said

    @Hélio: Have you seen my larger example? It uses the app factory pattern and includes Socket.IO, this is goingt o help with the project organization: https://github.com/miguelgrinberg/Flask-SocketIO-Chat

  • #312 Hélio Mendonça said

    Hi Miguel
    Now my "large project" also works like a charm! :)
    I did not realized the existence of that sample project of yours!
    Many thanks for your help.

  • #313 Hélio Mendonça said

    Hi Miguel, just one more thing:

    Inside my views.py I have a thread to periodically sense the presence of an eventual RFID card (getting its uid). In that thread, if I get an uid, I have to check if it exists in a users table using SQLAlchemy, and in that case emit a message to the page using socketio. Here is the code of that thread:

    def rfid_thread():
    global MIFAREReader, exitRFID

    while not exitRFID:
        time.sleep(1)
        uid = MIFAREReader.MRFC522_readUID()
        if uid is not None:
            user = User.query.filter_by(rfid=uid).first()
            if user is not None:
                msg = {'user': user.username, "rfid": uid}
                socketio.emit('rfid', msg, namespace='/rfid')
    

    This code originates a runtime error “application not registered on db instance and no application bound to current context”, that I can avoid including the line “with app.app_context():” before making the query.

    But in order to make app known I have to include the line “from run import app”, which makes the socketio stop emiting!!!

    Could you please tell me how can I solve this?

    Best Regards
    Helio

  • #314 Miguel Grinberg said

    @Hélio: Very strange that a "from run import app" makes socketio stop working. I'm guessing there is a circular dependency problem in there, but I can't be sure without seeing your code.

    In any case, you can pass the "app" instance as an argument to your thread, you do not need to import the global variable.

  • #315 Hélio Mendonça said

    Hi Miguel, thanks again for your reply.

    I realised that the majority of my problems are related with object references since the “app factory pattern” although more flexible is also more complex with its several levels (the manage.py top level, the app level, the views.py at the blueprint level, etc.).
    In my case, the only way I found to instantiate “app” inside views.py (where the thread is located) was making the “from run import app” (note that my run.py is yours manage.py). Even if I pass the “app” as an argument to the thread, I need to instantiate it inside the views.py that is “faraway” of the top level where it is created by calling create_app() inside app/init.py.

    A bit confusing right? :) I will try to create a simpler sample code that can reproduce the problem so you can check if possible.

    Regards,

  • #316 Hélio Mendonça said

    Miguel,

    I updated the sample project socio (https://github.com/hsmptg/sokio) that illustrate this last problem.
    As you can check if the thread target function is the "background_thread_ok", the socketio.emit() works ok, but if I use the target "background_thread_ko" since it has the "from manage import app" the socketio stop emitting!

    If I pass the app to the thread as an argument the problem remains, since I still do not know how to avoid the "from manage import app" inside the views.py!

    I wonder if you could help me on this!

    Regards

  • #317 Miguel Grinberg said

    @Hélio: as I mentioned above, you don't have to import the app from your top level module, you can pass the app instance as an argument to your thread. You would start the thread as follows:

    thread = Thread(target=background_thread_ko, args=(current_app._get_current_object(),))

    And then in your thread:

    def background_thread_ko(app):
    # use app as needed here, no need to import anything

  • #318 Hélio Mendonça said

    Miguel, as I told you I already had tried passing the app as an argument to the thread. The problem it seems that I was missing the magic words:
    app <=> current_app._get_current_object()
    :D
    Now all is working!
    Many thanks for your help!!!

  • #319 Miguel Grinberg said

    @Hélio: yes, unfortunately that ugly _get_current_object() call is required, since current_app is not the real app instance by a proxy object that contains it. Glad it's working now!

  • #320 M Bach said

    Miguel,
    Have you seen any cases (or know of why this may be) in which the socket emits from client side are not always caught on the server side?

    I have some cases where the link between the server and client are excellent, but pressing the buttons that fire the emits don't always start the simplest of actions (like a print statement) on the sever side. At times, it's nearly 50/50 being heard/missed.

    Also, do you have any multi thread examples? Every time I add a second thread using your example on github, the thread appears to be suppressed and doesn't continue on with it's designed function.

    Thanks! Love this library!

  • #321 Miguel Grinberg said

    @M: You will need to debug the problem. You can enable SocketIO logging on the client and server sides, to see where are the events dropped. Regarding threads, the example in the official repository includes a background thread. The important thing if you are using eventlet or gevent is to use the green threads provided by these frameworks, not the standard Python threads. You can achieve that by monkey patching, or by invoking the proper thread calls for each of these.

  • #322 M Bach said

    Miguel,
    I'm using eventlet at the moment. I'm not totally following the monkey_patching in the example, but it's not possible to spawn a second thread in the same manner as your background_thread() in the example?

    So I need to use eventlet.spawn() or something after the monkey_patching() call?

  • #323 Miguel Grinberg said

    @M: If you monkey patch, then you can use the Python thread calls, as these are redirected to the Eventlet versions as part of the patching. Alternatively you can use eventlet.spawn(). Both options achieve the same end result. As long as you use green threads, you can spawn as many as you like.

  • #324 Raj Damani said

    @socketio.on('my event', namespace='/test')
    def test_message(message):
    session['receive_count'] = session.get('receive_count', 0) + 1
    emit('my response',{'data': message['data'], 'count': session['receive_count']})

    how does it work ? I mean how and when test_message() will be called and executed please explain.

  • #325 Miguel Grinberg said

    @Raj: are you familiar with the Socket.IO protocol? You may want to read about it, this extension maps closely to that protocol.

Leave a Comment