3.0 KiB
Architecture
Lander is (almost) completely made up of self-written components. This file describes how these various components are designed and interact.
Server
Lander is an HTTP/1.1 server, meaning we need some sort of server framework.
Event loop
At the bottom of the stack is an implementation of an event loop, heavily inspired by the Build Your Own Redis with C/C++ book, written by James Smith. This book was instrumental in understanding how an event loop works, and I can hardly recommending checking it out!
The event loop relies on the concept of non-blocking I/O. Instead of e.g.
starting a read
command and waiting for its data to be retrieved, we can
instead use the poll
syscall to wait for sockets to be ready for operation.
Thanks to this, we can let the kernel wait for I/O for us, allowing our program
to process data immediately using large CPU-bound bursts of work.
Each cycle of the event loop consists of the same, surprisingely simple, steps:
- Execute the
poll
syscall on our list of currently open file descriptors, and wait for it to return - For each file descriptor that received an event, we
- Process its event according to the state of the connection
- Close the connection if its state has changed to "end"
- Finally, if the main file descriptor received an event, we try to accept a connection
Each connection can be in one of three states: req
, res
or end
. Requests
always start in the req
state, which is the reading & processing state. While
in the req
state, the connection will try to read data from the socket (an
incoming request). After each read, the data processing function is called,
which tries to interpret the currently buffered data and process the request.
This function will then write some data to the write buffer, after which the
request is switched to the res
state. In this state, no data is processed,
and all that the event loop tries to do is write the entire write buffer to the
socket. After this is done, the request switches back to the req
mode.
By letting the res
state transition back into the req
state, we can both
allow request pipelining (where multiple requests are sent over the same
connection) and allow the data handler to process requests greater than the
connection buffer size. Note that the event loop solely processes data stored
in its buffers, and switching between the req
and res
state does not mean
that the upper layer using this event loop has also fully processed its
request. In the context of the event loop, a response is solely what's in its
write buffer.
To allow building upon this event loopp, it provides each connection with a context struct, and optionally a global context that's the same for every connection. The creation of these contexts, along with the data handler function, can be provided by a higher layer, providing the building blocks for higher-level loop implementations. This concept is used to implement the next layer of Lander, namely the HTTP loop.