Basiese HTTP Bediener in C: Van TCP na HTTP

Inleiding

In hierdie plasing kyk ons na ‘n basiese implementasie van die HTTP protokol. Operasies (GETPOSTDELETE ens.), versoek roetes, versoek parameters, sleutel-waarde pare, en die versoek liggaam sal hier illustreer word. Die oorspronklike artike is te vinde hier.

In die vorige plasing het ons gekyk na ‘n basiese TCP bediener. Ter herinnering, hier is die volledige program van die vorige plasing:

/* echo_server.c */
/* compile using gcc -Wall -Wextra -o echo_server echo_server.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>      /* close() */
#include <sys/socket.h>  /* socket(), bind(), listen(), accept() */
#include <netinet/in.h>  /* sockaddr_in */
#include <arpa/inet.h>   /* inet_ntoa() */

#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 socket */
    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);

    char buf[BUFSIZE];

    while (1) {
        /* Blokkeer totdat 'n kliënt koppel */
        client_fd = accept(server_fd, (struct sockaddr *)&adress, &adress_len);
        if (client_fd < 0) {
            perror("accept() failed");
            continue;
        }

        printf("New client: %s\n", inet_ntoa(adress.sin_addr));

        ssize_t n;
        while ((n = recv(client_fd, buf, BUFSIZE, 0)) > 0) {
            /* Stuur presies dieselfde data terug */
            send(client_fd, buf, (size_t)n, 0);
        }

        close(client_fd);
        printf("Client disconnected.\n");
    }

    close(server_fd);
    return 0;
}

HTTP Versoeke

‘n HTTP versoek neem die volgende vorm aan:

Metode /a/b/c HTTP-Weergawe\r\n
Sleutel-1: Waarde-1\r\n
Sleutel-2: Waarde-2\r\n
...
Sleutel-N: Waarde-N\r\n
\r\n
HTTP-Liggaam

As praktiese illustrasie, voer die volgende in een terminal uit:

$ nc -l 8080

Dan, in ‘n ander terminaal, voer die volgende uit:

$ curl http://localhost:8080/a/b/c

Die afvoer van die netcat program behoort soortgelyk aan die volgende te lyk:

GET /a/b/c HTTP/1.1
Host: localhost:8080
User-Agent: curl/8.5.0
Accept: */*

Om te wys dat die \r\n karakters inderdaad aanwesig was, stuur netcat se afvoer in xxd in soos volg:

$ nc -l 8080 | xxd

Die afvoer behoort soortgelyk aan die volgende voorbeeld te lyk:

00000000: 4745 5420 2f61 2f62 2f63 2048 5454 502f  GET /a/b/c HTTP/
00000010: 312e 310d 0a48 6f73 743a 206c 6f63 616c  1.1..Host: local
00000020: 686f 7374 3a38 3038 300d 0a55 7365 722d  host:8080..User-
00000030: 4167 656e 743a 2063 7572 6c2f 382e 352e  Agent: curl/8.5.
00000040: 300d 0a41 6363 6570 743a 202a 2f2a 0d0a  0..Accept: */*..
00000050: 0d0a                                     ..

Hier is die \r\n duidelik sigbaar as 0d 0a. Merk dat na die laaste sleutel-waarde paar in die versoek daar twee \r\n karakter sekwense voorkom (0d0a 0d0a, aan die einde).

Laastens, met betrekking tot HTTP versoeke, om die HTTP liggaam te illustreer, kan dit met die volgende gebruik word:

$ curl -X POST -H "Content-Type: application/json" http://localhost:8080/a/b/c --data '{"a":"b"}'

Die afvoer van netcat behoort soortgelyk aan die volgende voorbeeld te wees, behalwe dat daar nou ‘n liggaamdeel bykom:

