Changed to use spring, and improved socket gateway logic.
This commit is contained in:
parent
c9b617172a
commit
9be9a15fa0
|
@ -18,23 +18,21 @@
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
<version>2.6.6</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>nl.andrewl.starship-arena</groupId>
|
<groupId>nl.andrewl.starship-arena</groupId>
|
||||||
<artifactId>core</artifactId>
|
<artifactId>core</artifactId>
|
||||||
<version>${project.parent.version}</version>
|
<version>${project.parent.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- https://mvnrepository.com/artifact/org.eclipse.jetty/jetty-servlet -->
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.eclipse.jetty</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>jetty-servlet</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
<version>11.0.8</version>
|
<version>1.18.22</version>
|
||||||
</dependency>
|
<scope>provided</scope>
|
||||||
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-jdk14 -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.slf4j</groupId>
|
|
||||||
<artifactId>slf4j-jdk14</artifactId>
|
|
||||||
<version>2.0.0-alpha7</version>
|
|
||||||
<scope>runtime</scope>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,90 +1,11 @@
|
||||||
package nl.andrewl.starship_arena.server;
|
package nl.andrewl.starship_arena.server;
|
||||||
|
|
||||||
import nl.andrewl.starship_arena.server.model.Arena;
|
import org.springframework.boot.SpringApplication;
|
||||||
import nl.andrewl.starship_arena.server.servlet.ArenasServlet;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
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;
|
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
public class StarshipArenaServer {
|
public class StarshipArenaServer {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(StarshipArenaServer.class);
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(StarshipArenaServer.class, args);
|
||||||
private final Map<UUID, Arena> 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<Arena> getArenas() {
|
|
||||||
List<Arena> 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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<ArenaResponse> 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package nl.andrewl.starship_arena.server.api.dto;
|
||||||
|
|
||||||
|
public record ArenaCreationPayload(
|
||||||
|
String name
|
||||||
|
) {}
|
|
@ -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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<UUID, Arena> arenas = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public List<ArenaResponse> getArenas() {
|
||||||
|
return arenas.values().stream()
|
||||||
|
.sorted(Comparator.comparing(Arena::getCreatedAt))
|
||||||
|
.map(ArenaResponse::new)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Arena> 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +0,0 @@
|
||||||
package nl.andrewl.starship_arena.server.servlet;
|
|
||||||
|
|
||||||
public class ArenaRequest {
|
|
||||||
public String name;
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<ArenaResponse> 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());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
Loading…
Reference in New Issue