Changed to use record-net.
This commit is contained in:
parent
dfc7ac6978
commit
1fc23a1101
12
core/pom.xml
12
core/pom.xml
|
@ -17,6 +17,13 @@
|
||||||
<maven.compiler.target>17</maven.compiler.target>
|
<maven.compiler.target>17</maven.compiler.target>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>jitpack.io</id>
|
||||||
|
<url>https://jitpack.io</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
|
<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
|
||||||
<dependency>
|
<dependency>
|
||||||
|
@ -24,5 +31,10 @@
|
||||||
<artifactId>gson</artifactId>
|
<artifactId>gson</artifactId>
|
||||||
<version>2.9.0</version>
|
<version>2.9.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.andrewlalis</groupId>
|
||||||
|
<artifactId>record-net</artifactId>
|
||||||
|
<version>1.1.0</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
|
@ -0,0 +1,5 @@
|
||||||
|
package nl.andrewl.starship_arena.core.net;
|
||||||
|
|
||||||
|
import nl.andrewl.record_net.Message;
|
||||||
|
|
||||||
|
public record ChatSend(String msg) implements Message {}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package nl.andrewl.starship_arena.core.net;
|
||||||
|
|
||||||
|
import nl.andrewl.record_net.Message;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record ChatSent(UUID clientId, long timestamp, String msg) implements Message {}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package nl.andrewl.starship_arena.core.net;
|
||||||
|
|
||||||
|
import nl.andrewl.record_net.Message;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record ClientConnectRequest(
|
||||||
|
UUID arenaId,
|
||||||
|
UUID clientId
|
||||||
|
) implements Message {}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package nl.andrewl.starship_arena.core.net;
|
||||||
|
|
||||||
|
import nl.andrewl.record_net.Message;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record ClientConnectResponse(boolean success, UUID clientId) implements Message {}
|
|
@ -5,12 +5,24 @@ import java.io.InputStream;
|
||||||
import java.io.UncheckedIOException;
|
import java.io.UncheckedIOException;
|
||||||
|
|
||||||
public class ResourceUtils {
|
public class ResourceUtils {
|
||||||
|
/**
|
||||||
|
* Gets an input stream for the given resource on the classpath.
|
||||||
|
* @param name The name of the resource.
|
||||||
|
* @return An input stream to read the resource.
|
||||||
|
* @throws UncheckedIOException If the resource could not be loaded.
|
||||||
|
*/
|
||||||
public static InputStream get(String name) {
|
public static InputStream get(String name) {
|
||||||
InputStream in = ResourceUtils.class.getResourceAsStream(name);
|
InputStream in = ResourceUtils.class.getResourceAsStream(name);
|
||||||
if (in == null) throw new UncheckedIOException(new IOException("Could not load resource: " + name));
|
if (in == null) throw new UncheckedIOException(new IOException("Could not load resource: " + name));
|
||||||
return in;
|
return in;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a classpath resource as a string.
|
||||||
|
* @param name The name of the resource.
|
||||||
|
* @return The string representation of that resource.
|
||||||
|
* @throws UncheckedIOException If the resource could not be loaded.
|
||||||
|
*/
|
||||||
public static String getString(String name) {
|
public static String getString(String name) {
|
||||||
try (var in = get(name)) {
|
try (var in = get(name)) {
|
||||||
return new String(in.readAllBytes());
|
return new String(in.readAllBytes());
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
# Client - Server Communication Protocol
|
||||||
|
This document describes the format of messages that will be sent back and forth between starship arena clients and servers. It is organized according to the lifecycle of an arena.
|
||||||
|
|
||||||
|
## Connecting
|
||||||
|
Clients connect to an arena by connecting to the starship arena server's **gateway TCP socket**. Upon connecting, clients must immediately send the following information:
|
||||||
|
```
|
||||||
|
arenaId: UUID (16 bytes),
|
||||||
|
reconnecting: boolean (1 byte),
|
||||||
|
if reconnecting:
|
||||||
|
clientId: UUID (16 bytes)
|
||||||
|
```
|
||||||
|
The `arenaId` is the unique id of the arena that the client is attempting to join. `reconnecting` indicates to the server if the client is trying to reconnect to an arena that they already joined, and if so, the server expects the client to send a `clientId` which is their unique id which they were assigned when first joining.
|
||||||
|
|
||||||
|
If the connection was successful, the server will respond:
|
||||||
|
```
|
||||||
|
success: boolean (1 byte),
|
||||||
|
if success:
|
||||||
|
clientId: UUID (16 bytes)
|
||||||
|
```
|
||||||
|
If the client receives `success == true`, then they have been successfully connected and should expect to read their `clientId` (this is provided even if the client is reconnecting, for simplicity). If the client receives `success == false`, then they can close the socket. The server will close the socket immediately after a failed connection.
|
||||||
|
|
||||||
|
### General Message Information
|
||||||
|
Unless otherwise specified, all client/server messages are always prefixed with a standard header:
|
||||||
|
```
|
||||||
|
messageType: byte,
|
||||||
|
messageSize: int (4 bytes)
|
||||||
|
```
|
||||||
|
The `messageType` indicates what type of message was sent, and what structure it will have. The `messageSize` indicates the size, in bytes, of the message, excluding the header data itself.
|
||||||
|
|
||||||
|
### Chat Messages
|
||||||
|
At any point while a client is connected to an arena, it may send and receive chat messages. To send a chat message, a client sends the following:
|
||||||
|
```
|
||||||
|
messageType: 1,
|
||||||
|
messageSize: msg.length(),
|
||||||
|
msg: String
|
||||||
|
```
|
||||||
|
Chat messages will be relayed by the arena to all clients using the following format:
|
||||||
|
```
|
||||||
|
messageType: 2,
|
||||||
|
messageSize: 16 + 8 + msg.length(),
|
||||||
|
clientId: UUID (16 bytes),
|
||||||
|
timestamp: long (8 bytes),
|
||||||
|
msg: String
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Messages
|
||||||
|
Generally, error messages take the following form:
|
||||||
|
```
|
||||||
|
messageType: 0,
|
||||||
|
messageSize: msg.length(),
|
||||||
|
msg: String
|
||||||
|
```
|
||||||
|
They can be sent by the server as a response to any client message that prompted that error.
|
||||||
|
|
||||||
|
## Staging
|
||||||
|
During the staging part of the arena lifecycle, clients can of course chat, but they can also send commands to configure their experience prior to the start of the game. This includes the following:
|
||||||
|
- Setting their username
|
||||||
|
- Choosing to be a spectator or a player (all clients are spectators by default)
|
||||||
|
- Choosing a team to be on (if choice is allowed, and if teams exist)
|
||||||
|
- Choosing a spacecraft model (if choice is allowed)
|
||||||
|
- Fetch arena configuration settings
|
||||||
|
|
||||||
|
### Client Config Message
|
||||||
|
This message is sent by the client to indicate that they'd like to change their current configuration.
|
||||||
|
```
|
||||||
|
messageType: 10,
|
||||||
|
messageSize: see below,
|
||||||
|
role: byte (0 = spectator, 1 - 128 = player or team number)
|
||||||
|
```
|
|
@ -1,22 +1,45 @@
|
||||||
package nl.andrewl.starship_arena.server;
|
package nl.andrewl.starship_arena.server;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import nl.andrewl.record_net.Message;
|
||||||
|
import nl.andrewl.starship_arena.core.net.ChatSend;
|
||||||
import nl.andrewl.starship_arena.server.model.Arena;
|
import nl.andrewl.starship_arena.server.model.Arena;
|
||||||
|
import nl.andrewl.starship_arena.server.model.ChatMessage;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.*;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static nl.andrewl.starship_arena.server.SocketGateway.SERIALIZER;
|
||||||
|
|
||||||
public class ClientManager extends Thread {
|
public class ClientManager extends Thread {
|
||||||
private final Arena arena;
|
|
||||||
private final Socket clientSocket;
|
|
||||||
@Getter
|
@Getter
|
||||||
private final UUID clientId;
|
private final UUID clientId;
|
||||||
|
@Getter
|
||||||
|
private String clientUsername;
|
||||||
|
private final Arena arena;
|
||||||
|
|
||||||
public ClientManager(Arena arena, Socket clientSocket, UUID id) {
|
private final Socket clientSocket;
|
||||||
|
private final InputStream in;
|
||||||
|
private final OutputStream out;
|
||||||
|
private final DataInputStream dIn;
|
||||||
|
private final DataOutputStream dOut;
|
||||||
|
|
||||||
|
public ClientManager(Arena arena, Socket clientSocket, UUID id) throws IOException {
|
||||||
this.arena = arena;
|
this.arena = arena;
|
||||||
this.clientSocket = clientSocket;
|
this.clientSocket = clientSocket;
|
||||||
this.clientId = id;
|
this.clientId = id;
|
||||||
|
this.in = clientSocket.getInputStream();
|
||||||
|
this.out = clientSocket.getOutputStream();
|
||||||
|
this.dIn = new DataInputStream(clientSocket.getInputStream());
|
||||||
|
this.dOut = new DataOutputStream(clientSocket.getOutputStream());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void send(byte[] data) throws IOException {
|
||||||
|
synchronized (out) {
|
||||||
|
out.write(data);
|
||||||
|
out.flush();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void shutdown() {
|
public void shutdown() {
|
||||||
|
@ -30,7 +53,14 @@ public class ClientManager extends Thread {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
while (!clientSocket.isClosed()) {
|
while (!clientSocket.isClosed()) {
|
||||||
|
try {
|
||||||
|
Message msg = SERIALIZER.readMessage(in);
|
||||||
|
if (msg instanceof ChatSend cs) {
|
||||||
|
arena.chatSent(new ChatMessage(clientId, System.currentTimeMillis(), cs.msg()));
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,10 @@ package nl.andrewl.starship_arena.server;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import nl.andrewl.record_net.Message;
|
||||||
|
import nl.andrewl.record_net.Serializer;
|
||||||
|
import nl.andrewl.starship_arena.core.net.ClientConnectRequest;
|
||||||
|
import nl.andrewl.starship_arena.core.net.ClientConnectResponse;
|
||||||
import nl.andrewl.starship_arena.server.data.ArenaStore;
|
import nl.andrewl.starship_arena.server.data.ArenaStore;
|
||||||
import nl.andrewl.starship_arena.server.model.Arena;
|
import nl.andrewl.starship_arena.server.model.Arena;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
@ -10,13 +14,8 @@ import org.springframework.context.event.EventListener;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import javax.annotation.PreDestroy;
|
import javax.annotation.PreDestroy;
|
||||||
import java.io.DataInputStream;
|
|
||||||
import java.io.DataOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.DatagramSocket;
|
import java.net.*;
|
||||||
import java.net.InetSocketAddress;
|
|
||||||
import java.net.ServerSocket;
|
|
||||||
import java.net.Socket;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -27,6 +26,13 @@ import java.util.UUID;
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class SocketGateway implements Runnable {
|
public class SocketGateway implements Runnable {
|
||||||
|
public static final int INITIALIZATION_TIMEOUT = 1000;
|
||||||
|
public static final Serializer SERIALIZER = new Serializer();
|
||||||
|
static {
|
||||||
|
SERIALIZER.registerType(1, ClientConnectRequest.class);
|
||||||
|
SERIALIZER.registerType(2, ClientConnectResponse.class);
|
||||||
|
}
|
||||||
|
|
||||||
private final ServerSocket serverSocket;
|
private final ServerSocket serverSocket;
|
||||||
private final DatagramSocket serverUdpSocket;
|
private final DatagramSocket serverUdpSocket;
|
||||||
private final ArenaStore arenaStore;
|
private final ArenaStore arenaStore;
|
||||||
|
@ -90,28 +96,32 @@ public class SocketGateway implements Runnable {
|
||||||
* @param clientSocket The socket to the client.
|
* @param clientSocket The socket to the client.
|
||||||
*/
|
*/
|
||||||
private void processIncomingConnection(Socket clientSocket) {
|
private void processIncomingConnection(Socket clientSocket) {
|
||||||
try (
|
try {
|
||||||
var in = new DataInputStream(clientSocket.getInputStream());
|
clientSocket.setSoTimeout(INITIALIZATION_TIMEOUT); // Set limited timeout so new connections don't waste resources.
|
||||||
var out = new DataOutputStream(clientSocket.getOutputStream())
|
Message msg = SERIALIZER.readMessage(clientSocket.getInputStream());
|
||||||
) {
|
if (msg instanceof ClientConnectRequest cm) {
|
||||||
UUID arenaId = new UUID(in.readLong(), in.readLong());
|
UUID arenaId = cm.arenaId();
|
||||||
UUID clientId;
|
UUID clientId = cm.clientId();
|
||||||
boolean reconnecting = in.readBoolean();
|
if (clientId == null) clientId = UUID.randomUUID();
|
||||||
if (reconnecting) {
|
var oa = arenaStore.getById(arenaId);
|
||||||
clientId = new UUID(in.readLong(), in.readLong());
|
if (oa.isPresent()) {
|
||||||
} else {
|
Arena arena = oa.get();
|
||||||
clientId = UUID.randomUUID();
|
ClientManager clientManager = new ClientManager(arena, clientSocket, clientId);
|
||||||
|
arena.registerClient(clientManager);
|
||||||
|
SERIALIZER.writeMessage(new ClientConnectResponse(true, clientId));
|
||||||
|
clientSocket.setSoTimeout(0); // Reset timeout to infinity after successful initialization.
|
||||||
|
clientManager.start();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var oa = arenaStore.getById(arenaId);
|
// If the connection wasn't valid, return a no-success response.
|
||||||
if (oa.isPresent()) {
|
SERIALIZER.writeMessage(new ClientConnectResponse(false, null));
|
||||||
Arena arena = oa.get();
|
clientSocket.close();
|
||||||
ClientManager clientManager = new ClientManager(arena, clientSocket, clientId);
|
} catch (SocketTimeoutException e) {
|
||||||
arena.registerClient(clientManager);
|
try {
|
||||||
out.writeBoolean(true);
|
|
||||||
clientManager.start();
|
|
||||||
} else {
|
|
||||||
out.writeBoolean(false);
|
|
||||||
clientSocket.close();
|
clientSocket.close();
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new RuntimeException(ex);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
|
|
@ -1,11 +1,20 @@
|
||||||
package nl.andrewl.starship_arena.server.model;
|
package nl.andrewl.starship_arena.server.model;
|
||||||
|
|
||||||
|
import nl.andrewl.starship_arena.core.net.ChatSent;
|
||||||
import nl.andrewl.starship_arena.server.ClientManager;
|
import nl.andrewl.starship_arena.server.ClientManager;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
|
import static nl.andrewl.starship_arena.server.SocketGateway.SERIALIZER;
|
||||||
|
|
||||||
public class Arena {
|
public class Arena {
|
||||||
private final UUID id;
|
private final UUID id;
|
||||||
|
@ -15,6 +24,7 @@ public class Arena {
|
||||||
private ArenaStage currentStage = ArenaStage.STAGING;
|
private ArenaStage currentStage = ArenaStage.STAGING;
|
||||||
|
|
||||||
private final Map<UUID, ClientManager> clients = new ConcurrentHashMap<>();
|
private final Map<UUID, ClientManager> clients = new ConcurrentHashMap<>();
|
||||||
|
private final List<ChatMessage> chatMessages = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
public Arena(String name) {
|
public Arena(String name) {
|
||||||
this.id = UUID.randomUUID();
|
this.id = UUID.randomUUID();
|
||||||
|
@ -50,6 +60,22 @@ public class Arena {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void chatSent(ChatMessage chat) throws IOException {
|
||||||
|
chatMessages.add(chat);
|
||||||
|
byte[] data = SERIALIZER.writeMessage(new ChatSent(chat.clientId(), chat.timestamp(), chat.message()));
|
||||||
|
broadcast(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void broadcast(byte[] data) {
|
||||||
|
for (var cm : clients.values()) {
|
||||||
|
try {
|
||||||
|
cm.send(data);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
return o == this || o instanceof Arena a && id.equals(a.id);
|
return o == this || o instanceof Arena a && id.equals(a.id);
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
package nl.andrewl.starship_arena.server.model;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record ChatMessage(UUID clientId, long timestamp, String message) {
|
||||||
|
}
|
Loading…
Reference in New Issue