#!/usr/bin/python # I wanted to play with Ajax Push with Twisted Python, so I thought # I'd start with the old standby: a chat app. The first step is to # see what happens when you hold a connection open for an arbitrarily # long time with Twisted Web: how do you tell when the connection # closes, so as to clean up resources? How do you avoid memory leaks? # Do the connections get flushed at reasonable times?
# On memory leaks: if I open 1000 chat sessions to this program, the # first time its memory usage grows from 7M to 13M; the second time # its memory usage grows from 13M to 16M; and the third time its # memory usage does not grow. I think that means I avoided memory # leaks. # I tested this program with Twisted 2.0 under Python 2.3.3 and 2.3.5. from twisted.web import resource, server, http from twisted.internet import reactor import random # iframe closing tags are apparently necessary. tophtml = """<html><head><title>Chat channel</title> <style> iframe { border: 0 } </style> </head><body> <h1>HTML chat demo</h1> <p>Inspired by Ka-Ping Yee's awesome <a href="http://zesty.ca/chat/" >libdynim GIF chat app</a>.</p> <!-- Like it, this uses no Java, DHTML, or reloading; unlike it, this uses frames and no JavaScript (Ping's demo works with or without JavaScript, but without JavaScript, it reloads the page when you speak.) --> <iframe width="100%%" height="50" src="?frame=form;sess_id=%(sess_id)s"> </iframe> <iframe width="100%%" height="300" src="?frame=output;sess_id=%(sess_id)s"> </iframe> </body></html> """ formhtml = """<html><head><title>Chat post form</title></head><body> <form method="POST"> <input name="sess_id" type="hidden" value="%(sess_id)s" /> to: <input name="destination" /> say: <input name="chatline" size="80" /> <input type="submit" value="send" /> </form> </html> """ class ChatSession: "A persistent connection to a user's browser." def __init__(self, channel, request, sess_id): (self.channel, self.request, self.sess_id) = (channel, request, sess_id) self.deferred = request.notifyFinish() self.deferred.addCallback(self.stop) self.deferred.addErrback(self.stop) def stop(self, reason): "Called when the request finishes to close the channel." print "%s stopping: %s" % (self.sess_id, reason) self.channel.delete_session(self.sess_id) def sendmsg(self, origin, msg): "Display a chat message to the user." self.request.write("""<div> <%(origin)s> %(msg)s </div> """ % {'origin': origin, 'msg': msg}) class ChatChannel(resource.Resource): "A resource representing a chat room, plus private messages." isLeaf = True def __init__(self): resource.Resource.__init__(self) # ??? necessary?? self.sessions = {} def render_GET(self, request): "Handle HTTP GET requests by dispatching on 'frame' arg." if request.args.has_key('frame'): frame = request.args['frame'][0] if frame == 'form': return self.render_form(request) elif frame == 'output': return self.do_chat_output(request) sess_id = random.randrange(1000) # not secure, like everything here return tophtml % {'sess_id': sess_id} def render_form(self, request): "The form used for posting." return formhtml % {'sess_id': request.args['sess_id'][0]} def do_chat_output(self, request): "Open a persistent ChatSession." sess_id = request.args['sess_id'][0] # Note that this may remove a previous ChatSession from # self.sessions: self.sessions[sess_id] = ChatSession(self, request, sess_id) # The next line is a hack due to Donovan Preston: increases # browsers' per-server connection limit, which is normally 2 # if the server seems to support HTTP 1.1 connection # keepalive, to 8. request.setHeader('connection', 'close') request.write("<html><head><title>session %s</title><body>\n" % sess_id) return server.NOT_DONE_YET def render_POST(self, request): "Send back 204 No Content to an utterance of a chat line." def arg(name): return request.args[name][0] self.handle_chatline(arg('destination'), arg('sess_id'), arg('chatline')) request.setResponseCode(http.NO_CONTENT) return "" def handle_chatline(self, dest, sess_id, chatline): "Send a chat line from a source to a destination, '' meaning 'all'." try: if dest: self.sessions[dest].sendmsg(sess_id, '(private) ' + chatline) self.sessions[sess_id].sendmsg('server', 'private message sent') else: for session in self.sessions.values(): session.sendmsg(sess_id, chatline) except Exception, e: self.sessions[sess_id].sendmsg('error', str(e)) def delete_session(self, sess_id): "Delete a session by ID --- if it's there." try: del self.sessions[sess_id] except KeyError: pass # We put the side-effecting stuff here so this can be either imported # as a module and used inside some other twisted.web server (which I # haven't tried), or run on its own, on port 8086. if __name__ == '__main__': port = 8086 reactor.listenTCP(port, server.Site(ChatChannel())) print "ok, running on port", port reactor.run()