Skip to content

Commit 1a3744e

Browse files
committed
Crude WebSocket implementation
1 parent 6fc7f51 commit 1a3744e

17 files changed

+558
-92
lines changed

Diff for: src/main/java/dev/latvian/apps/tinyserver/HTTPServer.java

+60-61
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,18 @@
55
import dev.latvian.apps.tinyserver.http.HTTPMethod;
66
import dev.latvian.apps.tinyserver.http.HTTPPathHandler;
77
import dev.latvian.apps.tinyserver.http.HTTPRequest;
8-
import dev.latvian.apps.tinyserver.http.response.HTTPResponse;
98
import dev.latvian.apps.tinyserver.http.response.HTTPResponseBuilder;
109
import dev.latvian.apps.tinyserver.http.response.HTTPStatus;
10+
import dev.latvian.apps.tinyserver.ws.WSEndpointHandler;
1111
import dev.latvian.apps.tinyserver.ws.WSHandler;
1212
import dev.latvian.apps.tinyserver.ws.WSSession;
1313
import dev.latvian.apps.tinyserver.ws.WSSessionFactory;
1414
import org.jetbrains.annotations.Nullable;
1515

16+
import java.io.BufferedInputStream;
1617
import java.io.BufferedOutputStream;
17-
import java.io.BufferedReader;
1818
import java.io.IOException;
1919
import java.io.InputStream;
20-
import java.io.InputStreamReader;
2120
import java.io.OutputStream;
2221
import java.net.InetAddress;
2322
import java.net.ServerSocket;
@@ -30,7 +29,7 @@
3029
import java.util.HashMap;
3130
import java.util.HashSet;
3231
import java.util.Map;
33-
import java.util.UUID;
32+
import java.util.concurrent.ConcurrentHashMap;
3433
import java.util.function.Supplier;
3534
import java.util.stream.Collectors;
3635

@@ -44,6 +43,7 @@ public class HTTPServer<REQ extends HTTPRequest> implements Runnable, ServerRegi
4443
private int port = 8080;
4544
private int maxPortShift = 0;
4645
private boolean daemon = false;
46+
private int bufferSize = 8192;
4747

