Add a WebSocket Route to your Flask 2.x Application

Posted by
on under

The WebSocket protocol was standardized 10 years ago (in 2011, can you believe it?) so I'm sure for many of it you does not need an introduction. But in case you are not familiar with it, WebSocket is an extension to the HTTP protocol that provides a permanent, bi-directional communication channel between a client and the server, where both sides can send and receive data in real time, free of the constraints of the request/response cycle of HTTP.

Flask, being a minimalist web framework, does not have WebSocket support built-in. The old Flask-Sockets extension, which has not been maintained in the last few years, provided support for this protocol. My own Flask-SocketIO extension has also been a favorite, but this is Socket.IO, which is a more complex protocol that combines WebSocket and HTTP.

The good news is that if you are using Flask 2 you now have a new extension (also created by myself) called Flask-Sock, which provides modern WebSocket support for your application. In this article I'm going to show you how to work with this extension.

This article was voted by my supporters on Patreon. Would you like to support my work, and as a thank you be able to vote on my future articles and also have access to a chat room where I hang out? Become a Patron!

Introducing the Flask-Sock Extension

The Flask-Sock extension is installed with pip:

pip install flask-sock

The extension is added to your Flask application by creating a Sock() instance. If you have a global app object, you can use the direct initialization method:

sock = Sock(app)

If instead you create your application instance in a factory function, then the two-step initialization method works as well:

sock = Sock()

def create_app():
    app = Flask(__name__)
    sock.init_app(app)

The sock instance has a route decorator, that works pretty much like Flask's own, but it adds the WebSocket protocol handshake so that the route can speak WebSocket instead of HTTP. Here is a simple server that echoes back to the client anything it sends:

@sock.route('/echo')
def echo(ws):
    while True:
        data = ws.receive()
        ws.send(data)

The ws object that is passed to the route is the actual WebSocket connection, which the function can use to exchange information with the client through the send() and receive() methods. It goes without saying that when multiple clients are connected at the same time, each gets its own ws object, and the communication between the server and each client is private.

Looking at the code example above, the main difference between a Flask route and a WebSocket route is that the WebSocket function is designed to run for a long time. WebSocket connections often last for as long as the user has a page in the browser open, so it may be several minutes or even hours. Unlike standard HTTP routes, your WebSocket handler will very likely have to be implemented with a loop, so that it remains active for as long as it is needed. The WebSocket function is not expected to return a response, the Flask-Sock extension takes care of generating the response as required by the WebSocket protocol.

How do you end a WebSocket connection? The most common situation is that the user navigates away from the page that created the connection, At this point the browser will close the WebSocket, and from then on the ws.receive() and ws.send() methods are going to raise a ConnectionClosed exception, which is automatically handled by the Flask-Sock extension to end the route gracefully. You can catch this exception in the route to perform custom cleanup if necessary.

A second way to end the WebSocket connection occurs when the server initiates the close. To do this, the WebSocket route needs to call ws.close(), or alternatively, just exit the function.

The ws.route decorator is fully integrated with Flask, and in fact, it uses the app.route decorator internally. In particular, all these things work:

  • Variable components in the URL. If your URL has variables, the ws argument must be included before those variables in the function definition.
  • Additional decorators. You can add any decorators that are designed to work with Flask routes, such as Flask-Login's login_required and any others.
  • Before and after request handlers. These are invoked for WebSocket routes as well.
  • current_app, request, g and session. These work the same as in Flask routes, with one exception regarding the session variable, explained below.

An important feature of HTTP that is not available in WebSocket is the ability to set cookies. In HTTP, cookies are set in the response headers. WebSocket does not use an HTTP response, so it is not possible to set a cookie when the WebSocket connection ends. This means that changes made to the session object in the WebSocket handler function are not saved.

Running your Flask + WebSocket server

To run your WebSocket enabled web server during development, just run your application in the way you normally do. Both the flask run and app.run() methods of running the server are compatible with Flask-Sock.

To run your Flask + WebSocket server in production you can use Gunicorn. You will normally want to enable multithreading, because as discussed earlier, a WebSocket route will run for a long time, and without threads each WebSocket connection would consume an entire worker. The following example creates a Gunicorn server with four workers, each using 100 threads, allowing a maximum of 400 active WebSocket connections (though you will always want to leave some threads available to handle other HTTP requests):

gunicorn -b 0.0.0.0:5000 --workers 4 --threads 100 module:app

