Changed to use record-net.

This commit is contained in:
Andrew Lalis 2022-04-16 14:21:18 +02:00
parent dfc7ac6978
commit 1fc23a1101
11 changed files with 225 additions and 31 deletions

View File

@ -17,6 +17,13 @@
<maven.compiler.target>17</maven.compiler.target>
</properties>
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
<dependency>
@ -24,5 +31,10 @@
<artifactId>gson</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>com.github.andrewlalis</groupId>
<artifactId>record-net</artifactId>
<version>1.1.0</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,5 @@
package nl.andrewl.starship_arena.core.net;
import nl.andrewl.record_net.Message;
public record ChatSend(String msg) implements Message {}

View File

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

View File

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

View File

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

View File

@ -5,12 +5,24 @@ import java.io.InputStream;
import java.io.UncheckedIOException;
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) {
InputStream in = ResourceUtils.class.getResourceAsStream(name);
if (in == null) throw new UncheckedIOException(new IOException("Could not load resource: " + name));
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) {
try (var in = get(name)) {
return new String(in.readAllBytes());

69
server/protocol.md Normal file
View File

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

View File

@ -1,22 +1,45 @@
package nl.andrewl.starship_arena.server;
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.ChatMessage;
import java.io.IOException;
import java.io.*;
import java.net.Socket;
import java.util.UUID;
import static nl.andrewl.starship_arena.server.SocketGateway.SERIALIZER;
public class ClientManager extends Thread {
private final Arena arena;
private final Socket clientSocket;
@Getter
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.clientSocket = clientSocket;
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() {
@ -30,7 +53,14 @@ public class ClientManager extends Thread {
@Override
public void run() {
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();
}
}
}
}

View File

@ -2,6 +2,10 @@ package nl.andrewl.starship_arena.server;
import lombok.Getter;
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.model.Arena;
import org.springframework.beans.factory.annotation.Value;
@ -10,13 +14,8 @@ import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import javax.annotation.PreDestroy;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.*;
import java.util.UUID;
/**
@ -27,6 +26,13 @@ import java.util.UUID;
@Service
@Slf4j
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 DatagramSocket serverUdpSocket;
private final ArenaStore arenaStore;
@ -90,28 +96,32 @@ public class SocketGateway implements Runnable {
* @param clientSocket The socket to the client.
*/
private void processIncomingConnection(Socket clientSocket) {
try (
var in = new DataInputStream(clientSocket.getInputStream());
var out = new DataOutputStream(clientSocket.getOutputStream())
) {
UUID arenaId = new UUID(in.readLong(), in.readLong());
UUID clientId;
boolean reconnecting = in.readBoolean();
if (reconnecting) {
clientId = new UUID(in.readLong(), in.readLong());
} else {
clientId = UUID.randomUUID();
try {
clientSocket.setSoTimeout(INITIALIZATION_TIMEOUT); // Set limited timeout so new connections don't waste resources.
Message msg = SERIALIZER.readMessage(clientSocket.getInputStream());
if (msg instanceof ClientConnectRequest cm) {
UUID arenaId = cm.arenaId();
UUID clientId = cm.clientId();
if (clientId == null) clientId = UUID.randomUUID();
var oa = arenaStore.getById(arenaId);
if (oa.isPresent()) {
Arena arena = oa.get();
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 (oa.isPresent()) {
Arena arena = oa.get();
ClientManager clientManager = new ClientManager(arena, clientSocket, clientId);
arena.registerClient(clientManager);
out.writeBoolean(true);
clientManager.start();
} else {
out.writeBoolean(false);
// If the connection wasn't valid, return a no-success response.
SERIALIZER.writeMessage(new ClientConnectResponse(false, null));
clientSocket.close();
} catch (SocketTimeoutException e) {
try {
clientSocket.close();
} catch (IOException ex) {
throw new RuntimeException(ex);
}
} catch (IOException e) {
e.printStackTrace();

View File

@ -1,11 +1,20 @@
package nl.andrewl.starship_arena.server.model;
import nl.andrewl.starship_arena.core.net.ChatSent;
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.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import static nl.andrewl.starship_arena.server.SocketGateway.SERIALIZER;
public class Arena {
private final UUID id;
@ -15,6 +24,7 @@ public class Arena {
private ArenaStage currentStage = ArenaStage.STAGING;
private final Map<UUID, ClientManager> clients = new ConcurrentHashMap<>();
private final List<ChatMessage> chatMessages = new CopyOnWriteArrayList<>();
public Arena(String name) {
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
public boolean equals(Object o) {
return o == this || o instanceof Arena a && id.equals(a.id);

View File

@ -0,0 +1,6 @@
package nl.andrewl.starship_arena.server.model;
import java.util.UUID;
public record ChatMessage(UUID clientId, long timestamp, String message) {
}