4848
public HTTPServer(Supplier<REQ> requestFactory) {
4949
this.requestFactory = requestFactory;
@@ -71,6 +71,10 @@ public void setDaemon(boolean daemon) {
7171
this.daemon = daemon;
7272
}
7373

74+
public void setBufferSize(int bufferSize) {
75+
this.bufferSize = bufferSize;
76+
}
77+
7478
public int start() {
7579
if (serverSocket != null) {
7680
throw new IllegalStateException("Server is already running");
@@ -123,21 +127,9 @@ public void http(HTTPMethod method, String path, HTTPHandler<REQ> handler) {
123127
}
124128
}
125129

126-
private record WSEndpointHandler<REQ extends HTTPRequest, WSS extends WSSession<REQ>>(WSSessionFactory<REQ, WSS> factory) implements WSHandler<REQ, WSS>, HTTPHandler<REQ> {
127-
@Override
128-
public Map<UUID, WSS> sessions() {
129-
return Map.of();
130-
}
131-
132-
@Override
133-
public HTTPResponse handle(REQ req) {
134-
return HTTPStatus.NOT_IMPLEMENTED;
135-
}
136-
}
137-
138130
@Override
139131
public <WSS extends WSSession<REQ>> WSHandler<REQ, WSS> ws(String path, WSSessionFactory<REQ, WSS> factory) {
140-
var handler = new WSEndpointHandler<>(factory);
132+
var handler = new WSEndpointHandler<>(factory, new ConcurrentHashMap<>(), daemon);
141133
get(path, handler);
142134
return handler;
143135
}
@@ -153,16 +145,33 @@ public void run() {
153145
}
154146
}
155147

148+
private String readLine(InputStream in) throws IOException {
149+
var sb = new StringBuilder();
150+
int b;
151+
152+
while ((b = in.read()) != -1) {
153+
if (b == '\n') {
154+
break;
155+
}
156+
157+
if (b != '\r') {
158+
sb.append((char) b);
159+
}
160+
}
161+
162+
return sb.toString();
163+
}
164+
156165
private void handleClient(Socket socket) {
157166
InputStream in = null;
158167
OutputStream out = null;
168+
WSSession<REQ> upgradedToWebSocket = null;
159169

160170
try {
161-
in = socket.getInputStream();
162-
var reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
163-
var firstLineStr = reader.readLine();
171+
in = new BufferedInputStream(socket.getInputStream(), bufferSize);
172+
var firstLineStr = readLine(in);
164173

165-
if (firstLineStr == null || !firstLineStr.toLowerCase().endsWith(" http/1.1")) {
174+
if (!firstLineStr.toLowerCase().endsWith(" http/1.1")) {
166175
return;
167176
}
168177

@@ -212,9 +221,9 @@ private void handleClient(Socket socket) {
212221
var headers = new HashMap<String, String>();
213222

214223
while (true) {
215-
var line = reader.readLine();
224+
var line = readLine(in);
216225

217-
if (line == null || line.isBlank()) {
226+
if (line.isBlank()) {
218227
break;
219228
}
220229

@@ -262,7 +271,7 @@ private void handleClient(Socket socket) {
262271
var builder = createBuilder(req, null);
263272
builder.setStatus(HTTPStatus.NO_CONTENT);
264273
builder.setHeader("Allow", allowed.stream().map(HTTPMethod::name).collect(Collectors.joining(",")));
265-
out = new BufferedOutputStream(socket.getOutputStream());
274+
out = new BufferedOutputStream(socket.getOutputStream(), bufferSize);
266275
builder.write(out, writeBody);
267276
out.flush();
268277
} else if (method == HTTPMethod.TRACE) {
@@ -276,7 +285,7 @@ private void handleClient(Socket socket) {
276285
var handler = rootHandlers.get(method);
277286

278287
if (handler != null) {
279-
req.init(new String[0], CompiledPath.EMPTY, headers, query, in);
288+
req.init(this, new String[0], CompiledPath.EMPTY, headers, query, in);
280289
builder = createBuilder(req, handler.handler());
281290
}
282291
} else {
@@ -287,14 +296,14 @@ private void handleClient(Socket socket) {
287296
var h = hl.staticHandlers().get(path);
288297

289298
if (h != null) {
290-
req.init(pathParts, h.path(), headers, query, in);
299+
req.init(this, pathParts, h.path(), headers, query, in);
291300
builder = createBuilder(req, h.handler());
292301
} else {
293302
for (var dynamicHandler : hl.dynamicHandlers()) {
294303
var matches = dynamicHandler.path().matches(pathParts);
295304

296305
if (matches != null) {
297-
req.init(matches, dynamicHandler.path(), headers, query, in);
306+
req.init(this, matches, dynamicHandler.path(), headers, query, in);
298307
builder = createBuilder(req, dynamicHandler.handler());
299308
break;
300309
}
@@ -308,53 +317,43 @@ private void handleClient(Socket socket) {
308317
builder.setStatus(HTTPStatus.NOT_FOUND);
309318
}
310319

311-
System.out.println("Request: " + method.name() + " /" + path);
312-
System.out.println("- Query:");
313-
314-
for (var e : query.entrySet()) {
315-
System.out.println(" " + e.getKey() + ": " + e.getValue());
316-
}
317-
318-
System.out.println("- Variables:");
319-
320-
for (var e : req.variables().entrySet()) {
321-
System.out.println(" " + e.getKey() + ": " + e.getValue());
322-
}
320+
out = new BufferedOutputStream(socket.getOutputStream(), bufferSize);
321+
builder.write(out, writeBody);
322+
out.flush();
323323

324-
System.out.println("- Headers:");
324+
upgradedToWebSocket = (WSSession) builder.wsSession();
325325

326-
for (var e : headers.entrySet()) {
327-
System.out.println(" " + e.getKey() + ": " + e.getValue());
326+
if (upgradedToWebSocket != null) {
327+
upgradedToWebSocket.start(socket, in, out);
328+
upgradedToWebSocket.onOpen(req);
328329
}
329-
330-
out = new BufferedOutputStream(socket.getOutputStream());
331-
builder.write(out, writeBody);
332-
out.flush();
333330
}
334331
}
335332
} catch (Exception ex) {
336333
ex.printStackTrace();
337334
}
338335

339-
try {
340-
if (in != null) {
341-
in.close();
336+
if (upgradedToWebSocket == null) {
337+
try {
338+
if (in != null) {
339+
in.close();
340+
}
341+
} catch (Exception ignored) {
342342
}
343-
} catch (Exception ignored) {
344-
}
345343

346-
try {
347-
if (out != null) {
348-
out.close();
344+
try {
345+
if (out != null) {
346+
out.close();
347+
}
348+
} catch (Exception ignored) {
349349
}
350-
} catch (Exception ignored) {
351-
}
352350

353-
try {
354-
if (socket != null) {
355-
socket.close();
351+
try {
352+
if (socket != null) {
353+
socket.close();
354+
}
355+
} catch (Exception ignored) {
356356
}
357-
} catch (Exception ignored) {
358357
}
359358
}
360359

@@ -369,7 +368,7 @@ public HTTPResponseBuilder createBuilder(REQ req, @Nullable HTTPHandler<REQ> han
369368

370369
if (handler != null) {
371370
try {
372-
handler.handle(req).build(builder);
371+
builder.setResponse(handler.handle(req));
373372
} catch (Exception ex) {
374373
builder.setStatus(HTTPStatus.INTERNAL_ERROR);
375374
handlePayloadError(builder, ex);
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
package dev.latvian.apps.tinyserver;
22

33
public record StatusCode(int code, String message) {
4+
@Override
5+
public String toString() {
6+
return code + " " + message;
7+
}
48
}

Diff for: src/main/java/dev/latvian/apps/tinyserver/http/HTTPRequest.java

+21-2
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
package dev.latvian.apps.tinyserver.http;
22

33
import dev.latvian.apps.tinyserver.CompiledPath;
4+
import dev.latvian.apps.tinyserver.HTTPServer;
45

56
import java.io.IOException;
67
import java.io.InputStream;
78
import java.nio.charset.StandardCharsets;
9+
import java.util.Collections;
810
import java.util.HashMap;
911
import java.util.Map;
1012

1113
public class HTTPRequest {
14+
private HTTPServer<?> server;
1215
private String[] path = new String[0];
1316
private Map<String, String> variables = Map.of();
1417
private Map<String, String> query = Map.of();
1518
private Map<String, String> headers = Map.of();
1619
private InputStream bodyStream = null;
1720

18-
public void init(String[] path, CompiledPath compiledPath, Map<String, String> headers, Map<String, String> query, InputStream bodyStream) {
21+
public void init(HTTPServer<?> server, String[] path, CompiledPath compiledPath, Map<String, String> headers, Map<String, String> query, InputStream bodyStream) {
22+
this.server = server;
1923
this.path = path;
2024

2125
if (compiledPath.variables() > 0) {
@@ -35,6 +39,10 @@ public void init(String[] path, CompiledPath compiledPath, Map<String, String> h
3539
this.bodyStream = bodyStream;
3640
}
3741

42+
public HTTPServer<?> server() {
43+
return server;
44+
}
45+
3846
public Map<String, String> variables() {
3947
return variables;
4048
}
@@ -43,6 +51,10 @@ public Map<String, String> query() {
4351
return query;
4452
}
4553

54+
public Map<String, String> headers() {
55+
return Collections.unmodifiableMap(headers);
56+
}
57+
4658
public String header(String name) {
4759
return headers.getOrDefault(name.toLowerCase(), "");
4860
}
@@ -60,7 +72,14 @@ public InputStream bodyStream() {
6072
}
6173

6274
public byte[] bodyBytes() throws IOException {
63-
return bodyStream().readAllBytes();
75+
var h = header("content-length");
76+
77+
if (h.isEmpty()) {
78+
return bodyStream().readAllBytes();
79+
}
80+
81+
int len = Integer.parseInt(h);
82+
return bodyStream().readNBytes(len);
6483
}
6584

6685
public String body() throws IOException {

Diff for: src/main/java/dev/latvian/apps/tinyserver/http/response/HTTPResponseBuilder.java

+17
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package dev.latvian.apps.tinyserver.http.response;
22

33
import dev.latvian.apps.tinyserver.content.ResponseContent;
4+
import dev.latvian.apps.tinyserver.ws.WSResponse;
5+
import dev.latvian.apps.tinyserver.ws.WSSession;
6+
import org.jetbrains.annotations.Nullable;
47

58
import java.io.OutputStream;
69
import java.nio.charset.StandardCharsets;
@@ -17,6 +20,7 @@ public class HTTPResponseBuilder {
1720
private HTTPStatus status = HTTPStatus.NO_CONTENT;
1821
private final Map<String, String> headers = new HashMap<>();
1922
private ResponseContent body = null;
23+
private WSSession<?> wsSession = null;
2024

2125
public void setStatus(HTTPStatus status) {
2226
this.status = status;
@@ -60,4 +64,17 @@ public void write(OutputStream out, boolean writeBody) throws Exception {
6064
body.write(out);
6165
}
6266
}
67+
68+
public void setResponse(HTTPResponse response) throws Exception {
69+
response.build(this);
70+
71+
if (response instanceof WSResponse res) {
72+
wsSession = res.session();
73+
}
74+
}
75+
76+
@Nullable
77+
public WSSession<?> wsSession() {
78+
return wsSession;
79+
}
6380
}

Diff for: src/main/java/dev/latvian/apps/tinyserver/ws/EmptyWSHandler.java

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ public Map<UUID, WSSession<HTTPRequest>> sessions() {
1414
return Map.of();
1515
}
1616

17+
@Override
18+
public void broadcast(Frame frame) {
19+
}
20+
1721
@Override
1822
public void broadcastText(String payload) {
1923
}

0 commit comments

Comments
 (0)