create simple server abstraction

This commit is contained in:
2026-09-21 16:26:24 -07:00
parent b7da986f8c
commit e123c6344d
3 changed files with 264 additions and 104 deletions
+26 -104
View File
@@ -1,115 +1,37 @@
#include <stdlib.h> #include <stdlib.h>
#include <stdio.h> #include <stdio.h>
#include <sys/socket.h> #include "server.h"
#include <netinet/in.h> #include <stdint.h>
#include <unistd.h>
#include <string.h>
void handle_bootstrap(request *req, response *resp) {
printf("received a bootstrap request\n");
resp->http_code = 200;
resp->body = "bootstrap";
}
void handle_payment(request *req, response *resp) {
printf("received a payment request\n");
resp->http_code = 200;
resp->body = "payment complete";
}
int main(int argc, char **argv) { int main(int argc, char **argv) {
printf("hello world\n"); printf("hello world\n");
// http is built on top of tcp uint16_t port = 8080;
// http/1.0 creates a new tcp connection every request server* s = create_server("0.0.0.0", port);
if (!s) {
// for an http server implmentation, we listen for packets in a loop, and group request packets printf("ERROR: unable to create server\n");
// then we respond with a response broken down into separate packets (headers, body, etc) return -1;
int fd = socket(AF_INET, SOCK_STREAM, 0);
// ?? connect or bind the socket to the address?
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(8080),
.sin_addr = INADDR_ANY,
};
int yes = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
int r = bind(fd, (struct sockaddr *) &addr, sizeof(addr));
if (r == -1) {
printf("ERROR: unable to bind to address\n");
return r;
} }
// go ahead and listen for incoming connections register_route(s, "/bootstrap", handle_bootstrap);
r = listen(fd, 3); register_route(s, "/payment", handle_payment);
if (r == -1) {
printf("ERROR: unable to use socket to listen for incoming connections\n"); int r = start_server(s);
return r; if (r != 0) {
printf("ERROR: unable to start server\n");
return -1;
} }
printf("Listening on port 8080\n");
fflush(stdout);
// we loop and do blocking accept calls for incoming connections
while (true) {
// we pass in a sockaddr which is filled with the incoming client's information
struct sockaddr client_addr;
int addr_len = sizeof(client_addr);
int client_fd = accept(fd, &client_addr, (socklen_t *) &addr_len);
if (client_fd == -1) {
printf("ERROR: unable to accept incoming client connection\n");
return -1;
}
// new socket is created for the one client connection
// we can recv from this socket
size_t size = 1000;
char *buf = malloc(size);
// TCP is a byte stream so we need to keep calling recv until an end condition (\r\n). HTTP is the protocol which specifies this, not TCP streams
ssize_t received_bytes = recv(client_fd, buf, size, 0);
if (received_bytes > 0) {
printf("received bytes: %d\n", (int) received_bytes);
printf("%.*s\n", received_bytes, buf);
}
// parse the request
char current_line[1000];
int current_line_char = 0;
for (int i = 0; i < received_bytes; i++) {
printf("%c", buf[i]);
current_line[current_line_char] = buf[i];
current_line_char++;
if (buf[i] == '\n') {
printf("line complete: %.*s\n", current_line_char, current_line);
current_line_char = 0;
}
// TODO: find the path from the first header `GET /bootstrap HTTP/1.1`
// decode the request further
}
fflush(stdout);
free(buf);
const char *response =
"HTTP/1.1 200 OK\r\n"
"Content-Length: 5\r\n"
"Content-Type: text/plain\r\n"
"Connection: close\r\n"
"\r\n"
"hello";
r = send(client_fd, response, strlen(response), 0);
if (r == -1) {
printf("ERROR: in sending response");
fflush(stdout);
}
printf("sent %zd of %zu bytes\n", r, strlen(response));
fflush(stdout);
close(client_fd);
}
// TODO: scaffolding
// keep track of multiple requests, examine the request data
// decode the request data
// then respond with particular html page
// close socket
close(fd);
// for an http client library, we need to create a tcp connection, send headers, and then body and other parts of the request in a structure that follows the protocol RFC
} }
+191
View File
@@ -0,0 +1,191 @@
#include "server.h"
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
typedef struct {
char *path;
route_handler handler;
} handler_metadata;
struct server_s {
uint16_t port;
char *addr;
int server_fd;
handler_metadata **handlers;
size_t handlers_len;
};
server *create_server(const char *addr, uint16_t port_number) {
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) {
printf("ERROR: unable to create socket\n");
return NULL;
}
server *s = malloc(sizeof(server));
s->server_fd = fd;
s->port = port_number;
s->addr = strdup(addr);
s->handlers = NULL;
s->handlers_len = 0;
return s;
}
void destroy_server(server *s) {
// TODO: go through all current and pending connections and close them
if (s->server_fd != -1) {
close(s->server_fd);
}
free(s->addr);
for (int i = 0; i < s->handlers_len; i++) {
free(s->handlers[i]);
}
free(s->handlers);
free(s);
}
int start_server(server *s) {
struct in_addr sin_addr;
int r = inet_aton(s->addr, &sin_addr);
if (r == 0) {
printf("ERROR: unable to get address from provided input\n");
return -1;
}
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(s->port),
.sin_addr = sin_addr,
};
int yes = 1;
setsockopt(s->server_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
r = bind(s->server_fd, (struct sockaddr *) &addr, sizeof(addr));
if (r == -1) {
printf("ERROR: unable to bind to address\n");
return -1;
}
r = listen(s->server_fd, SOMAXCONN);
if (r == -1) {
printf("ERROR: unable to use socket to listen for incoming connections\n");
return -1;
}
while (true) {
struct sockaddr_storage client_addr;
socklen_t client_addr_len = sizeof(struct sockaddr_storage);
int client_fd = accept(s->server_fd, (struct sockaddr *)&client_addr, &client_addr_len);
if (client_fd == -1) {
printf("WARNING: unable to accept incoming client communication\n");
continue;
}
if (client_addr.ss_family == AF_INET) {
struct sockaddr_in *v4_addr = (struct sockaddr_in *)&client_addr;
// use v4_addr->sin_port, etc.
}
else if (client_addr.ss_family == AF_INET6) {
struct sockaddr_in6 *v6_addr = (struct sockaddr_in6 *)&client_addr;
// use v6_addr->sin6_port, etc.
}
// TODO: call recv until we have the end of the headers and request
// append to a buffer, resizing along the way?
// TODO: helper method which takes the client_fd and creates a request
// we can call recv until we receive \r\n\r\n
bool processing = true;
char data[4096] = {0};
size_t data_size = sizeof(data);
size_t total_bytes = 0;
while (processing) {
// TODO: if data buffer full, realloc
ssize_t received_bytes = recv(client_fd, data + total_bytes, data_size - total_bytes - 1, 0);
if (received_bytes > 0) {
total_bytes += received_bytes;
}
// check if the last of the data is \r\n\r\n
if (total_bytes >= 4 && data[total_bytes - 1] == '\n' && data[total_bytes - 2] == '\r'
&& data[total_bytes - 3] == '\n' && data[total_bytes - 4] == '\r') {
printf("Data: %s\n", data);
printf("Found end of the request\n");
processing = false;
}
}
request *req = malloc(sizeof(request));
req->contents = data;
response *resp = malloc(sizeof(response));
resp->http_code = 404; // default
resp->body = "default";
// find the relevant route handler, call it
for (int i = 0; i < s->handlers_len; i++) {
printf("checking if route handler matches. path: %s\n", s->handlers[i]->path);
if (strstr(data, s->handlers[i]->path) != NULL) {
s->handlers[i]->handler(req, resp);
printf("response: %d\n", resp->http_code);
char response_content[1000];
snprintf(response_content, sizeof(response_content),
"HTTP/1.1 %d OK\r\n"
"Content-Length: %d\r\n"
"Content-Type: text/plain\r\n"
"Connection: close\r\n"
"\r\n"
"%s",
resp->http_code, strlen(resp->body), resp->body);
int r = send(client_fd, response_content, strlen(response_content), 0);
if (r == -1) {
printf("ERROR: in sending response\n");
}
break;
}
}
free(req);
free(resp);
close(client_fd);
}
return 0;
}
int register_route(server *s, const char *route, route_handler handler) {
handler_metadata *new_handler = malloc(sizeof(handler_metadata));
// TODO: handle error from malloc
new_handler->path = strdup(route);
new_handler->handler = handler;
// relloc to make room for one more pointer
size_t new_len = s->handlers_len + 1;
handler_metadata **new_array = realloc(s->handlers, new_len * sizeof(handler_metadata *));
// TODO: handle error of realloc
s->handlers = new_array;
s->handlers[s->handlers_len] = new_handler;
s->handlers_len = new_len;
printf("registered route: %s\n", route);
printf("total handlers registered: %d\n", s->handlers_len);
}
+47
View File
@@ -0,0 +1,47 @@
#ifndef SERVER_H
#define SERVER_H
#include <stdint.h>
#include <stddef.h>
typedef struct server_s server;
typedef struct {
char *key;
char *value;
} header;
typedef struct {
/* header *headers; */
/* size_t headers_len; */
/* char *path; */
/* char *method; */
/* char *body; */
char *contents;
} request;
typedef struct {
uint16_t http_code;
char *body;
} response;
// TODO: handlers need a way to respond to requests, maybe return a response with http code and body response
// we will provide them an already-allocated resp struct which they may fill
typedef void (*route_handler)(request *req, response *resp);
// addr should be in dot-separated notation
// returns NULL if error during creation
server *create_server(const char *addr, uint16_t port_number);
void destroy_server(server *s);
// blocking call which handles inbound connections and requests
// returns -1 on internal failure
int start_server(server *s);
int register_route(server *s, const char *route, route_handler handler);
// TODO: implementation wise, we need the server to handle incoming connections, read the incoming bytes of the connection in another thread, and then route handlers from different threads...will this lead to various problems?
/* - what if a route handler uses some shared resource (e.g. service handler) to fulfill the request? and different calls of the handler from different threads calls that service handler? */
/* - keep it single-threaded for now. process one request at a time */
#endif // SERVER_H