A multithreaded HTTP/1.1 web server built from scratch in C++ on Linux.
RawServe was built to deeply understand the low-level mechanics of web servers. By writing the server from scratch, this project provides hands-on exploration of POSIX networking, HTTP parsing, routing, concurrency, synchronization, and resource management.
RawServe was not built using an existing HTTP framework. The project directly combines:
POSIX sockets
+
custom HTTP parser
+
custom router
+
custom HashMap
+
ThreadPool
+
middleware
+
static file server
+
JSON API support
This makes it an educational systems engineering project demonstrating how these fundamental components interact to form a working web server.
The architecture is divided into application setup and request processing. RawServe uses a blocking POSIX-socket server model with a fixed worker ThreadPool.
main
├── create Router
├── register routes/middleware
└── create Server(router)
↓
start()
Client
↓
Server
↓
accept()
↓
ThreadPool
↓
Worker
↓
Read HTTP bytes
↓
Parser
↓
Request
↓
Router
├── Middleware
├── Exact/Dynamic Route
└── Static File Fallback
↓
Controller
↓
Response
↓
send()
↓
Client
main() configures the application while the Server and ThreadPool execute the request lifecycle. The parser creates a structured Request object from raw bytes, which is then passed to the Router. The Router coordinates middleware execution, route matching, and static file fallback to determine the correct controller logic.
- Networking: Direct use of Linux POSIX socket APIs (
socket(),bind(),listen(),accept(),SO_REUSEADDR,SO_RCVTIMEO,SO_SNDTIMEO, and handling of partial sends). - HTTP Parsing: Parsing of HTTP/1.1 request lines, case-insensitive headers (normalized to lowercase), query parameters (including URL percent-decoding and
+handling), and request bodies viaContent-Length. - Routing: Exact route matching, HTTP method-based routing (GET, POST, PUT, PATCH, DELETE), and support for multiple dynamic path parameters (e.g.,
/user/:id/post/:postId). - Middleware: Middleware functions can inspect requests and either continue processing or halt and return a response immediately.
- Static Files: Serves static files from a
public/directory with MIME type detection and 404/403 error handling. - JSON API Support: Provides JSON parsing and JSON response generation using
nlohmann/json, includingapplication/jsonresponse headers. - Concurrency: A custom ThreadPool orchestrating
std::threadworkers and synchronization primitives. - Custom HashMap: A custom bucket-based
HashMapimplementation for route storage.
TCP is a byte stream rather than a message protocol. RawServe accumulates incoming bytes until the HTTP header terminator:
\r\n\r\n
is detected, and then uses the Content-Length header to determine how much request body data is required.
RawServe implements a learning-oriented subset of HTTP/1.1 sufficient for the implemented request, routing, static-file, JSON, and connection-handling features. Persistent connections are supported at a basic sequential request level; advanced pipelining and more complex stream-state handling are not implemented.
RawServe supports strict exact matches (e.g., GET /about) and dynamic path parameters (e.g., GET /user/:id).
The Router also implements a GET fallback that attempts static-file serving: if no exact/dynamic application route matches a GET request, the Router attempts to serve a file from public/.
GET request
↓
exact/dynamic application route?
├── yes → controller
└── no → static-file fallback (serves from public/)
Middleware executes sequentially before the target controller. The middleware signature receives the Request by const reference and returns a Response*:
std::function<Response*(const Request&)>- If
nullptris returned, the router proceeds to the next middleware or the controller. - If a valid
Response*is returned, the request is immediately halted and the response is dispatched.
To handle concurrent clients, RawServe implements a fixed-size custom ThreadPool. The final implementation uses:
- A fixed number of worker threads.
- A synchronized task queue of type
std::queue<int>containing client socket file descriptors, along with a per-client handler callback (std::function<void(int)>). std::mutexandstd::condition_variableto safely coordinate task distribution.- Workers wait when no task is available.
- The server submits accepted client socket descriptors to the queue.
- Workers execute the handler for the client, blocking on network I/O.
- Workers are properly joined during shutdown.
Because I/O operations inside the task are blocking, the number of simultaneously processed client connections is bounded by the fixed worker count; additional accepted connections wait in the task queue. This avoids unlimited thread creation while ensuring the main thread remains available to accept new connections.
Handling Ctrl+C (SIGINT) initiates a server shutdown sequence:
Ctrl+C
↓
SIGINT handler
↓
running flag changes
↓
accept loop stops
↓
active client connections are shut down
↓
workers finish/exit
↓
ThreadPool joins workers
↓
server socket is cleaned up
The project features a custom HashMap used internally for route storage. It implements a hash function, a bucket array, and handles collisions via linked lists. Standard operations like insertion, lookup, and removal are supported (Note: this is an educational implementation and is not optimized or production-grade).
web_server/
├── main.cpp
├── server.h
├── router.h
├── parser.h
├── request.h
├── response.h
├── threadpool.h
├── hashmap.h
├── http_exception.h
├── file_util.h
├── utils.h
└── public/
Compile the server using standard GCC (requires C++17 and pthreads). Ensure nlohmann/json is available on your system.
g++ main.cpp -std=c++17 -pthread -o main
./mainBelow are realistic usage examples demonstrating the actual RawServe API structure.
#include "request.h"
#include "response.h"
#include "router.h"
#include "server.h"
#include <iostream>
int main() {
Router router;
// Logging Middleware
router.use([](const Request& request) -> Response* {
std::cout << request.method
<< " "
<< request.path
<< std::endl;
return nullptr; // Continue processing
});
// Example of middleware rejecting a request
router.use([](const Request& request) -> Response* {
if (request.headers.find("authorization") == request.headers.end()) {
std::string body = "Unauthorized";
std::vector<std::string> headers = {
"Content-Type: text/plain",
"Content-Length: " + std::to_string(body.size())
};
return new Response(401, headers, body); // Stop the request
}
return nullptr;
});
// ... route registration ...
Server server(&router);
server.start();
return 0;
}Controllers take a const Request* and return a Response*.
// Exact route
Response* home(const Request* request) {
std::string body = "This is home page";
std::vector<std::string> headers;
headers.push_back("Content-Type: text/plain");
headers.push_back(
"Content-Length: " + std::to_string(body.size())
);
return new Response(200, headers, body);
}
// Dynamic route
Response* get_user(const Request* request) {
std::string id = request->params.at("id");
std::string body = "User ID: " + id;
std::vector<std::string> headers = {
"Content-Type: text/plain",
"Content-Length: " + std::to_string(body.size())
};
return new Response(200, headers, body);
}
int main() {
Router router;
router.get("/", home);
router.get("/user/:id", get_user);
Server server(&router);
server.start();
return 0;
}RawServe implements safeguards such as:
- Path traversal protection and filesystem canonicalization for static file serving.
- Request-size limits.
- Receive and send socket timeouts.
These measures reduce several common risks, but the project has not undergone a security audit and should not be deployed as production infrastructure.
- Linux/POSIX focused: Reliance on Linux socket APIs means it does not port natively to Windows.
- Blocking socket I/O: Network I/O is fully blocking, which occupies worker threads during client read/writes.
- Fixed-size ThreadPool: Bounded concurrency means the server can only handle a fixed number of simultaneous active connections.
- No epoll / non-blocking event loop: Does not utilize asynchronous multiplexing I/O.
- Limited HTTP compliance: Implements a functional subset but does not claim full RFC-compliance.
- No TLS/HTTPS: Unencrypted HTTP traffic only.
- No chunked transfer encoding.
- No HTTP/2 or HTTP/3.
- Limited testing: Lacks comprehensive automated testing or benchmarking frameworks.
- Not production ready: Built for educational purposes, missing production security audits.
Future work on this project may include:
- epoll and event-driven architecture
- Non-blocking sockets
- TLS/HTTPS support
- Chunked transfer encoding
- Stronger HTTP compliance
- HTTP/2 or WebSockets multiplexing
- Automated testing and benchmarks
- Configuration files and structured logging
RawServe is licensed under the MIT License.