docs: start architure file

c-web-server
Jef Roosens 2023-05-27 12:21:19 +02:00
parent 68f9227436
commit 90d83bc5d4
Signed by: Jef Roosens
GPG Key ID: B75D4F293C7052DB
2 changed files with 59 additions and 1 deletions

58
ARCHITECTURE.md 100644
View File

@ -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.

View File

@ -172,7 +172,7 @@ void event_loop_run(event_loop *el, int port) {
// poll for active fds
// 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) {
critical(1, "Poll failed, errno: %i", errno);