From 56a3bcf9e945741330860f44e6799aa7e825ba17 Mon Sep 17 00:00:00 2001 From: ruroru <111705692+ruroru@users.noreply.github.com> Date: Mon, 25 May 2026 10:51:09 +0000 Subject: [PATCH] add robaho httpserver --- frameworks/robaho-httpserver/Dockerfile | 13 + frameworks/robaho-httpserver/meta.json | 23 ++ frameworks/robaho-httpserver/pom.xml | 74 ++++++ .../robaho-httpserver/src/main/java/Main.java | 249 ++++++++++++++++++ .../src/main/resources/fortunes.html | 12 + 5 files changed, 371 insertions(+) create mode 100644 frameworks/robaho-httpserver/Dockerfile create mode 100644 frameworks/robaho-httpserver/meta.json create mode 100644 frameworks/robaho-httpserver/pom.xml create mode 100644 frameworks/robaho-httpserver/src/main/java/Main.java create mode 100644 frameworks/robaho-httpserver/src/main/resources/fortunes.html diff --git a/frameworks/robaho-httpserver/Dockerfile b/frameworks/robaho-httpserver/Dockerfile new file mode 100644 index 00000000..b8e38084 --- /dev/null +++ b/frameworks/robaho-httpserver/Dockerfile @@ -0,0 +1,13 @@ +FROM maven:3.9-eclipse-temurin-21 AS build +WORKDIR /app +COPY pom.xml . +RUN mvn dependency:go-offline -q +COPY src ./src +RUN mvn package -DskipTests -q + +FROM eclipse-temurin:21-jre +WORKDIR /app +COPY --from=build /app/target/robaho-httpserver-1.0.0.jar app.jar +COPY --from=build /app/target/libs ./libs +EXPOSE 8080 +ENTRYPOINT ["java", "-Drobaho.net.httpserver.nodelay=true", "-jar", "app.jar"] diff --git a/frameworks/robaho-httpserver/meta.json b/frameworks/robaho-httpserver/meta.json new file mode 100644 index 00000000..816a0c53 --- /dev/null +++ b/frameworks/robaho-httpserver/meta.json @@ -0,0 +1,23 @@ +{ + "display_name": "robaho-httpserver", + "language": "Java", + "type": "production", + "engine": "robaho-httpserver", + "description": "High-performance Java HTTP server using robaho/httpserver, a drop-in replacement for com.sun.net.httpserver with improved throughput.", + "repo": "https://github.com/robaho/httpserver", + "enabled": true, + "tests": [ + "baseline", + "pipelined", + "limited-conn", + "json", + "json-comp", + "upload", + "static", + "async-db", + "fortunes", + "api-4", + "api-16" + ], + "maintainers": [] +} diff --git a/frameworks/robaho-httpserver/pom.xml b/frameworks/robaho-httpserver/pom.xml new file mode 100644 index 00000000..1c6300b3 --- /dev/null +++ b/frameworks/robaho-httpserver/pom.xml @@ -0,0 +1,74 @@ + + + 4.0.0 + + com.httparena + robaho-httpserver + 1.0.0 + jar + + + 21 + ${java.version} + ${java.version} + + + + + io.github.robaho + httpserver + 1.0.25 + + + com.fasterxml.jackson.core + jackson-databind + 2.19.0 + + + io.pebbletemplates + pebble + 3.2.4 + + + io.vertx + vertx-pg-client + 5.0.0.CR7 + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + + com.httparena.Main + true + libs/ + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.7.1 + + + copy-dependencies + package + copy-dependencies + + ${project.build.directory}/libs + + + + + + + diff --git a/frameworks/robaho-httpserver/src/main/java/Main.java b/frameworks/robaho-httpserver/src/main/java/Main.java new file mode 100644 index 00000000..af939b96 --- /dev/null +++ b/frameworks/robaho-httpserver/src/main/java/Main.java @@ -0,0 +1,249 @@ +package com.httparena; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.pebbletemplates.pebble.PebbleEngine; +import io.pebbletemplates.pebble.template.PebbleTemplate; +import io.vertx.pgclient.PgBuilder; +import io.vertx.pgclient.PgConnectOptions; +import io.vertx.sqlclient.*; +import robaho.net.httpserver.HttpServer; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; + +import java.io.*; +import java.net.InetSocketAddress; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.Executors; +import java.util.zip.GZIPOutputStream; + +public class Main { + + static final ObjectMapper mapper = new ObjectMapper(); + static final PebbleEngine pebble = new PebbleEngine.Builder().autoEscaping(true).build(); + static final PebbleTemplate fortunesTemplate = pebble.getTemplate("templates/fortunes.html"); + static List dataset = List.of(); + static Pool pgPool; + + public static void main(String[] args) throws Exception { + loadDataset(); + initPostgres(); + + HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); + server.setExecutor(Executors.newVirtualThreadPerTaskExecutor()); + + server.createContext("/baseline11", new BaselineHandler()); + server.createContext("/baseline2", new BaselineHandler()); + server.createContext("/pipeline", new PipelineHandler()); + server.createContext("/json/", new JsonHandler()); + server.createContext("/upload", new UploadHandler()); + server.createContext("/static/", new StaticHandler()); + if (pgPool != null) { + server.createContext("/async-db", new AsyncDbHandler()); + server.createContext("/fortunes", new FortunesHandler()); + } + + server.start(); + } + + static void loadDataset() { + String path = System.getenv().getOrDefault("DATASET_PATH", "/data/dataset.json"); + try { + dataset = mapper.readValue(new File(path), new TypeReference<>() {}); + } catch (Exception ignored) {} + } + + static void initPostgres() { + String url = System.getenv("POSTGRES_URL"); + if (url == null || url.isEmpty()) return; + try { + URI uri = new URI(url); + String[] userInfo = uri.getUserInfo().split(":"); + PgConnectOptions connectOptions = new PgConnectOptions() + .setHost(uri.getHost()) + .setPort(uri.getPort()) + .setDatabase(uri.getPath().substring(1)) + .setUser(userInfo[0]) + .setPassword(userInfo[1]); + PoolOptions poolOptions = new PoolOptions().setMaxSize(64); + pgPool = PgBuilder.pool() + .with(poolOptions) + .connectingTo(connectOptions) + .build(); + } catch (Exception ignored) {} + } + + static Map parseQuery(String query) { + Map params = new HashMap<>(); + if (query == null) return params; + for (String pair : query.split("&")) { + int eq = pair.indexOf('='); + if (eq > 0) params.put(pair.substring(0, eq), pair.substring(eq + 1)); + } + return params; + } + + static void sendText(HttpExchange ex, String text) throws IOException { + byte[] bytes = text.getBytes(); + ex.getResponseHeaders().set("Content-Type", "text/plain"); + ex.sendResponseHeaders(200, bytes.length); + ex.getResponseBody().write(bytes); + ex.close(); + } + + static void sendJson(HttpExchange ex, Object obj) throws IOException { + byte[] bytes = mapper.writeValueAsBytes(obj); + String accept = ex.getRequestHeaders().getFirst("Accept-Encoding"); + if (accept != null && accept.contains("gzip")) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (GZIPOutputStream gz = new GZIPOutputStream(baos)) { gz.write(bytes); } + bytes = baos.toByteArray(); + ex.getResponseHeaders().set("Content-Encoding", "gzip"); + } + ex.getResponseHeaders().set("Content-Type", "application/json"); + ex.sendResponseHeaders(200, bytes.length); + ex.getResponseBody().write(bytes); + ex.close(); + } + + // --- Handlers --- + + static class BaselineHandler implements HttpHandler { + public void handle(HttpExchange ex) throws IOException { + Map params = parseQuery(ex.getRequestURI().getQuery()); + int a = Integer.parseInt(params.getOrDefault("a", "0")); + int b = Integer.parseInt(params.getOrDefault("b", "0")); + int sum = a + b; + if ("POST".equals(ex.getRequestMethod())) { + String body = new String(ex.getRequestBody().readAllBytes()); + sum += Integer.parseInt(body.trim()); + } + sendText(ex, String.valueOf(sum)); + } + } + + static class PipelineHandler implements HttpHandler { + public void handle(HttpExchange ex) throws IOException { + sendText(ex, "ok"); + } + } + + static class JsonHandler implements HttpHandler { + public void handle(HttpExchange ex) throws IOException { + String path = ex.getRequestURI().getPath(); + int count = Integer.parseInt(path.substring(path.lastIndexOf('/') + 1)); + Map params = parseQuery(ex.getRequestURI().getQuery()); + int m = Integer.parseInt(params.getOrDefault("m", "1")); + int n = Math.min(Math.max(count, 0), dataset.size()); + List> items = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + Item item = dataset.get(i); + Map map = new LinkedHashMap<>(); + map.put("id", item.id()); + map.put("name", item.name()); + map.put("category", item.category()); + map.put("price", item.price()); + map.put("quantity", item.quantity()); + map.put("active", item.active()); + map.put("tags", item.tags()); + map.put("rating", item.rating()); + map.put("total", (long) item.price() * item.quantity() * m); + items.add(map); + } + sendJson(ex, Map.of("items", items, "count", items.size())); + } + } + + static class UploadHandler implements HttpHandler { + public void handle(HttpExchange ex) throws IOException { + long size = ex.getRequestBody().transferTo(OutputStream.nullOutputStream()); + sendText(ex, String.valueOf(size)); + } + } + + static class StaticHandler implements HttpHandler { + public void handle(HttpExchange ex) throws IOException { + String path = ex.getRequestURI().getPath(); + String file = path.substring("/static/".length()); + Path filePath = Path.of("/data/static", file); + if (!Files.exists(filePath)) { + ex.sendResponseHeaders(404, -1); + ex.close(); + return; + } + byte[] data = Files.readAllBytes(filePath); + String contentType = Files.probeContentType(filePath); + if (contentType == null) contentType = "application/octet-stream"; + ex.getResponseHeaders().set("Content-Type", contentType); + ex.sendResponseHeaders(200, data.length); + ex.getResponseBody().write(data); + ex.close(); + } + } + + static class AsyncDbHandler implements HttpHandler { + public void handle(HttpExchange ex) throws IOException { + Map params = parseQuery(ex.getRequestURI().getQuery()); + int min = Integer.parseInt(params.getOrDefault("min", "10")); + int max = Integer.parseInt(params.getOrDefault("max", "50")); + int limit = Math.min(Math.max(Integer.parseInt(params.getOrDefault("limit", "50")), 1), 50); + + RowSet rows; + try { + rows = pgPool.preparedQuery("SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE price BETWEEN $1 AND $2 LIMIT $3") + .execute(Tuple.of(min, max, limit)) + .toCompletionStage().toCompletableFuture().join(); + } catch (Exception e) { + ex.sendResponseHeaders(500, -1); + ex.close(); + return; + } + + List items = new ArrayList<>(); + for (Row row : rows) { + List tags = mapper.readValue(row.getString("tags"), new TypeReference<>() {}); + items.add(new Item(row.getInteger("id"), row.getString("name"), row.getString("category"), + row.getInteger("price"), row.getInteger("quantity"), row.getBoolean("active"), + tags, new Rating(row.getInteger("rating_score"), row.getInteger("rating_count")))); + } + sendJson(ex, Map.of("items", items, "count", items.size())); + } + } + + static class FortunesHandler implements HttpHandler { + public void handle(HttpExchange ex) throws IOException { + RowSet rows; + try { + rows = pgPool.preparedQuery("SELECT id, message FROM fortune") + .execute() + .toCompletionStage().toCompletableFuture().join(); + } catch (Exception e) { + ex.sendResponseHeaders(500, -1); + ex.close(); + return; + } + + List fortunes = new ArrayList<>(); + for (Row row : rows) { + fortunes.add(new Fortune(row.getInteger("id"), row.getString("message"))); + } + fortunes.add(new Fortune(0, "Additional fortune added at request time.")); + fortunes.sort((a, b) -> a.message().compareTo(b.message())); + + StringWriter writer = new StringWriter(); + fortunesTemplate.evaluate(writer, Map.of("fortunes", fortunes)); + byte[] bytes = writer.toString().getBytes(); + ex.getResponseHeaders().set("Content-Type", "text/html; charset=utf-8"); + ex.sendResponseHeaders(200, bytes.length); + ex.getResponseBody().write(bytes); + ex.close(); + } + } + + record Fortune(int id, String message) {} + record Item(int id, String name, String category, int price, int quantity, boolean active, List tags, Rating rating) {} + record Rating(int score, int count) {} +} diff --git a/frameworks/robaho-httpserver/src/main/resources/fortunes.html b/frameworks/robaho-httpserver/src/main/resources/fortunes.html new file mode 100644 index 00000000..f51cabce --- /dev/null +++ b/frameworks/robaho-httpserver/src/main/resources/fortunes.html @@ -0,0 +1,12 @@ + + +Fortunes + + + +{% for f in fortunes %} + +{% endfor %} +
idmessage
{{ f.id }}{{ f.message }}
+ +