Changed to use spring, and improved socket gateway logic.
This commit is contained in:
parent
c9b617172a
commit
9be9a15fa0
|
@ -18,23 +18,21 @@
|
|||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<version>2.6.6</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>nl.andrewl.starship-arena</groupId>
|
||||
<artifactId>core</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.eclipse.jetty/jetty-servlet -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-servlet</artifactId>
|
||||
<version>11.0.8</version>
|
||||
</dependency>
|
||||
<!-- 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>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.22</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</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;
|
||||
|
||||
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<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();
|
||||
}
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(StarshipArenaServer.class, args);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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