Skip to content

Commit fc4a7d3

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

5 files changed

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