Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0f7e668
Refactor parsing logic into multiple functions
yabjames Dec 18, 2025
788a8fb
Refactor: range for-loops, add const and static class methods where p…
yabjames Dec 18, 2025
c79ce8d
Reformat using clang-format
yabjames Dec 18, 2025
4ef4a24
Reformat AtomicQueue.h using clang-format
yabjames Dec 18, 2025
98b8c50
Separate parsing logic into a different class
yabjames Dec 18, 2025
cc63d52
Add a split_path method
yabjames Dec 22, 2025
e767dbf
Add testing for the HttpParser class
yabjames Dec 22, 2025
5228670
Reformat #include directives by removing relative file paths
yabjames Dec 22, 2025
8d036f3
Add condition for parsing '/' route
yabjames Dec 25, 2025
132bba0
Implement a test for using split_path on "/" route
yabjames Dec 25, 2025
4db1a9c
Implement a way to match client request routes and open server routes
yabjames Dec 25, 2025
62131c2
Refactor structs into a utils header file
yabjames Dec 26, 2025
ee00d05
Refactor data structures into new files and namespace
yabjames Dec 27, 2025
18c6ab6
Implement a PathParams data structure
yabjames Dec 28, 2025
e6a906d
Refactor match_route to only modify the request path params if there …
yabjames Dec 28, 2025
faad4b1
Refactor Route struct to a different file
yabjames Dec 28, 2025
4e70a7a
Refactor handle_client to use the new route and path params matching …
yabjames Dec 28, 2025
9a1f766
Add testing for path parameters
yabjames Dec 28, 2025
0abeb41
Update path parameter test
yabjames Dec 28, 2025
cf9df2b
Add a test to handle no path parameters
yabjames Dec 28, 2025
5ad24bd
Add a function to the Response struct to convert it into a string
yabjames Dec 28, 2025
9608887
Clang reformat
yabjames Dec 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .clang-format
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
BasedOnStyle: LLVM
IndentWidth: 4
TabWidth: 4
UseTab: Always
12 changes: 10 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.15)
project(http_server VERSION 1.0 LANGUAGES CXX)
project(http_server VERSION 1.0 LANGUAGES C CXX)

set(LIBRARY "server_library")
set(TEST_TARGET "server_tests_bin")
Expand All @@ -13,9 +13,14 @@ find_package(Threads REQUIRED)
# server library
add_library(${LIBRARY}
src/HttpServer.cpp
src/HttpParser.cpp
src/PathParams.cpp
include/HttpServer.h
include/AtomicQueue.h
include/constants.h
include/HttpParser.h
include/PathParams.h
include/Route.h
)
target_include_directories(${LIBRARY} PUBLIC include)
target_compile_features(${LIBRARY} PRIVATE cxx_std_17)
Expand All @@ -25,7 +30,10 @@ target_link_libraries(${LIBRARY} PRIVATE Threads::Threads)

# unit tests executable
enable_testing()
add_executable(${TEST_TARGET} test/HttpServerTest.cpp)
add_executable(${TEST_TARGET}
test/HttpServerTest.cpp
test/HttpParserTest.cpp
)
target_link_libraries(${TEST_TARGET}
PRIVATE ${LIBRARY} GTest::gtest_main
)
Expand Down
84 changes: 43 additions & 41 deletions include/AtomicQueue.h
Original file line number Diff line number Diff line change
@@ -1,45 +1,47 @@
#include <mutex>
#pragma once

#include <atomic>
#include <condition_variable>
#include <mutex>
#include <queue>

