Is it just me, or do people not see photography as a valid hobby?
Here’s a picture of my cats being very supportive of my homework:
Turns out people look at you funny when they ask what you’re doing and you say “… I’m taking pictures of my cats.”
Anyway, on to technology…
Comet! It’s new! It’s improved! Long polling is awesome! And Apache really sucks at it!
Luckily, though, the workflow project isn’t using Apache. It’s using Python Twisted, which is event loop based, and makes server-push stuff like long polling remarkably easy. But why flap my lips when I can show off code?
<html> <head> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script> </head> <body> <div class="chat-messages" style="width:300px;height:400px;border:1px solid black"> </div><br/> <input type="text" class="chat-new"/> <button class="submit">Submit</button> </body> <script src="main.js"></script> </html>
Okay, that’s not very exciting. Create a webpage with a large textbox, a small textfield, and a submit button? Let’s find something a little more invigorating:
var myId = -1; function startLongPoll() { // Open the long poll connection $.ajax({“url”: “/ajax/open”, “data”: {“id”: myId}}).done(function(html) { var obj = eval(“(”+html+“)”);
// We got a new chat message! Add it to the chat-messages box
var text = obj.from+": "+obj.text;
$(".chat-messages").append("<span>"+text+"</span><br/>");
// The server closed the connection, so reopen it
startLongPoll();
}); }
$(document).ready(function() { // Join the long poll server $.ajax({“url”: “/ajax/join”}).done(function(html) { var obj = eval(“(”+html+“)”); myId = obj[“id”]; startLongPoll(); });
$(“.submit”).click(function() { $.ajax({“url”: “/ajax/chat”, “data”: {“id”: myId, “text”: $(“.chat-new”).val()}}); }); });
Okay, that’s more like it. This is the main.js code that the index file referred to, and makes up the entirety of the client code. Essentially, the function startLongPoll() opens a long-running background AJAX request to /ajax/open. The server will keep the /ajax/open request open until it needs to send some data, at which point the AJAX request will close and trigger the done() event which is also defined in startLongPoll(). Now for the server code:
from twisted.web import server, static, resource from twisted.internet import reactor import json, base64
class Client: def init(self): self.current_request = None pass
def send(self, s): if self.current_request: self.current_request.write(s) self.current_request.finish() self.current_request = None
def quit(self): self.send(json.dumps({“type”: “status”, “message”: “goodbye”})) pass
def open(self, request): self.current_request = request
clients = []
class MyHandler(resource.Resource): def init(self): resource.Resource.init(self)
class WebHandler(resource.Resource): isLeaf = True def init(self): resource.Resource.init(self)
def render_GET(self, request): command = “/”.join(request.postpath) print command if command == “join”: # Create a client, send them their ID clients.append(Client()) return json.dumps({“id”: len(clients)-1})
# Everything else *should* have an associated ID
if "id" not in request.args:
print "ERROR"
return ""
client_id = int(request.args["id"][0])
if command == "quit":
clients[client_id].quit()
return json.dumps({"id": client_id, "message": "goodbye"})
# Opens a long-push socket...
if command == "open":
clients[client_id].open(request)
return server.NOT_DONE_YET
# They're sending us a chat message.
if command == "chat":
# Send it to everyone else
i = 0
for client in clients:
client.send(json.dumps({"from": client_id, "text": request.args["text"][0]}))
i += 1
return json.dumps({"id": client_id, "message": "sent"})
return json.dumps({"id": client_id, "message": "invalid-command"})
root = MyHandler() root.putChild(“static”, static.File(“.”)) root.putChild(“ajax”, WebHandler()) reactor.listenTCP(9090, server.Site(root)) reactor.run()
Now, there’s what I call code. This is the server portion. At the beginning we define the Client class, which handles all of the long polling requests. The important thing to note about this is the send() function, which writes the given data to the (still open) request, and closes the request. Remember that in the client code, when a request is closed, we add the data to the chat-messages box, and re-open the connection at /ajax/open.
The clients variable is a list of these Client objects. Then we define a MyHandler class that doesn’t do anything. Then we have the WebHandler class, which does most of the processing. The important thing to note is the function render_GET, but more specifically, the “if command == ‘foobar’” statements within it. Each one corresponds to a different URL.
The first such statement just creates a Client object for anyone who hits that URL.
if command == "join": # Create a client, send them their ID clients.append(Client()) return json.dumps({"id": len(clients)-1})
This is how clients know their ID. After the client has found their ID, they hit the open URL:
if command == "open": clients[client_id].open(request) return server.NOT_DONE_YET
Also pretty straight-forward. The client_id variable is passed as a GET argument. Note the “return server.NOT_DONE_YET” line - This tells the Twisted event handler that we have processed the request, and that we’re going to respond to it later. We remember the request object in the Client class, and when the send() function is called is when we write the data to the request and close it.
if command == “chat”: # Send it to everyone else i = 0 for client in clients: client.send(json.dumps({“from”: client_id, “text”: request.args[“text”][0]})) i += 1
return json.dumps({“id”: client_id, “message”: “sent”})
When a human presses the “submit” button, the client hits the chat URL. Again, this code is pretty simple: Broadcast what the chat message was to all of the clients. The clients will receive the chat message, and reopen the long-polling connection.
The end result is a chat app that’s real time, without having the workload or lag of pinging the server once or twice a second for every client.
Back to the workflow project: This technology has now been incorporated, and is used to enable collaboration between multiple people on the same workflow, Google-docs style.
Is that impressive or what?