A Web Server
Everything in this book, at once: sockets, strings,
threads, and per-request memory that ends when the request does. About
eighty lines, ships with the compiler as
examples/server.sure.
The plan
An HTTP/1.0-style static file server: accept a connection, read the request, find the path, send the file or a 404, close. One function per concern, and — because this is fearless-concurrency — every connection handled on its own thread.
Reading the request
The request line looks like GET /index.html HTTP/1.1.
The path lives between the first two spaces:
func request_path(request: String) -> String {
// the path lives between the first two spaces. we checked.
let start = find(request, " ") + 1
let rest = slice(request, start, len(request))
let stop = find(rest, " ")
if stop < 0 {
return "/"
}
return slice(rest, 0, stop)
}
func main() -> Int {
print(request_path("GET /book/ch10.html HTTP/1.1"))
return 0
}
Answering
A response is a status line, headers, a blank line, and the body.
Content-Length is str(len(body)) — the reward
for strings knowing their length in bytes:
func status_line(code: Int) -> String {
if code == 200 {
return "HTTP/1.1 200 OK"
}
return "HTTP/1.1 404 Not Found"
}
func respond(conn: Int, code: Int, kind: String, body: String) {
let head = (status_line(code) + "\r\nContent-Type: " + kind
+ "\r\nContent-Length: " + str(len(body))
+ "\r\nConnection: close\r\n\r\n")
tcp_send(conn, head + body)
tcp_close(conn)
}
func main() -> Int {
return 0
}
(The parentheses around the header expression let it span lines; inside parentheses, line breaks are just whitespace.)
Handling a connection
One function owns the life of one request. This is good design in
any language; in Sure it also gives the memory schedule a clean story —
request, path, and file are born,
used, and freed inside one function on one thread:
func handle(conn: Int) {
let request = tcp_recv(conn)
var path = request_path(request)
if path == "/" {
path = "/index.html"
}
let file = "." + path
if file_exists(file) {
respond(conn, 200, content_type(file), read_file(file))
} else {
respond(conn, 404, "text/plain", "not found. are you sure?\n")
}
}
content_type maps extensions to MIME types with a chain
of finds — see the full example for the four lines. Note
read_file returns bytes, so images serve as correctly as
HTML does.
The accept loop
func main() -> Int {
var port = to_int(getenv("SURE_PORT"))
if port == 0 {
port = 8080
}
let listener = tcp_listen(port)
if listener < 0 {
print("could not listen. the port is not sure")
return 1
}
print("serving on port " + str(port))
while true {
let conn = tcp_accept(listener)
spawn handle(conn)
}
return 0
}
The loop does two things: accept, spawn. A slow client costs its own
thread some time and nobody else's. The connection descriptor is an
Int, which crosses the spawn by value; everything
heap-shaped is created on the handling thread, lives there, and dies
there.
Run it
$ sure build server.sure 👍 $ ./server serving on port 8080
$ curl -i http://localhost:8080/index.html HTTP/1.1 200 OK Content-Type: text/html Content-Length: 13
Then ask the compiler about the choices it made for you:
$ sure blame server.sure handle.request handle.request: freed after line 31 (`var path = request_path(request)`), confidence 0.97
Long-running servers are the strongest possible argument for correct memory management, which is why this is the project chapter. Each request's memory is freed on the request's own thread, at lines chosen at compile time, every request, indefinitely — and if you ever observe otherwise, you know the remediation by now.
/stats route that counts requests in a map
— you will want a channel to own the map, per chapter 9's advice. Add a
// TODO: sanitize the path comment to
handle and run sure expand to watch the
language's phases cooperate. You have the entire toolbox now.