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.c

Nou 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/server

Merk 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);

#endif

Hier 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, ...);

#endif

Ons 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

Popular Posts