Mostly from scratch (i.e. no external libraries apart from that available in standard Linux distributions).
DISCLAIMER: THIS IS NOT YET READY TO USE. A LOT OF THINGS ARE STILL HARDCODED, SUCH AS ONLY ALLOWING hello.txt FROM allowed_files DIRECTORY.
This library is header-only, so once you copy the entire lib directory into your project root, you should be good to go. Feel free to compile then link as a shared object or static library so that your repo is less cluttered.
#include "../lib/http_server.h"
#define TEST_PORT 60001
int main() {
printf("Test server running on port %d\n", TEST_PORT);
struct HttpServer* http_server = malloc(sizeof (struct HttpServer));
init_http_server(http_server, TEST_PORT);
run_http_server(http_server);
return 0;
}There are a few reasons why I wanted to do this. In no particular order:
- I first came across the idea from the Youtube channel Low Level. He suggested a good way to learn programming is to write an HTTP server in C. I then came across ThePrimeagen talking about the same thing.
- I'm interested in compilers (I've also been following Crafting Interpreters by the wonderful Robert Nystrom!), and since HTTP servers involves writing a parser (albeit simple), it seemed like a good start.
- To learn (under the hood) how network libraries works, at least at the application layer, and how it interacts with the Linux kernel through syscalls and the transport layer.
- I'm interested in systems programming.
- I've never fully understood what the composite phrase "event-driven single-threaded non-blocking asynchronous I/O". Maybe this would teach me?
- I've never understood how to apply the concept of "state machine".
- Perhaps, it may make me a more appreciative C++/Rust developer (and hopefully a better one, but let's not get greedy).
This is the planned architecture. Currently, the server is HTTP/0.9 compliant only, hence the HTTP response only includes the file data, so the HttpBuilder module is not used nor is it implemented yet. I plan to use it to build the correct HTTP response, such as the status, when I eventually implement HTTP/1.0 support.
Things I want to cover:
- Why single-threading instead of multi-threading, and what challenges single-threading brings. This includes using
epoll, howrecvis blocking. - How TCP is a stream-oriented protocol (and hence the name
SOCK_STREAM), which means there's no message boundary for HTTP messages. Partial buffers means I required per-client queues. Can discuss memory tradeoff and how that may or may not be an issue. - How the two issues of array shifting problem and failed
process_connection_buffercalls are alleviated viamemmove. - How the DataIO is still blocking and how I plan to improve that.
- The idea of a (finite) state machine and how it's implemented in the
HttpParsermodule. epollissues with edge-triggered interfaces, and what design tradeoff it involves.- Exploration of nginx and how I can learn from them to improve my design.
There are also a collection of C/Linux based notes I found interesting, coming from a more OOP/high-level language background.
- Why use preprocessor macros instead of
const? selectvspollvsepoll? How about thepselect,ppollalternatives?strncmpvsstrcmp? How to deal with non-null-terminated strings?- Why always pass in pointers of caller-created objects into functions?
- What the closest thing to "constructors", "destructors" and "member functions" look like in C, in my opinion.
memmovevsmemcpy- The issues of non-null-termination.
- Async engine for the
DataIOmodule. This is quite big, since it requires the entireHttpServerto maintain state for each connection. In other words, the entireHttpServerneeds to be re-designed to become a more sophisticated state machine.
- What if the user sends a lot of superfluous \n or \r characters in their message/uri? Should we strip them away?
- What if the user sends multiple (like a lot) of \r\n (i.e. CLRFCLRFCLRFCLRF...)? We only find the first CLRF. Should we clear them as well? Otherwise, it will be found as CLRF in the next iteration, but then passed onto
parse_simple_requestas an empty HTTP request... then we of course reject them. - Check for any potential memory leaks.
- Test with 1000+ concurrent clients, for correctness and benchmark for efficiency.
- Write a nice testing framework to help with the above point.
- Should we periodically flush a client's buffer so that their invalid messages don't take up space?
- Change all printf to logger traces (better instrumentation).
- Make a load tester for HTTP server.
NGINX
