In this section of Land of Lisp… We are going to make a web server.
Since communication over a network is a process that is riddles with all sorts of errors, the first thing we are going to look at is how Lisp handles errors.
Errors of all sorts are handled by signalling a condition. This is
often done with error:
(error "You call this an error? kek")The condition raised here is just a simple error… But we can create
errors of our own by using define-condition:
(define-condition foo () ()
(:report (lambda (condition stream)
(princ "Oh, you poor FOO!" stream))))And then we can signal it like so:
(error 'foo)We can intercept errors with handler-case:
(handler-case (error 'foo)
(foo () "Foo was raised... And grew up into a nice condition.")
(bar () "Bar was signalled!"))Foo was raised... And grew up into a nice condition.
We can also use unwind-protect to make the second form in the
expression to evaluate no matter what, so it would be similar to how
finally is used in Java. You can use this to close resources like
sockets or file streams in the event of an error.
(unwind-protect (/ 1 0) ;; Break the universe
(princ "Hurrgadurrgah!")) ;; But even so, princ this important messageunwind-protect is inside all of the with- macros, so if we wrap
our calls in those, crucial operations relating to the with-
resource are protected like this automagically.
Now, with this knowledge, we can write our server!
URL encoding escapes certain characters by replacing them with
%<character code in hexedecimal>… So the first order of business
if writing a function that can decode these escaped characters:
(defun http-char (c1 c2 &optional (default #\Space))
(let ((code (parse-integer
(coerce (list c1 c2) 'string)
:radix 16
:junk-allowed t)))
(if code
(code-char code)
default)))So, to decode “%3f”, we can pass the two last characters to our function to get the character that was escaped:
(http-char #\3 #\f)#\?
Now that we have decoding for individual URL-encoded characters, we can make something to decode an entire parameter:
(defun decode-param (s)
(labels ((f (lst)
(when lst
(case (car lst)
(#\% (cons (http-char (cadr lst) (caddr lst))
(f (cdddr lst))))
(#\+ (cons #\space (f (cdr lst))))
(otherwise (cons (car lst) (f (cdr lst))))))))
(coerce (f (coerce s 'list)) 'string)))On top of the existing http-char usage, we also replace ”+” with a
space in yet another string-eating function.
So:
(decode-param "Aha%3F+Cool.")Aha? Cool.
The next logical step is to parse a list of parameters from a request string.
(defun parse-params (s)
(let* ((i1 (position #\= s))
(i2 (position #\& s)))
(cond (i1 (cons (cons (intern (string-upcase (subseq s 0 i1)))
(decode-param (subseq s (1+ i1) i2)))
(and i2 (parse-params (subseq s (1+ i2))))))
((equal s "") nil)
(t s))))This function uses something we haven’t seen yet, the intern
function, which accepts a string, and returns a Lisp symbol of it.
This uses position to locate the first index in the passed in string
that an \= and & are at… The \= splits the parameter between
the key and the value… The & indicates the end if it exists. If
not, it gets short-circuited so that it may determine if the function
should recurse or not.
Another oddity about the above is that subseq is called regardless
of the status of the potentially nil i2. This is because if there
is a nil passed into subseq, it acts as if the parameter wasn’t
even passed in and doesn’t end the sequence prematurely:
(subseq "wwebsite on the internet" 1 nil)website on the internet
With this function in place, we can see the following:
(princ (parse-params "name=Goodra&gender=male&age=20"))((NAME . Goodra) (GENDER . male) (AGE . 20))
The suggested implementation above can be improved with a tail-call, but this will work adequately for the time being.
We need to be able to parse requests for the path in a URL, which means that we need another string eater that will extract the request path from the first line of an HTTP request.
The following is the suggested implementation for such:
(defun parse-url (s)
(let* ((url (subseq s (+ 2 (position #\space s))
(position #\space s :from-end t)))
(x (position #\? url)))
(if x
(cons (subseq url 0 x) (parse-params (subseq url (1+ x))))
(cons url '()))))This function appears to look for the first space and the last space
in the request string, and extracts the string between it. Then it
determines if there are any parameters in the URL, and if so, to
cons them to the path.
This all means that we can now do this:
(parse-url "GET /pokemon HTTP/1.1")Or this:
(parse-url "GET /pokemon?id=303 HTTP/1.1")Now, we should write a function that will parse the rest of the request – the headers!
(defun get-header (stream)
(let* ((s (read-line stream))
(h (let ((i (position #\: s)))
(when i
(cons (intern (string-upcase (subseq s 0 i)))
(subseq s (+ i 2)))))))
(when h
(cons h (get-header stream)))))Which we can test like:
(princ-to-string (get-header (make-string-input-stream "id: 303
pokemon: Mawile
")))((ID . 303) (POKEMON . Mawile))
We use the make-string-input-stream above to test the
stream-accepting get-header function… This is an example of the
ability to test any stream function with simple strings.
Now, there is sometimes one more part of a common request, the request body.
The request body of things like POST requests are simply request
parameters, and we can read them with the existing parse-params
function we wrote above:
(defun get-content-params (stream header)
(let ((length (cdr (assoc 'content-length header))))
(when length
(let ((content (make-string (parse-integer length))))
(read-sequence content stream)
(parse-params content)))))Now that we can read all of the information passed to us from a
browser, lets see how we can wrap it all together with a serve
function!
(defun serve (request-handler)
(let ((socket (socket-server 8080)))
(unwind-protect
(loop (with-open-stream (stream (socket-accept socket))
(let* ((url (parse-url (read-line stream)))
(path (car url))
(header (get-header stream))
(params (append (cdr url)
(get-content-params stream header)))
(*standard-output* stream))
(funcall request-handler path header params))))
(socket-server-close socket))))That was easy, since all the heavy-lifting has already been done, and the rest of it is implementation-specific.
Pretty much the above function listens on port 8080 for a request, parses requests from a browser, and then calls in a passed-in function: the request handler function.
The request handler function is where our actual server logic will
go, and anything it outputs to *standard-output*, will get returned
to the browser that sent the request. Speaking of which:
The request handler function we pass into our server will be the place that all of the logic in our server is contained.
Here is an example request handler:
(defun hello-request-handler (path header params)
(if (equal path "greeting")
(let ((name (assoc 'name params)))
(format t "HTTP/1.0 200 OK~%Content-Type: text/html; charset=UTF-8~%~%")
(if (not name)
(princ "<html><form>What is your name?<input name='name' /></form></html>")
(format t "<html>Nice to meet you ~a!</html>" (cdr name))))
(princ "Sorry... I don't know that page.")))Note: I added the HTTP/1.0 and Content-Type lines because
Firefox kept wanting to render this all as plaintext…which is wrong.
Since the way we pass it into the server allows for printed output to be its response, we can test this request handler really, really easily:
(hello-request-handler "lolcats" '() '())Sorry... I don't know that page.
(hello-request-handler "greeting" '() '())HTTP/1.0 200 OK Content-Type: text/html; charset=UTF-8 <html><form>What is your name?<input name='name' /></form></html>
And finally:
(hello-request-handler "greeting" '() '((name . "Mawile")))To run this site, we can simply run serve with our request handler:
(serve #'hello-request-handler)