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