mirror of
https://github.com/Mercantec-GHC/h4-projekt-gruppe-0-sm.git
synced 2025-04-28 00:34:06 +02:00
add http_server_i_c.md
This commit is contained in:
parent
7d26ff5d5a
commit
e5bad9cf74
331
docs/http_server_i_c.md
Normal file
331
docs/http_server_i_c.md
Normal file
@ -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).
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user