This project implements a lightweight, multi-threaded HTTP Server in Java from scratch. It was designed to demonstrate core networking concepts, concurrent programming using thread pools, and the Command Pattern for handling requests via Servlets.
The server allows for dynamic registration of "Servlets" (request handlers) that can process GET, POST, and other HTTP methods. It also features a custom RequestParser capable of handling standard URL parameters as well as a specific "meta-data" body format.
The core of the system is the MyHTTPServer class, which wraps a standard Java ServerSocket.
- Listening Loop: The server runs a continuous loop (in its own
run()method) that accepts incoming client connections (Socket). - Non-blocking Accept: We use
serverSocket.setSoTimeout(1000)to ensure theaccept()call doesn't block indefinitely. This allows the server to check avolatile boolean runningflag periodically, enabling a graceful shutdown whenclose()is called.
To handle multiple clients simultaneously without blocking the main server thread, we use a Thread Pool (ExecutorService).
- Why a Thread Pool? Creating a new thread for every request is expensive and can exhaust system resources (Context Switching overhead). A Fixed Thread Pool limits the number of active threads (e.g., 10), queuing additional requests until a thread becomes available.
- Workflow:
- Main thread calls
accept()and gets aSocket. - Main thread submits a task (
handleClient(socket)) to the thread pool. - A worker thread picks up the task, parses the request, executes the servlet, and closes the socket.
- Main thread calls
Defines the contract for the server. It decouples the usage of a server (start, stop, add servlet) from its implementation. This allows for easy swapping of server implementations (e.g., single-threaded vs multi-threaded) without changing client code.
Represents a pluggable request handler.
- Pattern: This implements the Command Pattern. The server is the invoker, and the Servlet is the command.
- Design: It is a
FunctionalInterface, allowing users to define handlers cleanly using Lambda Expressions:server.addServlet("GET", "/hello", (req, out) -> out.write("Hello!".getBytes()));
A static utility class responsible for reading the raw input stream and converting it into a structured RequestInfo object.
- Parsing logic:
- Extracts HTTP Method and URI.
- Parses Query Parameters (
?key=value). - Reads Headers (specifically
Content-Length). - Custom Logic: Handles a special body format where "meta-data" lines (key=value) can precede the actual content body, separated by a blank line.
The concrete implementation of the server.
- Servlet Registry: Uses a
ConcurrentHashMap<String, Map<String, Servlet>>to store servlets. This mapsHTTP Method->URI->Servlet. - Routing Algorithm:
- First, it attempts an Exact Match for the URI.
- If that fails, it uses Longest Prefix Matching. This means if you register
/apiand/api/v1, a request to/api/v1/userswill correctly route to/api/v1.
// Create a server on port 8080 with a pool of 10 threads
HTTPServer server = new MyHTTPServer(8080, 10);
server.start();// Register a simple GET handler
server.addServlet("GET", "/api/greet", (req, out) -> {
String name = req.getParameters().getOrDefault("name", "World");
String response = "Hello, " + name;
// Write standard HTTP response
out.write(("HTTP/1.1 200 OK\r\n\r\n" + response).getBytes());
});server.close(); // Gracefully shuts down socket and thread pool