summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndy Wingo <wingo@pobox.com>2017-02-11 12:33:28 +0100
committerAndy Wingo <wingo@pobox.com>2017-02-11 12:34:48 +0100
commit80d68a29f5434fd5055c819ee1d84bdc4cce0477 (patch)
treecd0f5223372f6ec13c9c1158a81e3847604159bd
parentAdd condition variable implementation (diff)
downloadguile-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.am1
-rw-r--r--examples/concurrent-web-hello.scm7
-rw-r--r--fibers.texi52
-rw-r--r--fibers/web/server.scm282
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?)))