From e5bad9cf74b2b2f025d5383b3bc6e78cb3325588 Mon Sep 17 00:00:00 2001 From: SimonFJ20 Date: Mon, 10 Mar 2025 12:55:46 +0100 Subject: [PATCH] add http_server_i_c.md --- docs/http_server_i_c.md | 331 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 331 insertions(+) create mode 100644 docs/http_server_i_c.md diff --git a/docs/http_server_i_c.md b/docs/http_server_i_c.md new file mode 100644 index 0000000..8527236 --- /dev/null +++ b/docs/http_server_i_c.md @@ -0,0 +1,331 @@ + +# HTTP server i C + +Jeg vil her forsøge at beskrive definerende problemer og væsentlige designdetaljer i vores implementering. + +## Showcase + +### Application server + +Vi tog udgangspunkt i NodeJS' ExpressJS i designet af HTTP-serverens API. (Mere specifikt Deno's Oak, som basically er et rewrite af ExpressJS). + +Eksempel på ExpressJS: +```ts +const app = express(); + +app.get("/products/all", (req, res) => { + res.json({ + ok: true, + products: [ + { id: 1, name: "Letmælk", price: 1200 }, + { id: 2, name: "Smør", price: 2400 }, + ], + }) +}); + +app.listen(8080); +``` + +Eksempel på vores HTTP server API: +```c +int main(void) +{ + HttpServer* server = http_server_new((HttpServerOpts){ + .port = 8080, + .workers = 8, /* 8 worker threads*/ + }); + + if (!server) { + fprintf(stderr, "could not start server\n"); + } + + http_server_get(server, "/products/all", get_products_all_handler); + + http_server_listen(server); + + http_server_free(server); +} + +void get_products_all_handler(HttpCtx* ctx) +{ + RESPOND_JSON(ctx, 200, + "{" + "\"ok\":true," + "\"products\":[" + "{\"id\":1,\"name\":\"Letmælk\",\"price\":1200}," + "{\"id\":2,\"name\":\"Smør\",\"price\":2400}" + "]" + "}"); +} +``` + +### Request body + +Vi har lavet værktøjer til at håndtere request body på POST-requests. Her sammenlignes det med ExpressJS. + +Eksempel i ExpressJS: +```ts +type LoginReq = { username: string, password: string }; + +app.post("/login", (req, res) => { + const reqBody = req.body as LoginReq; + + console.log(`username: ${reqBody.username}, password: ${reqBody.password}`); + // ... +}); +``` + +Eksempel med vores implementation: +```c +typedef struct { + char* username; + char* password; +} LoginReq; + +// Defined elsewhere. +void login_req_destroy(LoginReq* model); +int login_req_from_json(LoginReq* model, const JsonValue* json); + +void post_login(HttpCtx* ctx) +{ + const char* body_string = http_ctx_req_body(ctx); + JsonValue* body_json = json_parse(body_string, strlen(body_string)); + + if (!body_json) { + RESPOND_BAD_REQUEST("malformed body"); + return; + } + + LoginReq req_body; + int parse_res = login_req_from_json(&req_body, body_json); + free(body_json); + + if (parse_res != 0) { + RESPOND_BAD_REQUEST("malformed body"); + return; + } + + printf( + "username: %s, password: %s\n", + req_body.username, + req_body.password); + + // ... + loing_req_destroy(&req_body); +} +``` + +JSON-parseren er bespoke. + +### HTTP headers + +Vi har funktionalitet, så man både kan undersøge request headers og sætte response headers. + +#### Request headers + +```c +void get_is_authorized(HttpCtx* ctx) +{ + if (!http_ctx_req_headers_has(ctx, "Auth-Token")) { + RESPOND_JSON(ctx, 200, "{\"authorized\":false}"); + return; + } + char* token = http_ctx_req_headers_get(ctx, "Auth-Token"); + + if (strcmp(token, "...") != 0) { + // ... + } + // ... +} +``` + +#### Response headers + +```c +void post_login(HttpCtx* ctx) +{ + // ... + http_ctx_res_headers_set("Auth-Token", "..."); + // ... +} +``` + +### Referencer + +- HTTP-serverens offentlige API er defineret i [src/http_server.h](../backend/src/http_server.h). +- JSON-parserens offentlige API er defineret i [src/json.h](../backend/src/json.h). +- Eksempler på `*_from_json`-funktioner kan findes i [src/models.c](../backend/src/models.c). + +## HTTP-server med Linux + +### API'en og hoveddatatypen + +HTTP-serveren er defineret med en offentlig API, defineret i [src/http_server.h](../backend/src/http_server.h). Implementationen er defineret i [src/http_server_internal.h](../backend/src/http_server_internal.h) og [src/http_server..c](../backend/src/http_server.c). + +Den offentlige API er defineret sådan: +```c +// src/http_server.h:7 +typedef struct HttpServer HttpServer; + +// ... + +/// On ok, HttpServer +/// On error, returns NULL and prints. +HttpServer* http_server_new(HttpServerOpts opts); +void http_server_free(HttpServer* server); +/// On ok, returns 0. +/// On error, returns -1 and prints; +int http_server_listen(HttpServer* server); + +// ... +``` + +Hoveddatatypen `HttpServer` er opaque. I implementationen er den defineret sådan: +```c +// src/http_server_internal.h:89 +struct HttpServer { + int file; + SockAddrIn addr; + Cx ctx; + Worker* workers; + size_t workers_size; + HandlerVec handlers; + HttpHandlerFn not_found_handler; + void* user_ctx; +}; +``` + +`HttpServer`-typen bliver konstrueret med følgende funktion: +```c +// src/http_server.c:16 +HttpServer* http_server_new(HttpServerOpts opts) +{ + + int server_fd = socket(AF_INET, SOCK_STREAM, 0); + // ... + int res = bind(server_fd, (SockAddr*)&server_addr, sizeof(server_addr)); + // ... + res = listen(server_fd, 16); + // ... + HttpServer* server = malloc(sizeof(HttpServer)); + *server = (HttpServer) { + .file = server_fd, + // ... + .workers = malloc(sizeof(Worker) * opts.workers_amount), + .workers_size = opts.workers_amount, + // ... + }; + // ... + for (size_t i = 0; i < opts.workers_amount; ++i) { + worker_construct(&server->workers[i], &server->ctx); + } + // ... + return server; +} +``` + +Error-håndtering og diverse konstruktion er udeladt. + +Funktionen benytter Linux-funktionerne `socket`, `bind` og `listen` til at lave et server-socket, binde den til en port og sætte socket'et til at lytte på request. + +Funktionen konstruerer også et array af workers. + +### Request-kø + +Når en klient opretter forbindelse til serveren, tilføjer vi klienten til en klientkø eller request-kø (`req_queue`). Denne ligger på det interne server struct `Cx`. + +```c +// src/http_server_internal.h:24 +typedef struct { + const HttpServer* server; + pthread_mutex_t mutex; + pthread_cond_t cond; + ReqQueue req_queue; +} Cx; +``` + +En HTTP-server har én enkelt instans af dette struct. En pointer til denne instans bliver passed rundt mellem alle serverens worker-threads. Derfor skal struct'et understøtte multithreading, som er derfor den har en mutex. + +Måden workers'ne venter på nye requests/klienter, er ved at lytte på condition-variablen `cond`. + +`ReqQueue`-typen er en dynamisk array/vector, som er defineret med `DEFINE_VEC`-makro'en. Se [src/http_server_internal.h](../backend/src/http_server_internal.h:24) og [src/collection.h](../backend/src/collection.h) + +### Server listen + +For at kunne få request, skal serveren sættes til at lytte efter requests. Dette gøres med `http_server_listen`-funktionen, som er defineret sådan: + +```c +// src/http_server.c:77 +int http_server_listen(HttpServer* server) +{ + Cx* ctx = &server->ctx; + + while (true) { + SockAddrIn client_addr; + socklen_t addr_size = sizeof(client_addr); + + int res = accept(server->file, (SockAddr*)&client_addr, &addr_size); + // ... + + Client req = { .file = res, client_addr }; + pthread_mutex_lock(&ctx->mutex); + + res = request_queue_push(&ctx->req_queue, req); + /// ... + pthread_mutex_unlock(&ctx->mutex); + pthread_cond_signal(&ctx->cond); + } +} +``` + +For det første køres dette kode i en uendelig løkke (`while (true) {`), for at `accept` alle forbindelser. `accept`-funktionen pauser afvikling til en ny client forbinder. + +For det andet benytter funktionen Linux-funktionen `accept`, som henter nye forbindelser til et socket. + +For det tredje laver funktionen synkronisering, når den tilføjer til request-køen. Efter den har tilføjet til køen, notifyer den én worker, som så vil håndtere klienten. Dette gøres med `pthread_cond_signal`, som vækker en enkelt worker, ud af alle workers som lytter på `cond`-variablen. + +### Worker listen + +På samme måde som serveren lytter på klient-forbindelser udefra, lytter workers til klient-forbindelser/requests på request-køen. Denne gøres med `worker_listen`-funktion, som er defineret sådan: + +```c +// src/http_server.c:237 +static inline void worker_listen(Worker* worker) +{ + Cx* ctx = worker->ctx; + while (true) { + pthread_testcancel(); + + pthread_mutex_lock(&ctx->mutex); + + // If there is a request in the queue, handle the request + // and look again. + if (request_queue_size(&ctx->req_queue) > 0) { + // Pop a client from the queue. + Client req; + request_queue_pop(&ctx->req_queue, &req); + pthread_mutex_unlock(&ctx->mutex); + + worker_handle_request(worker, &req); + continue; + } + + // If there's no request in the queue, sleep until notified. + // + // This function equires mutex to be locked, + // but will release mutex when waiting, RTFM. + pthread_cond_wait(&ctx->cond, &ctx->mutex); + + pthread_mutex_unlock(&ctx->mutex); + } +} +``` + +Her bruges condition-variablen til at vente på requests, hvis der ikke allerede er requests i køen. + +### Referencer + +- HTTP-serverens offentlige API er defineret i [src/http_server.h](../backend/src/http_server.h). +- HTTP-serverens implementation er defineret i [src/http_server_internal.h](../backend/src/http_server_internal.h) og [src/http_server.c](../backend/src/http_server.c). +- Vector-implementationen, dvs. `DEFINE_VEC`-makroen er defineret I [src/collection.h](../backend/src/collection.h). +