Thursday, February 16, 2012

Websockets and Python

I had recently built a push notification framework using AJAX and a FireFox quirk described here (http://ajaxpatterns.org/HTTP_Streaming) with a Tornado webserver to keep the socket open ala long poll, however, this only works on Firefox and is really a kludgy way to do things so I recently bit the bullet and dove into HTML5 Websockets.

Even though Websockets are not supported on Firefox yet (and probably not IE, I don't know and after working with JavaScript over the past 10 years I really don't care about IE) I think moving forward this is the way to go. What I discovered is using Websockets is really really easy. I copied some basic code from StackOverflow (http://stackoverflow.com/questions/4372657/websocket-handshake-problem-using-python-server) modified it to be a little easier on the eyes and included a universal timer tick so all users can see they are in sync in real-time, and viola - we have the following 2 really small code snippets.

To test this app on your environment you will need a separate web server that can serve out the .html page to a browser on the web. I use APACHE but any web server will do including the one built into python which can be run like this ...

sudo python -m SimpleHTTPServer 80

If you want it to handle port 80 (http), or ...

python -m SimpleHTTPServer 8888

If you want it to run on a simple user port.

Once you have that working so you can point a browser to your client.html page and run it, you can start your python server (the code below) like this ...

python server.py

This assumes you name the client file 'client.html' and the server file 'server.py'. Note this is not an optimal solution as I am not sure how well this approach will scale (python threads are not real fast). I usually use Tornado for my personal projects that have to scale but I read that Tornado is not working well with Websockets yet. If anyone knows otherwise, please let me know.

Anyway, assuming all is well, this will show a page with a universal timer tick which should be in sync across all connected browsers/users and a message area where any messages sent from anyone currently connected are displayed.

Don't forget to modify your code to use your IP address. The IP address in the code points to a mini dev server I have which may or may not be active when you try out the code. Also, I have only tested it on Safari. It may or may not work on other browsers.

Now that I have this code working I can use it to build my HTML5 2D Game Engine framework which I will release in a future post.

Now on to the actual code ...


First, the client code (JavaScript in HTML) ...
<!DOCTYPE html>
<html lang="en">
<head>
<title>Test</title>
<script type="application/javascript">
var ws;

function init() {
var servermsg = document.getElementById("servermsg");

ws = new WebSocket("ws://174.143.214.70:9876/");
ws.onopen = function(){
servermsg.innerHTML = servermsg.innerHTML + "<br>Server connected";
};
ws.onmessage = function(e){
da = e.data.split(":");
cmd = da[0];
data = da[1];
// servermsg.innerHTML = servermsg.innerHTML + "<br>Recieved data: " + e.data;
if (cmd == "tick"){
tic.innerHTML = data;
}else{
servermsg.innerHTML = servermsg.innerHTML + "<br>" + data;
}
};
ws.onclose = function(){
console.log("Server disconnected");
servermsg.innerHTML = servermsg.innerHTML + "<br>Disconnected";
};
}
function postmsg(){
var text = document.getElementById("message").value;
ws.send(text);
servermsg.innerHTML = servermsg.innerHTML + "<br>Sent: " + text;
return false;
}
</script>
</head>
<body onload="init();">
<span id="tic">0</span><br/>
<input type="text" name="message" value="" id="message">
<button onClick="postmsg();">Send</button>
<div id="servermsg" style="overflow:auto; height:400px; width:400px; border:1px solid black"><h1>Message log:</h1></div>
</body>
</html>

And next, the server code (written in Python as the title implies) ...


#!/usr/bin/env python

import socket
import threading
import struct
import hashlib
import binascii
import time

PORT = 9876

# evil globals
requestList = []
exitFlag = 0
tic = 0
lock = threading.Lock()
clients = []

# timer tick
class ThreadClass(threading.Thread):
def run(self):
global exitFlag
global tic
global requestList
global lock
global clients
invalidConnections = []
while exitFlag == 0:
data = "tick:%s" % (tic)
data = chr(0) + data + chr(255)
print data
# Broadcast tic data to all clients
lock.acquire()
[conn.send(data) for conn in clients]
lock.release()
tic = tic + 1
time.sleep(1)

print "\nNotice - exiting thread !"


def create_handshake_resp(handshake):
final_line = ""
lines = handshake.splitlines()
for line in lines:
parts = line.partition(": ")
if parts[0] == "Sec-WebSocket-Key1":
key1 = parts[2]
elif parts[0] == "Sec-WebSocket-Key2":
key2 = parts[2]
elif parts[0] == "Host":
host = parts[2]
elif parts[0] == "Origin":
origin = parts[2]
final_line = line

spaces1 = key1.count(" ")
spaces2 = key2.count(" ")
num1 = int("".join([c for c in key1 if c.isdigit()])) / spaces1
num2 = int("".join([c for c in key2 if c.isdigit()])) / spaces2

token = hashlib.md5(struct.pack('>II8s', num1, num2, final_line)).digest()

return (
"HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
"Upgrade: WebSocket\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Origin: %s\r\n"
"Sec-WebSocket-Location: ws://%s/\r\n"
"\r\n"
"%s") % (
origin, host, token)


def handle(s, addr):
global exitFlag
global lock
global clients
# acknowledge the connection
data = s.recv(1024)
s.send(create_handshake_resp(data))

while exitFlag == 0:
print "Waiting for data from", s, addr
data = s.recv(1024)
print "Done"
if not data:
print "No data"
break

print "%s Bytes of Data recvd from %s --->%s" % (len(data), addr, data)
data = data[1:-1]
data = chr(0) + "msg:" + data + chr(255)

# Broadcast received data to all clients
lock.acquire()
[conn.send(data) for conn in clients]
lock.release()

print 'Client closed:', addr
lock.acquire()
clients.remove(s)
lock.release()
s.close()

def start_server():
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('', PORT))
s.listen(1)
while 1:
conn, addr = s.accept()
print 'Connected by', addr
clients.append(conn)
threading.Thread(target = handle, args = (conn, addr)).start()

t = ThreadClass()
try:
t.start()
start_server()
except (KeyboardInterrupt, SystemExit):
print "\nNotice - CTL+C Received, Shutting Down, 1 Sec ..."
exitFlag = 1
time.sleep(2)
# note - should probably use join here
print "\nAll Clean, Exiting Server."





That's it. Its just that simple.

1 comment:

Unknown said...

Hi,
Tried out your code as discussed, using FireFox 13 on Ubuntu 11.10 and i get a connected by message in the Python Shell but when i try to send a message to the server i get a "can't establish a connection to server" error in the Web Console.

Please help