Chapter 10

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.

Where to go from here. Serve a directory of your own. Add a /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.