If you prefer to use a greenlet based asynchronous web server, Flask-Sock is also compatible with eventlet and gevent. The documentation has all the details. The uWSGI web server is currently not supported.

A Complete Example

Let's have a look at a complete example. Here is the Flask application code:

from flask import Flask, render_template
from flask_sock import Sock

app = Flask(__name__)
sock = Sock(app)


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


@sock.route('/echo')
def echo(sock):
    while True:
        data = sock.receive()
        sock.send(data)

This is based in the echo server code shown above. The application has a root URL that returns the index.html client page, and a /echo route that implements a WebSocket echo endpoint.

Here is the index.html page, which you have to save in a templates subdirectory:

<!doctype html>
<html>
  <head>
    <title>Flask-Sock Demo</title>
  </head>
  <body>
    <h1>Flask-Sock Demo</h1>
    <div id="log"></div>
    <br>
    <form id="form">
      <label for="text">Input: </label>
      <input type="text" id="text" autofocus>
    </form>
    <script>
      const log = (text, color) => {
        document.getElementById('log').innerHTML += `<span style="color: ${color}">${text}</span><br>`;
      };

      const socket = new WebSocket('ws://' + location.host + '/echo');
      socket.addEventListener('message', ev => {
        log('<<< ' + ev.data, 'blue');
      });
      document.getElementById('form').onsubmit = ev => {
        ev.preventDefault();
        const textField = document.getElementById('text');
        log('>>> ' + textField.value, 'red');
        socket.send(textField.value);
        textField.value = '';
      };
    </script>
  </body>
</html>

The HTML page includes a <script> section with the client-side logic. The log() auxiliary function writes a string to the page, using the given color. This is used to write outgoing data in red, and incoming data in blue, as you can see in the screenshot below:

Flask-Sock Echo Server Demo

The application uses the WebSocket class provided by the browser to make a connection to the /echo endpoint. It then installs a listener for the message event, which fires whenever the server sends data through the connection. The handler prints the data to the page. The page has a form with an <input> element that accepts text from the user. A second event handler is defined on the submit event for the form. The handler for this event logs the text entered by the user to the page, and then writes it to the WebSocket connection, using its send() method.

To try out this application, save the Python file as app.py and the HTML file as index.html in a templates subdirectory. Then run the application with flask run. Connect with your browser at the http://localhost:5000 URL to use the application.

What Flask-Sock Doesn't Do

Flask-Sock is designed to be a simple extension to implement WebSocket communication between a client and the server. It is not a goal of this extension to implement advanced workflows involving the broadcasting of messages to all or to subsets of clients. Your application can implement this if desired, but if you need something of this complexity you may be better off using Flask-SocketIO. I have written an article about it too!

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!

