Changed to use spring, and improved socket gateway logic.

This commit is contained in:
Andrew Lalis 2022-04-09 12:15:49 +02:00
parent c9b617172a
commit 9be9a15fa0
12 changed files with 228 additions and 170 deletions

View File

@ -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>

View File

@ -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();
}
}
}

View File

@ -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();
}
} }
} }

View File

@ -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);
}
}

View File

@ -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()
);
}
}

View File

@ -0,0 +1,5 @@
package nl.andrewl.starship_arena.server.api.dto;
public record ArenaCreationPayload(
String name
) {}

View File

@ -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()
);
}
}

View File

@ -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.");
}
}
}

View File

@ -1,5 +0,0 @@
package nl.andrewl.starship_arena.server.servlet;
public class ArenaRequest {
public String name;
}

View File

@ -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()
);
}
}

View File

@ -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());
}
}

View File

@ -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