the other day i tried to come up with a minimalistic Ajax setup — minimalistic in the sense of minimal coding overhead. my ingredients of choice were jquery and JavaScript on the client side and python on the server side. the goal was to use both GET and POST REST calls for obtaining the dynamic data and utilize JSON as the data format.
the python server code makes use of python’s BaseHTTPServer.HTTPServer
and BaseHTTPServer.BaseHTTPRequestHandler
classes:
AjaxServer
extends BaseHTTPServer.HTTPServer
and supplies Handler
which in turn extends BaseHTTPServer.BaseHTTPRequestHandler
. the Handler
class also implements the GET and POST REST interfaces.
the server supports
text/html
GETs on file names from the document root, retrievingindex.html
for GETs on directories, andapplication/json
GETs on/ajax/time
returning the current time/ajax/processcount
returning the number of currently running processesapplication/json
POSTs on any URL returning the length of the posted content
the client is a simple HTML5 page that uses jquery’s ajax()
support to invoke the GET REST and POST REST calls on our python server.
client code
<!DOCTYPE html> <html lang=”en”> <head> <meta charset=”utf-8”> <title>AJAX sandbox</title> <meta name=”viewport” content=”width=device-width, initial-scale=1.0”> <meta name=”description” content=”not found document”> <meta name=”author” content=”hud@zurich.ibm.com”> <link href=”css/bootstrap.css” rel=”stylesheet”> <style> body { padding-top: 60px; / 60px to make the container go all the way to the bottom of the topbar / } </style><!– Le HTML5 shim, for IE6-8 support of HTML5 elements –> <!–[if lt IE 9]> <script src=”http://html5shim.googlecode.com/svn/trunk/html5.js”></script> <![endif]–> </head> <body id=”home”> <div>OK, let’s go!</div>
<div>current time: <span id=”ajax-datetime”>waiting for time update…</span></div>
<div>current process count: <span id=”ajax-processcount”>waiting for process count update…</span></div>
<div><input id=”typehere” type=”text” value=”type here” /></div> <div id=”ajax-processedinput”>waiting for result</div>
<!– JavaScript at the bottom to speed up page loading –> <script src=”/js/jquery-1.7.2.min.js”></script> <script src=”/js/bootstrap.min.js”></script> <script src=”/js/ajax-datetime.js”></script> <script src=”/js/ajax-processcount.js”></script> <script src=”/js/ajax-processinput.js”></script> </body> </html>
ajax-datetime.js
, ajax-processcount.js
, ajax-processinput.js
javascripts:
ajax-datetime.js
calls the /ajax/time
GET REST method every 3 seconds and sets the content of the #ajax-datetime
div element to the return value of the GET REST call:
$(document).ready(function() { function updateDateTime() { $.ajax({ cache: false, url: “/ajax/time”, type: “GET”, contentType: “application/json” }).done(function(json) { $(“#ajax-datetime”).html(json.time); }); }setInterval(updateDateTime, (3 * 1000));});
ajax-processcount.js
:
$(document).ready(function() {function updateProcessCount() { $.ajax({ cache: false, url: "/ajax/processcount", type: "GET", contentType: "application/json" }).done(function(json) { $("#ajax-processcount").html(json.processcount); }).error(function(json) { $("#ajax-processcount").html("connection to server lost..."); }); } setInterval(updateProcessCount, (5 * 1000));});
ajax-processcount.js
invokes the /ajax/processcount
GET REST method and does so every 5 seconds — demonstrating the asynchronous nature of AJAX rather nicely.
while the previous two javascripts used GET to retrieve information from our python server, the next, ajax-processinput.js
attaches itself to the #typehere
text input field and POSTs the content of the field to /ajax/processinput
, returning the result of the POST REST call via the #ajax-processedinput
div element:
$(document).ready(function() {function processInput() { $.ajax({ cache: false, url: "/ajax/processinput", type: "POST", contentType: "application/json", dataType: "json", data: JSON.stringify({ "input": $("#typehere").val() }) }).done(function(json) { $("#ajax-processedinput").html(json.processed); }).error(function(json) { $("#ajax-processedinput").html("connection to server lost..."); }); } $("#typehere").change(function () { processInput(); });});
ajax(...)
that i’m sending JSON — the contentType: "application/json"
line — jquery will not convert the data i’m sending into JSON and i need to explicitly turn it into JSON myself:
… data: JSON.stringify({ “input”: $(“#typehere”).val() }) …
server code
the server code consists of two modules: ajaxserver
and handler
.
handler
contains the Handler
class which extends BaseHTTPServer.BaseHTTPRequestHandler
and implements the GET and POST REST calls explained earlier:
!/usr/bin/env python2.6
-- encoding: utf-8 --
’‘’ handler.Handler is the HTTP handler for ajaxserver ‘’‘
import BaseHTTPServer import logging as log import os import subprocess
try: import simplejson as json except ImportError: import json
from datetime import datetime
class Handler(BaseHTTPServer.BaseHTTPRequestHandler):
def logRequest(s): log.debug("%s: %s/%s" %(s.client_address, s.command, s.path)) def do_GET(self): self.logRequest() path = self.path if 'Content-type' in self.headers: contentType = self.headers['Content-type'] else: contentType = 'text/html' log.debug("ajaxserver.GET: Path %s" % path) log.debug("ajaxserver.GET: Content-type %s" % contentType) if contentType.startswith('text/html'): (respCode, respContent, respContentType) = self.do_GET_HTML() elif contentType.startswith('application/json'): (respCode, respContent, respContentType) = self.do_GET_JSON() self.send_response(respCode, "thank you") self.send_header('Content-type', respContentType) self.send_header('Content-length', len(respContent)) self.end_headers() self.wfile.write(respContent) def do_POST(self): self.logRequest() path = self.path contentType = self.headers['Content-type'] log.debug("ajaxserver.POST: Path %s" % path) log.debug("ajaxserver.POST: Content-type %s" % contentType) if not contentType.startswith('application/json'): self.send_response(404, "oh, shiny!") self.send_header('Content-type', 'application/json') self.end_headers() self.wfile.write(json.dumps({ "error": "only accepting application/json" })) return contentLength = int(self.headers['Content-length']) content = self.rfile.read(contentLength) log.debug("ajaxserver.POST: Content-length %d" % contentLength) log.debug("ajaxserver.POST: Content: %s" % content) data = json.loads(content) if 'input' not in data: log.debug("ajaxserver.POST: missing input parameter: %s" % content) self.send_response(404, "oh, shiny!") self.send_header('Content-type', 'application/json') self.end_headers() self.wfile.write(json.dumps({ "error": "missing input parameter" })) return self.send_response(200, "oh, shiny!") self.send_header('Content-type', 'application/json') self.end_headers() self.wfile.write(json.dumps({ "processed": len(data['input'])})) return def do_GET_HTML(self): docRoot = self.server.docRoot docPath = "%s%s" % (docRoot, self.path) errorPath = "%s/not-found.html" % docRoot if self.path.startswith('../'): log.debug('ajaxserver.GET(HTML): "%s" trying to escape from the sandbox' % self.path) docFile = open(errorPath, 'r') code = 404 elif docPath.endswith('/') and os.path.exists("%sindex.html" % docPath): log.debug('ajaxserver.GET(HTML): serving %sindex.html for %s' % (docPath, self.path)) docFile = open("%sindex.html" % docPath, 'r') code = 200 elif os.path.exists(docPath): log.debug('ajaxserver.GET(HTML): serving %s for %s' % (docPath, self.path)) docFile = open(docPath, 'r') code = 200 else: log.debug('ajaxserver.GET(HTML): no document found for %s' % self.path) docFile = open(errorPath, 'r') code = 404 doc = docFile.read() docFile.close() return (code, doc, 'text/html') def utilProcessCount(self): psCmd = subprocess.Popen(["ps", "ax"], stdout=subprocess.PIPE) wcCmd = subprocess.Popen(["wc", "-l"], stdin=psCmd.stdout, stdout=subprocess.PIPE) count = wcCmd.communicate()[0] return json.dumps({ 'processcount': int(count) - 1 }) def do_GET_JSON(self): if self.path.startswith('/ajax/time'): return (200, '{ "time": "%s" }' % datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC'), 'application/json') if self.path.startswith('/ajax/processcount'): return (200, self.utilProcessCount(), 'application/json') return (200, '{ "resp": "bark!" }', 'application/json')
ajaxserver
contains the AjaxServer
class which extends the BaseHTTPServer.HTTPServer
class and is the driver of my simple python web server. it also contains the setup code to instantiate the server and start it:
!/usr/bin/env python2.6
-- encoding: utf-8 --
’‘’ ajaxServer is a simple proof-of-concept web server for serving AJAX pages and their contents. ‘’‘
import BaseHTTPServer import logging as log import logging.handlers import os import re import subprocess import sys import urllib2
import handler
from datetime import date
class AjaxServer(BaseHTTPServer.HTTPServer):
def __init__(self, serverAddress, requestHandlerClass, docRoot): BaseHTTPServer.HTTPServer.__init__(self, serverAddress, requestHandlerClass) self.docRoot = docRootfrom python module doc
def run(server_class=BaseHTTPServer.HTTPServer, handler_class=BaseHTTPServer.BaseHTTPRequestHandler, root=’root’, ip=’‘, port=8000):
server_address = (ip, port) httpd = server_class(server_address, handler_class, root) log.info("ajaxserver: waiting for requests") httpd.serve_forever()if name == ‘main’:
log.basicConfig(level = log.DEBUG, format = '%(asctime)s %(levelname)-8s %(message)s', datefmt = '%a, %d %b %Y %H:%M:%S') ip = '127.0.0.1' port = 8000 docRoot = '../root'try:
# start the server log.info("ajaxserver: starting HTTP server on %s:%d" %(ip, port)) run(server_class=AjaxServer, handler_class=handler.Handler, ip=ip, port=port, root=docRoot) log.info("ajaxserver: terminating")except KeyboardInterrupt, k: print “\r>>terminated by user, good bye!>>” log.info(“aborted by user, terminating”)
running it
start the ajax server:
then point your browser to http://localhost:8000/ — in your terminal running./ajaxserver Sun, 01 Jul 2012 16:35:12 INFO ajaxserver: starting HTTP server on 127.0.0.1:8000 Sun, 01 Jul 2012 16:35:12 INFO ajaxserver: waiting for requests
ajaxserver
you’ll see a bunch of log lines showing you the requests triggered by loading the index.html
page.
the browser will initially show just the static text of index.html
—
— after a couple of seconds the current date and time will be displayed and continuously updated, followed by the current process count of your machine. when you enter text into the text input field and tab out, the length of your input will be shown next to it.
all three activities are happening independently from one another, the page itself is never reloaded (as you can verify in the ajaxserver
log output).