Skip to content

Commit b832612

Browse files
committed
Switch webserver implementation from single-thread nio to blocking IO using a virtual-thread per connection
1 parent f6fa964 commit b832612

14 files changed

+435
-570
lines changed

common/src/main/java/de/bluecolored/bluemap/common/web/BlueMapResponseModifier.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ public HttpResponse handle(HttpRequest request) {
4949
HttpResponse response = delegate.handle(request);
5050

5151
HttpStatusCode status = response.getStatusCode();
52-
if (status.getCode() >= 400 && !response.hasData()){
53-
response.setData(status.getCode() + " - " + status.getMessage() + "\n" + this.serverName);
52+
if (status.getCode() >= 400 && response.getBody() != null){
53+
response.setBody(status.getCode() + " - " + status.getMessage() + "\n" + this.serverName);
5454
}
5555

5656
response.addHeader("Server", this.serverName);

common/src/main/java/de/bluecolored/bluemap/common/web/FileRequestHandler.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ private HttpResponse generateResponse(HttpRequest request) throws IOException {
8585
// redirect to have correct relative paths
8686
if (Files.isDirectory(filePath) && !request.getPath().endsWith("/")) {
8787
HttpResponse response = new HttpResponse(HttpStatusCode.SEE_OTHER);
88-
response.addHeader("Location", "/" + path + "/" + (request.getGETParamString().isEmpty() ? "" : "?" + request.getGETParamString()));
88+
response.addHeader("Location", "/" + path + "/" + (request.getRawQueryString().isEmpty() ? "" : "?" + request.getRawQueryString()));
8989
return response;
9090
}
9191

@@ -151,7 +151,7 @@ private HttpResponse generateResponse(HttpRequest request) throws IOException {
151151

152152
//send response
153153
try {
154-
response.setData(Files.newInputStream(filePath));
154+
response.setBody(Files.newInputStream(filePath));
155155
return response;
156156
} catch (FileNotFoundException e) {
157157
return new HttpResponse(HttpStatusCode.NOT_FOUND);

common/src/main/java/de/bluecolored/bluemap/common/web/JsonDataRequestHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public HttpResponse handle(HttpRequest request) {
4848
HttpResponse response = new HttpResponse(HttpStatusCode.OK);
4949
response.addHeader("Cache-Control", "no-cache");
5050
response.addHeader("Content-Type", "application/json");
51-
response.setData(dataSupplier.get());
51+
response.setBody(dataSupplier.get());
5252
return response;
5353
}
5454

common/src/main/java/de/bluecolored/bluemap/common/web/LoggingRequestHandler.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@
3131
import lombok.NonNull;
3232
import lombok.Setter;
3333

34-
import java.net.URI;
35-
3634
@Getter @Setter
3735
@AllArgsConstructor
3836
public class LoggingRequestHandler implements HttpRequestHandler {
@@ -65,7 +63,9 @@ public HttpResponse handle(HttpRequest request) {
6563
}
6664

6765
String method = request.getMethod();
68-
URI address = request.getAddress();
66+
String path = request.getPath();
67+
String queryString = request.getRawQueryString();
68+
String address = queryString == null ? path : path + "?" + queryString;
6969
String version = request.getVersion();
7070

7171
// run request
@@ -81,7 +81,7 @@ public HttpResponse handle(HttpRequest request) {
8181
source,
8282
xffSource,
8383
method,
84-
address.toString(),
84+
address,
8585
version,
8686
statusCode,
8787
statusMessage

common/src/main/java/de/bluecolored/bluemap/common/web/MapStorageRequestHandler.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ private void writeToResponse(CompressedInputStream data, HttpResponse response,
122122
request.hasHeaderValue("Accept-Encoding", compression.getId())
123123
) {
124124
response.addHeader("Content-Encoding", compression.getId());
125-
response.setData(data);
125+
response.setBody(data);
126126
} else if (
127127
compression != Compression.GZIP &&
128128
!response.hasHeaderValue("Content-Type", "image/png") &&
@@ -134,9 +134,9 @@ private void writeToResponse(CompressedInputStream data, HttpResponse response,
134134
data.decompress().transferTo(os);
135135
}
136136
byte[] compressedData = byteOut.toByteArray();
137-
response.setData(new ByteArrayInputStream(compressedData));
137+
response.setBody(new ByteArrayInputStream(compressedData));
138138
} else {
139-
response.setData(data.decompress());
139+
response.setBody(data.decompress());
140140
}
141141
}
142142

common/src/main/java/de/bluecolored/bluemap/common/web/http/HttpConnection.java

Lines changed: 36 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -25,134 +25,57 @@
2525
package de.bluecolored.bluemap.common.web.http;
2626

2727
import de.bluecolored.bluemap.core.logger.Logger;
28+
import lombok.RequiredArgsConstructor;
2829

30+
import java.io.BufferedInputStream;
31+
import java.io.BufferedOutputStream;
32+
import java.io.EOFException;
2933
import java.io.IOException;
30-
import java.net.InetAddress;
31-
import java.net.InetSocketAddress;
32-
import java.net.SocketAddress;
33-
import java.nio.channels.Channel;
34-
import java.nio.channels.SelectableChannel;
35-
import java.nio.channels.SelectionKey;
36-
import java.nio.channels.SocketChannel;
37-
import java.util.concurrent.CompletableFuture;
38-
import java.util.concurrent.Executor;
34+
import java.net.Socket;
35+
import java.net.SocketTimeoutException;
3936

40-
public class HttpConnection implements SelectionConsumer {
37+
public class HttpConnection implements Runnable {
4138

39+
private final Socket socket;
40+
private final HttpRequestInputStream requestIn;
41+
private final HttpResponseOutputStream responseOut;
4242
private final HttpRequestHandler requestHandler;
43-
private final Executor responseHandlerExecutor;
44-
private HttpRequest request;
45-
private CompletableFuture<HttpResponse> futureResponse;
46-
private HttpResponse response;
4743

48-
public HttpConnection(HttpRequestHandler requestHandler) {
49-
this(requestHandler, Runnable::run); //run synchronously
50-
}
51-
52-
public HttpConnection(HttpRequestHandler requestHandler, Executor responseHandlerExecutor) {
44+
public HttpConnection(Socket socket, HttpRequestHandler requestHandler) throws IOException {
45+
this.socket = socket;
5346
this.requestHandler = requestHandler;
54-
this.responseHandlerExecutor = responseHandlerExecutor;
55-
}
56-
57-
@Override
58-
public void accept(SelectionKey selectionKey) {
59-
if (!selectionKey.isValid()) return;
6047

61-
SelectableChannel selChannel = selectionKey.channel();
62-
63-
if (!(selChannel instanceof SocketChannel)) return;
64-
SocketChannel channel = (SocketChannel) selChannel;
48+
this.requestIn = new HttpRequestInputStream(new BufferedInputStream(socket.getInputStream()), socket.getInetAddress());
49+
this.responseOut = new HttpResponseOutputStream(new BufferedOutputStream(socket.getOutputStream()));
50+
}
6551

52+
public void run() {
6653
try {
54+
while (socket.isConnected() && !socket.isClosed() && !socket.isInputShutdown() && !socket.isOutputShutdown()) {
55+
HttpRequest request = requestIn.read();
56+
if (request == null) continue;
6757

68-
if (request == null) {
69-
SocketAddress remote = channel.getRemoteAddress();
70-
InetAddress remoteInet = null;
71-
if (remote instanceof InetSocketAddress)
72-
remoteInet = ((InetSocketAddress) remote).getAddress();
73-
74-
request = new HttpRequest(remoteInet);
75-
}
76-
77-
// receive request
78-
if (!request.write(channel)) {
79-
if (!selectionKey.isValid()) return;
80-
selectionKey.interestOps(SelectionKey.OP_READ);
81-
return;
82-
}
83-
84-
// process request
85-
if (futureResponse == null) {
86-
futureResponse = CompletableFuture.supplyAsync(
87-
() -> requestHandler.handle(request),
88-
responseHandlerExecutor
89-
);
90-
futureResponse.handle((response, error) -> {
91-
if (error != null) {
92-
Logger.global.logError("Unexpected error handling request", error);
93-
response = new HttpResponse(HttpStatusCode.INTERNAL_SERVER_ERROR);
94-
}
95-
96-
try {
97-
response.read(channel); // do an initial read to trigger response sending intent
98-
this.response = response;
99-
} catch (IOException e) {
100-
handleIOException(channel, e);
101-
}
102-
103-
return null;
104-
});
105-
}
106-
107-
if (response == null) return;
108-
if (!selectionKey.isValid()) return;
109-
110-
// send response
111-
if (!response.read(channel)){
112-
selectionKey.interestOps(SelectionKey.OP_WRITE);
113-
return;
58+
try (HttpResponse response = requestHandler.handle(request)) {
59+
responseOut.write(response);
60+
}
11461
}
115-
116-
// reset to accept new request
117-
request.clear();
118-
response.close();
119-
futureResponse = null;
120-
response = null;
121-
selectionKey.interestOps(SelectionKey.OP_READ);
122-
62+
} catch (EOFException | SocketTimeoutException ignore) {
63+
// ignore known exceptions that happen when browsers or us close the connection
12364
} catch (IOException e) {
124-
handleIOException(channel, e);
125-
}
126-
}
127-
128-
private void handleIOException(Channel channel, IOException e) {
129-
request.clear();
130-
131-
if (response != null) {
65+
if ( // ignore known exceptions that happen when browsers close the connection
66+
e.getMessage() == null ||
67+
!e.getMessage().equals("Broken pipe")
68+
) {
69+
Logger.global.logDebug("Exception in HttpConnection: " + e);
70+
}
71+
} catch (Exception e) {
72+
Logger.global.logDebug("Exception in HttpConnection: " + e);
73+
} finally {
13274
try {
133-
response.close();
134-
} catch (IOException e2) {
135-
Logger.global.logWarning("Failed to close response: " + e2);
75+
socket.close();
76+
} catch (IOException e) {
77+
Logger.global.logDebug("Exception closing HttpConnection: " + e);
13678
}
137-
response = null;
138-
}
139-
140-
if (futureResponse != null) {
141-
futureResponse.thenAccept(response -> {
142-
try {
143-
response.close();
144-
} catch (IOException e2) {
145-
Logger.global.logWarning("Failed to close response: " + e2);
146-
}
147-
});
148-
futureResponse = null;
149-
}
150-
151-
Logger.global.logDebug("Failed to process selection: " + e);
152-
try {
153-
channel.close();
154-
} catch (IOException e2) {
155-
Logger.global.logWarning("Failed to close channel" + e2);
15679
}
15780
}
15881

common/src/main/java/de/bluecolored/bluemap/common/web/http/HttpHeader.java

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,40 +24,52 @@
2424
*/
2525
package de.bluecolored.bluemap.common.web.http;
2626

27+
import lombok.Getter;
28+
2729
import java.util.*;
2830

2931
public class HttpHeader {
3032

31-
private final String key;
32-
private final String value;
33+
@Getter private final String key;
34+
@Getter private String value;
3335
private List<String> values;
3436
private Set<String> valuesLC;
3537

36-
public HttpHeader(String key, String value) {
38+
public HttpHeader(String key, String... values) {
3739
this.key = key;
38-
this.value = value;
40+
this.value = String.join(",", values);
3941
}
4042

41-
public String getKey() {
42-
return key;
43+
public synchronized void add(String... values) {
44+
if (value.isEmpty()) {
45+
set(values);
46+
return;
47+
}
48+
49+
this.value = value + "," + String.join(",", values);
50+
this.values = null;
51+
this.valuesLC = null;
4352
}
4453

45-
public String getValue() {
46-
return value;
54+
public synchronized void set(String... values) {
55+
this.value = String.join(",", values);
56+
this.values = null;
57+
this.valuesLC = null;
4758
}
4859

49-
public List<String> getValues() {
60+
public synchronized List<String> getValues() {
5061
if (values == null) {
51-
values = new ArrayList<>();
62+
List<String> vs = new ArrayList<>();
5263
for (String v : value.split(",")) {
53-
values.add(v.trim());
64+
vs.add(v.trim());
5465
}
66+
values = Collections.unmodifiableList(vs);
5567
}
5668

5769
return values;
5870
}
5971

60-
public boolean contains(String value) {
72+
public synchronized boolean contains(String value) {
6173
if (valuesLC == null) {
6274
valuesLC = new HashSet<>();
6375
for (String v : getValues()) {
@@ -68,4 +80,21 @@ public boolean contains(String value) {
6880
return valuesLC.contains(value);
6981
}
7082

83+
@Override
84+
public boolean equals(Object o) {
85+
if (o == null || getClass() != o.getClass()) return false;
86+
HttpHeader that = (HttpHeader) o;
87+
return Objects.equals(key, that.key) && Objects.equals(value, that.value);
88+
}
89+
90+
@Override
91+
public int hashCode() {
92+
return Objects.hash(key, value);
93+
}
94+
95+
@Override
96+
public String toString() {
97+
return key + ": " + value;
98+
}
99+
71100
}

common/src/main/java/de/bluecolored/bluemap/common/web/http/SelectionConsumer.java renamed to common/src/main/java/de/bluecolored/bluemap/common/web/http/HttpHeaderCarrier.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,25 @@
2424
*/
2525
package de.bluecolored.bluemap.common.web.http;
2626

27-
import java.nio.channels.SelectionKey;
28-
import java.util.function.Consumer;
27+
import java.util.Locale;
28+
import java.util.Map;
2929

30-
public interface SelectionConsumer extends Consumer<SelectionKey> {}
30+
public interface HttpHeaderCarrier {
31+
32+
Map<String, HttpHeader> getHeaders();
33+
34+
default void addHeader(String name, String... values) {
35+
getHeaders().put(name.toLowerCase(Locale.ROOT), new HttpHeader(name, values));
36+
}
37+
38+
default HttpHeader getHeader(String key) {
39+
return getHeaders().get(key.toLowerCase(Locale.ROOT));
40+
}
41+
42+
default boolean hasHeaderValue(String key, String value) {
43+
HttpHeader header = getHeader(key);
44+
if (header == null) return false;
45+
return header.contains(value);
46+
}
47+
48+
}

0 commit comments

Comments
 (0)