Skip to content

Commit c161e4f

Browse files
rurorujj
authored andcommitted
add robaho httpserver
1 parent 386f918 commit c161e4f

5 files changed

Lines changed: 371 additions & 0 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
FROM maven:3.9-eclipse-temurin-21 AS build
2+
WORKDIR /app
3+
COPY pom.xml .
4+
RUN mvn dependency:go-offline -q
5+
COPY src ./src
6+
RUN mvn package -DskipTests -q
7+
8+
FROM eclipse-temurin:21-jre
9+
WORKDIR /app
10+
COPY --from=build /app/target/robaho-httpserver-1.0.0.jar app.jar
11+
COPY --from=build /app/target/libs ./libs
12+
EXPOSE 8080
13+
ENTRYPOINT ["java", "-jar", "app.jar"]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"display_name": "robaho-httpserver",
3+
"language": "Java",
4+
"type": "production",
5+
"engine": "robaho-httpserver",
6+
"description": "High-performance Java HTTP server using robaho/httpserver, a drop-in replacement for com.sun.net.httpserver with improved throughput.",
7+
"repo": "https://github.com/robaho/httpserver",
8+
"enabled": true,
9+
"tests": [
10+
"baseline",
11+
"pipelined",
12+
"limited-conn",
13+
"json",
14+
"json-comp",
15+
"upload",
16+
"static",
17+
"async-db",
18+
"fortunes",
19+
"api-4",
20+
"api-16"
21+
],
22+
"maintainers": []
23+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<groupId>com.httparena</groupId>
8+
<artifactId>robaho-httpserver</artifactId>
9+
<version>1.0.0</version>
10+
<packaging>jar</packaging>
11+
12+
<properties>
13+
<java.version>21</java.version>
14+
<maven.compiler.source>${java.version}</maven.compiler.source>
15+
<maven.compiler.target>${java.version}</maven.compiler.target>
16+
</properties>
17+
18+
<dependencies>
19+
<dependency>
20+
<groupId>io.github.robaho</groupId>
21+
<artifactId>httpserver</artifactId>
22+
<version>1.0.25</version>
23+
</dependency>
24+
<dependency>
25+
<groupId>com.fasterxml.jackson.core</groupId>
26+
<artifactId>jackson-databind</artifactId>
27+
<version>2.19.0</version>
28+
</dependency>
29+
<dependency>
30+
<groupId>io.pebbletemplates</groupId>
31+
<artifactId>pebble</artifactId>
32+
<version>3.2.4</version>
33+
</dependency>
34+
<dependency>
35+
<groupId>io.vertx</groupId>
36+
<artifactId>vertx-pg-client</artifactId>
37+
<version>5.0.0.CR7</version>
38+
</dependency>
39+
</dependencies>
40+
41+
<build>
42+
<plugins>
43+
<plugin>
44+
<groupId>org.apache.maven.plugins</groupId>
45+
<artifactId>maven-jar-plugin</artifactId>
46+
<version>3.4.2</version>
47+
<configuration>
48+
<archive>
49+
<manifest>
50+
<mainClass>com.httparena.Main</mainClass>
51+
<addClasspath>true</addClasspath>
52+
<classpathPrefix>libs/</classpathPrefix>
53+
</manifest>
54+
</archive>
55+
</configuration>
56+
</plugin>
57+
<plugin>
58+
<groupId>org.apache.maven.plugins</groupId>
59+
<artifactId>maven-dependency-plugin</artifactId>
60+
<version>3.7.1</version>
61+
<executions>
62+
<execution>
63+
<id>copy-dependencies</id>
64+
<phase>package</phase>
65+
<goals><goal>copy-dependencies</goal></goals>
66+
<configuration>
67+
<outputDirectory>${project.build.directory}/libs</outputDirectory>
68+
</configuration>
69+
</execution>
70+
</executions>
71+
</plugin>
72+
</plugins>
73+
</build>
74+
</project>
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
package com.httparena;
2+
3+
import com.fasterxml.jackson.core.type.TypeReference;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import io.pebbletemplates.pebble.PebbleEngine;
6+
import io.pebbletemplates.pebble.template.PebbleTemplate;
7+
import io.vertx.pgclient.PgBuilder;
8+
import io.vertx.pgclient.PgConnectOptions;
9+
import io.vertx.sqlclient.*;
10+
import robaho.net.httpserver.HttpServer;
11+
import com.sun.net.httpserver.HttpExchange;
12+
import com.sun.net.httpserver.HttpHandler;
13+
14+
import java.io.*;
15+
import java.net.InetSocketAddress;
16+
import java.net.URI;
17+
import java.nio.file.Files;
18+
import java.nio.file.Path;
19+
import java.util.*;
20+
import java.util.concurrent.Executors;
21+
import java.util.zip.GZIPOutputStream;
22+
23+
public class Main {
24+
25+
static final ObjectMapper mapper = new ObjectMapper();
26+
static final PebbleEngine pebble = new PebbleEngine.Builder().autoEscaping(true).build();
27+
static final PebbleTemplate fortunesTemplate = pebble.getTemplate("fortunes.html");
28+
static List<Item> dataset = List.of();
29+
static Pool pgPool;
30+
31+
public static void main(String[] args) throws Exception {
32+
loadDataset();
33+
initPostgres();
34+
35+
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
36+
server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
37+
38+
server.createContext("/baseline11", new BaselineHandler());
39+
server.createContext("/baseline2", new BaselineHandler());
40+
server.createContext("/pipeline", new PipelineHandler());
41+
server.createContext("/json/", new JsonHandler());
42+
server.createContext("/upload", new UploadHandler());
43+
server.createContext("/static/", new StaticHandler());
44+
if (pgPool != null) {
45+
server.createContext("/async-db", new AsyncDbHandler());
46+
server.createContext("/fortunes", new FortunesHandler());
47+
}
48+
49+
server.start();
50+
}
51+
52+
static void loadDataset() {
53+
String path = System.getenv().getOrDefault("DATASET_PATH", "/data/dataset.json");
54+
try {
55+
dataset = mapper.readValue(new File(path), new TypeReference<>() {});
56+
} catch (Exception ignored) {}
57+
}
58+
59+
static void initPostgres() {
60+
String url = System.getenv("DATABASE_URL");
61+
if (url == null || url.isEmpty()) return;
62+
try {
63+
URI uri = new URI(url);
64+
String[] userInfo = uri.getUserInfo().split(":");
65+
PgConnectOptions connectOptions = new PgConnectOptions()
66+
.setHost(uri.getHost())
67+
.setPort(uri.getPort())
68+
.setDatabase(uri.getPath().substring(1))
69+
.setUser(userInfo[0])
70+
.setPassword(userInfo[1]);
71+
PoolOptions poolOptions = new PoolOptions().setMaxSize(64);
72+
pgPool = PgBuilder.pool()
73+
.with(poolOptions)
74+
.connectingTo(connectOptions)
75+
.build();
76+
} catch (Exception ignored) {}
77+
}
78+
79+
static Map<String, String> parseQuery(String query) {
80+
Map<String, String> params = new HashMap<>();
81+
if (query == null) return params;
82+
for (String pair : query.split("&")) {
83+
int eq = pair.indexOf('=');
84+
if (eq > 0) params.put(pair.substring(0, eq), pair.substring(eq + 1));
85+
}
86+
return params;
87+
}
88+
89+
static void sendText(HttpExchange ex, String text) throws IOException {
90+
byte[] bytes = text.getBytes();
91+
ex.getResponseHeaders().set("Content-Type", "text/plain");
92+
ex.sendResponseHeaders(200, bytes.length);
93+
ex.getResponseBody().write(bytes);
94+
ex.close();
95+
}
96+
97+
static void sendJson(HttpExchange ex, Object obj) throws IOException {
98+
byte[] bytes = mapper.writeValueAsBytes(obj);
99+
String accept = ex.getRequestHeaders().getFirst("Accept-Encoding");
100+
if (accept != null && accept.contains("gzip")) {
101+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
102+
try (GZIPOutputStream gz = new GZIPOutputStream(baos)) { gz.write(bytes); }
103+
bytes = baos.toByteArray();
104+
ex.getResponseHeaders().set("Content-Encoding", "gzip");
105+
}
106+
ex.getResponseHeaders().set("Content-Type", "application/json");
107+
ex.sendResponseHeaders(200, bytes.length);
108+
ex.getResponseBody().write(bytes);
109+
ex.close();
110+
}
111+
112+
// --- Handlers ---
113+
114+
static class BaselineHandler implements HttpHandler {
115+
public void handle(HttpExchange ex) throws IOException {
116+
Map<String, String> params = parseQuery(ex.getRequestURI().getQuery());
117+
int a = Integer.parseInt(params.getOrDefault("a", "0"));
118+
int b = Integer.parseInt(params.getOrDefault("b", "0"));
119+
int sum = a + b;
120+
if ("POST".equals(ex.getRequestMethod())) {
121+
String body = new String(ex.getRequestBody().readAllBytes());
122+
sum += Integer.parseInt(body.trim());
123+
}
124+
sendText(ex, String.valueOf(sum));
125+
}
126+
}
127+
128+
static class PipelineHandler implements HttpHandler {
129+
public void handle(HttpExchange ex) throws IOException {
130+
sendText(ex, "ok");
131+
}
132+
}
133+
134+
static class JsonHandler implements HttpHandler {
135+
public void handle(HttpExchange ex) throws IOException {
136+
String path = ex.getRequestURI().getPath();
137+
int count = Integer.parseInt(path.substring(path.lastIndexOf('/') + 1));
138+
Map<String, String> params = parseQuery(ex.getRequestURI().getQuery());
139+
int m = Integer.parseInt(params.getOrDefault("m", "1"));
140+
int n = Math.min(Math.max(count, 0), dataset.size());
141+
List<Map<String, Object>> items = new ArrayList<>(n);
142+
for (int i = 0; i < n; i++) {
143+
Item item = dataset.get(i);
144+
Map<String, Object> map = new LinkedHashMap<>();
145+
map.put("id", item.id());
146+
map.put("name", item.name());
147+
map.put("category", item.category());
148+
map.put("price", item.price());
149+
map.put("quantity", item.quantity());
150+
map.put("active", item.active());
151+
map.put("tags", item.tags());
152+
map.put("rating", item.rating());
153+
map.put("total", (long) item.price() * item.quantity() * m);
154+
items.add(map);
155+
}
156+
sendJson(ex, Map.of("items", items, "count", items.size()));
157+
}
158+
}
159+
160+
static class UploadHandler implements HttpHandler {
161+
public void handle(HttpExchange ex) throws IOException {
162+
long size = ex.getRequestBody().transferTo(OutputStream.nullOutputStream());
163+
sendText(ex, String.valueOf(size));
164+
}
165+
}
166+
167+
static class StaticHandler implements HttpHandler {
168+
public void handle(HttpExchange ex) throws IOException {
169+
String path = ex.getRequestURI().getPath();
170+
String file = path.substring("/static/".length());
171+
Path filePath = Path.of("/data/static", file);
172+
if (!Files.exists(filePath)) {
173+
ex.sendResponseHeaders(404, -1);
174+
ex.close();
175+
return;
176+
}
177+
byte[] data = Files.readAllBytes(filePath);
178+
String contentType = Files.probeContentType(filePath);
179+
if (contentType == null) contentType = "application/octet-stream";
180+
ex.getResponseHeaders().set("Content-Type", contentType);
181+
ex.sendResponseHeaders(200, data.length);
182+
ex.getResponseBody().write(data);
183+
ex.close();
184+
}
185+
}
186+
187+
static class AsyncDbHandler implements HttpHandler {
188+
public void handle(HttpExchange ex) throws IOException {
189+
Map<String, String> params = parseQuery(ex.getRequestURI().getQuery());
190+
int min = Integer.parseInt(params.getOrDefault("min", "10"));
191+
int max = Integer.parseInt(params.getOrDefault("max", "50"));
192+
int limit = Math.min(Math.max(Integer.parseInt(params.getOrDefault("limit", "50")), 1), 50);
193+
194+
RowSet<Row> rows;
195+
try {
196+
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")
197+
.execute(Tuple.of(min, max, limit))
198+
.toCompletionStage().toCompletableFuture().join();
199+
} catch (Exception e) {
200+
ex.sendResponseHeaders(500, -1);
201+
ex.close();
202+
return;
203+
}
204+
205+
List<Item> items = new ArrayList<>();
206+
for (Row row : rows) {
207+
List<String> tags = mapper.readValue(row.getString("tags"), new TypeReference<>() {});
208+
items.add(new Item(row.getInteger("id"), row.getString("name"), row.getString("category"),
209+
row.getInteger("price"), row.getInteger("quantity"), row.getBoolean("active"),
210+
tags, new Rating(row.getInteger("rating_score"), row.getInteger("rating_count"))));
211+
}
212+
sendJson(ex, Map.of("items", items, "count", items.size()));
213+
}
214+
}
215+
216+
static class FortunesHandler implements HttpHandler {
217+
public void handle(HttpExchange ex) throws IOException {
218+
RowSet<Row> rows;
219+
try {
220+
rows = pgPool.preparedQuery("SELECT id, message FROM fortune")
221+
.execute()
222+
.toCompletionStage().toCompletableFuture().join();
223+
} catch (Exception e) {
224+
ex.sendResponseHeaders(500, -1);
225+
ex.close();
226+
return;
227+
}
228+
229+
List<Fortune> fortunes = new ArrayList<>();
230+
for (Row row : rows) {
231+
fortunes.add(new Fortune(row.getInteger("id"), row.getString("message")));
232+
}
233+
fortunes.add(new Fortune(0, "Additional fortune added at request time."));
234+
fortunes.sort((a, b) -> a.message().compareTo(b.message()));
235+
236+
StringWriter writer = new StringWriter();
237+
fortunesTemplate.evaluate(writer, Map.of("fortunes", fortunes));
238+
byte[] bytes = writer.toString().getBytes();
239+
ex.getResponseHeaders().set("Content-Type", "text/html; charset=utf-8");
240+
ex.sendResponseHeaders(200, bytes.length);
241+
ex.getResponseBody().write(bytes);
242+
ex.close();
243+
}
244+
}
245+
246+
record Fortune(int id, String message) {}
247+
record Item(int id, String name, String category, int price, int quantity, boolean active, List<String> tags, Rating rating) {}
248+
record Rating(int score, int count) {}
249+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head><title>Fortunes</title></head>
4+
<body>
5+
<table>
6+
<tr><th>id</th><th>message</th></tr>
7+
{% for f in fortunes %}
8+
<tr><td>{{ f.id }}</td><td>{{ f.message }}</td></tr>
9+
{% endfor %}
10+
</table>
11+
</body>
12+
</html>

0 commit comments

Comments
 (0)