66 comments
  • #26 Miguel Grinberg said

    @Amine: your cron job needs to communicate with the server process in some way and pass the data. The cron job process does not have access to the ws instances for your clients, so it cannot send to them directly.

  • #27 Quantum-Quant said

    Hi Miguel, thank you for this great tutorial! In the documentation for Flask-SocketIO, you explicitly mention that gunicorn doesn't support running multiple workers unless one uses a load balancer such as nginx (https://flask-socketio.readthedocs.io/en/latest/deployment.html#using-multiple-workers). In this tutorial, however, it seems that Flask-Sock doesn't have this restriction. Is my understanding correct? And if so, why is that?

    Thank you!

  • #28 Miguel Grinberg said

    @Quantum: Flask-Sock implements a WebSocket connection, not Socket.IO which is much more complex. For example, Flask-Sock does not handle broadcasting or rooms.

  • #29 Zaheer Abbas said

    Hi miguel, I wanted to save data received from websockets to file, But when I use the following code:

    @sock.route("/media-stream")
    def echo(sock):
    print("Connection opened")
    id = uuid.uuid4().hex
    while True:
    data = sock.receive()
    decoded_data = base64.b64decode(data)
    print(decoded_data)
    wf = wave.open(id + ".wav", "rb")
    wf.write(decoded_data)

    The client reconnects every time and does not work, can you please suggest how can I go about doing this using flask-sock

  • #30 Miguel Grinberg said

    @Zaheer: you need to fix your code. You are opening the file for read access, not write. You are also opening a new file for every message, without ever closing anything. You should open a single instance of the wave file outside the loop, in my opinion.

  • #31 Amine said

    Hello Miguel, i'm using flask-sock for a project i'm working on. And i noticed that with multiple users, only the last one to access the web app receives the messages. The work flow is setup in a way that there's a global variable that contains an empty string. It gets filled and emptied using a cronjob, and i test against its content to execute whats under the socket which is "ws.send(message)". My question is this a constraint of flask-sock? Should i switch to flask-socket-io if i want to "broadcast" to all clients connected? Not the last one to access the app if that has a name.

  • #32 Miguel Grinberg said

    @Amine: This is maybe a misunderstanding on your part on how Flask-Sock works. When a client connects, the server assigns a websocket object to it and passes it to the route. This object is used to send and receive messages between the server and this client. There is no such thing as a broadcast, if you want to send the same data to multiple clients, you have to send on each of the websocket objects for the intended recipients.

  • #33 Amine said

    Hello Miguel, thank you for the explanation on how flask-sock handles websocket connection. Now i understand.
    Turns out my problem was the usage of a global variable to execute whats under the socket route. The last client to connect would the first to receive the message ( and empty the global variable so no more messages for the rest of the clients ). I now append all websocket objects related to clients that connect to the route in a global list, and iterate through it anywhere in my code to send data to all clients. It works like a charm! Thank you.

  • #34 Mariano said

    hola! Miguel
    I'm trying to use this library, and it works perfectly for me to send and receive data, but I'm trying to use the data outside the echo() function and I can't, is there any way to use the data variable in other parts of the code?
    My code receives a video frame in a byte string, I send it to the video_feed() function to display it in the html
    (THIS ONLY WORKS IF I COMMENT THE DECORATOR @sock.route('/')) if I comment it works fine but I can't receive data, any ideas? anything would help me, thank you very much and sorry for my long question.

    @sock.route('/') -> (If I comment this line it works but logically I am left without receiving data)
    def echo():
            global data
            img= np.fromstring(data, np.uint8)
            img = cv2.imdecode(np.frombuffer(img, dtype=np.byte), flags=cv2.IMREAD_COLOR)
            _, frame = cv2.imencode('.jpeg', img) 
            yield (b'--frame\r\nContent-Type: image/jpeg\r\n\r\n' + frame.tobytes() + b'\r\n')
    
            img= np.fromstring(data, np.uint8)
            img = cv2.imdecode(np.frombuffer(img, dtype=np.byte), flags=cv2.IMREAD_COLOR)
            _, frame = cv2.imencode('.jpeg', img) 
            yield (b'--frame\r\nContent-Type: image/jpeg\r\n\r\n' + frame.tobytes() + b'\r\n')
    
    @app.route("/video_feed")
    def video_feed():
        time.sleep(0.2)
        return Response(echo(),  -------> using here
            mimetype = "multipart/x-mixed-replace; boundary=frame")
    
  • #35 Miguel Grinberg said

    @Mariano: I don't really understand what you are trying to do, but the yield keyword is used in Flask streaming responses. This is not for websocket routes created with Flask-Sock. Please check the examples in the Flask-Sock repository and the official documentation to learn how to use this extension.

  • #36 Chris Gravel said

    Hi Miguel! First off, thanks for the open source! I was looking for a web sockets implementation that integrated with Flask and this looks to be just the thing; I landed here after first digging into your SocketIO implementation mentioned on SO: https://stackoverflow.com/questions/23111654/websockets-in-flask.

    My question here is regarding the concurrency model. I read your post and you mentioned greenlets which scales better than threads processes. On my server I have a global object instance which I want to synchronize access to with a lock. E.g. Process one websocket request fully, before processing another. Is this possible?

  • #37 Miguel Grinberg said

    @Chris: Yes, there are greenlet versions of all synchronization primitives, including Lock.

  • #38 chandan sharma said

    Hi Miguel,

    In socket.send(), If I want to pass multiple variables, how to do that?

    Thanks,
    Chandan

  • #39 Miguel Grinberg said

    @chandan: the socket.send() function accepts a string or a byte sequence. Those are the only types that can be passed in a WebSocket connection. If you want something more elaborate, you have to convert your data to one of these two. For example, you can send a JSON encoded string.

  • #40 chandan sharma said

    Hi Miguel,
    Thanks for your quick response. My requirement is to have multiple areas on webpage(DIV elements) to listen to server events (for fetching Linux systems stats like - load, disk stats, memory) and hence I am fetching these stats using system commands like - w, df -h, 'free -m`. The results of these commands are stored in variables as subprocess.STDOUT. So, now if I need to use all of these variables in socket.send(), then converting these results into string and then using in javascript becomes difficult as I cannot directly use the mentioned variables into javascript socket.addEventListener method. Is there any better way of implementing this using Flask-sock ?

    Thanks,
    Chandan

  • #41 Miguel Grinberg said

    @Chandan: Unfortunately I do not understand the problem that you have. A network connection can only transmit a sequence of bytes. This is true of WebSocket as it is of HTTP and any other network protocol. The WebSocket protocol includes metadata with each message to indicate of the sequence of bytes should be interpreted as a string or as binary data, and this is controlled by the type of the argument you pass to send(), which can be string or bytes respectively. For higher level types it is always necessary to convert them to either a string or bytes, and then on the browser side the reverse conversion must be performed. This technique is standard and is used not only for WebSocket messages but also for HTTP APIs. As I said above, JSON is a good and relatively easy format to use, with good support both in Python and JavaScript.

  • #42 chandan sharma said

    Hi Miguel,

    Thanks for your feedback and I was able to do this via socket-IO.

    Thanks,
    Chandan

  • #43 Shravya said

    Hi Miguel, thank you for this amazing extension. The echo server endpoint works as expected and I am able to keep a persistent connection.

    I had a few questions regarding this implementation.
    1. Is it possible to connect to multiple endpoints while keeping the connection alive?
    2. if we have the ws object as a global variable, or a list of global variables, how to identify the client?

  • #44 Miguel Grinberg said

    @Shravya: 1) yes, you can have multiple endpoints running concurrently (as long as you have enough threads, since each client connection takes one). 2) It is up to you to create a mapping between clients and websocket objects. You can use your own authentication information for this.

  • #45 muhammad nasr said

    Hi Miguel,
    Thanks for flask-sock, as always every thing you do is useful for flask-users.

    I am using flask-sock for recording voice and send it to google to return speech to text.
    in the client
    socket.addEventListener("open", () => {

      sock.send('start');
    });
    

    @sock.route('/echo')
    def echo_chat(ws):
    while True:
    data = ws.receive()
    listen_print_loop(responses, stream,
    socket=ws, message=data) # it return responses

    it works perfect in the start
    when I want to close streaming, I send from the client
    socket.addEventListener("open", () => {
    sock.send('close');
    });
    then I pass it to responses loop
    if message == 'close':
    stream.closed = True
    break
    it close then back to echo with the first message ('start') , and back to recording
    I tried to close from the echo but It also back to the first message ('start') and continue
    I can't find a a way to stop recording with socket
    I would be grateful for your help.

  • #46 Miguel Grinberg said

    @muhammad: your code is incomplete. I don't know what responses and stream are for.

  • #47 muhammad nasr said

    sorry for that, I didn't want to send a whole code to bother you,
    the stream is a class for Microphone which records audio from the user mic
    the response comes from google.cloud.speech which converts speech to text

  • #48 Miguel Grinberg said

    @muhammad: It's really hard for me to fully understand what you are doing with so many missing details, but one thing I don't see is any logic to exit the while loop in your echo_chat() function. That should happen to break the websocket connection, I think.

  • #49 pasta said

    Hi miguel, I'm using your extension successfully. Everything works well but when I move to another page it triggers a ConnectionError:

    Traceback (most recent call last):
    File "/home/pi/env/lib/python3.9/site-packages/flask/app.py", line 2548, in call
    return self.wsgi_app(environ, start_response)
    File "/home/pi/env/lib/python3.9/site-packages/flask/app.py", line 2532, in wsgi_app
    return response(environ, start_response)
    File "/home/pi/env/lib/python3.9/site-packages/flask_sock/init.py", line 86, in call
    raise ConnectionError()
    ConnectionError

    I tried to catch ConnectionClose and return the function to close the socket but does not work. The code works perfectly but this error "me impacienta". Great work and thanks for everything

  • #50 Miguel Grinberg said

    @pasta: It is very difficult to support WebSocket connections with Werkzeug, sometimes this happens. Make sure you are on the latest Flask/Werkzeug, as I believe I haven't seen this types of problems with recent versions. In any case, when you switch to Gunicorn for a production deployment this is not going to happen.

Leave a Comment