POST /a/b/c HTTP/1.1
Host: localhost:8080
User-Agent: curl/8.5.0
Accept: */*
Content-Type: application/json
Content-Length: 9

{"a":"b"}

Die {"a":"b"} is die HTTP liggaam, en dit kom na die eerste \r\n\r\n karakters voor. Dus verdeel \r\n\r\n die HTTP versoek kop van die liggaam.

HTTP Antwoorde

Die HTTP protokol vir antwoorde is soortgelyk aan die van versoeke. Dit werk as volg:

HTTP-Weergawe Sukses-Kode Sukses-Boodskap
Sleutel-1: Waarde-1\r\n
Sleutel-2: Waarde-2\r\n
\r\n
HTTP-Liggaam

Om dit in aksie te sien kan jy die webbediener van jou keuse loop. In die eerste terminaal kan jy die volgende uitvoer:

$ echo 0123456789abcdef > ./a.txt
$ darkhttpd ./ --port 8080

Hier stel ons ‘n lêer a.txt op wat ons met die darkhttpd webbediener sal opdien. Indien jy nie darkhttpd het nie, kan jy dit installeer met apt get darkhttpd.

In ‘n ander terminaal kan ons weer netcat gebruik om ‘n manuale versoek te stuur:

$ netcat localhost 8080

Plak dan die volgende in die terminaal in:

GET /a.txt HTTP/1.1
Host: localhost:8080
User-Agent: curl/8.5.0
Accept: */*

Die darkhttpd webbediener behoort dan met die volgende afvoer terug te rapporteer wat in die netcat afvoer sal vertoon:

HTTP/1.1 200 OK
Date: Sun, 05 Apr 2026 14:46:09 GMT
Server: darkhttpd/1.16
Accept-Ranges: bytes
Connection: close
Content-Length: 17
Content-Type: text/plain
Last-Modified: Sun, 05 Apr 2026 14:45:49 GMT

0123456789abcdef

Ons kan nou kyk na hoe ons die C program kan aanpas om met die HTTP protokol te werk.

Basiese HTTP Antwoord in C Program

In die volgende voorbeeld konstrueer ons ‘n HTTP antwoord in die C program. Ons genereer JSON met ‘n lukrake waarde in die value sleutel.

/* echo_server.c */
...
#include <stdlib.h>
#include <time.h>
...

int main(void) {
    ...
    srand(time(NULL)); // inisialiseer die lukrake nommer generator uit die standaard C biblioteek
    ...
    while (1) {
        ...
        while ((n = recv(client_fd, buf, BUFSIZE, 0)) > 0) {
            char json_body[64]; // buffer vir JSON afvoer
            char full_response[256]; // buffer vir volledige afvoer
            int random_num = rand() % 100; // lukrake waarde van 0 to 99
            int body_len = sprintf(json_body, "{\"value\": %d}", random_num); // konstrueer JSON afvoer
            // konstrueer volle HTTP 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 HTTP antwoord oor konneksie
            send(client_fd, full_response, (size_t)total_len, 0);
        }
        ...
    }
    ...
}

Dit behoort duidelik te wees dat die antwoord wat hier teruggevoer word die vorm aanneem van ‘n standaard HTTP antwoord. Ons maak gebruik van karakter buffers en die snprintf funksie om die antwoord op te bou.

Van belang is die Content-Length in die HTTP antwoord kopstuk. HTTP kliënte soos webblaaiers gebruik dit om outomaties die konneksie van hulle kant af te onderbreek nadat al die inhoud van die HTTP antwoord liggaam klaar gelees is.

Met die veranderings in plek kan ons die program herkompileer as volg:

$ gcc -Wall -Wextra -o echo_server echo_server.c

In een terminaal kan die program dan geloop word, en in ‘n ander terminaal kan ons nou curl gebruik om die konneksie te maak na ons webbediener:

$ curl http://localhost:8080

Die afvoer behoort soortgelyk aan die volgende te wees:

{"value": 23}

Om meer inligting uit die curl program te kry, gebruik die -v vlag:

$ curl -v http://localhost:8080

Die afvoer behoort soortgelyk aan die volgende te wees:

* Host localhost:8080 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:8080...
* connect to ::1 port 8080 from ::1 port 43570 failed: Connection refused
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.5.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/json
< Content-Length: 13
< Connection: close
<
* Closing connection
{"value": 42}

Hier kan ‘n mens sien dat die Content-Type gestel was na application/json, en dat die HTTP liggaam lengte 13 was.

Interpretasie van die HTTP Versoek

Ons kan nou daarna kyk om die HTTP versoek te lees en te interpreteer. Om die versoek te lees doen ons die volgende:

