In part two I describe some additional changes to the server code. Part Three makes no changes to the server code, but discusses some advantages of continuation based frameworks. Part Four extends the framework, adding a callback system, and fixing an important bug. The modal-web-server-0.1.tar.gz file contains the version used in this document.
There has been quite a bit of discussion lately in various places about continuation based web servers and continuation based web frameworks. Using the Scheme programming language it can be quite easy to put together a very simple continuation based web server. This document shows one approach to get a working server using Chicken Scheme that has enough features to allow playing around with some of the possiblities of this kind of web server design.
I've called the server a 'Modal' web server. 'Modal Web Server' or 'Modal Web Framework' is the term that Avi Bryant, the author of Seaside, has coined to describe continuation based web server designs.
I chose Chicken Scheme to base the example on because it has a working HTTP library that is easy to use. It is also a very portable Scheme implementation implemented in standard C.
This example is not suitable for production use, although it could be extended for this purpose, and at the end of this document I will explain some of the limitations. The modal-web-server-0.1.tar.gz file contains the version described by this document.
A modal web server can be built upon an existing web server framework fairly easily if the language you are using supports continuations. That's why I'm using Scheme in this example.
The advantage of this type of server design is that the control flow for a web application does not need to be split up into a page request/response model, or state machine. Instead you can write the control flow in a procedural manner and the framework manages the problem of maintaining state, allowing the use of the back button and forking of browser windows and keeping everything consistent.
The start to this type of server is being able to register a function that displays HTML with a URL. When that URL is called the function is called and the HTML it generates is displayed to the user.
This is fairly easy to do in a language with first class functions. Using Chicken Scheme with modal-web-server.scm (from modal-web-server-0.1.tar.gz) loaded you get exactly this.
Start a server with:
This starts an HTTP server running on port 5000. To make it easy to generate HTML I use the Scheme SSAX libraries. This allows generating HTML and XML using a format called SXML. A 'hello world' function is as simple as:
(define (page1 url) (sxml->html-string '(html (head (title "Hello World!")) (body (p "Hello World!")))))
'page1' is a function that when called returns the HTML for a page that displays the text 'Hello World!'. To register this with the server and assign it a URL so it can be accessed from the browser use:
(register-function (lambda () (show page1)))
The result of this call is a URL that is associated with the function. Accessing this URL from the browser will run the function and display the 'Hello World!' page. In this example we don't use the 'url' argument passed to 'page1'. This argument is an URL which can be placed inside an anchor on the page which when requested, continues computation of the function that originally called 'show'.
An example is easier than an explanation. 'show-message' is a function that displays an HTML page with a message and an 'Ok' anchor. The 'Ok' anchor goes to an URL as described above:
(define (show-message msg) (show (lambda (url) (sxml->html-string `(html (head (title ,msg)) (body (p ,msg) (p (a (@ (href ,url)) "Ok")))))))
We can use this by registering with the server a function that calls it a couple of times:
(register-function (lambda () (show-message "Hello") (show-message "World")))
Accessing the registered function displays the first 'Hello' page. Clicking 'Ok' will then resume the function and display the second 'World' page. Clicking 'Ok on that page exits the function.
Between calls to 'show' or 'show-message' we can create state, modify it, and it will be saved between page requests. The common example for modal web applications is the 'Counter' example. This example displays a single page with a number on it. Clicking '++' increases the number. Clicking '--' decreases the number.
To do this example in our framework I use a helper function 'function-href'. This function generates an anchor that when pressed calls a given function:
;; Returns an HTML fragment for an anchor that, when clicked, calls ;; the given function. (define (function-href text function) (let ((url (register-function function))) `(a (@ (href ,url)) ,text)))
Now we write the 'show-counter' function:
(define (show-counter initial-count) (let ((count initial-count)) (let loop () (show (lambda (url) (sxml->html-string `(html (head (title ,(format "Count: ~a" count))) (body (h1 ,(number->string count)) (table (tr (td ,(function-href "++" (lambda () (++! count) (loop))) ,(function-href "--" (lambda () (--! count) (loop))))))))))))))
The calls to 'function-href' generate the anchor link to execute the given functions. These functions then call 'loop' to go back to the initial show call and re-display the page. Again, registering the function is easy:
(register-function (lambda () (show-counter 0)))
You should now be able to run the application. Click '++' and '--' to demonstrate the calling of the functions.
A nice feature of modal frameworks is that requests to display other pages can appear pretty much anywhere and they behave as expected. Here's the counter example modified to check if the count exceeds 5. If so, it displays a message to that effect and resets the counter to zero:
(define (show-counter-with-test initial-count) (let ((count initial-count)) (let loop () (show (lambda (url) (sxml->html-string `(html (head (title ,(format "Count: ~a" count))) (body (h1 ,(number->string count)) (table (tr (td ,(function-href "++" (lambda () (++! count) (when (> count 5) (show-message "Count cannot exceed 5!") (set! count 0)) (loop))) ,(function-href "--" (lambda () (--! count) (loop))))))))))))))
Register it and experiment with the count going greater than 5:
(register-function (lambda () (show-counter-with-test 0)))
One thing you may notice with this example is how the state of the counter is shared if you create a new browser window and reference the same URL. For example, take the count up to '2'. Copy the URL and start another browser. Paste the URL in that browser and increment or decrement the count a few times. Go back to the original browser and refresh the screen or increment/decrement it. You'll see it has shared the value of the count with the second browser window.
If the sharing of the state is not desired, in this example we can work around it by not incrementing the count, instead we show a new page with a new count:
(define (show-counter-no-share initial-count) (let loop ((count initial-count)) (show (lambda (url) (sxml->html-string `(html (head (title ,(format "Count: ~a" count))) (body (h1 ,(number->string count)) (table (tr (td ,(function-href "++" (lambda () (loop (++ count)))) ,(function-href "--" (lambda () (loop (-- count))))))))))))))
Try the same sharing example and you should see that the counter state is independant. In a 'real world' framework the management of state that needs to be shared, and state that doesn't, is non-trivial. Avi Bryant wrote a nice weblog posting about this.
The implementation of the modal web server is quite simple in this example. Production use would require quite a bit of work to get things robust. Some of the limitations include:
The core of the server is the 'show' function. This function stores the current continuation and assigns a unique URL to it. It then calls the function it is given to generate the HTML, passing that unique URL to it. It registers a handler for that URL that will retrieve the continuation from the registry (a simple hashtable) and call it. Here is that function:
;; The 'show' call is used to display an HTML page to the browser. It ;; then suspends, and gets resumed when the user chooses to continue ;; the computation by selecting a 'next' URL. 'show' is given a 'page' ;; which is a function that should return the HTML that is to be ;; displayed on the users browser. That function takes a single 'url' ;; argument which is the URL the uesr should request to continue the ;; computation at the point after this show call. ;; It works this way: ;; ;; 1) Store the current continuation in the kid-registry with a unique ;; URL assigned to it. ;; ;; 2) Call the page function and send the HTML it returns to the users browser. ;; ;; 3) Call 'suicide' to exit the current request handler. ;; ;; 4) At some point the user chooses to access the URL that was passed to the ;; 'page' function. ;; ;; 5) This URL calls the continuation that was saved in (1) which ;; results in the call/cc returning at this point, and show ;; returning. Effectively continuing the computatation. ;; ;; Show returns the request-body of the call, which is usually an ;; assoc list of the form results of the page. (define (show page) (call/cc (lambda (k) (let ((kid (get-unique-continuation-id))) (hash-table-set! kid-registry kid k) (http:add-resource (string-append "/" (symbol->string kid)) (lambda (r a) (call/cc (lambda (exit) (suicide exit) (let ((k (hash-table-ref kid-registry kid))) (k (http:request-body r))))))) (process-page-function-result (page (symbol->string kid))) ((suicide) #f)))))
'suicide' is a parameter (Think of it as a thread local variable). It holds the continuation captured as soon as the HTTP handler is entered. Calling it immediately exits the handler. This is done as soon as the HTML from the page function is sent to the client browser to 'suspend' the current function being run.
When the URL that we pass to the page is called, it retrieves the continuation from the hash table and calls it passing the 'http:request-body' of the current request. This contains the 'result' of any POST or GET request parameters. This effectively means 'show' returns the value of the POST allowing access to form values, etc. I don't show an example of using this here but in a future update I will.
The 'process-page-function-result' takes the HTML generated from the page function and sends it to the client by writing it to standard output (which has been overridden to send the data across the HTTP response socket):
;; Take the result returned from a page function and send the data to ;; the web browser. If the page function returns #f then take no ;; action. (define (process-page-function-result result) (when result (http:write-response-header) (print "Content-type: text/html\r\nContent-length: " (string-length result) "\r\n\r\n" result)))
'register-function', which we've been using to assign an URL to functions does a similar job to 'show', except it doesn't expect the function to generate HTML. It only exists to run the function:
(define (register-function function) (let ((kid (get-unique-continuation-id))) (hash-table-set! kid-registry kid function) (http:add-resource (string-append "/" (symbol->string kid)) (lambda (r a) (call/cc (lambda (exit) (suicide exit) (let ((k (hash-table-ref kid-registry kid))) (k) (process-page-function-result "") ((suicide) #f)))))) (symbol->string kid)))
As you can see, in a language which supports continuations natively it is quite easy to tack on a modal web framework to an existing HTTP framework.
I'll continue using this framework to show examples of what can be done with a modal web framework and different implementation options. I welcome an questions. If you wish to email me or continue reading about updates to this framework, see my weblog.
Other, more full featured, frameworks are in existance:
Of these, Seaside is probably the most full featured web application framework that uses the continuation model.
Core Technology, the company I work for, has a commercial continuation based framework that supports clustering, load balancing and persistent sessions, amongst other features. It serves as the 'engine' for a number of products built on top of it to make building enterprise level web applications easier. So there is definitely commercial interest in these ideas.
Copyright (c) 2004, Chris Double. All Rights Reserved.