Quantcast
Channel: AFPy's Planet
Viewing all articles
Browse latest Browse all 3409

[hautefeuille] Websocket : Python et Gevent

$
0
0

Introduction

L’objectif est de construire un système minimaliste de discussion en temps réel. Les technologies les plus prometteuses pour réaliser cette tâche sont les WebSockets. Les websockets permettent l’ouverture d’un canal bidirectionnel entre un serveur et un client. Les échanges sont plus rapides et les messages échangés sont de tailles réduites. Les gains en production sont évidents : amélioration des performances, allégement de la consommation de la bande passante. On utilisera ici la librairie Gevent pour encapsuler notre application websocket. Le code devra gérer l’echo en broadcast des messages, le buffering des messages et la surveillance des connexions cassées ou perdues.

Prototype de l’application

#!/usr/bin/python
#-*- coding : utf-8 -*-

# @author : julien@hautefeuille.eu
# @date : 06/18/2012
# @version : 0.5
# @install : gevent, gevent-websocket

import os
import datetime
import json
import gevent
from gevent.pywsgi import WSGIServer
import geventwebsocket
from gevent import monkey
monkey.patch_all()

class BroadcastServer(object):
        def __init__(self):
                self.buffer = []
                self.all_socket = set()
                self.fail_socket = set()
                path = os.path.dirname(geventwebsocket.__file__)
                agent = "gevent-websocket/%s" % (geventwebsocket.__version__)
                print "Running %s from %s" % (agent, path)
                self.server = WSGIServer(("0.0.0.0", 443), self.websocket_handler,
                handler_class=geventwebsocket.WebSocketHandler)
                self.server.serve_forever()

        def websocket_handler(self, environ, start_response):
                websocket = environ.get('wsgi.websocket')
                session = environ.get('REMOTE_ADDR')
                annonce = '%s connected' % session

                if websocket is None: # Switch to standard http mode
                        return self.http_handler(environ, start_response)

                websocket.send(json.dumps({'buffer': self.buffer}))
                self.broadcast_message(json.dumps({'annonce' : annonce}))
                websocket.send(json.dumps({'annonce' : "You are welcome"}))

                try:
                        while True:
                                time = datetime.datetime.now()                  # Getting date/time
                                message = websocket.receive()                   # Receiving message from client

                                if message is None:
                                        annonce = '%s deconnected' % session
                                        self.broadcast_message(json.dumps({'annonce' : annonce}))
                                        break

                                premessage = json.dumps({'ip' : session,
                                'date' : str(time), 'message' : message})

                                self.tracking_socket(websocket)                 # Tracking
                                self.buffer.append(premessage)                  # Buffering
                                self.cleaning_buffer()                                  # Optimize buffer
                                self.broadcast_message(premessage)              # Broadcast prepared message
                                self.cleaning_socket()                                  # Cleaning broken sockets

                        websocket.close()                                                       # Clean socket closing
                except geventwebsocket.WebSocketError, ex:
                        print "%s : %s" % (ex.__class__.__name__, ex)

        def http_handler(self, environ, start_response):        # Standard http mode
                if environ["PATH_INFO"] == "/":
                        start_response("200 OK", [("Content-Type", "text/html")])
                        return open('index.html').readlines()
                else:
                        start_response("400 Bad Request", [])
                        return ["WebSocket connection is expected !"]

        def broadcast_message(self, message):
                for s in self.all_socket:
                        try:
                                s.send(message)
                                print "Send to all"
                                print message
                        except Exception:
                                self.fail_socket.add(s)
                                print "Failed sockets"
                                print self.fail_socket
                                continue

        def tracking_socket(self, socket):
                if socket not in self.all_socket:
                        self.all_socket.add(socket)
                        print "socket added"
                        print self.all_socket

        def cleaning_socket(self):
                if self.fail_socket:
                        for s in self.fail_socket:
                                print "Trying to close socket"
                                s.close()
                                if s in self.all_socket:
                                        self.all_socket.discard(s)
                        self.fail_socket.clear()
                        print "Socket remove"

        def cleaning_buffer(self):
                if len(self.buffer) > 10:
                        del self.buffer[0]
                        print "Buffer cleaned"

if __name__ == "__main__":
        app = BroadcastServer()

Le client en Javascript (jQuery)

<!DOCTYPE html>
<meta charset="utf-8" />
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script type="text/javascript">
        window.onload = function() {

        var ws_uri = "ws://192.168.0.33:443";
        ws = new WebSocket(ws_uri);

        ws.onmessage = function (event) {

                var result = jQuery.parseJSON(event.data);

                if (typeof(result.buffer) != 'undefined') { // buffer message
                        $.each(result.buffer, function(i, object) {
                                var buffer_data = jQuery.parseJSON(object);
                                $('#affichage').append(buffer_data.ip + ' ' + buffer_data.date + ' ' + buffer_data.message + '*<br>');
                        });
                };

                if (typeof(result.date) != 'undefined') { // New posted message
                        $('#affichage').append(result.ip + ' ' + result.date + ' ' + result.message + '<br>');
                };

                if (typeof(result.annonce) != 'undefined') { // Annonce
                        $('#affichage').append(result.annonce + '<br>');
                };

                console.log("Got echo : " + event.data);
        };

        ws.onopen = function (event) {
                $('#status').html('<b>Status : connection opened</b>')
                ws.send("Enters...")
                console.log('Web Socket State::' + 'OPEN');
        };

        ws.onclose = function (event) {
                var code = event.code;
                var reason = event.reason;
                var wasClean = event.wasClean;
                $('#status').html('<b>Status : connection losed</b>');
                console.log('Web Socket State::' + 'CLOSED ' + event.code + ' ' + event.reason + ' ' + event.wasClean);
        };

        ws.onerror = function(event) {
                $('#status').html('<b>Status connection error</b>');
                console.log('Web Socket State::' + 'ERROR');
        };

        send_area = function() {
                var r = $('#area').val();
                ws.send(r);
        };
}
</script>
</head>
<title>ShootaWall</title>
<div id="zone">
<textarea rows="4" cols="50" id="area"></textarea>
<button onclick='send_area();'>Send</button>
</div>
<div id="status">Not Connected</div>
<div id="affichage"></div>
<style>
#status { background: #ddd;}
</style>
</html>

Conclusion

J’ai apprécié la puissance et la simplicité de la librairie Gevent. Les personnes habituées au développement tradionnel d’applications web devront apprendre ou ré-apprendre à penser en terme de canaux bidirectionnels, ce n’est pas forcément naturel. Je pense qu’il pourrait être bénéfique d’utiliser des librairies dédiées à l’échange de messages de ce type (AutoBahn, ZeroMQ). Il me semble que sur une application plus conséquente, l’échange par message peut rapidement devenir un casse-tête. J’aime la structuration de l’application aidée par Gevent. J’aime beaucoup moins l’idée d’un développement Javascript du côté client. A mon goût, le code peut devenir difficilement maintenable. Je ne suis néanmoins pas un développeur Javascript qui possède sans doute une méthodologie particulière de développement.

Je cherche à présent une solution élégante pour authentifier des utilisateurs sur une application websocket. Les websockets s’appuyant sur http, je devrais trouver mon bonheur dans les solutions existantes.


Viewing all articles
Browse latest Browse all 3409

Trending Articles