In the latest release of JDK 21, virtual threads have become a stable feature, revolutionizing the way we write concurrent programs in Java. With virtual threads, it is now easier than ever to create highly performant and scalable applications. In this blog post, we will explore how to build a minimal yet fully functional HTTP server from scratch using virtual threads.
Virtual threads, also known as fibers, are lightweight threads that can be scheduled cooperatively by the JVM. Unlike traditional threads, which are managed by the operating system, virtual threads are managed at the application level, resulting in reduced overhead and improved scalability. By leveraging virtual threads, we can achieve higher concurrency without the need for complex thread management.
To get started, we need to set up a new Java project and ensure that we are using JDK 21 or later. We can create a new Maven project or use any other build tool of our choice. Once the project is set up, we can proceed with writing the code for our HTTP server.
Our goal is to create a minimal HTTP server that can handle incoming requests and return appropriate responses. We will be using the built-in HttpServer
class from the JDK to handle the server functionality. However, we will leverage virtual threads to handle incoming connections and process the requests concurrently.
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.util.concurrent.Executors;
public class MinimalHttpServer {
public static void main(String[] args) throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
server.createContext("/", exchange -> {
// Handle incoming requests here
var response = "Hello World";
exchange.sendResponseHeaders(200, response.length());
OutputStream os = exchange.getResponseBody();
os.write(response.getBytes());
exchange.close();
});
server.start();
System.out.println("Server started on port 8080");
}
}
In the code above, we create a new instance of HttpServer
and bind it to port 8080. We then set the executor to a virtual thread executor, which allows us to process requests using virtual threads. Finally, we define a context for handling incoming requests and start the server.
Now, let's explore how we can build an HTTP server from scratch without relying on any existing HTTP server libraries.
To build an HTTP server from scratch, we need to handle the low-level details of the HTTP protocol, such as parsing incoming requests, generating appropriate responses, and managing connections.
Here are the basic steps to create a simple HTTP server:
- Create a Server Socket: Start by creating a server socket that listens for incoming connections on a specific port. You can use the
ServerSocket
class from thejava.net
package for this purpose. - Accept Incoming Connections: Once the server socket is created, you need to accept incoming connections from clients. Use a loop to continuously accept connections by calling the
accept()
method on the server socket. Each accepted connection will return aSocket
object representing the client's connection. - Handle Requests: After accepting a connection, read the incoming request from the client. Parse the request to extract important information such as the HTTP method, headers, and requested resource. Based on the request, generate an appropriate response.
- Send Response: Once you have generated the response, send it back to the client over the socket's output stream. Make sure to include the necessary headers, such as the Content-Type and Content-Length.
- Close Connection: After sending the response, close the connection by calling the
close()
method on the socket.
By implementing these steps, you can create a basic HTTP server that handles incoming requests and sends back responses. Keep in mind that building a full-featured HTTP server from scratch can be a complex task, especially when dealing with advanced features like handling concurrent connections and managing sessions. However, it provides you with complete control and flexibility over the server's behavior.
To complete this challenge, I sought assistance from ChatGPT to implement the server functionality. You can find the link to the chat history. Feel free to review it if you're interested in learning about our process, which involved multiple trials and fixes. Below is the final working code.
package org.funsf.kiss;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executors;
public class HTTPServer {
private int port;
public HTTPServer(int port) {
this.port = port;
}
public void start() {
try (var executor = Executors.newVirtualThreadPerTaskExecutor();
ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("Server started at port " + port);
while (true) {
Socket clientSocket = serverSocket.accept();
executor.execute(new RequestHandler(clientSocket));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
class HTTPRequest {
private final BufferedReader reader;
private Map<String, String> headers = new HashMap<>();
public HTTPRequest(InputStream input) {
reader = new BufferedReader(new InputStreamReader(input));
parseHeaders();
}
private void parseHeaders() {
try {
String line;
while ((line = reader.readLine()) != null) {
if (line.isEmpty()) {
break; // End of headers
}
String[] headerParts = line.split(": ", 2);
if (headerParts.length == 2) {
headers.put(headerParts[0], headerParts[1]);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public String getHeader(String headerName) {
return headers.get(headerName);
}
// Implement methods to extract other parts of the request (e.g., HTTP method, path, etc.).
}
class HTTPResponse {
private final OutputStream output;
private PrintWriter writer;
public HTTPResponse(OutputStream output) {
this.output = output;
this.writer = new PrintWriter(output, true);
}
public void sendResponse(int status, String contentType, String body) throws IOException {
byte[] bodyBytes = body.getBytes();
writer.println("HTTP/1.1 " + status + " OK");
writer.println("Content-Type: " + contentType);
writer.println("Content-Length: " + bodyBytes.length); // Add Content-Length
writer.println();
writer.flush();
output.write(bodyBytes, 0, bodyBytes.length);
writer.close(); // Close the writer when done
}
// Implement methods to set other headers if needed.
}
interface Handler {
void handle(HTTPRequest request, HTTPResponse response);
}
class HelloWorldHandler implements Handler {
@Override
public void handle(HTTPRequest request, HTTPResponse response) {
try {
response.sendResponse(200, "Content-Type: text/plain", "Hello, World!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
class RequestHandler implements Runnable {
private final Socket socket;
public RequestHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
while (true) {
InputStream input = socket.getInputStream();
OutputStream output = socket.getOutputStream();
HTTPRequest httpRequest = new HTTPRequest(input);
HTTPResponse httpResponse = new HTTPResponse(output);
new HelloWorldHandler().handle(httpRequest, httpResponse);
// Check if the client wants to keep the connection alive
if (!isConnectionKeepAlive(httpRequest)) {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private boolean isConnectionKeepAlive(HTTPRequest request) {
// Get the value of the "Connection" header from the request
String connectionHeader = request.getHeader("Connection");
// If the "Connection" header is not present or its value is not "keep-alive," return false.
// If the value is "keep-alive," return true to indicate that the connection should be kept alive.
return connectionHeader != null && connectionHeader.equalsIgnoreCase("keep-alive");
}
}
public class ServerMain {
public static void main(String[] args) {
int port = 8080;
HTTPServer server = new HTTPServer(port);
server.start();
}
}
In this blog post, we have explored how to build a minimal HTTP server using virtual threads in Java. By leveraging the power of virtual threads, we can create highly performant and scalable applications without the need for complex thread management. The introduction of virtual threads in JDK 21 has opened up exciting possibilities for concurrent programming in Java.
Stay tuned for more updates and advanced techniques on virtual threads in future blog posts.