Modulariseerde HTTP Bediener in C: 'n Verbeterde Weergawe
Inleiding
In hierdie plasing kyk ons na modularisering van die basiese HTTP bediener beskryf in die vorige artikel. Die oorspronklike artikel is te vinde hier. Tans lyk diƩ monolitiese bediener soos volg:
/* server.c */
#include <arpa/inet.h> /* inet_ntoa() */
#include <netinet/in.h> /* sockaddr_in */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h> /* socket(), bind(), listen(), accept() */
#include <unistd.h> /* close() */
#define PORT 8080
#define BUFSIZE 1024
int main(void) {
int server_fd, client_fd;
struct sockaddr_in adress;
socklen_t adress_len = sizeof(adress);
/* Skep die konneksie */
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
perror("socket() failed");
exit(EXIT_FAILURE);
}
/* Stel adres inligting op */
memset(&adress, 0, sizeof(adress));
adress.sin_family = AF_INET;
adress.sin_addr.s_addr = INADDR_ANY;
adress.sin_port = htons(PORT);
if (bind(server_fd, (struct sockaddr *)&adress, sizeof(adress)) < 0) {
perror("bind() failed");
exit(EXIT_FAILURE);
}
if (listen(server_fd, 5) < 0) {
perror("listen() failed");
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
while (1) {
// Blokkeer totdat 'n kliƫnt koppel
client_fd = accept(server_fd, (struct sockaddr *)&adress, &adress_len);
if (client_fd < 0) {
// wys fout as ons nie besig is om die bediener tans te stop nie
if (server->is_running) {
perror("accept() failed");
}
continue;
}
// bou JSON
char full_response[512];
char json_body[32];
int body_len = sprintf(json_body, "{\"value\": \"stub\"}");
// bou antwoord
int total_len = snprintf(full_response, sizeof(full_response),
"HTTP/1.1 200 OK\r\n"
"Content-Type: application/json\r\n"
"Content-Length: %d\r\n"
"Connection: close\r\n"
"\r\n"
"%s",
body_len, json_body);
// stuur antwoord
send(client_fd, full_response, (size_t)total_len, 0);
// sluit konneksie
close(client_fd);
}
close(server_fd);
return 0;
}Die program kan gekompileer word met gcc -Wall server.c. Ons kan dit verander om GNU make te gebruik in plaas daarvan om dit elke keer handmatig te kompileer. Hiervoor moet ons ‘n paar veranderings maak aan die struktuur van die projek. Die projekstruktuur verander nou na die volgende:
./
├── Makefile
├── bin
│ └── server
└── src
└── main.c
Skep ‘n vouer src en skuif server.c na src/main.c. Skep dan die volgende Makefile:
CC = gcc
CFLAGS = -Wall
LDFLAGS =
SRC_DIR = src
OBJ_DIR = obj
BIN_DIR = bin
BIN = $(BIN_DIR)/server
SRCS = $(wildcard $(SRC_DIR)/*.c)
OBJS = $(SRCS:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o)
all: $(BIN)
$(BIN): $(OBJS)
@mkdir -p $(BIN_DIR)
$(CC) $(OBJS) $(LDFLAGS) -o $(BIN)
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
@mkdir -p $(OBJ_DIR)
$(CC) $(CFLAGS) -c $< -o $@
.PHONY: clean run
clean:
@rm -rf $(OBJ_DIR) $(BIN_DIR)
run: $(BIN)
@$(BIN)Nou kan die projek gebou word deur eenvoudig make te loop. Die gekompileerde program sal geplaas word in die ./bin vouer. Die projek kan skoongemaak word deur make clean te loop.
Dit is ook nou moontlik om nuwe .c lĆŖers te skep en make sal dan outomaties die .c lĆŖers se objekte (.o lĆŖers) kompileer en hulle almal aan die einde saamkoppel in die uitvoerbare program (bin/server).
Met dit dan in plek kan ons begin deur die webbediener te modulariseer sodat alles nie in een lĆŖer is nie. Begin deur twee nuwe lĆŖers te skep: src/server.h en src/server.c.
$ touch ./src/server.h
$ touch ./src/server.cNou kan ons make loop:
$ make
gcc -Wall -c src/main.c -o obj/main.o
gcc -Wall -c src/server.c -o obj/server.o
gcc obj/main.o obj/server.o -o bin/serverMerk op dat obj/main.o en obj/server.o gekompileer is en saam in bin/server gekombineer was.
Vir die koppelvlak src/server.h het ons die volgende:
#ifndef SERVER_H
#define SERVER_H
#include <netinet/in.h>
#define DEFAULT_PORT 8080
#define DEFAULT_BACKLOG 5
typedef struct {
int port;
int server_fd;
struct sockaddr_in address;
int backlog;
int is_running;
} Server;
// lewensiklus funksies
Server* server_create();
void server_configure(Server *server); // opsionele konfigurasie logika
void server_start(Server *server);
void server_stop(Server *server);
void server_cleanup(Server *server);
#endif
Die struct Server struktuur is ‘n datastruktuur wat ons gebruik vir konfigurasie asook om tred te hou van die interne toestand van die bediener. ‘n Paar lewensiklusfunksies word gelys wat ons in staat stel om die bediener te skep, te konfigureer, te begin, te stop, en af te sluit.
Die implementasie src/server.c lyk soos volg:
#include "server.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
Server* server_create() {
Server *server = malloc(sizeof(Server));
if (!server) return NULL;
server->port = DEFAULT_PORT;
server->backlog = DEFAULT_BACKLOG;
server->server_fd = -1;
server->is_running = 0;
// pas verstekwaardes toe
memset(&server->address, 0, sizeof(server->address));
server->address.sin_family = AF_INET;
server->address.sin_addr.s_addr = INADDR_ANY;
server->address.sin_port = htons(DEFAULT_PORT);
return server;
}
void server_configure(Server *server) {
server->server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server->server_fd < 0) {
perror("socket() failed");
exit(EXIT_FAILURE);
}
// laat poorthergebruik toe sodat mens foute kan vermy te make met die poort
// wat reeds in gebruik is as die bediener oor begin word
int opt = 1;
setsockopt(server->server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
if (bind(server->server_fd, (struct sockaddr *)&server->address, sizeof(server->address)) < 0) {
perror("bind() failed");
exit(EXIT_FAILURE);
}
}
void server_start(Server *server) {
if (listen(server->server_fd, server->backlog) < 0) {
perror("listen() failed");
return;
}
printf("Server listening on port %d...\n", server->port);
server->is_running = 1;
while (server->is_running) {
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int client_fd = accept(server->server_fd, (struct sockaddr *)&client_addr, &addr_len);
if (client_fd < 0) {
perror("accept() failed");
continue;
}
// hard kodeerde antwoord logika
const char *json_body = "{\"value\": \"stub\"}";
char full_response[512];
int body_len = strlen(json_body);
int total_len = snprintf(full_response, sizeof(full_response),
"HTTP/1.1 200 OK\r\n"
"Content-Type: application/json\r\n"
"Content-Length: %d\r\n"
"Connection: close\r\n\r\n%s",
body_len, json_body);
send(client_fd, full_response, (size_t)total_len, 0);
close(client_fd);
}
}
void server_stop(Server *server) {
server->is_running = 0;
if (server->server_fd != -1) {
close(server->server_fd);
server->server_fd = -1;
}
}
void server_cleanup(Server *server) {
if (server) {
server_stop(server);
free(server);
}
}Laastens, die hoofprogram src/main.c:
#include "server.h"
#include <stdio.h>
#include <signal.h>
// globale wyser na bediener vir gebruik in handle_sigint funksie
Server *app_server = NULL;
//vir as jy ctrl+c gedruk het
void handle_sigint(int sig) {
printf("\n[Ctrl+C] Stopping server...\n");
if (app_server) {
server_stop(app_server);
}
}
int main(void) {
// skep bediener
app_server = server_create();
// konfigureer bediener
server_configure(app_server);
// registreer seinafhandelaar
signal(SIGINT, handle_sigint);
printf("Press Ctrl+C to shut down.\n");
// begin bediener
server_start(app_server);
// sluit bediener af
server_cleanup(app_server);
printf("Server exited cleanly.\n");
return 0;
}Die hooffunksie is nou baie eenvoudiger en die hele program is nou minder lomp. Vervolgens kan ons ekstra gereedskap byvoeg by src/util.c. Eerstens die koppelvlak (src/util.h):
#ifndef UTIL_H
#define UTIL_H
#include <stddef.h>
#include <sys/types.h>
typedef struct {
char method[10];
char path[100];
char protocol[20];
char *query; // wys na die deel na die '?' karakter in die roete
char *body; // wys na die begin van die HTTP liggaam in die buffer
} HTTPRequest;
// Lees client_fd in buffer in tot \r\n\r\n of daar nie meer spasie is nie
// Gee die hoeveelheid grepe terug wat gelees was, of -1 as daar 'n fout was
ssize_t read_http_header(int client_fd, char *buf, size_t buf_size);
// Interpreteer die rou buffer in 'n struktuur in
// Gee 0 terug as alles reg was, andersins -1
int parse_http_request(char *buf, HTTPRequest *req);
#endifHier het ons ‘n funksie read_http_header om die HTTP kopstuk in ‘n buffer in te lees, asook parse_http_request wat die ingeleesde buffer interpreteer in die HTTPRequest datastruktuur. Vir die implementasie het ons die volgende in src/util.c:
#include "util.h"
#include <string.h>
#include <stdio.h>
ssize_t read_http_header(int client_fd, char *buf, size_t buf_size) {
ssize_t total_received = 0;
ssize_t n;
memset(buf, 0, buf_size);
while (total_received < (ssize_t)buf_size - 1) {
n = recv(client_fd, buf + total_received, 1, 0);
if (n <= 0) return n; // konneksie onderbreek of 'n fout
total_received += n;
buf[total_received] = '\0';
// Toets of ons die einde van die HTTP kopstuk bereik het
if (strstr(buf, "\r\n\r\n") != NULL) {
break;
}
}
return total_received;
}
int parse_http_request(char *buf, HTTPRequest *req) {
// verstekstruktuur
req->query = "";
req->body = NULL;
// kry die metode, roete, en protokol
if (sscanf(buf, "%s %s %s", req->method, req->path, req->protocol) != 3) {
return -1;
}
// hanteer die roete parameters
char *q = strchr(req->path, '?');
if (q) {
*q = '\0'; // Null-termineer die roete by die '?' karakter
req->query = q + 1; // Wys query na die deel net na die '?'
}
// spoor die begin van die HTTP liggaam op (na \r\n\r\n)
char *body_start = strstr(buf, "\r\n\r\n");
if (body_start) {
req->body = body_start + 4;
}
return 0;
}Met dit in plek kan ons dit in src/server.c soos volg gebruik:
#include "util.h"
...
void server_start(Server *server) {
...
while (server->is_running) {
...
if (client_fd < 0) {
...
}
char buf[1024];
HTTPRequest req;
if (read_http_header(client_fd, buf, sizeof(buf)) > 0) {
if (parse_http_request(buf, &req) == 0) {
// hanteer antwoord hier
printf("Request: %s %s\n", req.method, req.path);
}
}
...
}
}Insgelyks kan ons ‘n funksie skryf om makliker HTTP antwoorde te skep. In src/util.h voeg ons nog ‘n funksie by om hiermee te help:
#ifndef UTIL_H
#define UTIL_H
#include <stdarg.h>
...
// Skep volle HTTP antwoord en stoor dit in buffer
// Voorbeeld: build_http_response(buf, sizeof(buf), 200, "OK", "Content-Type", "application/json", NULL, "{\"status\":\"ok\"}");
int build_http_response(char *buf, size_t buf_size, int status_code, const char *status_msg, ...);
#endifOns gebruik stdarg.h uit die standaard C biblioteek om deur die argumente te itereer. Hier is die implementasie (src/util.c):
#include <stdarg.h>
...
int build_http_response(char *buf, size_t buf_size, int status_code, const char *status_msg, ...) {
va_list args;
va_start(args, status_msg);
// skryf eerste lyn
int offset = snprintf(buf, buf_size, "HTTP/1.1 %d %s\r\n", status_code, status_msg);
// skryf opskrifte
char *header_name;
while ((header_name = va_arg(args, char *)) != NULL) {
char *header_value = va_arg(args, char *);
if (header_value) {
offset += snprintf(buf + offset, buf_size - offset, "%s: %s\r\n", header_name, header_value);
}
}
// beƫindig kop
offset += snprintf(buf + offset, buf_size - offset, "\r\n");
// voeg liggaam by
char *body = va_arg(args, char *);
if (body) {
offset += snprintf(buf + offset, buf_size - offset, "%s", body);
}
va_end(args);
return offset; // gee die totale lengte terug
}Dit kan dan in src/server.c soos volg gebruik word:
#include "util.h"
...
void server_start(Server *server) {
...
while (server->is_running) {
...
if (client_fd < 0) {
...
}
// skryf HTTP antwoord terug
char response_buffer[1024];
const char *json = "{\"value\": \"stub\"}";
build_http_response(
response_buffer, sizeof(response_buffer),
200, "OK",
"Content-Type", "application/json",
"Connection", "close",
NULL, // brandwagwaarde
json // HTTP liggaam
);
send(client_fd, response_buffer, strlen(response_buffer), 0);
close(client_fd);
}
}Opsomming
Ons het hier gekyk na om ‘n meer ontwikkelaar vriendelike weergawe van die HTTP bediener te skryf.
In die volgende artikel sal ons kyk na verdere modularisering, verdere gereedskap, en hoe ons funksies kan koppel an spesifieke roetes.
(Afrikaanse artikel)


Comments
Post a Comment