11 KiB
HTTP server i C
Important
Projektet er under udvikling, og har filerne ændret sig siden dette er blevet. Gå til seneste kompatible commit 71e6c48.
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.
Eksempel på ExpressJS:
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:
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:
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:
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
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
void post_login(HttpCtx* ctx)
{
// ...
http_ctx_res_headers_set("Auth-Token", "...");
// ...
}
Referencer
- HTTP-serverens offentlige API er defineret i src/http_server.h.
- JSON-parserens offentlige API er defineret i src/json.h.
- Eksempler på
*_from_json
-funktioner kan findes i src/models.c. - NodeJS NPM-pakken 'express', kan findes på npmjs.com.
HTTP-server med Linux
API'en og hoveddatatypen
HTTP-serveren er defineret med en offentlig API, defineret i src/http_server.h. Implementationen er defineret i src/http_server_internal.h og src/http_server..c.
Den offentlige API er defineret sådan:
// 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:
// 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:
// 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
.
// 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 og 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:
// 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:
// 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.
- HTTP-serverens implementation er defineret i src/http_server_internal.h og src/http_server.c.
- Vector-implementationen, dvs.
DEFINE_VEC
-makroen er defineret I src/collection.h.
HTTP-parsing
For at kunne håndtere HTTP-request, skal serveren kunne parse requestene, der kommer ind. HTTP-requests sendes som strings og består hovedsagligt af 2 dele: header og body. Body'en er en string, som håndteres forskelligt afhængigt af headeren. Headeren er en liste af entries, hvor den første entry siger noget om request-type, -sti og HTTP-version. Efterfølgende entries i header-listen er key/value-par af 'header entries'.
Header parsing
Når en worker får en klient-forbindelse, læser den en fast mængde bytes fra klienten. Denne mængde er nok til at læse hele headeren. Dette bliver gjort i worker_handle_request
-funktionen.
Når headeren er læst, bliver den parsed i parse_header
-funktionen. Denne funktion splitter requesten med newlines (\r\n
defineret i HTTP-standarden) og pakker requesten ud i et Req
-struct.
JSON body-parsing
Hvis en klient sender en POST-request med en JSON-body, skal serveren kunne parse body'en. Dette gøres manuelt af HTTP-serverens brugerdefinerede request-handlers. Vi har implementeret en bespoke JSON-parser til dette formål.
Referencer
- Implementeringen af header parsing er defineret i src/http_server_internal.h og src/http_server.c.
- JSON-parserens offentlige API er defineret i src/json.h og implementeringen er defineret i src/json.c.