A lightweight HTTP web server built from scratch in Java — no frameworks, no servlet
containers. The server is implemented directly on top of the Java NIO APIs
(ServerSocketChannel and Selector), with hand-written HTTP parsing and response
serialization.
This project is a deep dive into how web servers actually work under the hood: the reactor pattern, non-blocking I/O, HTTP message parsing, and request routing.
Client ──TCP──▶ ServerSocketChannel ──▶ Selector (reactor thread)
│
accept / read / write events
│
HttpDecoder (raw text → HttpRequest)
│
HttpHandler (route dispatch)
│
RequestRunner (your controller lambda)
│
ResponseWriter (HttpResponse → raw text)
│
SocketChannel (response written back)
TcpEventHandler runs a single-threaded event loop. For every selection key it:
- Accepts new client connections (optionally gated by a simple rate limiter), registers them for read/write, and tracks them by a UUID.
- Reads incoming bytes into a buffer, decodes them into an
HttpRequest, and executes the matching route handler synchronously. - Writes the serialized response back to the channel and closes the connection.
- Raw Java NIO reactor — a single selector thread multiplexes all connections using
SelectionKeyinterest sets (OP_ACCEPT | OP_READ | OP_WRITE). - Framework-free HTTP stack — the HTTP request/response lifecycle (parsing, routing, serialization) is implemented by hand; there is no Tomcat, Netty, or servlet API.
- Fluent routing API — register handlers as lambdas:
EventWebServer server = new EventWebServer(8888); server.handleRoute(HttpMethod.GET, "/records", request -> new HttpResponse.Builder() .setStatusCode(200) .setEntity("Plain text entity") .addHeader("Content-Type", "text/plain") .build()); server.start();
- Hand-written HTTP decoder — parses the request line, headers, and body from raw
bytes into a typed
HttpRequestmodel. - Response writer — serializes
HttpResponseobjects (status line, headers, entity) back into HTTP wire format. - Correct status semantics — unknown routes return
404 Not Found, malformed requests return400 Bad Request. - Connection tracking — every accepted connection is registered under a UUID with bidirectional lookups, and responses are staged per connection before being flushed.
- Simple rate limiter applied to new connection accepts.
- Graceful shutdown hook —
stop()closes the selector and server channel. - Unit tests for the HTTP model and request lifecycle (JUnit 4 & 5).
- Java 21
- Maven
- JUnit 4 / JUnit Jupiter (tests)
git clone https://github.com/haithembenkhalef/custom-web-server.git
cd custom-web-server
mvn compile exec:java -Dexec.mainClass="com.webby.App"Being an educational from-scratch implementation, it deliberately trades completeness for clarity:
- Request processing is synchronous on the selector thread — a slow handler blocks the whole event loop.
- One
ByteBufferread per request: requests larger than the buffer or split across TCP segments are not reassembled. - HTTP/1.0-style semantics: the connection is closed after each response (no keep-alive or pipelining).
- No chunked transfer encoding, no
Expect: 100-continue, no compression. - The rate limiter is a simple accept gate, not a per-client token bucket.
Planned improvements, roughly in order:
- Offload request handling to a worker
ExecutorService(pool objects already exist but are not wired in) so slow routes never block the reactor. - Ring-buffer pipeline (LMAX Disruptor) — the original design goal: publish read
events onto a ring buffer and consume them with dedicated handlers, removing locks and
garbage from the hot path. The dependency and the
eventspackage are already in place for this. - Full request reassembly — accumulate partial reads until the full request
(headers +
Content-Lengthbody) has arrived. - HTTP/1.1 keep-alive & pipelining — persistent connections, correct
Connectionheader handling, and ordered pipelined responses. - Proper per-client rate limiting (token bucket) instead of a global accept gate.
- Content negotiation & richer routing — path parameters, wildcards, filters/middleware.
- Configuration — port, buffer sizes, and pool sizes via file or CLI args instead of hard-coded values.
- Structured logging & metrics — request counts, latency, active connections.
- Test coverage — integration tests over real sockets and decoder edge-case tests.
Most developers use web servers every day without ever seeing what happens behind the scene. This repository exists to close that gap: every layer — socket multiplexing, HTTP parsing, routing, serialization — is explicit, small, and readable, making it a useful reference for anyone learning network programming or building high-performance Java services.