HTTP Lêerbediening in C: Implementasie en Voorbeeld Web-Applikasie
Inleiding
In die vorige artikel het ons gekyk hoe om gelyktydig HTTP versoeke te hanteer. Nou kyk ons na hoe om ‘n HTTP-lêerbediener te implementeer. Met dié aspek in plek kan ons dan HTML, CSS, JavaScript, en ander lêers bedien. Die oorspronklike artikel kan hier besigtig word.
Implementeering
Voordat ons begin kan ons nuwe hulper funksies skep om die lêer inhouds tipe te bepaal sowel as om te toets dat die roete geldig is. Definieer die volgende funksies in ./src/util.h:
#ifndef UTIL_H
#define UTIL_H
...
// kry inhouds tipe
const char* get_mime_type(const char *path);
// toets of roete veilig is
int is_path_safe(const char *base_dir, const char *requested_path);
#endifImplementeer hierdie as volg in ./src/utils.c:
#include <limits.h>
...
int is_path_safe(const char *base_dir, const char *requested_path) {
char resolved_base[PATH_MAX];
char resolved_requested[PATH_MAX];
// kry absolute roete an hanteer ".." en "." fragmente
if (!realpath(base_dir, resolved_base)) return 0;
if (!realpath(requested_path, resolved_requested)) return 0;
// toets of die uiteindelike roete met die basis ooreenstem
return strncmp(resolved_base, resolved_requested, strlen(resolved_base)) == 0;
}
const char* get_mime_type(const char *path) {
const char *ext = strrchr(path, '.');
if (!ext) return "application/octet-stream";
// Basiese MIME-tipes
if (strcasecmp(ext, ".html") == 0 || strcasecmp(ext, ".htm") == 0) return "text/html";
if (strcasecmp(ext, ".css") == 0) return "text/css";
if (strcasecmp(ext, ".js") == 0) return "application/javascript";
if (strcasecmp(ext, ".png") == 0) return "image/png";
if (strcasecmp(ext, ".jpg") == 0 || strcasecmp(ext, ".jpeg") == 0) return "image/jpeg";
if (strcasecmp(ext, ".gif") == 0) return "image/gif";
if (strcasecmp(ext, ".json") == 0) return "application/json";
if (strcasecmp(ext, ".txt") == 0) return "text/plain";
return "application/octet-stream"; // Verstek tipe vir binêre data
}Die is_path_safe funksie word gebruik om te verseker dat die HTTP kliënt nie die res van die vouers en lêers buite die monteerde vouer kan bereik nie. Byvoorbeeld as /opt/www monteer is wil ‘n mens nie hê dat ‘n versoek na ../../etc/passwd gemaak moet kan word nie.
Nou kan ons kyk na die aanpassings in ./src/server.h:
typedef struct Mount {
char *url_path; // HTTP versoek roete, b.v. "/static"
char *fs_path; // lêerstelsel vouer roete op hardeksyf, b.v., "./www"
struct Mount *next; // koppel na volgende montering
} Mount;
typedef struct Server {
...
Route *routes;
Mount *mounts; // nuwe veld
...
} Server;
void server_mount_add(Server *server, const char *fs_path, const char *url_path);
void server_mount_remove(Server *server, const char *url_path);Om meer buigbaarheid te bied is die struct Mount struktuur opgestel dat verskillende roetes byvoeg kan word. ‘n Nuwe veld is in die struct Server struktuur bygevoeg wat wys na al die monterings punte. Twee funksies word definieer om monterings vir ‘n spesifieke bediener by te las en te verwyder.
Hier volg die implementasie van ./src/server.c
#include <sys/stat.h> // nuut ingesluit om met lêers te werk
...
void server_mount_add(Server *server, const char *fs_path, const char *url_path) {
Mount *new_mount = malloc(sizeof(Mount));
new_mount->fs_path = strdup(fs_path);
new_mount->url_path = strdup(url_path);
new_mount->next = server->mounts;
server->mounts = new_mount;
}
void server_mount_remove(Server *server, const char *url_path) {
Mount **curr = &server->mounts;
while (*curr) {
Mount *entry = *curr;
if (strcmp(entry->url_path, url_path) == 0) {
*curr = entry->next;
free(entry->fs_path);
free(entry->url_path);
free(entry);
return;
}
curr = &entry->next;
}
}
int serve_static_file(int client_fd, const char *path) {
struct stat st;
char final_path[PATH_MAX];
strncpy(final_path, path, PATH_MAX);
// kyk of die roete bestaan
if (stat(final_path, &st) != 0) {
return -1;
}
// as dit 'n vouer is, probeer om index.html by te voeg
if (S_ISDIR(st.st_mode)) {
size_t len = strlen(final_path);
// Voeg skuinsstreep by as dit ontbreek
if (len > 0 && final_path[len - 1] != '/') {
strncat(final_path, "/", PATH_MAX - len - 1);
}
strncat(final_path, "index.html", PATH_MAX - strlen(final_path) - 1);
// kyk of die nuwe index.html pad bestaan en 'n lêer is
if (stat(final_path, &st) != 0 || !S_ISREG(st.st_mode)) {
return -1; // geen index.html gevind nie
}
}
// maak seker dit is 'n gewone lêer voor ons lees
if (!S_ISREG(st.st_mode)) return -1;
FILE *f = fopen(final_path, "rb");
if (!f) return -1;
// kry die lêer grootte vir Content-Length
char content_len[32];
snprintf(content_len, sizeof(content_len), "%ld", st.st_size);
char res_buf[2048];
build_http_response(res_buf, sizeof(res_buf), 200, "OK",
"Content-Type", get_mime_type(final_path),
"Content-Length", content_len,
"Connection", "close", NULL, NULL);
send(client_fd, res_buf, strlen(res_buf), 0);
// stroom die lêer uit
char file_buf[4096];
size_t n;
while ((n = fread(file_buf, 1, sizeof(file_buf), f)) > 0) {
if (send(client_fd, file_buf, n, 0) <= 0) break;
}
fclose(f);
return 0;
}
void server_cleanup(Server *server) {
if (server) {
server_stop(server);
// nuut bygevoeg: maak alle monterings skoon
Mount *curr = server->mounts;
while (curr) {
Mount *next = curr->next;
free(curr->fs_path);
free(curr->url_path);
free(curr);
curr = next;
}
...
}
}Die server_mount_add en server_mount_remove funksies hanteer die byvoeging en verwydering van nuwe lêer monterings roetes dinamies in ‘n soortgelyke manier wat van tevoere implenteer was vir hanterings funksie roetes. Die belangrike funksie om na te kyk is serve_static_file. Hier probeer ons kyk of dit ‘n lêer of ‘n vouer is. Indien dit ‘n lêer is wil ons dit opdien. Indien dit ‘n vouer is en daar ‘n index.html lêer in die vouer is wil ons dit opdien. Indien dit ‘n vouer is en daar geen index.html lêer is dan is dit ‘n fout. Indien die roete glad nie bestaan nie is dit ook ‘n fout.
As ons ‘n lêer opdien gebruik ons get_mime_type om die inhouds tipe te spesifiseer in die HTTP antwoord. Ons kry ook die inhouds lengte van die lêer en spesifiseer dit in die HTTP antwoord.
Volgende kan ons ‘n aanpassing in ./src/worker.c maak:
...
#include <limits.h>
void handle_client_task(void *arg) {
...
int handled = 0; // vlag aan te dui of die versoek reeds hanteer is
if (read_http_header(client_fd, buf, sizeof(buf)) > 0) {
HTTPRequest req;
if (parse_http_request(buf, &req, server) == 0) {
route_handler handler = route_get_handler(server, req.path);
if (handler) {
char *body = handler(client_fd, &req);
...
handled = 1; // dui aan dat die versoek reeds hanteer is
}
if (!handled) { // indien nie hanteer deur hanteerders nie ...
int mount_served = 0;
// toets lêer roetes om te sien of dit van toepassing is vir die versoek roete
Mount *m = server->mounts;
while (m) {
if (strncmp(req.path, m->url_path, strlen(m->url_path)) == 0) {
// bou die fisiese roete wat ons gaan gebruik om van die harde skyf af te lees
char phys_path[PATH_MAX];
const char *relative_file = req.path + strlen(m->url_path);
snprintf(phys_path, sizeof(phys_path), "%s/%s", m->fs_path, relative_file);
// toets roete om te sien of dit veilig is
if (is_path_safe(m->fs_path, phys_path)) {
// indien veilig bedien lêers
if (serve_static_file(client_fd, phys_path) == 0) {
// vlag aan te dui of die versoek reeds hanteer is
handled = 1;
}
}
}
m = m->next;
}
}
if (!handled) {
// indien 'n roete hanteerder or a monterings hanteerder nie uitgevoer was nie lewer fout boodskap
const char *not_found = "{\"error\": \"Not Found\"}";
build_http_response(res_buf, sizeof(res_buf), 404, "Not Found",
"Content-Type", "application/json", NULL, not_found);
send(client_fd, res_buf, strlen(res_buf), 0);
}
}
}
...
}Nou kan ons ‘n aanpassing maak in ./src/main.c om monterings punte te registreer:
...
int main(int argc, char *argv[]) {
app_server = server_create();
...
server_configure(app_server);
// registreer monterings punte
server_mount_add(app_server, "./www", "/");
server_mount_add(app_server, "./assets", "/static");
...
}Toets
Op hierdie punt kan die bediener herkompileer word deur make te loop. Om die lêerbediener aspek te toets kan ons 'n vouer vir die een monterings punt met 'n HTML lêer maak soos volg:
mkdir ./www
echo '<h1>Test</h1><img src="/static/a.png"/>' > ./www/index.htmlOns maak dan ook 'n ander vouer vir die ander monterings punt waar ons 'n binêre dokument kan stoor:
mkdir -p ./assets
convert rose: ./assets/a.pngHier gebruik ons die ImageMagick convert program om ‘n toets prentjie te genereer.
Nou behoort behoort die HTML lêer en prentjie opgedien te word as http://localhost:8080 besoek word in ‘n web blaaier:
Merk op dat die Content-Type van die prentjie korrek terug gestuur was as image/png.
Ons kan nou ook ‘n bladsy skep om die bediener se konfigurasie te lees en te verstel. Om dit te doen, verander ./www/index.html soos volg:
<!DOCTYPE html>
<html>
<head>
<title>Srv</title>
<style>
body { font-family: sans-serif; padding: 20px; line-height: 1.5; }
.box { border: 1px solid #ccc; padding: 10px; margin: 10px 0; border-radius: 4px; }
pre { background: #eee; padding: 5px; }
</style>
</head>
<body>
<h3>Server Status</h3>
<div class="box">
Mode: <b id="m">-</b> |
Threads: <b id="t">0</b> |
Queue: <b id="q">0</b>
</div>
<input type="number" id="i" value="4" style="width:50px">
<button onclick="up()">Set Threads</button>
<pre id="r">{}</pre>
<script>
const $ = id => document.getElementById(id);
async function tick() {
try {
const res = await fetch('/api/status');
const d = await res.json();
$('m').innerText = d.mode;
$('t').innerText = d.threadpool.threads;
$('q').innerText = d.threadpool.queued_tasks;
$('r').innerText = JSON.stringify(d, null, 2);
} catch(e) {}
}
async function up() {
const res = await fetch(`/config/pool?threads=${$('i').value}`);
const d = await res.json();
tick();
}
setInterval(tick, 1000);
tick();
</script>
</body>
</html>Hier gebruik ons die hanteerder funksies wat van tevore implenteer was. Ons lees /api/status elke sekonde en stip dit op die skerm en roep /config/pool om die poel grootte te verander as ons op die Set Threads knoppie druk.
Deur die netwerk versoeke bladsy in die webblaaier oop te maak kan ‘n mens die versoeke wat gemaak word sien:
Een laaste punt. Met webblaaiers is dit algemeen dat die konneksie onderbreek kan word in die middel van die versoek. Dit is moontlik om dit te simuleer deur CTRL-R in te hou vir ‘n tyd lank op ‘n roete waar ‘n lêers bedien word. Op een of ander punt sal die HTTP bediener nie meer in staat wees enige versoeke te hanteer nie.
Om dit te korrigeer kan die volgende aanpassing gemaak word aan ./src/main.c:
#include <unistd.h> // vir write funksie
void handle_sigpipe(int sig) {
// ons gebruik write() want printf() is onveilig in 'n seinhandler
const char *msg = "[Signal] SIGPIPE caught (Connection dropped)\n";
write(STDOUT_FILENO, msg, 45);
}
...
int main(int argc, char *argv[]) {
// konneksie onderbrekings kan 'n SIGPIPE fout veroorsaak
// stel hanteering vir daardie spesifieke scenario op
struct sigaction sa;
sa.sa_handler = handle_sigpipe;
sigemptyset(&sa.sa_mask);
// verseker dat ander stelsel versoeke nie onnodig misluk nie
sa.sa_flags = SA_RESTART;
sigaction(SIGPIPE, &sa, NULL);
...
}Hier word die verstek gedrag om die program te stop as dit ‘n SIGPIPE sein ontvang verander om eerder ‘n fout boodskap te gee.
Opsomming
In hierdie artikel het ons ‘n aanpassing gemaak sodat ons lêers kan bedien. Eerder as om HTML, JavaScript, en CSS kode direk op te dien met hanteerder funksies, kan ons nou die lêers in ‘n spesifieke vouer skep, daardie vouer monteer en dit dan opdien.
In die volgende artiekel sal kyk na hoe om ‘n HTTP liggaam te hanteer.
(Afrikaanse artikel)




Comments
Post a Comment