diff options
| author | Andy Wingo <wingo@pobox.com> | 2017-02-11 12:33:28 +0100 |
|---|---|---|
| committer | Andy Wingo <wingo@pobox.com> | 2017-02-11 12:34:48 +0100 |
| commit | 80d68a29f5434fd5055c819ee1d84bdc4cce0477 (patch) | |
| tree | cd0f5223372f6ec13c9c1158a81e3847604159bd | |
| parent | Add condition variable implementation (diff) | |
| download | guile-fibers-80d68a29f5434fd5055c819ee1d84bdc4cce0477.tar.gz | |
Add concurrent web server
* fibers.texi (Concurrent Web Server): New section.
* fibers/web/server.scm: Add web server that can run handlers
concurrently.
* examples/concurrent-web-hello.scm: New file.
* Makefile.am (SOURCES): Add (fibers web server).
| -rw-r--r-- | Makefile.am | 1 | ||||
| -rw-r--r-- | examples/concurrent-web-hello.scm | 7 | ||||
| -rw-r--r-- | fibers.texi | 52 | ||||
| -rw-r--r-- | fibers/web/server.scm | 282 |
4 files changed, 335 insertions, 7 deletions
diff --git a/Makefile.am b/Makefile.am index fe8eb67..7e203a4 100644 --- a/Makefile.am +++ b/Makefile.am @@ -40,6 +40,7 @@ SOURCES = \ fibers/stack.scm \ fibers/repl.scm \ fibers/timers.scm \ + fibers/web/server.scm \ web/server/fibers.scm extlibdir = $(libdir)/guile/$(GUILE_EFFECTIVE_VERSION)/extensions diff --git a/examples/concurrent-web-hello.scm b/examples/concurrent-web-hello.scm new file mode 100644 index 0000000..29ed102 --- /dev/null +++ b/examples/concurrent-web-hello.scm @@ -0,0 +1,7 @@ +(use-modules (fibers web server)) + +(define (handler request body) + (values '((content-type . (text/plain))) + "Hello, World!")) + +(run-server handler) diff --git a/fibers.texi b/fibers.texi index c6833c1..3a1f78f 100644 --- a/fibers.texi +++ b/fibers.texi @@ -1092,9 +1092,10 @@ fiber while it owns any mutex. Here are some small examples to get you started. @menu -* Ping:: An echo server and client. -* Memcached:: A simple memcached server and client. -* Web Server:: Guile's non-blocking web server. +* Ping:: An echo server and client. +* Memcached:: A simple memcached server and client. +* Web Server Backend:: A backend for Guile's web server. +* Concurrent Web Server:: A more concurrent web server. @end menu More examples would be great, especially demonstrating interesting @@ -1260,12 +1261,18 @@ sys 0m1.456s Here we see a throughput of around 12000 puts plus 12000 gets per second; pretty good for a basic implementation. -@node Web Server -@section Web Server +@node Web Server Backend +@section Web Server Backend Fibers includes a ``backend'' for Guile's built-in web server that -uses fibers. To run a web server that serves each client from fibers, -specify the @code{fibers} backend when running your web server: +uses non-blocking fibers to read requests and write responses. Fibers +also includes a standalone web server that uses Guile's HTTP +facilities, but not its web server framework. @xref{Concurrent Web +Server}, for more on the standalone web server. + +To run a web server that serves each client from +fibers, specify the @code{fibers} backend when running your web +server: @example (use-modules (web server)) @@ -1335,6 +1342,37 @@ request (or the client), so it's at this point that we introduce work sharing to our system, allowing us to naturally take advantage of multiple cores on our server. +@node Concurrent Web Server +@section Concurrent Web Server + +Guile's web server framework single-threads all web request handling. +The handler procedure can be passed a number of additional ``state'' +arguments, and is expected to return a corresponding number of +additional values to use as the next state. This is sometimes what +you want, but it does limit concurrency; instead it would be nice to +be able to not only the input and output running concurrently, but +also handlers too. + +For this reason, Fibers includes a simple standalone web server that +uses Guile's Guile's HTTP facilities, but not its web server +framework. To run a standalone web server, use the @code{(fibers web +server)} module: + +@example +(use-modules (fibers web server)) + +(define (handler request body) + (values '((content-type . (text/plain))) + "Hello, World!")) + +(run-server handler) +@end example + +Compared to the Fibers web server backend (@pxref{Web Server +Backend}), using the standalone fibers web server enables more +parallelism, as the handlers can run in parallel when you have +multiple cores. + @node Status @chapter Project status diff --git a/fibers/web/server.scm b/fibers/web/server.scm new file mode 100644 index 0000000..9f50277 --- /dev/null +++ b/fibers/web/server.scm @@ -0,0 +1,282 @@ +;;; Fibers web server + +;; Copyright (C) 2010-2013,2015,2017 Free Software Foundation, Inc. + +;; This library is free software; you can redistribute it and/or +;; modify it under the terms of the GNU Lesser General Public +;; License as published by the Free Software Foundation; either +;; version 3 of the License, or (at your option) any later version. +;; +;; This library is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +;; Lesser General Public License for more details. +;; +;; You should have received a copy of the GNU Lesser General Public +;; License along with this library; if not, write to the Free Software +;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +;; 02110-1301 USA + +;;; Commentary: +;;; +;;; (web server) is a web server implementation using Fibers. Unlike +;;; the standard Guile web server implementation which threads all +;;; handler calls through a single thread, this implementation +;;; allows multiple concurrent handler threads. +;;; +;;; Code: + +(define-module (fibers web server) + #:use-module (fibers) + #:use-module (fibers conditions) + #:use-module (rnrs bytevectors) + #:use-module (ice-9 binary-ports) + #:use-module (ice-9 iconv) + #:use-module (ice-9 match) + #:use-module ((srfi srfi-9 gnu) #:select (set-field)) + #:use-module (system repl error-handling) + #:use-module (web request) + #:use-module (web response) + #:export (run-server)) + +(define (set-nonblocking! port) + (fcntl port F_SETFL (logior O_NONBLOCK (fcntl port F_GETFL))) + (setvbuf port 'block 1024)) + +(define (make-default-socket family addr port) + (let ((sock (socket PF_INET SOCK_STREAM 0))) + (setsockopt sock SOL_SOCKET SO_REUSEADDR 1) + (fcntl sock F_SETFD FD_CLOEXEC) + (bind sock family addr port) + (set-nonblocking! sock) + sock)) + +(define (extend-response r k v . additional) + (define (extend-alist alist k v) + (let ((pair (assq k alist))) + (acons k v (if pair (delq pair alist) alist)))) + (let ((r (set-field r (response-headers) + (extend-alist (response-headers r) k v)))) + (if (null? additional) + r + (apply extend-response r additional)))) + +;; -> response body +(define (sanitize-response request response body) + "\"Sanitize\" the given response and body, making them appropriate for +the given request. + +As a convenience to web handler authors, RESPONSE may be given as +an alist of headers, in which case it is used to construct a default +response. Ensures that the response version corresponds to the request +version. If BODY is a string, encodes the string to a bytevector, +in an encoding appropriate for RESPONSE. Adds a +‘content-length’ and ‘content-type’ header, as necessary. + +If BODY is a procedure, it is called with a port as an argument, +and the output collected as a bytevector. In the future we might try to +instead use a compressing, chunk-encoded port, and call this procedure +later, in the write-client procedure. Authors are advised not to rely +on the procedure being called at any particular time." + (cond + ((list? response) + (sanitize-response request + (build-response #:version (request-version request) + #:headers response) + body)) + ((not (equal? (request-version request) (response-version response))) + (sanitize-response request + (adapt-response-version response + (request-version request)) + body)) + ((not body) + (values response #vu8())) + ((string? body) + (let* ((type (response-content-type response + '(text/plain))) + (declared-charset (assq-ref (cdr type) 'charset)) + (charset (or declared-charset "utf-8"))) + (sanitize-response + request + (if declared-charset + response + (extend-response response 'content-type + `(,@type (charset . ,charset)))) + (string->bytevector body charset)))) + ((procedure? body) + (let* ((type (response-content-type response + '(text/plain))) + (declared-charset (assq-ref (cdr type) 'charset)) + (charset (or declared-charset "utf-8"))) + (sanitize-response + request + (if declared-charset + response + (extend-response response 'content-type + `(,@type (charset . ,charset)))) + (call-with-encoded-output-string charset body)))) + ((not (bytevector? body)) + (error "unexpected body type")) + ((and (response-must-not-include-body? response) + body + ;; FIXME make this stricter: even an empty body should be prohibited. + (not (zero? (bytevector-length body)))) + (error "response with this status code must not include body" response)) + (else + ;; check length; assert type; add other required fields? + (values (let ((rlen (response-content-length response)) + (blen (bytevector-length body))) + (cond + (rlen (if (= rlen blen) + response + (error "bad content-length" rlen blen))) + (else (extend-response response 'content-length blen)))) + (if (eq? (request-method request) 'HEAD) + ;; Responses to HEAD requests must not include bodies. + ;; We could raise an error here, but it seems more + ;; appropriate to just do something sensible. + #f + body))))) + +(define (with-stack-and-prompt thunk) + (call-with-prompt (default-prompt-tag) + (lambda () (start-stack #t (thunk))) + (lambda (k proc) + (with-stack-and-prompt (lambda () (proc k)))))) + +;; -> response body +(define (handle-request handler request body) + (cond + ((not request) + ;; Bad request. + (values (build-response #:version '(1 . 0) #:code 400 + #:headers '((content-length . 0))) + #vu8())) + (else + (call-with-error-handling + (lambda () + (call-with-values (lambda () + (with-stack-and-prompt + (lambda () + (handler request body)))) + (lambda (response body) + (sanitize-response request response body)))) + #:on-error 'backtrace + #:post-error (lambda _ + (values (build-response #:code 500) #f)))))) + +(define (keep-alive? response) + (let ((v (response-version response))) + (and (or (< (response-code response) 400) + (= (response-code response) 404)) + (case (car v) + ((1) + (case (cdr v) + ((1) (not (memq 'close (response-connection response)))) + ((0) (memq 'keep-alive (response-connection response))))) + (else #f))))) + +(define (client-loop client handler) + (with-throw-handler #t + (lambda () + (let loop () + (cond + ((eof-object? (lookahead-u8 client)) + (close-port client)) + (else + (call-with-values + (lambda () + (catch #t + (lambda () + (let* ((request (read-request client)) + (body (read-request-body request))) + (values request body))) + (lambda (key . args) + (display "While reading request:\n" (current-error-port)) + (print-exception (current-error-port) #f key args) + (values #f #f)))) + (lambda (request body) + (call-with-values (lambda () + (handle-request handler request body)) + (lambda (response body) + (write-response response client) + (when body + (put-bytevector client body)) + (force-output client) + (if (and (keep-alive? response) + (not (eof-object? (lookahead-u8 client)))) + (loop) + (close-port client)))))))))) + (lambda (k . args) + (close-port client)))) + +(define (socket-loop socket handler) + (let loop () + (match (accept socket) + ((client . sockaddr) + ;; From "HOP, A Fast Server for the Diffuse Web", Serrano. + (setsockopt client SOL_SOCKET SO_SNDBUF (* 12 1024)) + (set-nonblocking! client) + ;; Always disable Nagle's algorithm, as we handle buffering + ;; ourselves. Ignore exceptions if it's not a TCP port, or + ;; TCP_NODELAY is not defined on this platform. + (false-if-exception + (setsockopt client IPPROTO_TCP TCP_NODELAY 0)) + (spawn-fiber (lambda () (client-loop client handler)) + #:parallel? #t) + (loop))))) + +(define (call-with-sigint thunk cvar) + (let ((handler #f)) + (dynamic-wind + (lambda () + (set! handler + (sigaction SIGINT (lambda (sig) (signal-condition! cvar))))) + thunk + (lambda () + (if handler + ;; restore Scheme handler, SIG_IGN or SIG_DFL. + (sigaction SIGINT (car handler) (cdr handler)) + ;; restore original C handler. + (sigaction SIGINT #f)))))) + +(define* (run-server handler #:key + (host #f) + (family AF_INET) + (addr (if host + (inet-pton family host) + INADDR_LOOPBACK)) + (port 8080) + (socket (make-default-socket family addr port))) + "Run the fibers web server. + +HANDLER should be a procedure that takes two arguments, the HTTP request +and request body, and returns two values, the response and response +body. + +For example, here is a simple \"Hello, World!\" server: + +@example + (define (handler request body) + (values '((content-type . (text/plain))) + \"Hello, World!\")) + (run-server handler) +@end example + +The response and body will be run through ‘sanitize-response’ +before sending back to the client." + ;; We use a large backlog by default. If the server is suddenly hit + ;; with a number of connections on a small backlog, clients won't + ;; receive confirmation for their SYN, leading them to retry -- + ;; probably successfully, but with a large latency. + (listen socket 1024) + (set-nonblocking! socket) + (sigaction SIGPIPE SIG_IGN) + (let ((finished? (make-condition))) + (call-with-sigint + (lambda () + (run-fibers + (lambda () + (spawn-fiber (lambda () (socket-loop socket handler))) + (wait finished?)))) + finished?))) |
