rackety "more" tutorial

This commit is contained in:
Matthew Flatt 2010-04-28 15:26:41 -06:00
parent 1991e9328f
commit 43cb91e6ee
11 changed files with 168 additions and 163 deletions

View File

@ -2,13 +2,13 @@
@(require scribble/manual @(require scribble/manual
scribble/urls scribble/urls
scribble/eval scribble/eval
"../quick/keep.ss" "../quick/keep.rkt"
(for-label scheme (for-label scheme
scheme/enter racket/enter
readline readline
net/url net/url
xml xml
scheme/control)) racket/control))
@(define quick @other-manual['(lib "quick.scrbl" "scribblings/quick")]) @(define quick @other-manual['(lib "quick.scrbl" "scribblings/quick")])
@(define guide @other-manual['(lib "guide.scrbl" "scribblings/guide")]) @(define guide @other-manual['(lib "guide.scrbl" "scribblings/guide")])
@ -16,7 +16,7 @@
@(define more-eval (make-base-eval)) @(define more-eval (make-base-eval))
@(interaction-eval #:eval more-eval @(interaction-eval #:eval more-eval
(define (show-load re?) (define (show-load re?)
(fprintf (current-error-port) " [~aloading serve.ss]\n" (if re? "re-" "")))) (fprintf (current-error-port) " [~aloading serve.rkt]\n" (if re? "re-" ""))))
@(interaction-eval #:eval more-eval @(interaction-eval #:eval more-eval
(define (serve n) void)) (define (serve n) void))
@(interaction-eval #:eval more-eval @(interaction-eval #:eval more-eval
@ -39,13 +39,13 @@
" in plain text: " " in plain text: "
(link file "step " which) "."))) (link file "step " which) ".")))
@title{@bold{More}: Systems Programming with PLT Scheme} @title{@bold{More}: Systems Programming with Racket}
@author["Matthew Flatt"] @author["Matthew Flatt"]
In contrast to the impression that @|quick| may give, PLT Scheme is In contrast to the impression that @|quick| may give, Racket is
not just another pretty face. Underneath the graphical facade of not just another pretty face. Underneath the graphical facade of
DrScheme lies a sophisticated toolbox for managing threads and DrRacket lies a sophisticated toolbox for managing threads and
processes, which is the subject of this tutorial. processes, which is the subject of this tutorial.
Specifically, we show how to build a secure, multi-threaded, Specifically, we show how to build a secure, multi-threaded,
@ -54,39 +54,39 @@ the language than in @|quick|, and we expect you to click on syntax or
function names that you don't recognize (which will take you to the function names that you don't recognize (which will take you to the
relevant documentation). Beware that the last couple of sections relevant documentation). Beware that the last couple of sections
present material that is normally considered difficult. If you're present material that is normally considered difficult. If you're
still new to Scheme and have relatively little programming experience, still new to Racket and have relatively little programming experience,
you may want to skip to @|guide|. you may want to skip to @|guide|.
To get into the spirit of this tutorial, we suggest that you set To get into the spirit of this tutorial, we suggest that you set
DrScheme aside for a moment, and switch to raw @exec{mzscheme} in a DrRacket aside for a moment, and switch to raw @exec{racket} in a
terminal. You'll also need a text editor, such as @exec{emacs} or terminal. You'll also need a text editor, such as @exec{emacs} or
@exec{vi}. Finally, you'll need a web client, perhaps @exec{lynx} or @exec{vi}. Finally, you'll need a web client, perhaps @exec{lynx} or
@exec{firefox}. @exec{firefox}.
@margin-note{Of course, if you're already spoiled, you can keep using @margin-note{Of course, if you're already spoiled, you can keep using
DrScheme.} DrRacket.}
@; ---------------------------------------------------------------------- @; ----------------------------------------------------------------------
@section{Ready...} @section{Ready...}
@link[url:download-drscheme]{Download PLT Scheme}, install, and then @link[url:download-drracket]{Download Racket}, install, and then
start @exec{mzscheme} with no command-line arguments: start @exec{racket} with no command-line arguments:
@verbatim[#:indent 2]{ @verbatim[#:indent 2]{
$ mzscheme $ racket
Welcome to MzScheme Welcome to Racket
> >
} }
If you're using a plain terminal, if you have GNU Readline installed If you're using a plain terminal, if you have GNU Readline installed
on your system, and if you'd like Readline support in @exec{mzscheme}, on your system, and if you'd like Readline support in @exec{racket},
then evaluate @scheme[(require readline)]. If you also evaluate then evaluate @racket[(require readline)]. If you also evaluate
@scheme[(install-readline!)], then your @filepath{~/.mzschemerc} is @racket[(install-readline!)], then your @filepath{~/.racketrc} is
updated to load Readline whenever you start @exec{mzscheme} for updated to load Readline whenever you start @exec{racket} for
interactive evaluation. interactive evaluation.
@margin-note{Unfortunately, for legal reasons related to GPL vs. LGPL, @margin-note{Unfortunately, for legal reasons related to GPL vs. LGPL,
@exec{mzscheme} cannot provide Readline automatically.} @exec{racket} cannot provide Readline automatically.}
@interaction[ @interaction[
(eval:alts (require readline) (void)) (eval:alts (require readline) (void))
@ -96,11 +96,11 @@ interactive evaluation.
@; ---------------------------------------------------------------------- @; ----------------------------------------------------------------------
@section{Set...} @section{Set...}
In the same directory where you started @exec{mzscheme}, create a text In the same directory where you started @exec{racket}, create a text
file @filepath{serve.ss}, and start it like this: file @filepath{serve.rkt}, and start it like this:
@schememod[ @racketmod[
scheme racket
(define (go) (define (go)
'yep-it-works) 'yep-it-works)
@ -111,35 +111,35 @@ scheme
@; ---------------------------------------------------------------------- @; ----------------------------------------------------------------------
@section{Go!} @section{Go!}
Back in @exec{mzscheme}, try loading the file and running @scheme[go]: Back in @exec{racket}, try loading the file and running @racket[go]:
@interaction[ @interaction[
#:eval more-eval #:eval more-eval
(eval:alts (enter! "serve.ss") (show-load #f)) (eval:alts (enter! "serve.rkt") (show-load #f))
(eval:alts (go) 'yep-it-works) (eval:alts (go) 'yep-it-works)
] ]
Try modifying @filepath{serve.ss}, and then run @scheme[(enter! Try modifying @filepath{serve.rkt}, and then run @racket[(enter!
"serve.ss")] again to re-load the module, and then check your changes. "serve.rkt")] again to re-load the module, and then check your changes.
@; ---------------------------------------------------------------------- @; ----------------------------------------------------------------------
@section{``Hello World'' Server} @section{``Hello World'' Server}
We'll implement the web server through a @scheme[serve] function that We'll implement the web server through a @racket[serve] function that
takes a IP port number for client connections: takes a IP port number for client connections:
@schemeblock[ @racketblock[
(define (serve port-no) (define (serve port-no)
...) ...)
] ]
The server accepts TCP connections through a @defterm{listener}, which The server accepts TCP connections through a @defterm{listener}, which
we create with @scheme[tcp-listen]. To make interactive development we create with @racket[tcp-listen]. To make interactive development
easier, we supply @scheme[#t] as the third argument to easier, we supply @racket[#t] as the third argument to
@scheme[tcp-listen], which lets us re-use the port number without @racket[tcp-listen], which lets us re-use the port number without
waiting on TCP timeouts. waiting on TCP timeouts.
@schemeblock[ @racketblock[
(define (serve port-no) (define (serve port-no)
(define listener (tcp-listen port-no 5 #t)) (define listener (tcp-listen port-no 5 #t))
...) ...)
@ -147,7 +147,7 @@ waiting on TCP timeouts.
The server must loop to accept connections from the listener: The server must loop to accept connections from the listener:
@schemeblock[ @racketblock[
(define (serve port-no) (define (serve port-no)
(define listener (tcp-listen port-no 5 #t)) (define listener (tcp-listen port-no 5 #t))
(define (loop) (define (loop)
@ -156,11 +156,11 @@ The server must loop to accept connections from the listener:
(loop)) (loop))
] ]
Our @scheme[accept-and-handle] function accepts a connection using Our @racket[accept-and-handle] function accepts a connection using
@scheme[tcp-accept], which returns two values: a stream for input from @racket[tcp-accept], which returns two values: a stream for input from
the client, and a stream for output to the client. the client, and a stream for output to the client.
@schemeblock[ @racketblock[
(define (accept-and-handle listener) (define (accept-and-handle listener)
(define-values (in out) (tcp-accept listener)) (define-values (in out) (tcp-accept listener))
(handle in out) (handle in out)
@ -171,7 +171,7 @@ the client, and a stream for output to the client.
To handle a connection, for now, we'll read and discard the request To handle a connection, for now, we'll read and discard the request
header, and then write a ``Hello, world!'' web page as the result: header, and then write a ``Hello, world!'' web page as the result:
@schemeblock[ @racketblock[
(define (handle in out) (define (handle in out)
(code:comment @#,t{Discard the request header (up to blank line):}) (code:comment @#,t{Discard the request header (up to blank line):})
(regexp-match #rx"(\r\n|^)\r\n" in) (regexp-match #rx"(\r\n|^)\r\n" in)
@ -181,23 +181,23 @@ header, and then write a ``Hello, world!'' web page as the result:
(display "<html><body>Hello, world!</body></html>" out)) (display "<html><body>Hello, world!</body></html>" out))
] ]
Note that @scheme[regexp-match] operates directly on the input stream, Note that @racket[regexp-match] operates directly on the input stream,
which is easier than bothering with individual lines. which is easier than bothering with individual lines.
@whole-prog["1"] @whole-prog["1"]
Copy the above three definitions---@scheme[serve], Copy the above three definitions---@racket[serve],
@scheme[accept-and-handle], and @scheme[handle]---into @racket[accept-and-handle], and @racket[handle]---into
@filepath{serve.ss} and re-load: @filepath{serve.rkt} and re-load:
@interaction[ @interaction[
#:eval more-eval #:eval more-eval
(eval:alts (enter! "serve.ss") (show-load #t)) (eval:alts (enter! "serve.rkt") (show-load #t))
(eval:alts (serve 8080) (void)) (eval:alts (serve 8080) (void))
] ]
Now point your browser to @tt{http://localhost:8080} (assuming that Now point your browser to @tt{http://localhost:8080} (assuming that
you used @scheme[8080] as the port number, and that the browser is you used @racket[8080] as the port number, and that the browser is
running on the same machine) to receive a friendly greeting from your running on the same machine) to receive a friendly greeting from your
web server. web server.
@ -205,10 +205,10 @@ web server.
@section{Server Thread} @section{Server Thread}
Before we can make the web server respond in more interesting ways, we Before we can make the web server respond in more interesting ways, we
need to get a Scheme prompt back. Typing Ctl-C in your terminal window need to get a Racket prompt back. Typing Ctl-C in your terminal window
interrupts the server loop: interrupts the server loop:
@margin-note{In DrScheme, instead of typing Ctl-C, click the @margin-note{In DrRacket, instead of typing Ctl-C, click the
@onscreen{Stop} button once.} @onscreen{Stop} button once.}
@interaction[ @interaction[
@ -225,15 +225,15 @@ number:
(eval:alts (serve 8080) (show-fail 8080)) (eval:alts (serve 8080) (show-fail 8080))
] ]
The problem is that the listener that we created with @scheme[serve] The problem is that the listener that we created with @racket[serve]
is still listening on the original port number. is still listening on the original port number.
To avoid this problem, let's put the listener loop in its own thread, To avoid this problem, let's put the listener loop in its own thread,
and have @scheme[serve] return immediately. Furthermore, we'll have and have @racket[serve] return immediately. Furthermore, we'll have
@scheme[serve] return a function that can be used to shut down the @racket[serve] return a function that can be used to shut down the
server thread and TCP listener: server thread and TCP listener:
@schemeblock[ @racketblock[
(define (serve port-no) (define (serve port-no)
(define listener (tcp-listen port-no 5 #t)) (define listener (tcp-listen port-no 5 #t))
(define (loop) (define (loop)
@ -251,7 +251,7 @@ Try the new one:
@interaction[ @interaction[
#:eval more-eval #:eval more-eval
(eval:alts (enter! "serve.ss") (show-load #t)) (eval:alts (enter! "serve.rkt") (show-load #t))
(define stop (serve 8081)) (define stop (serve 8081))
] ]
@ -274,7 +274,7 @@ as you like:
In the same way that we put the main server loop into a background In the same way that we put the main server loop into a background
thread, we can put each individual connection into its own thread: thread, we can put each individual connection into its own thread:
@schemeblock[ @racketblock[
(define (accept-and-handle listener) (define (accept-and-handle listener)
(define-values (in out) (tcp-accept listener)) (define-values (in out) (tcp-accept listener))
(thread (thread
@ -288,8 +288,8 @@ thread, we can put each individual connection into its own thread:
With this change, our server can now handle multiple threads at With this change, our server can now handle multiple threads at
once. The handler is so fast that this fact will be difficult to once. The handler is so fast that this fact will be difficult to
detect, however, so try inserting @scheme[(sleep (random 10))] before detect, however, so try inserting @racket[(sleep (random 10))] before
the @scheme[handle] call above. If you make multiple connections from the @racket[handle] call above. If you make multiple connections from
the web browser at roughly the same time, some will return soon, and the web browser at roughly the same time, some will return soon, and
some will take up to 10 seconds. The random delays will be independent some will take up to 10 seconds. The random delays will be independent
of the order in which you started the connections. of the order in which you started the connections.
@ -304,10 +304,10 @@ like to implement a timeout for each connection thread.
One way to implement the timeout is to create a second thread that One way to implement the timeout is to create a second thread that
waits for 10 seconds, and then kills the thread that calls waits for 10 seconds, and then kills the thread that calls
@scheme[handle]. Threads are lightweight enough in Scheme that this @racket[handle]. Threads are lightweight enough in Racket that this
watcher-thread strategy works well: watcher-thread strategy works well:
@schemeblock[ @racketblock[
(define (accept-and-handle listener) (define (accept-and-handle listener)
(define-values (in out) (tcp-accept listener)) (define-values (in out) (tcp-accept listener))
(define t (thread (define t (thread
@ -322,22 +322,25 @@ watcher-thread strategy works well:
] ]
This first attempt isn't quite right, because when the thread is This first attempt isn't quite right, because when the thread is
killed, its @scheme[in] and @scheme[out] streams remain open. We killed, its @racket[in] and @racket[out] streams remain open. We
could add code to the watcher thread to close the streams as well as could add code to the watcher thread to close the streams as well as
kill the thread, but Scheme offers a more general shutdown mechanism: kill the thread, but Racket offers a more general shutdown mechanism:
@defterm{custodians}. A custodian is a kind of container for all @defterm{custodians}. A custodian is a kind of container for all
resources other than memory, and it supports a resources other than memory, and it supports a
@scheme[custodian-shutdown-all] operation that terminates and closes @racket[custodian-shutdown-all] operation that terminates and closes
all resources within the container, whether they're threads, streams, all resources within the container, whether they're threads, streams,
or other kinds of limited resources. or other kinds of limited resources.
Whenever a thread or stream is created, it is placed into the current Whenever a thread or stream is created, it is placed into the current
custodian as determined by the @scheme[current-custodian] custodian as determined by the @racket[current-custodian]
parameter. To place everything about a connection into a custodian, we parameter. To place everything about a connection into a custodian, we
@scheme[parameterize] all the resource creations to go into a new @racket[parameterize] all the resource creations to go into a new
custodian: custodian:
@schemeblock[ @margin-note{See @secref[#:doc '(lib "scribblings/guide/guide.scrbl") "parameterize"]
for an introduction to parameters.}
@racketblock[
(define (accept-and-handle listener) (define (accept-and-handle listener)
(define cust (make-custodian)) (define cust (make-custodian))
(parameterize ([current-custodian cust]) (parameterize ([current-custodian cust])
@ -352,16 +355,16 @@ custodian:
(custodian-shutdown-all cust)))) (custodian-shutdown-all cust))))
] ]
With this implementation, @scheme[in], @scheme[out], and the thread With this implementation, @racket[in], @racket[out], and the thread
that calls @scheme[handle] all belong to @scheme[cust]. In addition, that calls @racket[handle] all belong to @racket[cust]. In addition,
if we later change @scheme[handle] so that it, say, opens a file, then if we later change @racket[handle] so that it, say, opens a file, then
the file handles will also belong to @scheme[cust], so they will be the file handles will also belong to @racket[cust], so they will be
reliably closed when @scheme[cust] is shut down. reliably closed when @racket[cust] is shut down.
In fact, it's a good idea to change @scheme[serve] so that it uses a In fact, it's a good idea to change @racket[serve] so that it uses a
custodian, too: custodian, too:
@schemeblock[ @racketblock[
(define (serve port-no) (define (serve port-no)
(define main-cust (make-custodian)) (define main-cust (make-custodian))
(parameterize ([current-custodian main-cust]) (parameterize ([current-custodian main-cust])
@ -374,7 +377,7 @@ custodian, too:
(custodian-shutdown-all main-cust))) (custodian-shutdown-all main-cust)))
] ]
That way, the @scheme[main-cust] created in @scheme[serve] not only That way, the @racket[main-cust] created in @racket[serve] not only
owns the TCP listener and the main server thread, it also owns every owns the TCP listener and the main server thread, it also owns every
custodian created for a connection. Consequently, the revised shutdown custodian created for a connection. Consequently, the revised shutdown
procedure for the server immediately terminates all active connections, procedure for the server immediately terminates all active connections,
@ -382,17 +385,17 @@ in addition to the main server loop.
@whole-prog["4"] @whole-prog["4"]
After updating the @scheme[serve] and @scheme[accept-and-handle] After updating the @racket[serve] and @racket[accept-and-handle]
functions as above, here's how you can simulate a malicious client: functions as above, here's how you can simulate a malicious client:
@interaction[ @interaction[
#:eval more-eval #:eval more-eval
(eval:alts (enter! "serve.ss") (show-load #t)) (eval:alts (enter! "serve.rkt") (show-load #t))
(define stop (serve 8081)) (define stop (serve 8081))
(eval:alts (define-values (cin cout) (tcp-connect "localhost" 8081)) (void)) (eval:alts (define-values (cin cout) (tcp-connect "localhost" 8081)) (void))
] ]
Now wait 10 seconds. If you try reading from @scheme[cin], which is Now wait 10 seconds. If you try reading from @racket[cin], which is
the stream that sends data from the server back to the client, you'll the stream that sends data from the server back to the client, you'll
find that the server has shut down the connection: find that the server has shut down the connection:
@ -422,12 +425,12 @@ URLs.
To parse the incoming URL and to more easily format HTML output, we'll To parse the incoming URL and to more easily format HTML output, we'll
require two extra libraries: require two extra libraries:
@schemeblock[ @racketblock[
(require xml net/url) (require xml net/url)
] ]
The @schememodname[xml] library gives us @scheme[xexpr->string], which The @racketmodname[xml] library gives us @racket[xexpr->string], which
takes a Scheme value that looks like HTML and turns it into actual takes a Racket value that looks like HTML and turns it into actual
HTML: HTML:
@interaction[ @interaction[
@ -435,11 +438,11 @@ HTML:
(xexpr->string '(html (head (title "Hello")) (body "Hi!"))) (xexpr->string '(html (head (title "Hello")) (body "Hi!")))
] ]
We'll assume that our new @scheme[dispatch] function (to be written) We'll assume that our new @racket[dispatch] function (to be written)
takes a requested URL and produces a result value suitable to use with takes a requested URL and produces a result value suitable to use with
@scheme[xexpr->string] to send back to the client: @racket[xexpr->string] to send back to the client:
@schemeblock[ @racketblock[
(define (handle in out) (define (handle in out)
(define req (define req
(code:comment @#,t{Match the first line to extract the request:}) (code:comment @#,t{Match the first line to extract the request:})
@ -456,8 +459,8 @@ takes a requested URL and produces a result value suitable to use with
(display (xexpr->string xexpr) out)))) (display (xexpr->string xexpr) out))))
] ]
The @schememodname[net/url] library gives us @scheme[string->url], The @racketmodname[net/url] library gives us @racket[string->url],
@scheme[url-path], @scheme[path/param-path], and @scheme[url-query] @racket[url-path], @racket[path/param-path], and @racket[url-query]
for getting from a string to parts of the URL that it represents: for getting from a string to parts of the URL that it represents:
@interaction[ @interaction[
@ -468,11 +471,11 @@ for getting from a string to parts of the URL that it represents:
(url-query u) (url-query u)
] ]
We use these pieces to implement @scheme[dispatch]. The We use these pieces to implement @racket[dispatch]. The
@scheme[dispatch] function consults a hash table that maps an initial @racket[dispatch] function consults a hash table that maps an initial
path element, like @scheme["foo"], to a handler function: path element, like @racket["foo"], to a handler function:
@schemeblock[ @racketblock[
(define (dispatch str-path) (define (dispatch str-path)
(code:comment @#,t{Parse the request as a URL:}) (code:comment @#,t{Parse the request as a URL:})
(define url (string->url str-path)) (define url (string->url str-path))
@ -493,17 +496,17 @@ path element, like @scheme["foo"], to a handler function:
(define dispatch-table (make-hash)) (define dispatch-table (make-hash))
] ]
With the new @scheme[require] import and new @scheme[handle], With the new @racket[require] import and new @racket[handle],
@scheme[dispatch], and @scheme[dispatch-table] definitions, our @racket[dispatch], and @racket[dispatch-table] definitions, our
``Hello World!'' server has turned into an error server. You don't have ``Hello World!'' server has turned into an error server. You don't have
to stop the server to try it out. After modifying @filepath{serve.ss} to stop the server to try it out. After modifying @filepath{serve.rkt}
with the new pieces, evaluate @scheme[(enter! "serve.ss")] and then with the new pieces, evaluate @racket[(enter! "serve.rkt")] and then
try again to connect to the server. The web browser should show an try again to connect to the server. The web browser should show an
``Unknown page'' error in red. ``Unknown page'' error in red.
We can register a handler for the @scheme["hello"] path like this: We can register a handler for the @racket["hello"] path like this:
@schemeblock[ @racketblock[
(hash-set! dispatch-table "hello" (hash-set! dispatch-table "hello"
(lambda (query) (lambda (query)
`(html (body "Hello, World!")))) `(html (body "Hello, World!"))))
@ -511,25 +514,25 @@ We can register a handler for the @scheme["hello"] path like this:
@whole-prog["5"] @whole-prog["5"]
After adding these lines and evaluating @scheme[(enter! "serve.ss")], After adding these lines and evaluating @racket[(enter! "serve.rkt")],
opening @tt{http://localhost:8081/hello} should produce the old opening @tt{http://localhost:8081/hello} should produce the old
greeting. greeting.
@; ---------------------------------------------------------------------- @; ----------------------------------------------------------------------
@section{Servlets and Sessions} @section{Servlets and Sessions}
Using the @scheme[query] argument that is passed to a handler by Using the @racket[query] argument that is passed to a handler by
@scheme[dispatch], a handler can respond to values that a user @racket[dispatch], a handler can respond to values that a user
supplies through a form. supplies through a form.
The following helper function constructs an HTML form. The The following helper function constructs an HTML form. The
@scheme[label] argument is a string to show the user. The @racket[label] argument is a string to show the user. The
@scheme[next-url] argument is a destination for the form results. The @racket[next-url] argument is a destination for the form results. The
@scheme[hidden] argument is a value to propagate through the form as a @racket[hidden] argument is a value to propagate through the form as a
hidden field. When the user responds, the @scheme["number"] field in hidden field. When the user responds, the @racket["number"] field in
the form holds the user's value: the form holds the user's value:
@schemeblock[ @racketblock[
(define (build-request-page label next-url hidden) (define (build-request-page label next-url hidden)
`(html `(html
(head (title "Enter a Number to Add")) (head (title "Enter a Number to Add"))
@ -547,7 +550,10 @@ the form holds the user's value:
Using this helper function, we can create a servlet that generates as Using this helper function, we can create a servlet that generates as
many ``hello''s as a user wants: many ``hello''s as a user wants:
@schemeblock[ @margin-note{See @secref[#:doc '(lib "scribblings/guide/guide.scrbl") "for"]
for an introduction to forms like @racket[for/list].}
@racketblock[
(define (many query) (define (many query)
(build-request-page "Number of greetings:" "/reply" "")) (build-request-page "Number of greetings:" "/reply" ""))
@ -563,23 +569,23 @@ many ``hello''s as a user wants:
@whole-prog["6"] @whole-prog["6"]
As usual, once you have added these to your program, update with As usual, once you have added these to your program, update with
@scheme[(enter! "serve.ss")], and then visit @racket[(enter! "serve.rkt")], and then visit
@tt{http://localhost:8081/many}. Provide a number, and you'll receive @tt{http://localhost:8081/many}. Provide a number, and you'll receive
a new page with that many ``hello''s. a new page with that many ``hello''s.
@; ---------------------------------------------------------------------- @; ----------------------------------------------------------------------
@section{Limiting Memory Use} @section{Limiting Memory Use}
With our latest @scheme["many"] servlet, we seem to have a new With our latest @racket["many"] servlet, we seem to have a new
problem: a malicious client could request so many ``hello''s that the problem: a malicious client could request so many ``hello''s that the
server runs out of memory. Actually, a malicious client could also server runs out of memory. Actually, a malicious client could also
supply an HTTP request whose first line is arbitrarily long. supply an HTTP request whose first line is arbitrarily long.
The solution to this class of problems is to limit the memory use of a The solution to this class of problems is to limit the memory use of a
connection. Inside @scheme[accept-and-handle], after the definition of connection. Inside @racket[accept-and-handle], after the definition of
@scheme[cust], add the line @racket[cust], add the line
@schemeblock[(custodian-limit-memory cust (* 50 1024 1024))] @racketblock[(custodian-limit-memory cust (* 50 1024 1024))]
@whole-prog["7"] @whole-prog["7"]
@ -591,15 +597,15 @@ for each other's memory use, so one misbehaving connection will not
interfere with a different one. interfere with a different one.
So, with the new line above, and assuming that you have a couple of So, with the new line above, and assuming that you have a couple of
hundred megabytes available for the @exec{mzscheme} process to use, hundred megabytes available for the @exec{racket} process to use,
you shouldn't be able to crash the web server by requesting a you shouldn't be able to crash the web server by requesting a
ridiculously large number of ``hello''s. ridiculously large number of ``hello''s.
Given the @scheme["many"] example, it's a small step to a web server Given the @racket["many"] example, it's a small step to a web server
that accepts arbitrary Scheme code to execute on the server. In that that accepts arbitrary Racket code to execute on the server. In that
case, there are many additional security issues besides limiting case, there are many additional security issues besides limiting
processor time and memory consumption. The processor time and memory consumption. The
@schememodname[scheme/sandbox] library provides support to managing @racketmodname[racket/sandbox] library provides support to managing
all those other issues. all those other issues.
@; ---------------------------------------------------------------------- @; ----------------------------------------------------------------------
@ -607,9 +613,9 @@ all those other issues.
As a systems example, the problem of implementing a web server exposes As a systems example, the problem of implementing a web server exposes
many system and security issues where a programming language can many system and security issues where a programming language can
help. The web-server example also leads to a classic, advanced Scheme help. The web-server example also leads to a classic, advanced Racket
topic: @defterm{continuations}. In fact, this facet of a web server topic: @defterm{continuations}. In fact, this facet of a web server
needs @defterm{delimited continuations}, which PLT Scheme provides. needs @defterm{delimited continuations}, which Racket provides.
The problem solved by continuations is related to servlet sessions and The problem solved by continuations is related to servlet sessions and
user input, where a computation spans multiple client connections user input, where a computation spans multiple client connections
@ -619,11 +625,11 @@ a mixture of techniques (e.g., to take advantage of the browser's
``back'' button). ``back'' button).
As the multi-connection computation becomes more complex, propagating As the multi-connection computation becomes more complex, propagating
arguments through @scheme[query] becomes increasingly tedious. For arguments through @racket[query] becomes increasingly tedious. For
example, we can implement a servlet that takes two numbers to add by example, we can implement a servlet that takes two numbers to add by
using the hidden field in the form to remember the first number: using the hidden field in the form to remember the first number:
@schemeblock[ @racketblock[
(define (sum query) (define (sum query)
(build-request-page "First number:" "/one" "")) (build-request-page "First number:" "/one" ""))
@ -647,7 +653,7 @@ using the hidden field in the form to remember the first number:
While the above works, we would much rather write such computations in While the above works, we would much rather write such computations in
a direct style: a direct style:
@schemeblock[ @racketblock[
(define (sum2 query) (define (sum2 query)
(define m (get-number "First number:")) (define m (get-number "First number:"))
(define n (get-number "Second number:")) (define n (get-number "Second number:"))
@ -656,32 +662,32 @@ a direct style:
(hash-set! dispatch-table "sum2" sum2) (hash-set! dispatch-table "sum2" sum2)
] ]
The problem is that @scheme[get-number] needs to send an HTML response The problem is that @racket[get-number] needs to send an HTML response
back for the current connection, and then it must obtain a response back for the current connection, and then it must obtain a response
through a new connection. That is, somehow it needs to convert the through a new connection. That is, somehow it needs to convert the
page generated by @scheme[build-request-page] into a @scheme[query] page generated by @racket[build-request-page] into a @racket[query]
result: result:
@schemeblock[ @racketblock[
(define (get-number label) (define (get-number label)
(define query (define query
... (build-request-page label ...) ...) ... (build-request-page label ...) ...)
(number->string (cdr (assq 'number query)))) (number->string (cdr (assq 'number query))))
] ]
Continuations let us implement a @scheme[send/suspend] operation that Continuations let us implement a @racket[send/suspend] operation that
performs exactly that operation. The @scheme[send/suspend] procedure performs exactly that operation. The @racket[send/suspend] procedure
generates a URL that represents the current connection's computation, generates a URL that represents the current connection's computation,
capturing it as a continuation. It passes the generated URL to a capturing it as a continuation. It passes the generated URL to a
procedure that creates the query page; this query page is used as the procedure that creates the query page; this query page is used as the
result of the current connection, and the surrounding computation result of the current connection, and the surrounding computation
(i.e., the continuation) is aborted. Finally, @scheme[send/suspend] (i.e., the continuation) is aborted. Finally, @racket[send/suspend]
arranges for a request to the generated URL (in a new connection) to arranges for a request to the generated URL (in a new connection) to
restore the aborted computation. restore the aborted computation.
Thus, @scheme[get-number] is implemented as follows: Thus, @racket[get-number] is implemented as follows:
@schemeblock[ @racketblock[
(define (get-number label) (define (get-number label)
(define query (define query
(code:comment @#,t{Generate a URL for the current computation:}) (code:comment @#,t{Generate a URL for the current computation:})
@ -695,40 +701,39 @@ Thus, @scheme[get-number] is implemented as follows:
(string->number (cdr (assq 'number query)))) (string->number (cdr (assq 'number query))))
] ]
We still have to implement @scheme[send/suspend]. Plain Scheme's We still have to implement @racket[send/suspend]. For that task, we
@scheme[call/cc] is not quite enough, so we import a library of import a library of control operators:
control operators:
@schemeblock[(require scheme/control)] @racketblock[(require racket/control)]
Specifically, we need @scheme[prompt] and @scheme[abort] from Specifically, we need @racket[prompt] and @racket[abort] from
@schememodname[scheme/control]. We use @scheme[prompt] to mark the @racketmodname[racket/control]. We use @racket[prompt] to mark the
place where a servlet is started, so that we can abort a computation place where a servlet is started, so that we can abort a computation
to that point. Change @scheme[handle] by wrapping an @scheme[prompt] to that point. Change @racket[handle] by wrapping an @racket[prompt]
around the call to @scheme[dispatch]: around the call to @racket[dispatch]:
@schemeblock[ @racketblock[
(define (handle in out) (define (handle in out)
.... ....
(let ([xexpr (prompt (dispatch (list-ref req 1)))]) (let ([xexpr (prompt (dispatch (list-ref req 1)))])
....)) ....))
] ]
Now, we can implement @scheme[send/suspend]. We use @scheme[call/cc] Now, we can implement @racket[send/suspend]. We use @racket[call/cc]
in the guise of @scheme[let/cc], which captures the current in the guise of @racket[let/cc], which captures the current
computation up to an enclosing @scheme[prompt] and binds that computation up to an enclosing @racket[prompt] and binds that
computation to an identifier---@scheme[k], in this case: computation to an identifier---@racket[k], in this case:
@schemeblock[ @racketblock[
(define (send/suspend mk-page) (define (send/suspend mk-page)
(let/cc k (let/cc k
...)) ...))
] ]
Next, we generate a new dispatch tag, and we record the mapping from Next, we generate a new dispatch tag, and we record the mapping from
the tag to @scheme[k]: the tag to @racket[k]:
@schemeblock[ @racketblock[
(define (send/suspend mk-page) (define (send/suspend mk-page)
(let/cc k (let/cc k
(define tag (format "k~a" (current-inexact-milliseconds))) (define tag (format "k~a" (current-inexact-milliseconds)))
@ -737,10 +742,10 @@ the tag to @scheme[k]:
] ]
Finally, we abort the current computation, supplying instead the page Finally, we abort the current computation, supplying instead the page
that is built by applying the given @scheme[mk-page] to a URL for the that is built by applying the given @racket[mk-page] to a URL for the
generated tag: generated tag:
@schemeblock[ @racketblock[
(define (send/suspend mk-page) (define (send/suspend mk-page)
(let/cc k (let/cc k
(define tag (format "k~a" (current-inexact-milliseconds))) (define tag (format "k~a" (current-inexact-milliseconds)))
@ -751,21 +756,21 @@ generated tag:
When the user submits the form, the handler associated with the form's When the user submits the form, the handler associated with the form's
URL is the old computation, stored as a continuation in the dispatch URL is the old computation, stored as a continuation in the dispatch
table. Calling the continuation (like a function) restores the old table. Calling the continuation (like a function) restores the old
computation, passing the @scheme[query] argument back to that computation, passing the @racket[query] argument back to that
computation. computation.
@whole-prog["9" #t] @whole-prog["9" #t]
In summary, the new pieces are: @scheme[(require scheme/control)], In summary, the new pieces are: @racket[(require racket/control)],
adding @scheme[prompt] inside @scheme[handle], the definitions of adding @racket[prompt] inside @racket[handle], the definitions of
@scheme[send/suspend], @scheme[get-number], and @scheme[sum2], and @racket[send/suspend], @racket[get-number], and @racket[sum2], and
@scheme[(hash-set! dispatch-table "sum2" sum2)]. Once you have @racket[(hash-set! dispatch-table "sum2" sum2)]. Once you have
the server updated, visit @tt{http://localhost:8081/sum2}. the server updated, visit @tt{http://localhost:8081/sum2}.
@; ---------------------------------------------------------------------- @; ----------------------------------------------------------------------
@section{Where to Go From Here} @section{Where to Go From Here}
The PLT Scheme distribution includes a production-quality web server The Racket distribution includes a production-quality web server
that addresses all of the design points mentioned here and more. that addresses all of the design points mentioned here and more.
To learn more, see the tutorial @other-manual['(lib To learn more, see the tutorial @other-manual['(lib
"web-server/scribblings/tutorial/continue.scrbl")], the "web-server/scribblings/tutorial/continue.scrbl")], the
@ -773,8 +778,8 @@ documentation @other-manual['(lib
"web-server/scribblings/web-server.scrbl")], or the research paper "web-server/scribblings/web-server.scrbl")], or the research paper
@cite["Krishnamurthi07"]. @cite["Krishnamurthi07"].
Otherwise, if you arrived here as part of an introduction to PLT Otherwise, if you arrived here as part of an introduction to
Scheme, then your next stop is probably @|guide|. Racket, then your next stop is probably @|guide|.
If the topics covered here are the kind that interest you, see also If the topics covered here are the kind that interest you, see also
@secref["concurrency" #:doc '(lib @secref["concurrency" #:doc '(lib
@ -783,10 +788,10 @@ If the topics covered here are the kind that interest you, see also
"scribblings/reference/reference.scrbl")]. "scribblings/reference/reference.scrbl")].
Some of this material is based on relatively recent research, and more Some of this material is based on relatively recent research, and more
information can be found in papers written by the authors of PLT information can be found in papers written by the authors of Racket,
Scheme, including papers on MrEd @cite["Flatt99"], memory accounting including papers on GRacket (formerly ``MrEd'') @cite["Flatt99"],
@cite["Wick04"], kill-safe abstractions @cite["Flatt04"], and memory accounting @cite["Wick04"], kill-safe abstractions
delimited continuations @cite["Flatt07"]. @cite["Flatt04"], and delimited continuations @cite["Flatt07"].
@; ---------------------------------------------------------------------- @; ----------------------------------------------------------------------

View File

@ -1,4 +1,4 @@
#lang scheme #lang racket
(define (go) (define (go)
'yep-it-works) 'yep-it-works)

View File

@ -1,4 +1,4 @@
#lang scheme #lang racket
(define (serve port-no) (define (serve port-no)
(define listener (tcp-listen port-no 5 #t)) (define listener (tcp-listen port-no 5 #t))

View File

@ -1,4 +1,4 @@
#lang scheme #lang racket
;; The `serve' function is revised to run the loop ;; The `serve' function is revised to run the loop
;; in a thread, and it returns a function to shut down ;; in a thread, and it returns a function to shut down

View File

@ -1,4 +1,4 @@
#lang scheme #lang racket
;; Only `accept-and-handle' changes, moving the ;; Only `accept-and-handle' changes, moving the
;; handle work into a thread. ;; handle work into a thread.

View File

@ -1,4 +1,4 @@
#lang scheme #lang racket
;; Both `server' and `accept-and-handle' change ;; Both `server' and `accept-and-handle' change
;; to use a custodian. ;; to use a custodian.

View File

@ -1,4 +1,4 @@
#lang scheme #lang racket
;; New imports: ;; New imports:
(require xml net/url) (require xml net/url)

View File

@ -1,4 +1,4 @@
#lang scheme #lang racket
;; For changes, skip down to `build-request-page', ;; For changes, skip down to `build-request-page',
;; after the line. ;; after the line.

View File

@ -1,4 +1,4 @@
#lang scheme #lang racket
;; There's only one change. It's in `accept-and-handle', ;; There's only one change. It's in `accept-and-handle',
;; and it's marked with "<<<". ;; and it's marked with "<<<".

View File

@ -1,4 +1,4 @@
#lang scheme #lang racket
;; No changes to old code --- just three new ;; No changes to old code --- just three new
;; functions and registrations at the end. ;; functions and registrations at the end.

View File

@ -1,10 +1,10 @@
#lang scheme #lang racket
;; See "<<<" for two small changes, then jump down ;; See "<<<" for two small changes, then jump down
;; to `send/suspend'. ;; to `send/suspend'.
(require xml net/url (require xml net/url
scheme/control) ;; <<< new import racket/control) ;; <<< new import
(define (serve port-no) (define (serve port-no)
(define main-cust (make-custodian)) (define main-cust (make-custodian))