Library
Networking — sockets and HTTP
TCP streams, listeners and the in-language HTTP request and response model, all driven by the event loop.
since 0.1.0-alpha.1linux
Description
Networking is built from the stream model (see lang.streams) and the event loop (see
lang.event-loop). Nothing blocks: a socket is read through callbacks, and the loop calls them
when data arrives.
Sockets. A TcpStream is a connected socket. You send with send(text) or <<, receive with
onData(callback), learn that the peer went away with onClose(callback), and release it with
close(). A TcpListener is a listening socket seen as a stream of connections:
connections(callback) hands a new TcpStream to the callback for every client. Both are
described in their own entries.
Connecting. std::connectTimeout(host, port, ms, callback) opens a connection without
blocking and calls callback with the connected descriptor, ready to wrap in a TcpStream, or
with -1 if the connection was refused, the host was unreachable or malformed, or ms
milliseconds passed first. A host written as an IPv6 literal (anything containing :) connects
over IPv6.
Sending never silently loses data. The socket is non-blocking, so a large payload may not fit
in one write. send queues the part the kernel did not take and drains it as room appears.
flush() is for the rare caller that wants to know: it suspends until everything queued has been
handed to the transport, and returns true once it has, or false if the connection closed
or failed first. It is a barrier on the local queue, not a confirmation from the peer.
HTTP is written in the language on top of those streams, so its building blocks are plain objects you can create and inspect without any network:
HeaderMapis an ordered, case-insensitive multimap of headers.addappends,setreplaces every entry of that name and appends once,first(name)returnsstring?,all(name)returns every value,has,remove,entries(), andrender()produce the wire text. Because order and duplicates are kept, severalSet-Cookieheaders survive.HttpRequesthasmethod,path,version,bodyandheaders, andheader(name)(an empty string when absent).parse(raw)reads a request that is already buffered, andfeed(chunk)reads one incrementally: it returnstrueonce the head and the whole body (as declared byContent-Length) have arrived.HttpResponse(status, body)hasheaders,withHeader(name, value),reason(),render()andparse(raw).render()computesContent-LengthandConnection, and drops anyContent-Length,Transfer-EncodingorConnectionheader you set, so the values on the wire are always consistent.parsedecodes aTransfer-Encoding: chunkedbody for you.HttpResponse::ofStream(status, headers, writer)is the streaming body mode. The server sends aTransfer-Encoding: chunkedhead and then callswriterwith a liveChunkedSink.sink.write(data)sends one chunk and suspends until the transport has taken it (so a slow client slows the writer down),sink.end()finishes the body,sink.onClose(callback)runs if the connection ends prematurely, andsink.isClosed()says whether the sink is finished either way.writeandendafter the sink is finished do nothing, and calls that overlap are kept in order so chunks never interleave. The writer may return and keep the sink open for later writes from a timer or subscription, which is the shape of server-sent events.isStreaming()tells the two modes apart.render()throws on a streaming response, because onlyHttpServermay drive it.ChunkedDecoderdecodes chunked transfer encoding from fragments of any size (feed(chunk),isDone).std::chunkEncode(data)andstd::chunkEnd()are the encoder side.
Headers: ordered, case-insensitive, duplicates kept
HeaderMap h = HeaderMap();
h.add("Set-Cookie", "a=1").add("Set-Cookie", "b=2");
h.set("Content-Type", "text/plain");
console.writeln(h.has("content-type"));
string? ct = h.first("CONTENT-TYPE");
console.writeln(ct ?? "none");
console.writeln(h.all("set-cookie").length());
h.set("Content-Type", "text/html");
console.writeln(h.length());
h.remove("Set-Cookie");
console.writeln(h.render().replace("\r\n", "|"));
true
text/plain
2
3
Content-Type: text/html|
Rules
- Text only. HTTP bodies are strings; binary bodies are not supported yet.
- One connection per client request.
HttpClientopens a connection per request and sendsConnection: close. There is no connection pooling. - Keep-alive on the server.
HttpServerkeeps a connection open for further requests unless the request sentConnection: closeor the handler failed, up to 100 requests per connection. A finite streamed response keeps the connection only after its last chunk has drained. - No pipelining. If bytes arrive while a response is still being sent, the server closes the connection.
- Handler errors become responses. An uncaught exception in a handler becomes a
500response withConnection: close, and the server keeps running. - Bodiless responses. For a
HEADrequest, or a1xx,204or304status, a streaming response sends only its head and never calls the writer. Chunked streaming needsHTTP/1.1: a streaming response to a request of any other version is replaced by505and the connection is closed. - Secure connections use the same objects; see
lang.tls.
Examples
A request can be parsed from text, whether it arrives all at once or in fragments:
Parsing HTTP requests
HttpRequest req = HttpRequest();
req.parse("POST /submit?x=1 HTTP/1.1\r\nHost: example.com\r\nContent-Length: 5\r\n\r\nhello");
console.writeln(req.method);
console.writeln(req.path);
console.writeln(req.version);
console.writeln(req.header("host"));
console.writeln(req.body);
string absent = req.header("missing");
console.writeln("missing header length: ${absent.length()}");
HttpRequest inc = HttpRequest();
console.writeln(inc.feed("GET /a HTTP/1.1\r\nHo"));
console.writeln(inc.feed("st: x\r\n\r\n"));
console.writeln(inc.path);
POST
/submit?x=1
HTTP/1.1
example.com
hello
missing header length: 0
false
true
/a
A response renders to the exact text sent on the wire. Note that the caller's own
Content-Length: 999 was dropped and the correct 5 computed. Parsing decodes chunked bodies:
Rendering, parsing and chunked bodies
HttpResponse ok = HttpResponse(200, "hello");
ok.withHeader("Content-Type", "text/plain").withHeader("Content-Length", "999");
string wire = ok.render();
console.writeln(wire.replace("\r\n", "<CRLF>\n"));
console.writeln(HttpResponse(404, "").reason());
HttpResponse parsed = HttpResponse(0, "");
parsed.parse("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n");
console.writeln(parsed.status);
console.writeln(parsed.body);
console.writeln(std::chunkEncode("abc").replace("\r\n", "<CRLF>"));
console.writeln(std::chunkEnd().replace("\r\n", "<CRLF>"));
HTTP/1.1 200 OK<CRLF>
Content-Length: 5<CRLF>
Connection: close<CRLF>
Content-Type: text/plain<CRLF>
<CRLF>
hello
Not Found
200
hello world
3<CRLF>abc<CRLF>
0<CRLF><CRLF>
A streaming response is a different mode, and only a server may render it:
Streaming responses
HeaderMap h = HeaderMap();
h.set("Content-Type", "text/event-stream");
HttpResponse live = HttpResponse::ofStream(200, h, (sink) => {
sink.write("data: hi\n\n");
sink.end();
});
console.writeln(live.isStreaming());
console.writeln(HttpResponse(200, "x").isStreaming());
try {
live.render();
} catch (RuntimeException e) {
console.writeln(e.message);
}
true
false
render() called on a streaming HttpResponse
Opening a connection with a deadline:
std::connectTimeout("example.com", 80, 2000, (fd) => {
if (fd < 0) {
console.writeln("could not connect");
return;
}
TcpStream conn = TcpStream(fd);
conn.onData((chunk) => console.writeln("received ${chunk.length()} bytes"));
conn.onClose(() => console.writeln("peer closed"));
conn << "GET / HTTP/1.0\r\n\r\n";
});
Notes
Not implemented: client-side redirects, parsing a URL string into host, port and path (the client takes them separately), a request timeout, request pipelining, chunked uploads from the client, and client-side connection pooling.
See also
- TcpStream — A connected network socket that reads and writes text.
- TcpListener — A listening socket that delivers each incoming connection as a
TcpStream. - HttpClient — A simple HTTP client that sends one request per connection and delivers the response to a callback.
- HttpServer — A web server that listens on a port and answers each request with a handler function.
- HeaderMap — An ordered collection of HTTP headers whose names are matched without regard to letter case.
- HttpRequest — An HTTP request as seen by a server: the request line, the headers and the body.
- HttpResponse — An HTTP response: a status code, headers and a body.
- ChunkedSink — The writer for a streaming HTTP response body.
- Timer — A source of ticks on the event loop.