diff --git a/server/pom.xml b/server/pom.xml
index 085e189..dac8242 100644
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -18,23 +18,21 @@
+
+ org.springframework.boot
+ spring-boot-starter-web
+ 2.6.6
+
nl.andrewl.starship-arena
core
${project.parent.version}
-
- org.eclipse.jetty
- jetty-servlet
- 11.0.8
-
-
-
- org.slf4j
- slf4j-jdk14
- 2.0.0-alpha7
- runtime
+ org.projectlombok
+ lombok
+ 1.18.22
+ provided
\ No newline at end of file
diff --git a/server/src/main/java/nl/andrewl/starship_arena/server/SocketGateway.java b/server/src/main/java/nl/andrewl/starship_arena/server/SocketGateway.java
new file mode 100644
index 0000000..fcd5ccd
--- /dev/null
+++ b/server/src/main/java/nl/andrewl/starship_arena/server/SocketGateway.java
@@ -0,0 +1,82 @@
+package nl.andrewl.starship_arena.server;
+
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import nl.andrewl.starship_arena.server.data.ArenaStore;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.context.event.ApplicationReadyEvent;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Service;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.net.DatagramSocket;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.UUID;
+
+/**
+ * The socket gateway is the central point at which all clients connect for any
+ * arena. Client connections are then handed off to the associated arena for
+ * management.
+ */
+@Service
+@Slf4j
+public class SocketGateway implements Runnable {
+ private final ServerSocket serverSocket;
+ private final DatagramSocket serverUdpSocket;
+ private final ArenaStore arenaStore;
+
+ @Value("${starship-arena.gateway.host}") @Getter
+ private String host;
+ @Value("${starship-arena.gateway.tcp-port}") @Getter
+ private short tcpPort;
+ @Value("${starship-arena.gateway.udp-port}") @Getter
+ private short udpPort;
+
+ public SocketGateway(ArenaStore arenaStore) throws IOException {
+ this.serverSocket = new ServerSocket();
+ this.serverUdpSocket = new DatagramSocket(null);
+ this.arenaStore = arenaStore;
+ serverSocket.setReuseAddress(true);
+ serverUdpSocket.setReuseAddress(true);
+ }
+
+ @EventListener(ApplicationReadyEvent.class)
+ public void startGateway() {
+ new Thread(this).start();
+ }
+
+ @Override
+ public void run() {
+ try {
+ serverSocket.bind(new InetSocketAddress(host, tcpPort));
+ log.info("Socket Gateway bound TCP on {}:{}", host, tcpPort);
+ serverUdpSocket.bind(new InetSocketAddress(host, udpPort));
+ log.info("Socket Gateway bound UDP on {}:{}", host, udpPort);
+ } catch (IOException e) {
+ e.printStackTrace();
+ return;
+ }
+ while (!serverSocket.isClosed()) {
+ try {
+ Socket clientSocket = serverSocket.accept();
+ processIncomingConnection(clientSocket);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private void processIncomingConnection(Socket clientSocket) throws IOException {
+ var din = new DataInputStream(clientSocket.getInputStream());
+ UUID arenaId = new UUID(din.readLong(), din.readLong());
+ var oa = arenaStore.getById(arenaId);
+ if (oa.isPresent()) {
+ new ClientManager(oa.get(), clientSocket).start();
+ } else {
+ clientSocket.close();
+ }
+ }
+}
diff --git a/server/src/main/java/nl/andrewl/starship_arena/server/StarshipArenaServer.java b/server/src/main/java/nl/andrewl/starship_arena/server/StarshipArenaServer.java
index eafb506..de8520e 100644
--- a/server/src/main/java/nl/andrewl/starship_arena/server/StarshipArenaServer.java
+++ b/server/src/main/java/nl/andrewl/starship_arena/server/StarshipArenaServer.java
@@ -1,90 +1,11 @@
package nl.andrewl.starship_arena.server;
-import nl.andrewl.starship_arena.server.model.Arena;
-import nl.andrewl.starship_arena.server.servlet.ArenasServlet;
-import org.eclipse.jetty.server.Connector;
-import org.eclipse.jetty.server.Server;
-import org.eclipse.jetty.server.ServerConnector;
-import org.eclipse.jetty.servlet.ServletContextHandler;
-import org.eclipse.jetty.servlet.ServletHolder;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.DataInputStream;
-import java.io.IOException;
-import java.net.InetSocketAddress;
-import java.net.ServerSocket;
-import java.net.Socket;
-import java.util.*;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+@SpringBootApplication
public class StarshipArenaServer {
- private static final Logger logger = LoggerFactory.getLogger(StarshipArenaServer.class);
-
- private final Map arenas = new ConcurrentHashMap<>();
- private final ServerSocket serverSocket;
- private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
-
- public static void main(String[] args) throws Exception {
- StarshipArenaServer server = new StarshipArenaServer();
- server.registerArena(new Arena());
- server.registerArena(new Arena("Andrew's Arena"));
-
- Server jettyServer = new Server(8080);
- Connector connector = new ServerConnector(jettyServer);
- jettyServer.addConnector(connector);
-
- ServletContextHandler servletContext = new ServletContextHandler();
- servletContext.setContextPath("/");
- servletContext.addServlet(new ServletHolder(new ArenasServlet(server)), "/arenas");
-
- jettyServer.setHandler(servletContext);
- jettyServer.start();
- server.acceptConnections();
- }
-
- public StarshipArenaServer() throws IOException {
- serverSocket = new ServerSocket();
- serverSocket.setReuseAddress(true);
- }
-
- public List getArenas() {
- List sortedArenas = new ArrayList<>(this.arenas.values());
- sortedArenas.sort(Comparator.comparing(Arena::getCreatedAt));
- return sortedArenas;
- }
-
- public void registerArena(Arena a) {
- arenas.put(a.getId(), a);
- }
-
- public void acceptConnections() throws Exception {
- serverSocket.bind(new InetSocketAddress("127.0.0.1", 8081));
- logger.info("Now accepting TCP connections.");
- while (!serverSocket.isClosed()) {
- Socket clientSocket = serverSocket.accept();
- logger.info("Client connected from {}", clientSocket.getRemoteSocketAddress());
- executor.submit(() -> {
- try {
- processIncomingConnection(clientSocket);
- } catch (IOException e) {
- e.printStackTrace();
- }
- });
- }
- }
-
- private void processIncomingConnection(Socket clientSocket) throws IOException {
- var din = new DataInputStream(clientSocket.getInputStream());
- UUID arenaId = new UUID(din.readLong(), din.readLong());
- Arena arena = arenas.get(arenaId);
- if (arena != null) {
- var cm = new ClientManager(arenas.get(arenaId), clientSocket);
- cm.start();
- } else {
- clientSocket.close();
- }
+ public static void main(String[] args) {
+ SpringApplication.run(StarshipArenaServer.class, args);
}
}
diff --git a/server/src/main/java/nl/andrewl/starship_arena/server/api/ArenasController.java b/server/src/main/java/nl/andrewl/starship_arena/server/api/ArenasController.java
new file mode 100644
index 0000000..15e58cb
--- /dev/null
+++ b/server/src/main/java/nl/andrewl/starship_arena/server/api/ArenasController.java
@@ -0,0 +1,31 @@
+package nl.andrewl.starship_arena.server.api;
+
+import lombok.RequiredArgsConstructor;
+import nl.andrewl.starship_arena.server.api.dto.ArenaCreationPayload;
+import nl.andrewl.starship_arena.server.api.dto.ArenaResponse;
+import nl.andrewl.starship_arena.server.data.ArenaStore;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping(path = "/arenas")
+@RequiredArgsConstructor
+public class ArenasController {
+ private final ArenaStore arenaStore;
+
+ @GetMapping
+ public List getArenas() {
+ return arenaStore.getArenas();
+ }
+
+ @GetMapping(path = "/{arenaId}")
+ public ArenaResponse getArena(@PathVariable String arenaId) {
+ return arenaStore.getArena(arenaId);
+ }
+
+ @PostMapping
+ public ArenaResponse createArena(@RequestBody ArenaCreationPayload payload) {
+ return arenaStore.registerArena(payload);
+ }
+}
diff --git a/server/src/main/java/nl/andrewl/starship_arena/server/api/GatewayController.java b/server/src/main/java/nl/andrewl/starship_arena/server/api/GatewayController.java
new file mode 100644
index 0000000..e060e45
--- /dev/null
+++ b/server/src/main/java/nl/andrewl/starship_arena/server/api/GatewayController.java
@@ -0,0 +1,24 @@
+package nl.andrewl.starship_arena.server.api;
+
+import lombok.RequiredArgsConstructor;
+import nl.andrewl.starship_arena.server.SocketGateway;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Map;
+
+@RestController
+@RequestMapping(path = "/gateway")
+@RequiredArgsConstructor
+public class GatewayController {
+ private final SocketGateway gateway;
+
+ @GetMapping
+ public Object getInfo() {
+ return Map.of(
+ "tcp", gateway.getTcpPort(),
+ "udp", gateway.getUdpPort()
+ );
+ }
+}
diff --git a/server/src/main/java/nl/andrewl/starship_arena/server/api/dto/ArenaCreationPayload.java b/server/src/main/java/nl/andrewl/starship_arena/server/api/dto/ArenaCreationPayload.java
new file mode 100644
index 0000000..39965e0
--- /dev/null
+++ b/server/src/main/java/nl/andrewl/starship_arena/server/api/dto/ArenaCreationPayload.java
@@ -0,0 +1,5 @@
+package nl.andrewl.starship_arena.server.api.dto;
+
+public record ArenaCreationPayload(
+ String name
+) {}
diff --git a/server/src/main/java/nl/andrewl/starship_arena/server/api/dto/ArenaResponse.java b/server/src/main/java/nl/andrewl/starship_arena/server/api/dto/ArenaResponse.java
new file mode 100644
index 0000000..347ee49
--- /dev/null
+++ b/server/src/main/java/nl/andrewl/starship_arena/server/api/dto/ArenaResponse.java
@@ -0,0 +1,19 @@
+package nl.andrewl.starship_arena.server.api.dto;
+
+import nl.andrewl.starship_arena.server.model.Arena;
+
+public record ArenaResponse(
+ String id,
+ String createdAt,
+ String name,
+ String currentStage
+) {
+ public ArenaResponse(Arena arena) {
+ this(
+ arena.getId().toString(),
+ arena.getCreatedAt().toString(),
+ arena.getName(),
+ arena.getCurrentStage().name()
+ );
+ }
+}
diff --git a/server/src/main/java/nl/andrewl/starship_arena/server/data/ArenaStore.java b/server/src/main/java/nl/andrewl/starship_arena/server/data/ArenaStore.java
new file mode 100644
index 0000000..5aa4d93
--- /dev/null
+++ b/server/src/main/java/nl/andrewl/starship_arena/server/data/ArenaStore.java
@@ -0,0 +1,43 @@
+package nl.andrewl.starship_arena.server.data;
+
+import nl.andrewl.starship_arena.server.api.dto.ArenaCreationPayload;
+import nl.andrewl.starship_arena.server.api.dto.ArenaResponse;
+import nl.andrewl.starship_arena.server.model.Arena;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Service;
+import org.springframework.web.server.ResponseStatusException;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Service
+public class ArenaStore {
+ private final Map arenas = new ConcurrentHashMap<>();
+
+ public List getArenas() {
+ return arenas.values().stream()
+ .sorted(Comparator.comparing(Arena::getCreatedAt))
+ .map(ArenaResponse::new)
+ .toList();
+ }
+
+ public Optional getById(UUID id) {
+ return Optional.ofNullable(arenas.get(id));
+ }
+
+ public ArenaResponse registerArena(ArenaCreationPayload payload) {
+ Arena arena = new Arena(payload.name());
+ this.arenas.put(arena.getId(), arena);
+ return new ArenaResponse(arena);
+ }
+
+ public ArenaResponse getArena(String arenaId) {
+ try {
+ Arena arena = arenas.get(UUID.fromString(arenaId));
+ if (arena == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
+ return new ArenaResponse(arena);
+ } catch (IllegalArgumentException e) {
+ throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid arena id.");
+ }
+ }
+}
diff --git a/server/src/main/java/nl/andrewl/starship_arena/server/servlet/ArenaRequest.java b/server/src/main/java/nl/andrewl/starship_arena/server/servlet/ArenaRequest.java
deleted file mode 100644
index f1438b1..0000000
--- a/server/src/main/java/nl/andrewl/starship_arena/server/servlet/ArenaRequest.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package nl.andrewl.starship_arena.server.servlet;
-
-public class ArenaRequest {
- public String name;
-}
diff --git a/server/src/main/java/nl/andrewl/starship_arena/server/servlet/ArenaResponse.java b/server/src/main/java/nl/andrewl/starship_arena/server/servlet/ArenaResponse.java
deleted file mode 100644
index 54b2ed0..0000000
--- a/server/src/main/java/nl/andrewl/starship_arena/server/servlet/ArenaResponse.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package nl.andrewl.starship_arena.server.servlet;
-
-import nl.andrewl.starship_arena.server.model.Arena;
-
-public record ArenaResponse(
- String id,
- String createdAt,
- String name,
- String currentStage
-) {
- public ArenaResponse(Arena a) {
- this(
- a.getId().toString(),
- a.getCreatedAt().toString(),
- a.getName(),
- a.getCurrentStage().name()
- );
- }
-}
diff --git a/server/src/main/java/nl/andrewl/starship_arena/server/servlet/ArenasServlet.java b/server/src/main/java/nl/andrewl/starship_arena/server/servlet/ArenasServlet.java
deleted file mode 100644
index 5eb9b49..0000000
--- a/server/src/main/java/nl/andrewl/starship_arena/server/servlet/ArenasServlet.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package nl.andrewl.starship_arena.server.servlet;
-
-import com.google.gson.Gson;
-import com.google.gson.JsonIOException;
-import com.google.gson.JsonSyntaxException;
-import jakarta.servlet.http.HttpServlet;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import nl.andrewl.starship_arena.server.StarshipArenaServer;
-import nl.andrewl.starship_arena.server.model.Arena;
-
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.util.List;
-
-public class ArenasServlet extends HttpServlet {
- private static final Gson gson = new Gson();
- private final StarshipArenaServer server;
-
- public ArenasServlet(StarshipArenaServer server) {
- this.server = server;
- }
-
- @Override
- protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
- List data = server.getArenas().stream()
- .map(ArenaResponse::new).toList();
- resp.setStatus(HttpServletResponse.SC_OK);
- resp.setContentType("application/json");
- resp.setCharacterEncoding("UTF-8");
- gson.toJson(data, resp.getWriter());
- }
-
- @Override
- protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
- if (!req.getContentType().equals("application/json")) resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Only JSON is allowed.");
- ArenaRequest arenaRequest;
- try {
- arenaRequest = gson.fromJson(new InputStreamReader(req.getInputStream()), ArenaRequest.class);
- } catch (JsonSyntaxException | JsonIOException e) {
- resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid JSON.");
- return;
- }
- Arena arena = arenaRequest.name == null ? new Arena() : new Arena(arenaRequest.name);
- server.registerArena(arena);
- resp.setStatus(HttpServletResponse.SC_CREATED);
- resp.setContentType("application/json");
- resp.setCharacterEncoding("UTF-8");
- gson.toJson(new ArenaResponse(arena), resp.getWriter());
- }
-}
diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties
new file mode 100644
index 0000000..28cce26
--- /dev/null
+++ b/server/src/main/resources/application.properties
@@ -0,0 +1,10 @@
+spring.main.banner-mode=off
+spring.main.log-startup-info=false
+
+server.tomcat.threads.min-spare=1
+server.tomcat.threads.max=10
+server.tomcat.processor-cache=50
+
+starship-arena.gateway.host=127.0.0.1
+starship-arena.gateway.tcp-port=8081
+starship-arena.gateway.udp-port=8082