docs: start architure file
parent
68f9227436
commit
90d83bc5d4
|
@ -0,0 +1,58 @@
|
||||||
|
# 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.
|
|
@ -172,7 +172,7 @@ void event_loop_run(event_loop *el, int port) {
|
||||||
|
|
||||||
// poll for active fds
|
// poll for active fds
|
||||||
// the timeout argument doesn't matter here
|
// the timeout argument doesn't matter here
|
||||||
int rv = poll(poll_args, (nfds_t)poll_args_count, 1000);
|
int rv = poll(poll_args, (nfds_t)poll_args_count, 0);
|
||||||
|
|
||||||
if (rv < 0) {
|
if (rv < 0) {
|
||||||
critical(1, "Poll failed, errno: %i", errno);
|
critical(1, "Poll failed, errno: %i", errno);
|
||||||
|
|
Loading…
Reference in New Issue