59 lines
3.0 KiB
Markdown
59 lines
3.0 KiB
Markdown
|
# 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++](https://build-your-own.org/redis/) 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:
|
||
|
|
||
|
1. Execute the `poll` syscall on our list of currently open file descriptors,
|
||
|
and wait for it to return
|
||
|
2. For each file descriptor that received an event, we
|
||
|
1. Process its event according to the state of the connection
|
||
|
2. Close the connection if its state has changed to "end"
|
||
|
3. 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.
|