This is a walkthrough for integrating push sockets with a Flask app using gevent-socketio
I will try my best to make this tutorial short and sweet and to the point. No fluff. Only the bare minimum. I won’t bother explaining what everything means (partly because even I haven’t completely wrapped my head around all of it)
You will find a git repository with each commit documenting a step in this walkthrough here: https://bitbucket.org/hasenj/flask-sockets-tutorial
That repo is a very simple flask app: a page that receives push messages from the server and displays these messages as they arrive (in real time).
The server will listen for shouts on /shout?msg=
, and broadcast each shout/msg to all connected clients.
Note: I’m not an expert on the subject. I wrote this because I wanted to do a simple thing (send a message over a socket from inside a flask request handler) but the information on the net was scattered and I wanted to just get to the core of it and get the smallest possible example working.
Note2: The example here only works with one server instance. If you run multiple instances of the server, it won’t really work as expected. For multiple-server setups, people usually use some kind of a message queue or a redis server to act as a central server that can notify all instances of the app server of what message to send over what socket.
With that cleared out of the way, let’s proceed.
1. Setup a basic Flask project
Commit: https://bitbucket.org/hasenj/flask-sockets-tutorial/commits/eb524632b66a608edbeb01032322b7f8
This is very simple and there isn’t much to it. If you’ve worked with virtualenv, Flask and jQuery before, everything here should make perfect sense.
Source the venv_init.sh file to initialize your virtual environment and install flask:
source venv_init.sh
Then run the server with:
python main.py
The server is now running on http://localhost:6020
In main.js
, we have a Javascript function addMessage
that receives a string and adds it to the screen. This is the function that we will call when we receive messages through the push socket.
Try it! Open the Javascript console on your browser and run:
addMessage("Hello");
You should see the word “Hello” in big orange-ish color. Try it again, and the new message will appear on top of the “Hello”. The idea being, as the page receives messages, it will stack them on top of each other just like that.
2. Add socket.io to the client and server
Commit: https://bitbucket.org/hasenj/flask-sockets-tutorial/commits/188ea626622dff233452baeae0a09873
Socket.io is a library that make working with sockets easier.
I grabbed the Javascript client from here:
curl https://raw.github.com/LearnBoost/socket.io-client/0.9.11/dist/socket.io.min.js> static/socket.io.js
For the server, we’ll use gevent-socketio
. Install it via pip (while the virtual environment is active!) and don’t forget to add it requirements.txt
pip install gevent-socketio
We have to switch the server from the built-in flask server to a gevent server. gevent-socketio
comes with the SocketIOServer
function that helps glue things together.
app.debug = True
port = 6020
SocketIOServer(('', port), app, resource="socket.io").serve_forever()
However, this will lose the auto reloading. Luckily, werkzeug has a run_with_reloader
decorator that can help us here:
@werkzeug.serving.run_with_reloader
def run_dev_server():
app.debug = True
port = 6020
SocketIOServer(('', port), app, resource="socket.io").serve_forever()
So now our “main” entry just calls run_dev_server:
if __name__ == "__main__":
run_dev_server()
Also, and this is very important, we have to call gevent.monkey.patch_all()
from gevent import monkey
# ..
monkey.patch_all()
I placed it at the top.
3. Setting up the socket endpoint on the server
Commit: https://bitbucket.org/hasenj/flask-sockets-tutorial/commits/192eb79c5ba0e3ead19a59075c38e18b2145e57d
When a client connects to a socket.io enabled server, it will always go to /socket.io/
. Even though you may never explicitly visit this url on the client side, this is where it goes when you connect to the server using socket.io.
@app.route('/socket.io/<path:rest>')
def push_stream(rest):
try:
socketio_manage(request.environ, {'/shouts': ShoutsNamespace}, request)
except:
app.logger.error("Exception while handling socketio connection",
exc_info=True)
Please note that we ignore the rest
parameter. It’s something that socket.io takes care of.
The function socketio_manage
takes care of setting up the socket for that connection. The second argument maps “urls” to “namespace classes”.
You can think of the url as a channel, and the namespace class manages sockets in that channel.
Read the docs: https://gevent-socketio.readthedocs.org/en/latest/namespace.html
The important bit in the following code is that we’re adding each new socket to a list that can be accessed globally (as a class variable) and we add a class method that can broadcast a message to all connected sockets:
class ShoutsNamespace(BaseNamespace):
sockets = {}
def recv_connect(self):
print "Got a socket connection" # debug
self.sockets[id(self)] = self
def disconnect(self, *args, **kwargs):
print "Got a socket disconnection" # debug
if id(self) in self.sockets:
del self.sockets[id(self)]
super(ShoutsNamespace, self).disconnect(*args, **kwargs)
# broadcast to all sockets on this channel!
@classmethod
def broadcast(self, event, message):
for ws in self.sockets.values():
ws.emit(event, message)
The methods recv_connect
and disconnect
are called by the socketio framework. We’re overriding them to hook into events so that when a new client is connected, we add its socket to the list, and when it disconnects, we remove its socket from the list.
Note: the method disconnect
does some necessary work behind the curtains, so it’s important to call the same method on the superclass!
Now we have things setup so that when clients connect to a socket on “/shouts”, we can broadcast something to all of them by calling ShoutsNamespace.broadcast
4. Connect the client to the server
Commit: https://bitbucket.org/hasenj/flask-sockets-tutorial/commits/d158b2214120bcfcef41e038e39eeeaeca053771
The client side code is probably the simplest part in all of this. Simply call io.connect('/shouts');
to get a socket object, then write some event handlers on the socket.
$(document).ready(function() {
var socket = io.connect('/shouts');
socket.on('connect', function() {
console.log("socket connected");
});
socket.on('disconnect', function() {
console.log("socket disconnected");
});
socket.on('message', function(data) {
console.log("Got message:", data);
addMessage(data);
});
});
The important thing here is the ‘message’ event handler: it calls our function addMessage
.
In this case, we’re expecting 'data’ (the thing sent over the socket) to be a string. This is not a limitation! It can be a complex nested JSON object. I’m just using strings here to make things simple.
5. Broadcast messages from the server to the client
Commit: https://bitbucket.org/hasenj/flask-sockets-tutorial/commits/9e2e854a46e1a8bc233dc5e879795db40ef00550
We’ll create the shout endpoint:
@app.route("/shout", methods=["GET"])
def say():
message = request.args.get('msg', None)
if message:
ShoutsNamespace.broadcast('message', message)
return Response("Message shouted!")
else:
return Response("Please specify your message in the 'msg' parameter")
This allows anyone to shout a message via the /shouts
end point.
Try it yourself! Open http://localhost:6020/shout?msg=PushIt in a new window and watch the main page get updated instantaneously! Open several pages and then shout a message again, and behold as they all updated in real time!
Summary
In this tutorial we did the following:
- Change the flask app setup to use the SocktIOServer
- Create a simple socketio namespace class that can broadcast a message.
- Connect to the socket from the client side and receive push events.
Notes
We barely touched the surface! There’s a lot more! For instance gevent-socketio has mixins that can help with broadcasting.
We used socket.io (a framework) instead of dealing with sockets directly. Socket.io handles events and namespaces, and it also handles automatically reconnecting to the socket server if it gets disconnected.
It’s all self contained, and we’re using only python! No redis, no message queues server, no node.js! Just python! (talking about the server side here).
Originally, I tried to make this work with pypy, but unfortunately gevent is not compatible with pypy.
This setup seems to lose the werkzeug debugger. I tried to re-enable it using wekzeug’s DebuggedApplication class, but it seemed to breaks sockets. I haven’t figured out why exactly.