🌐 HTTP Server
The HTTP Server is Sprout's embedded server implementation that replicates Tomcat's core functionality. It provides a flexible, configurable server infrastructure supporting both blocking I/O (BIO) and non-blocking I/O (NIO), with options for platform threads or virtual threads.
Overview
Sprout's HTTP Server provides:
- NIO-based Event Loop: High-performance non-blocking I/O using Java NIO Selector
- Hybrid BIO/NIO Mode: Flexible I/O strategy selection per protocol handler
- Virtual Thread Support: Modern concurrency with Java 21's virtual threads
- Platform Thread Pool: Traditional thread pool for compatibility
- Protocol Detection: Automatic HTTP/WebSocket protocol identification
- Pluggable Handler Architecture: Extensible protocol handler system
Server Architecture
Core Components
The HTTP Server consists of the following key components:
HttpServer
: Main server facade implementing lifecycle managementServerStrategy
: Pluggable server strategy interface (NIO event loop)ConnectionManager
: Connection acceptance and protocol routingProtocolDetector
: Protocol identification from initial bytesProtocolHandler
: Protocol-specific request handlingRequestExecutorService
: Thread management abstraction
Server Initialization Process
public class SproutApplication {
public static void run(Class<?> primarySource) throws Exception {
// 1. Create application context
ApplicationContext ctx = new SproutApplicationContext(packages);
ctx.refresh();
// 2. Get HttpServer bean
HttpServer server = ctx.getBean(HttpServer.class);
// 3. Start server
int port = server.start(8080);
System.out.println("Server started on port " + port);
}
}
Thread Execution Modes
Virtual Thread Mode (Default)
Virtual threads provide lightweight concurrency for high-throughput applications:
@Configuration
public class ServerConfiguration {
@Bean
public RequestExecutorService executorService(
AppConfig appConfig,
List<ContextPropagator> contextPropagators) {
String threadType = appConfig.getStringProperty(
"server.thread-type", "virtual"
);
if (threadType.equals("virtual")) {
return new VirtualRequestExecutorService(contextPropagators);
}
return new RequestExecutorPoolService(
appConfig.getIntProperty("server.thread-pool-size", 100)
);
}
}
VirtualRequestExecutorService Implementation
public class VirtualRequestExecutorService implements RequestExecutorService {
private final ExecutorService pool =
Executors.newVirtualThreadPerTaskExecutor();
private final List<ContextPropagator> propagators;
@Override
public void execute(Runnable task) {
// Capture current context and propagate to virtual thread
final ContextSnapshot snapshot = new ContextSnapshot(propagators);
pool.execute(snapshot.wrap(task));
}
}
Key Features:
- Creates a new virtual thread per task
- Minimal memory footprint (~1KB per thread)
- Automatic context propagation to child threads
- Suitable for millions of concurrent connections
Platform Thread Pool Mode
Traditional fixed-size thread pool for compatibility:
public class RequestExecutorPoolService implements RequestExecutorService {
private final ExecutorService pool;
public RequestExecutorPoolService(int threadPoolSize) {
this.pool = Executors.newFixedThreadPool(threadPoolSize);
}
@Override
public void execute(Runnable task) {
pool.execute(task);
}
}
Configuration:
# application.properties
server.thread-type=platform
server.thread-pool-size=200
Key Features:
- Fixed number of platform threads
- Predictable resource usage
- Compatible with all Java versions
- Suitable for moderate concurrency needs
I/O Execution Modes
Hybrid Mode (BIO with NIO Accept)
Combines NIO for connection acceptance with BIO for request processing:
public class BioHttpProtocolHandler implements AcceptableProtocolHandler {
private final RequestDispatcher dispatcher;
private final HttpRequestParser parser;
private final RequestExecutorService requestExecutorService;
@Override
public void accept(SocketChannel channel, Selector selector,
ByteBuffer initialBuffer) throws Exception {
// 1. Detach from NIO selector
detachFromSelector(channel, selector);
// 2. Switch to blocking mode
channel.configureBlocking(true);
Socket socket = channel.socket();
// 3. Delegate to worker thread
requestExecutorService.execute(() -> {
try (InputStream in = socket.getInputStream();
BufferedWriter out = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream()))) {
// 4. Read complete request (blocking)
String rawRequest = HttpUtils.readRawRequest(initialBuffer, in);
// 5. Parse and dispatch
HttpRequest<?> req = parser.parse(rawRequest);
HttpResponse res = new HttpResponse();
dispatcher.dispatch(req, res);
// 6. Write response (blocking)
writeResponse(out, res.getResponseEntity());
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
Flow:
- NIO Selector accepts connection
- Read initial bytes to detect protocol
- Switch channel to blocking mode
- Detach from selector and delegate to worker thread
- Blocking I/O for request/response in worker thread
Advantages:
- Simple programming model (blocking I/O)
- Works well with virtual threads
- Lower complexity than pure NIO
- Good throughput with virtual thread executor
Configuration:
server.execution-mode=hybrid
server.thread-type=virtual
Pure NIO Mode
Fully non-blocking I/O for maximum scalability:
public class NioHttpProtocolHandler implements AcceptableProtocolHandler {
private final RequestDispatcher dispatcher;
private final HttpRequestParser parser;
private final RequestExecutorService requestExecutorService;
@Override
public void accept(SocketChannel channel, Selector selector,
ByteBuffer initialBuffer) throws Exception {
// 1. Create stateful connection handler
HttpConnectionHandler handler = new HttpConnectionHandler(
channel, selector, dispatcher, parser,
requestExecutorService, initialBuffer
);
// 2. Register for READ events with handler as attachment
channel.register(selector, SelectionKey.OP_READ, handler);
// 3. Trigger initial read
handler.read(channel.keyFor(selector));
}
}
HttpConnectionHandler State Machine
public class HttpConnectionHandler implements ReadableHandler, WritableHandler {
private final SocketChannel channel;
private final Selector selector;
private final ByteBuffer readBuffer = ByteBuffer.allocate(8192);
private volatile ByteBuffer writeBuffer;
private HttpConnectionStatus currentState = HttpConnectionStatus.READING;
@Override
public void read(SelectionKey key) throws Exception {
if (currentState != HttpConnectionStatus.READING) return;
// 1. Non-blocking read
int bytesRead = channel.read(readBuffer);
if (bytesRead == -1) {
closeConnection(key);
return;
}
readBuffer.flip();
// 2. Check if request is complete
if (HttpUtils.isRequestComplete(readBuffer)) {
currentState = HttpConnectionStatus.PROCESSING;
key.interestOps(0); // Stop event detection
// 3. Extract request
byte[] requestBytes = new byte[readBuffer.remaining()];
readBuffer.get(requestBytes);
String rawRequest = new String(requestBytes, StandardCharsets.UTF_8);
// 4. Process in worker thread
requestExecutorService.execute(() -> {
try {
HttpRequest<?> req = parser.parse(rawRequest);
HttpResponse res = new HttpResponse();
dispatcher.dispatch(req, res);
// 5. Prepare response and switch to WRITING state
this.writeBuffer = HttpUtils.createResponseBuffer(
res.getResponseEntity()
);
this.currentState = HttpConnectionStatus.WRITING;
// 6. Register for WRITE events
key.interestOps(SelectionKey.OP_WRITE);
selector.wakeup();
} catch (Exception e) {
closeConnection(key);
e.printStackTrace();
}
});
readBuffer.clear();
}
}
@Override
public void write(SelectionKey key) throws IOException {
if (currentState != HttpConnectionStatus.WRITING || writeBuffer == null)
return;
// Non-blocking write
channel.write(writeBuffer);
if (!writeBuffer.hasRemaining()) {
// All data sent
currentState = HttpConnectionStatus.DONE;
closeConnection(key);
}
// If data remains, selector will trigger write again when ready
}
}
State Machine:
READING → PROCESSING → WRITING → DONE
↑ ↓
└─────────── (close/reset) ──────┘
Advantages:
- Maximum scalability (single thread handles thousands of connections)
- Minimal thread context switching
- Efficient resource utilization
- Best for high-concurrency scenarios
Configuration:
server.execution-mode=nio
server.thread-type=platform
server.thread-pool-size=100
NIO Event Loop Architecture
NioHybridServerStrategy
The main event loop implementation:
@Component
public class NioHybridServerStrategy implements ServerStrategy {
private final ConnectionManager connectionManager;
private volatile boolean running = true;
private Selector selector;
private ServerSocketChannel serverChannel;
@Override
public int start(int port) throws Exception {
// 1. Initialize NIO selector
selector = Selector.open();
serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(port));
serverChannel.configureBlocking(false);
// 2. Register ACCEPT event
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
// 3. Start event loop thread
running = true;
Thread t = new Thread(this::eventLoop, "sprout-nio-loop");
t.setDaemon(false);
t.start();
return ((InetSocketAddress) serverChannel.getLocalAddress()).getPort();
}
private void eventLoop() {
while (running) {
selector.select(); // Block until events are ready
for (Iterator<SelectionKey> it = selector.selectedKeys().iterator();
it.hasNext();) {
SelectionKey key = it.next();
it.remove();
if (!key.isValid()) {
cleanupConnection(key);
continue;
}
try {
// Accept new connections
if (key.isAcceptable()) {
connectionManager.acceptConnection(key, selector);
}
Object attachment = key.attachment();
// Handle readable events
if (key.isReadable() && attachment instanceof ReadableHandler rh) {
rh.read(key);
}
// Handle writable events
if (key.isWritable() && attachment instanceof WritableHandler wh) {
wh.write(key);
}
} catch (IOException ioe) {
cleanupConnection(key);
} catch (Exception e) {
e.printStackTrace();
cleanupConnection(key);
}
}
}
}
}
Event Loop Responsibilities:
- Accept new connections via
ConnectionManager
- Delegate READ events to
ReadableHandler
- Delegate WRITE events to
WritableHandler
- Connection cleanup on errors
Protocol Detection and Routing
Connection Acceptance Flow
@Component
public class DefaultConnectionManager implements ConnectionManager {
private final List<ProtocolDetector> detectors;
private final List<ProtocolHandler> handlers;
@Override
public void acceptConnection(SelectionKey selectionKey, Selector selector)
throws Exception {
// 1. Accept connection
ServerSocketChannel serverChannel =
(ServerSocketChannel) selectionKey.channel();
SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false);
// 2. Read initial bytes for protocol detection
ByteBuffer buffer = ByteBuffer.allocate(2048);
int bytesRead = clientChannel.read(buffer);
if (bytesRead <= 0) {
clientChannel.close();
return;
}
buffer.flip();
// 3. Detect protocol
String detectedProtocol = "UNKNOWN";
for (ProtocolDetector detector : detectors) {
detectedProtocol = detector.detect(buffer);
if (!"UNKNOWN".equals(detectedProtocol)) {
break;
}
}
if ("UNKNOWN".equals(detectedProtocol)) {
clientChannel.close();
return;
}
// 4. Route to appropriate handler
for (ProtocolHandler handler : handlers) {
if (handler.supports(detectedProtocol)) {
if (handler instanceof AcceptableProtocolHandler) {
((AcceptableProtocolHandler) handler)
.accept(clientChannel, selector, buffer);
return;
}
}
}
}
}
HTTP Protocol Detection
@Component
public class HttpProtocolDetector implements ProtocolDetector {
private static final Set<String> HTTP_METHODS = Set.of(
"GET ", "POST ", "PUT ", "DELETE ", "HEAD ",
"OPTIONS ", "PATCH ", "TRACE "
);
@Override
public String detect(ByteBuffer buffer) throws Exception {
// Save buffer position
buffer.mark();
// Read first 8 bytes
int readLimit = Math.min(buffer.remaining(), 8);
byte[] headerBytes = new byte[readLimit];
buffer.get(headerBytes);
// Restore buffer position
buffer.reset();
String prefix = new String(headerBytes, StandardCharsets.UTF_8);
// Check for HTTP method
if (HTTP_METHODS.stream().anyMatch(prefix::startsWith)) {
return "HTTP/1.1";
}
return "UNKNOWN";
}
}
Detection Process:
- Read first bytes from connection (non-destructive)
- Check for HTTP method keywords
- Return detected protocol or "UNKNOWN"
- Preserve buffer for subsequent processing
Request Completion Detection
HTTP Request Parsing
public final class HttpUtils {
public static boolean isRequestComplete(ByteBuffer buffer) {
// 1. Find header end (\r\n\r\n)
byte[] arr = new byte[buffer.remaining()];
buffer.get(arr);
String content = new String(arr, StandardCharsets.UTF_8);
int headerEnd = content.indexOf("\r\n\r\n");
if (headerEnd < 0) {
return false; // Headers incomplete
}
String headers = content.substring(0, headerEnd);
// 2. Check Content-Length or Transfer-Encoding
int contentLength = parseContentLength(headers);
boolean isChunked = isChunked(headers);
int bodyStart = headerEnd + 4;
int totalLength = content.length();
if (isChunked) {
// Chunked encoding: check for 0\r\n\r\n
String body = content.substring(bodyStart);
return isChunkedBodyComplete(body);
} else if (contentLength >= 0) {
// Content-Length: verify body size
int bodyReceived = totalLength - bodyStart;
return bodyReceived >= contentLength;
} else {
// No body (GET request)
return true;
}
}
}
Completion Criteria:
- Headers: Must contain
\r\n\r\n
- Content-Length: Body bytes must match declared length
- Chunked: Last chunk must be
0\r\n\r\n
- No Body: Complete after headers
Mode Comparison
Hybrid Mode (BIO + Virtual Threads)
Best For:
- High throughput with simple code
- Java 21+ projects
- Applications with moderate request processing time
Architecture:
[NIO Selector] → Accept → [Detect Protocol] → [Switch to BIO]
↓
[Virtual Thread]
↓
[Blocking Read/Write]
Pros:
- Simple, readable code
- Automatic backpressure
- Works well with virtual threads
- Easy to debug
Cons:
- Higher memory per connection than pure NIO
- Thread switching overhead
Pure NIO Mode
Best For:
- Maximum scalability
- Low-latency requirements
- Resource-constrained environments
Architecture:
[NIO Selector] → Accept → [Detect Protocol] → [Register READ]
↑ ↓
└──────────[Write Complete]───[Process in Pool Thread]
Pros:
- Single thread handles thousands of connections
- Minimal memory footprint
- Low latency
Cons:
- Complex state machine
- Harder to debug
- Requires careful buffer management
Configuration Guide
Recommended Configurations
High-Throughput API Server (Java 21+)
server.execution-mode=hybrid
server.thread-type=virtual
Maximum Scalability (Connection-heavy)
server.execution-mode=nio
server.thread-type=platform
server.thread-pool-size=200
Legacy Compatibility (Java 11/17)
server.execution-mode=hybrid
server.thread-type=platform
server.thread-pool-size=500
Best Practices
1. Choose the Right Mode
// For most applications (Java 21+)
server.execution-mode=hybrid
server.thread-type=virtual
// For extreme scalability needs
server.execution-mode=nio
server.thread-type=platform
2. Context Propagation
Virtual thread executor automatically propagates context:
public class VirtualRequestExecutorService {
@Override
public void execute(Runnable task) {
// Context is captured before task submission
final ContextSnapshot snapshot = new ContextSnapshot(propagators);
pool.execute(snapshot.wrap(task));
}
}
3. Graceful Shutdown
@Component
public class ServerShutdownHook {
private final HttpServer server;
@PreDestroy
public void shutdown() throws Exception {
server.stop();
}
}
4. Monitor Thread Usage
// Platform threads: monitor thread pool saturation
// Virtual threads: monitor memory and CPU usage
Performance Characteristics
Virtual Thread Mode
- Scalability: Excellent for high connection count
Platform Thread Pool Mode
- Scalability: Limited by thread pool size
NIO vs Hybrid
- NIO: Lower memory, higher complexity
- Hybrid: Higher throughput with virtual threads, simpler code
- Hybrid + Virtual: Best balance for modern applications
Extension Points
Custom Protocol Handler
@Component
public class CustomProtocolHandler implements AcceptableProtocolHandler {
@Override
public void accept(SocketChannel channel, Selector selector,
ByteBuffer buffer) throws Exception {
// Custom protocol handling logic
}
@Override
public boolean supports(String protocol) {
return "CUSTOM/1.0".equals(protocol);
}
}
Custom Protocol Detector
@Component
public class CustomProtocolDetector implements ProtocolDetector {
@Override
public String detect(ByteBuffer buffer) throws Exception {
// Inspect buffer and return protocol name
return "CUSTOM/1.0";
}
}