Wednesday, December 24, 2008

收藏 Using Django with Orbited http://darkporter.com/?p=7


Using Django with Orbited

First off, I have no idea why you want to. For most people there isn’t a reason to do this, but I did. I’ll trust you have your reasons as well.

Here’s our end result: Clients (browsers) connect to our Django-powered site and establish a comet connection. Now we can push messages to the clients, right from our Django code, at any time and with as much frequency as we like. Since it’s real streaming comet, each message goes over a persistent connection with no individual overhead.

Prerequisties: I’m assuming you’re using a real Djanjo setup (in a mod_python instance loaded from Apache).

Apache

The first thing to do is put Apache in Worker MPM mode, limited to a single process. This gives a single instance of mod_python/Django which is called into by many simulateous threads. Doing this allows us to have all waiting comet connections reference the same variables, and it makes it easy to signal the comet threads to wake up when they have something to send.

Making Apache use Worker MPM means recompiling it. I used these configure options, but I’ll admit I’m unclear what overrides what and what is redundant:

./configure --with-mpm=worker --enable-modules=all --enable-mods-shared=all
--enable-ssl --enable-proxy --enable-proxy-http --enable-rewrite --enable-deflate

Then you add this to your apache config file. You may need more than 100 threads depending on your application:


ServerLimit 1
ThreadLimit 100
StartServers 1
MaxClients 100
MinSpareThreads 1
MaxSpareThreads 100
ThreadsPerChild 100
MaxRequestsPerChild 0

Django

Set up Djanjo in the normal fashion, by installing mod_python then creating a Django project and app. I’m on a Mac so my document root is /Library/WebServer/Documents, and I set up my Django app in /Library/WebServer/darkporter. You should have something like this in your httpd.conf, and a Media symlink in Documents pointing to your django’s Media folder.


SetHandler python-program
PythonHandler django.core.handlers.modpython
SetEnv DJANGO_SETTINGS_MODULE darkporter.settings
PythonDebug On
PythonPath "['/Library/WebServer'] + sys.path"

SetHandler none

Orbited

Download and unzip Orbited (I used 0.6.0), and copy the /orbited subfolder under your django app root.

Here’s the idea: Your django app serves up a page on port 80 which includes all the JavaScript to make a comet connection to Orbited (on port 8000), then Orbited proxies over to your Django app which is also listening on port 9000.

The key here is we’ll be adding code to the top level __init__.py file in your Django app. This code only runs once (because of our single-process Apache), and it can set up not only Orbited and our custom comet handler, but it can also spawn any threads that might generate new messages to the client. Say for example your server is running iTunes, and you want to send a comet message whenever a new song plays. You can start a thread that polls iTunes every second and send updated conditions to the client.

First, starting orbited. That’s the easy part. You just add this to __init__.py:

import thread
from orbited import start

thread.start_new_thread(start.main, ())

There’s no reason we have to start Orbited from here, we could do it from the command line with start.py, but putting it here makes everything self-contained. All you have to do is start Apache and everything runs.

You also have to make one small change to Orbited itself, in start.py at the end, change the “reactor.run()” line to:

reactor.run(installSignalHandlers=0)

Now add this to the orbited.cfg in your /etc folder:

[access]
* -> localhost:9000

Comet Handler
Now the browser should be able to make a successful comet connection to Orbited, but Orbited proxies over to port 9000 and no one’s listening. So we add more code to __init__.py, like this (sorry the indentation is all messed up):

def listen():
comet_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
comet_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
comet_socket.bind( ('', 9000) )
comet_socket.listen(5)

while 1:
thread.start_new_thread(comet.handle_request, comet_socket.accept())

thread.start_new_thread(listen, ())

This could and probably should use Twisted, but I don’t know twisted very well. Now you’re going to create a comet.py file also at the root of your django project, this will house your comet handler that deals with comet connections on port 9000 (coming from Orbited). What you do here is up to you, but mine looks like this:

import threading

signal = threading.Condition()
messages = []

def handle_request(incoming_socket, incoming_address):
global signal, messages
while 1:
signal.acquire()
signal.wait()
try:
# Send something useful here
incoming_socket.send("Comet!")
except:
signal.release()
incoming_socket.close()
return
signal.release()

The thread just blocks until someone calls notifyAll() on the signal. They could do it from anywhere, from a Django view, from the iTunes polling thread I mentioned, from wherever. They’d just say:

comet.signal.acquire()
# Mess with shared variables, like comet.messages
comet.signal.notifyAll()
comet.signal.release()

Apache Proxy
We need a couple more things in the httpd.conf now:


SetHandler none



SetHandler none


ProxyPass /tcp http://localhost:8000/tcp
ProxyPass /orbited/ http://localhost:8000/

/tcp is the path that the Orbited javascript calls for when it tries to connect to the TCPSocket, and proxying it allows the Orbited javascript to use an xhr stream (since it’s the same port and same domain).
/orbited allows us to fetch the orbited static files like javascript, using only a root path. Instead of having to do this which depends on your particular hostname:

We can use a nice absolute path instead:

Clientside
The serverside should all be in place now. Use “apachectl start” and go to http://localhost and see what happens. If you get an error it will show up in the browser or in the Apache error logs. Assuming you can view a page, now we need to create a comet-enabled page.
Include the Orbited.js script as shown above, then add this javascript to initiate a comet connection:

var socket = new Orbited.TCPSocket(); // Orbited.settings.port defaults to 8000
socket.onerror = function() { alert('error'); }
socket.onopen = function() { alert('open'); };
socket.onclose = function() { alert('close'); }
socket.onread = function(s) { alert("Received string through comet -- " + s); }
socket.open("myserver.org", 9000);

Fill in the onread() function with something useful, and away you go!

No comments: