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.