while (1) {
		// Blokkeer totdat 'n kliënt koppel
		client_fd = accept(server_fd,(struct sockaddr *)&adress,&adress_len);
		if (client_fd < 0) {
				perror("accept() failed");
				continue;
		}

		// stoor versoek in buffer
		ssize_t total_received = 0;
		ssize_t n;
		memset(buf, 0, BUFSIZE);
		while (total_received < BUFSIZE - 1) {
				n = recv(client_fd, buf + total_received, 1, 0);
				if (n <= 0) break; // konneksie verbreek
				total_received += n;
				buf[total_received] = '\0'; // termineer altyd teks met nul karakter
				// toets vir kopstuk terminasie karakter sekwens
				if (strstr(buf, "\r\n\r\n") != NULL) {
						break; // voltooi
				}
		}
		...

Met die buffer ingelees, is die volgende stap om die HTTP versoek te interpreteer. Kom ons kyk na die volgende voorbeeld:

GET /a/b/c?d=e&f=g HTTP/1.1

Hierdie is die eerste lyn van ‘n versoek en daar is duidelik vier dele wat ons moet ontgin:

  • metode: GET

  • roete: /a/b/c

  • parameters: d=e&f=g

  • protokol: HTTP/1.1

Die volgende kode hanteer die interpretasie van die eerste lyn van die HTTP versoek:

char full_response[256]; // buffer vir volledige afvoer
char json_body[512]; // buffer vir JSON afvoer
char method[10], path[100], protocol[20]; // veranderlikes vir metode, roete, en protokol
char *query= ""; // veranderlike vir roete parameters
if (sscanf(buf, "%s %s %s", method, path, protocol) == 3) {
	// vir die roete self, sny alles af voor die '?' karakter
	char *q = strchr(path, '?');
	if (q) {
		// termineer die roete by die posisie van die eerste '?' karakter
		*q = '\0';
		// verstel die parameters veranderlike na die deel net na die '?' karakter
		query = q + 1;
	}
	// vir 'n korrekte versoek:
	if (strcmp(protocol, "HTTP/1.1") == 0) {
		// hanteer versoek
		// ...
		// ...
		// ...
	} else {
		// vir 'n inkorrekte protokol waarde
		// konstrueer JSON
		int body_len = sprintf(json_body, "{\"error\": \"Unsupported protocol\"}");
		// konstrueer antwoord
		int total_len = snprintf(
			full_response,
			sizeof(full_response),
			"HTTP/1.1 400 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
		);
		// antwoord
		send(client_fd, full_response, (size_t)total_len, 0);
	}
} else {
	// vir 'n inkorrekte gevormde versoek
	// konstrueer JSON
	int body_len = sprintf(json_body, "{\"error\": \"Malformed request\"}");
	// konstrueer antwoord
	int total_len = snprintf(
		full_response,
		sizeof(full_response),
		"HTTP/1.1 400 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
	);
	// antwoord
	send(client_fd, full_response, (size_t)total_len, 0);
}
close(client_fd);

In hierdie kode word buffers methodpath, en protocol opgestel asook ‘n karakter wyser query. Die scanf funksie word gebruik om die buffers in te lees van die eerste lyn af. Ons toets dat presies 3 waardes ingelees was. Indien daar nie 3 waardes ingelees was nie, dui dit aan dat daar een of ander fout was in die struktuur van die eerste lyn van die HTTP versoek. As so ‘n fout ondervind was, konstrueer ons ‘n antwoord wat die fout aandui en stuur dit terug na die kliënt toe. Terloops, hierdie is nie ‘n robuuste manier om die eerste lyn van die HTTP versoek te hanteer nie, maar dit is die eenvoudigste manier.

As alles reg was met die scanf inlees, toets ons vir die '?' karakter in die path veranderlike. Indien dit gevind was, vervang ons die '?' karakter met die teks terminasie karakter ('\0') en verstel die query veranderlike na die posisie in die teks net na die '?' karakter. Dit het die effek dat ons die path veranderlike verdeel in twee dele. Indien die '?' karakter nie gevind was nie, behou query sy oorspronklike waarde "".

Nadat al die veranderlikes opgestel was, toets ons of die protokol korrek gespesifiseer was:

    if (strcmp(protocol, "HTTP/1.1") == 0) {
        // geldig, gaan voort
		// ...
		// ...
		// ...
    } else {
		// inkorrekte gevormde versoek
		// konstrueer JSON
		int body_len = sprintf(json_body, "{\"error\": \"Malformed request\"}");
		// konstrueer antwoord
		int total_len = snprintf(
			full_response,
			sizeof(full_response),
			"HTTP/1.1 400 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
		);
		// antwoord
		send(client_fd, full_response, (size_t)total_len, 0);
	}

Vir ‘n geldige HTTP versoek kan ons ‘n HTTP antwoord opstel wat die veranderlikes in JSON formaat terugstuur:

// bou JSON
int body_len = sprintf(
	json_body,
	"{\"method\": \"%s\",\"path\":\"%s\",\"query\":\"%s\",\"protocol\":\"%s\"}",
	method,
	path,
	query,
	protocol
);
// 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);

Nou kan die program gekompileer en getoets word:

$ curl -X DELETE 'http://localhost:8080/a/b/c?d=e&f=g'
{"method": "DELETE","path":"/a/b/c","query":"d=e&f=g","protocol":"HTTP/1.1"}

Om sleutels en waardes in die query veranderlike te gebruik, kan ons die volgende funksie skryf:

// gegewe 'n sleutel, kry die waarde in query_params
char* get_query_param(const char *query_params, const char *key) {
    if (!query_params || !key) return NULL;

    char search_key[128];
    // soek vir "key=" om gedeeltelike ooreenstemmings, b.v. id=1... versus id=userid&...
    snprintf(search_key, sizeof(search_key), "%s=", key);

    // verkry die begin van die sleutel in query_params
    char *p = strstr(query_params, search_key);
    
    // maak seker dat ons met die begin van die teks werk of dat dit vorafgegaan word met '&'
    while (p != NULL) {
        if (p == query_params || *(p - 1) == '&') {
            return p + strlen(search_key); // voer die wyser na die waarde terug
        }
        p = strstr(p + 1, search_key);
    }

    return NULL;
}

Met hierdie funksie geïmplementeer, kan ons nou meer spesifieke hantering doen:

if (strcmp(path, "/add") == 0) {
	// roete om nommers bymekaar te tel
	char*a=get_query_param(query,"a");
	char*b=get_query_param(query,"b");
	if(a==NULL||b==NULL){
		// bou JSON
		int body_len = sprintf(
			json_body,
			"{\"error\": \"a and b parameters required\"}"
		);
		// 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
		);
		send(client_fd, full_response, (size_t)total_len, 0);
	}else{
		double sum=atof(a) + atof(b);
		// bou JSON
		int body_len = sprintf(
			json_body,
			"{\"value\": %f}",
			sum
		);
		// 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
		);
		send(client_fd, full_response, (size_t)total_len, 0);
	}
} else {
	// onhanteerde roete
	// bou JSON
	int body_len = sprintf(
		json_body,
		"{\"error\": \"invalid path %s\"}",
		path
	);
	// 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
	);
	send(client_fd, full_response, (size_t)total_len, 0);
}

In die kode bo toets ons vir die /add roete. As dit die roete is wat gespesifiseer was, verkry ons die a en b roete parameters se waardes, vertaal dit na double tipe waardes met die atof funksie, bereken die som, en konstrueer en stuur dan ‘n HTTP antwoord terug. Indien a of b parameters nie verskaf was nie, voer ons ‘n foutboodskap terug. Dit kan soos volg getoets word:

$ curl 'http://localhost:8080/add?a=1.2'
{"error": "a and b parameters required"}
$ curl 'http://localhost:8080/add?a=1.2&b=3.4'
{"value": 4.600000}

Opsomming

Die basiese konsepte van die HTTP protokol was beskryf. Ons het ‘n eenvoudige aanpassing aan ons webbediener gemaak om ‘n HTTP antwoord terug te stuur na die kliënt applikasie toe.

In die volgende artikel sal ons kyk na modularisering van die biedenier kode.


(Afrikaanse artikel)

Comments

Popular Posts