Saturday, July 16, 2011

CherryPy, Server Sent Events, and You.

There was a dearth of decent information on how to implement server side events for anything but PHP, so, being a mediocre python coder, I felt the need to correct this for my favorite web framework. A far better general (or client-side) writeup is at HTML5Rocks. The basic idea is pretty straightforward, subscribe to an event stream on the client, the server then sends a message when something happens, and there you go.

Because of the limits of HTTP, it's not quite "true" push. It's closer in implementation to typical Comet pattern, but an order of magnitude easier to actually use. This does have an effect on how you'd implement it in CherryPy, however.

First, our simple HTML file.

<html>
 <head>
  <title>Server Side Event Test</title>
  
 </head>
 <body>
  <script type="text/javascript">
   document.addEventListener('DOMContentLoaded', function () {
  
     var timeSrc  = new EventSource('sendTime');     
     timeSrc.addEventListener('time', function (event) {
     document.getElementById("Test").innerHTML += "\n " + event.data
     
     });
   
   
   }, false);
  </script>
  
  Test box! 
  <br/>
  <textarea id="Test"></textarea>

 </body>
</html>
Pretty straightforward. That sets up our event listener to listen for an event named 'time', from the 'sendTime' URL. (And appends it to a text box, to make it easier to see.).

import cherrypy
import os.path
import json
import time
from cherrypy.lib.static import serve_file

current_dir = os.path.dirname(os.path.abspath(__file__))


class Root():
    
    #Our toggle variable.
    timeFeedEnabled = False
    
    @cherrypy.expose
    def index(self):
        return serve_file(os.path.join(current_dir, 'index.html'), content_type='text/html')
        #serves our index client page.
    
    @cherrypy.expose
    def timeSwitch(self):
        #When this page gets requested, it'll toggle the time feed updating
        #and return an OK message.
        if self.timeFeedEnabled:    
            self.timeFeedEnabled = False
        else:
            self.timeFeedEnabled = True
        return "Feed Toggled"
    
    @cherrypy.expose
    def sendTime(self):
        #Set the expected headers...
        cherrypy.response.headers["Content-Type"] = "text/event-stream"
        if self.timeFeedEnabled:
            return "event: time\n" + "data: " + str(time.time()) + "\n\n";
        else:
            pass
        

if __name__ == '__main__':
    pageroot = Root()
    #And, standard cherrypy quickstart.
    cherrypy.quickstart(pageroot, config="test.conf")
No big shockers here. The main thing you might wonder about, is why our sendTime url is just a normal request handler. That's the underlying comet pattern helping us out a little. What'll happen, is the browser will ping the server, and if the feed is toggled on, it'll send the info. Which means, if you only want the feed to update when you have something worth updating, you'll need something similar to the timeFeedEnabled variable I have set up. If you don't, it'll just pass out, and it'll keep working.

If you want the feed to stop completely and not come back until a page refresh or other events (say, on log out or something), you can call  .close() on the javascript side, or change the headers to something other than 'text/event-stream', which will also make the web browser disconnect without retrying.

This is a quick and dirty example without any error checking, security, or any real robustness. For example, you'd want authentication and session stuff for anything major, obviously, as well as making sure the event-stream came from your domain. But it does show how to do it, without getting overly complicated.

3 comments:

  1. For the event stream to work correctly sendTime needs to be configured to be a response stream in the config file, or however you configure cherrypy

    ReplyDelete
  2. Yes, this is not a correct SSE example. You need to use response streams in cherrypy. This only works "by accident" since the browser will (should) reconnect after ~3 seconds. (And actually it only seems to reconnect a couple times in Firefox before giving up.)

    ReplyDelete
  3. anyone has a fixed example? with the 'response.stream' ?
    i tried, but i still get 3 seconds poll, which mean, probably, that i failed...

    ReplyDelete