#!/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>
        &lt;%(origin)s&gt; %(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()

Reply via email to