template<typename T>
class AtomicQueue {
private:
std::mutex mutex;
std::condition_variable cond_var;
std::queue<T> queue;

public:
AtomicQueue() = default;
~AtomicQueue() = default;

// Push an item and notify one waiting consumer.
void push(const T& value) {
{
// lock ends when going out of scope
std::lock_guard<std::mutex> lock(mutex);
queue.push(value);
}
cond_var.notify_one();
}

bool pop(T& result, const std::atomic<bool>& stop_flag) {
std::unique_lock<std::mutex> lock(mutex);

// when notified and if queue not empty, then proceed
// if the stop_flag is true, then it will instantly return from the pop()
while (queue.empty()) {
if (stop_flag.load()) {
return false;
}
cond_var.wait_for(lock, std::chrono::milliseconds(100));
}

result = queue.front();
queue.pop();

// unlock out of scope
return true;
}

template <typename T> class AtomicQueue {
private:
std::mutex mutex;
std::condition_variable cond_var;
std::queue<T> queue;

public:
AtomicQueue() = default;
~AtomicQueue() = default;

// Push an item and notify one waiting consumer.
void push(const T &value) {
{
// lock ends when going out of scope
std::lock_guard<std::mutex> lock(mutex);
queue.push(value);
}
cond_var.notify_one();
}

bool pop(T &result, const std::atomic<bool> &stop_flag) {
std::unique_lock<std::mutex> lock(mutex);

// when notified and if queue not empty, then proceed
// if the stop_flag is true, then it will instantly return from the
// pop()
while (queue.empty()) {
if (stop_flag.load()) {
return false;
}
cond_var.wait_for(lock, std::chrono::milliseconds(100));
}

result = queue.front();
queue.pop();

// unlock out of scope
return true;
}
};
32 changes: 32 additions & 0 deletions include/HttpParser.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#pragma once

#include "HttpServer.h"
#include "Route.h"

#include <string_view>
#include <vector>

class HttpParser {
public:
static bool match_route(
const HttpUtils::Route& route,
HttpServer::Request& request
);

static HttpUtils::Route path_to_route(std::string_view path);

static bool parse(std::string_view buffer, HttpServer::Request &out);

static std::vector<std::string_view> split_path(std::string_view path);

private:
static bool parse_method(std::string_view buffer, std::string_view &method,
size_t &offset);

static bool parse_route(std::string_view buffer, std::string_view &route,
size_t &offset);

static bool parse_body(std::string_view buffer, std::string_view &body,
const size_t &offset);

};
142 changes: 85 additions & 57 deletions include/HttpServer.h
Original file line number Diff line number Diff line change
@@ -1,90 +1,118 @@
#pragma once

#include "AtomicQueue.h"
#include "PathParams.h"
#include "Route.h"

#include <atomic>
#include <functional>
#include <thread>
#include <vector>
#include <sstream>
#include <string>
#include <atomic>
#include <string_view>
#include "../include/AtomicQueue.h"
#include <thread>
#include <vector>

class HttpServer {
public:
HttpServer();
public:
HttpServer();

~HttpServer();

std::atomic<bool> stop_flag{};

struct Request {
PathParams path_params;
std::string_view method;
std::string_view route;
std::string_view body;
};

~HttpServer();
struct Response {
std::string_view header;
std::string body;
std::string status_name;
std::string str() {
std::stringstream ss;
ss << "HTTP/1.1 " << status << " " << status_name << " \r\n";
ss << "Content-Length: " << body.size() << "\r\n";
ss << "Connection: close\r\n\r\n";
ss << body;
return ss.str();
}
int status; // e.g. 200, 404, 500
};

std::atomic<bool> stop_flag {};
using Handler = std::function<void(Request &, Response &)>;

struct Request {
std::string_view route;
std::string body;
};
static bool is_valid_request(std::string &request_buffer,
ssize_t bytes_read);

struct Response {
std::string_view header;
std::string body;
int status;
};
void get_mapping(std::string_view route, const Handler &fn);

using Handler = std::function<void(const Request&, Response&)>;
void post_mapping(std::string_view route, const Handler &fn);

void get_mapping(std::string_view route, const Handler& fn);
void put_mapping(std::string_view route, const Handler &fn);

void post_mapping(std::string_view route, const Handler& fn);
void patch_mapping(std::string_view route, const Handler &fn);

void put_mapping(std::string_view route, const Handler& fn);
void delete_mapping(std::string_view route, const Handler &fn);

void patch_mapping(std::string_view route, const Handler& fn);
void head_mapping(std::string_view route, const Handler &fn);

void delete_mapping(std::string_view route, const Handler& fn);
void options_mapping(std::string_view route, const Handler &fn);

void head_mapping(std::string_view route, const Handler& fn);
void connect_mapping(std::string_view route, const Handler &fn);

void options_mapping(std::string_view route, const Handler& fn);
void trace_mapping(std::string_view route, const Handler &fn);

void connect_mapping(std::string_view route, const Handler& fn);
/**
* Tells the server to start listening/accepting requests from a specified
* port. This function is blocking.
*/
void listen(int port);

void trace_mapping(std::string_view route, const Handler& fn);
/**
* Initializes a thread to start listening/accepting requests from a
* specified port. This function is non-blocking, so only use one active
* `listen()` or `start_listening()` method call for any given time.
*/
void start_listening(int port);

/**
* Tells the server to start listening/accepting requests from a specified port. This function is blocking.
*/
void listen(int port);
/**
* Tells the server to stop listening/accepting requests.
*/
void stop_listening();

/**
* Initializes a thread to start listening/accepting requests from a specified port. This function is non-blocking,
* so only use one active `listen()` or `start_listening()` method call for any given time.
*/
void start_listening(int port);
private:
AtomicQueue<int> queue;

/**
* Tells the server to stop listening/accepting requests.
*/
void stop_listening();
std::vector<std::thread> threads;

private:
AtomicQueue<int> queue;
std::unordered_map<std::string_view,
std::vector<std::pair<HttpUtils::Route, Handler>>>
routes;

std::vector<std::thread> threads;
// <method, route>
// std::unordered_map<std::string_view, std::vector<Route>> routes;

std::unordered_map<std::string_view, std::unordered_map<std::string_view, Handler>> routes;
std::unordered_map<std::string_view, std::vector<std::string_view>>
route_path_params;

void store_conn_fd(int conn_fd);
void store_conn_fd(int conn_fd);

int listener_fd {-1};
int listener_fd{-1};

/*
* @brief return a listener socket file descriptor
*/
int get_listener_socket(int port);
std::string_view get_method(int conn_fd, std::string_view path,
size_t &method_itr, bool &continues);

/**
* @brief Should be passed into a thread() worker to send a response back to an HTTP client.
* A side-effect is that it will toggle the occupancy in the thread_pool_occupied member array
* @param thread_pool_id Tells the SendResponse function which array index to toggle in the thread_pool_occupied array
* @param conn_file_descriptor Used to send the response through the associated socket
*/
void handle_client();
/*
* @brief return a listener socket file descriptor
*/
static int get_listener_socket(int port);

/**
* @brief Should be passed into a thread() worker to send a response back to
* an HTTP client.
*/
void handle_client();
};
29 changes: 29 additions & 0 deletions include/PathParams.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// Created by james on 12/26/25.
//

#pragma once

#include <optional>
#include <string_view>
#include <string>
#include <unordered_map>


class PathParams {
public:
explicit PathParams(
const std::unordered_map<std::string, std::string>& path_params)
: path_params(path_params) {
}

PathParams() = default;

std::optional<std::string_view> get_path_param(std::string_view key);

void add_param(std::string_view key, std::string_view value);

private:
std::unordered_map<std::string, std::string> path_params;

};
17 changes: 17 additions & 0 deletions include/Route.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#pragma once

#include <string>

namespace HttpUtils {

struct RouteSegment {
enum class Type { Literal, Parameter };

Type type;
std::string value;
};

struct Route {
std::vector<RouteSegment> segments;
};
}
Loading
Loading