From 682f9f9bc295aa3856d262fd84c88211fdb6f532 Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Wed, 6 Jul 2022 20:20:15 +0200 Subject: [PATCH] Added successful client connection flow. --- .../java/nl/andrewl/aos2_client/Client.java | 167 +++++++++--------- .../aos2_client/CommunicationHandler.java | 96 ++++++++++ .../andrewl/aos2_client/render/ChunkMesh.java | 8 +- .../aos2_client/render/WindowUtils.java | 55 ++++++ core/pom.xml | 6 + .../main/java/nl/andrewl/aos_core/Net.java | 8 + .../java/nl/andrewl/aos_core/model/Chunk.java | 5 + .../nl/andrewl/aos_core/model/Player.java | 24 ++- .../aos_core/net/ChunkHashMessage.java | 17 ++ .../aos_core/net/ConnectAcceptMessage.java | 9 +- .../aos_core/net/ConnectRejectMessage.java | 8 +- .../aos_core/net/ConnectRequestMessage.java | 2 +- .../nl/andrewl/aos_core/net/TcpReceiver.java | 39 ++++ .../aos_core/net/UdpMessageHandler.java | 10 ++ .../nl/andrewl/aos_core/net/UdpReceiver.java | 44 +++++ .../aos_core/net/udp/ChunkUpdateMessage.java | 19 ++ .../aos_core/net/udp/DatagramInit.java | 9 +- design/net.md | 6 +- ...r.java => ClientCommunicationHandler.java} | 82 ++++----- .../java/nl/andrewl/aos2_server/Server.java | 55 ++++-- 20 files changed, 514 insertions(+), 155 deletions(-) create mode 100644 client/src/main/java/nl/andrewl/aos2_client/CommunicationHandler.java create mode 100644 client/src/main/java/nl/andrewl/aos2_client/render/WindowUtils.java create mode 100644 core/src/main/java/nl/andrewl/aos_core/net/ChunkHashMessage.java create mode 100644 core/src/main/java/nl/andrewl/aos_core/net/TcpReceiver.java create mode 100644 core/src/main/java/nl/andrewl/aos_core/net/UdpMessageHandler.java create mode 100644 core/src/main/java/nl/andrewl/aos_core/net/UdpReceiver.java create mode 100644 core/src/main/java/nl/andrewl/aos_core/net/udp/ChunkUpdateMessage.java rename server/src/main/java/nl/andrewl/aos2_server/{ClientHandler.java => ClientCommunicationHandler.java} (61%) diff --git a/client/src/main/java/nl/andrewl/aos2_client/Client.java b/client/src/main/java/nl/andrewl/aos2_client/Client.java index 69a0705..bfb270f 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/Client.java +++ b/client/src/main/java/nl/andrewl/aos2_client/Client.java @@ -2,106 +2,101 @@ package nl.andrewl.aos2_client; import nl.andrewl.aos2_client.render.ChunkMesh; import nl.andrewl.aos2_client.render.ChunkRenderer; -import nl.andrewl.aos2_client.render.WindowInfo; +import nl.andrewl.aos2_client.render.WindowUtils; import nl.andrewl.aos_core.model.Chunk; import org.joml.Vector3i; -import org.lwjgl.glfw.Callbacks; -import org.lwjgl.glfw.GLFWErrorCallback; -import org.lwjgl.opengl.GL; +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; import java.util.Random; import static org.lwjgl.glfw.GLFW.*; import static org.lwjgl.opengl.GL46.*; -public class Client { - public static void main(String[] args) { - var windowInfo = initUI(); - long windowHandle = windowInfo.windowHandle(); +public class Client implements Runnable { + public static void main(String[] args) throws IOException { + InetAddress serverAddress = InetAddress.getByName(args[0]); + int serverPort = Integer.parseInt(args[1]); + String username = args[2].trim(); - Camera cam = new Camera(); - cam.setOrientationDegrees(90, 90); - cam.setPosition(-3, 3, 0); - glfwSetCursorPosCallback(windowHandle, cam); + Client client = new Client(serverAddress, serverPort, username); + client.run(); - Chunk chunk = Chunk.random(new Vector3i(0, 0, 0), new Random(1)); - Chunk chunk2 = Chunk.random(new Vector3i(1, 0, 0), new Random(1)); - Chunk chunk3 = Chunk.random(new Vector3i(1, 0, 1), new Random(1)); - Chunk chunk4 = Chunk.random(new Vector3i(0, 0, 1), new Random(1)); - chunk.setBlockAt(0, 0, 0, (byte) 0); - - for (int x = 0; x < Chunk.SIZE; x++) { - for (int z = 0; z < Chunk.SIZE; z++) { - chunk.setBlockAt(x, Chunk.SIZE - 1, z, (byte) 0); - } - } - - ChunkRenderer chunkRenderer = new ChunkRenderer(windowInfo.width(), windowInfo.height()); - chunkRenderer.addChunkMesh(new ChunkMesh(chunk)); - chunkRenderer.addChunkMesh(new ChunkMesh(chunk2)); - chunkRenderer.addChunkMesh(new ChunkMesh(chunk3)); - chunkRenderer.addChunkMesh(new ChunkMesh(chunk4)); - - while (!glfwWindowShouldClose(windowHandle)) { - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - - chunkRenderer.draw(cam); - - glfwSwapBuffers(windowHandle); - glfwPollEvents(); - - if (glfwGetKey(windowHandle, GLFW_KEY_W) == GLFW_PRESS) cam.move(Camera.FORWARD); - if (glfwGetKey(windowHandle, GLFW_KEY_S) == GLFW_PRESS) cam.move(Camera.BACKWARD); - if (glfwGetKey(windowHandle, GLFW_KEY_A) == GLFW_PRESS) cam.move(Camera.LEFT); - if (glfwGetKey(windowHandle, GLFW_KEY_D) == GLFW_PRESS) cam.move(Camera.RIGHT); - if (glfwGetKey(windowHandle, GLFW_KEY_SPACE) == GLFW_PRESS) cam.move(Camera.UP); - if (glfwGetKey(windowHandle, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS) cam.move(Camera.DOWN); - } - - chunkRenderer.free(); - - Callbacks.glfwFreeCallbacks(windowHandle); - glfwDestroyWindow(windowHandle); - glfwTerminate(); - glfwSetErrorCallback(null).free(); +// var windowInfo = WindowUtils.initUI(); +// long windowHandle = windowInfo.windowHandle(); +// +// Camera cam = new Camera(); +// cam.setOrientationDegrees(90, 90); +// cam.setPosition(-3, 3, 0); +// glfwSetCursorPosCallback(windowHandle, cam); +// +// Chunk chunk = Chunk.random(new Vector3i(0, 0, 0), new Random(1)); +// Chunk chunk2 = Chunk.random(new Vector3i(1, 0, 0), new Random(1)); +// Chunk chunk3 = Chunk.random(new Vector3i(1, 0, 1), new Random(1)); +// Chunk chunk4 = Chunk.random(new Vector3i(0, 0, 1), new Random(1)); +// +// chunk.setBlockAt(0, 0, 0, (byte) 0); +// +// for (int x = 0; x < Chunk.SIZE; x++) { +// for (int z = 0; z < Chunk.SIZE; z++) { +// chunk.setBlockAt(x, Chunk.SIZE - 1, z, (byte) 0); +// } +// } +// +// ChunkRenderer chunkRenderer = new ChunkRenderer(windowInfo.width(), windowInfo.height()); +// chunkRenderer.addChunkMesh(new ChunkMesh(chunk)); +// chunkRenderer.addChunkMesh(new ChunkMesh(chunk2)); +// chunkRenderer.addChunkMesh(new ChunkMesh(chunk3)); +// chunkRenderer.addChunkMesh(new ChunkMesh(chunk4)); +// +// while (!glfwWindowShouldClose(windowHandle)) { +// glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); +// +// chunkRenderer.draw(cam); +// +// glfwSwapBuffers(windowHandle); +// glfwPollEvents(); +// +// if (glfwGetKey(windowHandle, GLFW_KEY_W) == GLFW_PRESS) cam.move(Camera.FORWARD); +// if (glfwGetKey(windowHandle, GLFW_KEY_S) == GLFW_PRESS) cam.move(Camera.BACKWARD); +// if (glfwGetKey(windowHandle, GLFW_KEY_A) == GLFW_PRESS) cam.move(Camera.LEFT); +// if (glfwGetKey(windowHandle, GLFW_KEY_D) == GLFW_PRESS) cam.move(Camera.RIGHT); +// if (glfwGetKey(windowHandle, GLFW_KEY_SPACE) == GLFW_PRESS) cam.move(Camera.UP); +// if (glfwGetKey(windowHandle, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS) cam.move(Camera.DOWN); +// } +// +// chunkRenderer.free(); +// WindowUtils.clearUI(windowHandle); } - private static WindowInfo initUI() { - GLFWErrorCallback.createPrint(System.err).set(); - if (!glfwInit()) throw new IllegalStateException("Could not initialize GLFW."); - glfwDefaultWindowHints(); - glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); - glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); + private InetAddress serverAddress; + private int serverPort; + private String username; + private CommunicationHandler communicationHandler; + private volatile boolean running; - var vidMode = glfwGetVideoMode(glfwGetPrimaryMonitor()); - if (vidMode == null) throw new IllegalStateException("Could not get information about the primary monitory."); - long windowHandle = glfwCreateWindow(vidMode.width(), vidMode.height(), "Ace of Shades 2", glfwGetPrimaryMonitor(), 0); - if (windowHandle == 0) throw new RuntimeException("Failed to create GLFW window."); + public Client(InetAddress serverAddress, int serverPort, String username) { + this.serverAddress = serverAddress; + this.serverPort = serverPort; + this.username = username; + this.communicationHandler = new CommunicationHandler(); + } - glfwSetKeyCallback(windowHandle, (window, key, scancode, action, mods) -> { - if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE) { - glfwSetWindowShouldClose(windowHandle, true); - } - }); - - glfwSetInputMode(windowHandle, GLFW_CURSOR, GLFW_CURSOR_DISABLED); - glfwSetInputMode(windowHandle, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE); - - glfwSetWindowPos(windowHandle, 0, 0); - glfwSetCursorPos(windowHandle, 0, 0); - - glfwMakeContextCurrent(windowHandle); - glfwSwapInterval(1); - glfwShowWindow(windowHandle); - - GL.createCapabilities(); -// GLUtil.setupDebugMessageCallback(System.out); - glClearColor(0.0f, 0.0f, 0.0f, 0.0f); - glEnable(GL_CULL_FACE); - glEnable(GL_DEPTH_TEST); - glCullFace(GL_BACK); - - return new WindowInfo(windowHandle, vidMode.width(), vidMode.height()); + @Override + public void run() { + running = false; + try { + communicationHandler.establishConnection(serverAddress, serverPort, username); + System.out.println("Established connection to the server."); + } catch (IOException e) { + e.printStackTrace(); + running = false; + } + while (running) { + // Do game stuff + System.out.println("Running!"); + } } } diff --git a/client/src/main/java/nl/andrewl/aos2_client/CommunicationHandler.java b/client/src/main/java/nl/andrewl/aos2_client/CommunicationHandler.java new file mode 100644 index 0000000..c1243dd --- /dev/null +++ b/client/src/main/java/nl/andrewl/aos2_client/CommunicationHandler.java @@ -0,0 +1,96 @@ +package nl.andrewl.aos2_client; + +import nl.andrewl.aos_core.Net; +import nl.andrewl.aos_core.net.*; +import nl.andrewl.aos_core.net.udp.DatagramInit; +import nl.andrewl.record_net.Message; +import nl.andrewl.record_net.util.ExtendedDataInputStream; +import nl.andrewl.record_net.util.ExtendedDataOutputStream; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.Socket; + +public class CommunicationHandler { + private Socket socket; + private DatagramSocket datagramSocket; + private ExtendedDataInputStream in; + private ExtendedDataOutputStream out; + private int clientId; + + public int establishConnection(InetAddress address, int port, String username) throws IOException { + System.out.printf("Connecting to server at %s, port %d, with username \"%s\"...%n", address, port, username); + if (socket != null && !socket.isClosed()) { + socket.close(); + } + socket = new Socket(address, port); + socket.setSoTimeout(1000); + in = Net.getInputStream(socket.getInputStream()); + out = Net.getOutputStream(socket.getOutputStream()); + Net.write(new ConnectRequestMessage(username), out); + Message response = Net.read(in); + socket.setSoTimeout(0); + if (response instanceof ConnectRejectMessage rejectMessage) { + throw new IOException("Attempt to connect rejected: " + rejectMessage.reason()); + } + if (response instanceof ConnectAcceptMessage acceptMessage) { + this.clientId = acceptMessage.clientId(); + new Thread(new TcpReceiver(in, this::handleMessage)).start(); + establishDatagramConnection(); + new Thread(new UdpReceiver(datagramSocket, this::handleUdpMessage)).start(); + return acceptMessage.clientId(); + } else { + throw new IOException("Server returned an unexpected message: " + response); + } + } + + public void sendMessage(Message msg) { + try { + Net.write(msg, out); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void sendDatagramPacket(Message msg) { + try { + byte[] data = Net.write(msg); + DatagramPacket packet = new DatagramPacket(data, data.length, socket.getRemoteSocketAddress()); + datagramSocket.send(packet); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void establishDatagramConnection() throws IOException { + datagramSocket = new DatagramSocket(); + boolean connectionEstablished = false; + int attempts = 0; + while (!connectionEstablished && attempts < 100) { + sendDatagramPacket(new DatagramInit(clientId)); + byte[] buffer = new byte[UdpReceiver.MAX_PACKET_SIZE]; + DatagramPacket packet = new DatagramPacket(buffer, buffer.length); + datagramSocket.receive(packet); + Message msg = Net.read(buffer); + if (msg instanceof DatagramInit echo && echo.clientId() == clientId) { + connectionEstablished = true; + } else { + attempts++; + } + } + if (!connectionEstablished) { + throw new IOException("Could not establish a datagram connection to the server after " + attempts + " attempts."); + } + System.out.println("Established datagram communication with the server."); + } + + private void handleMessage(Message msg) { + System.out.println("Received message: " + msg); + } + + private void handleUdpMessage(Message msg, DatagramPacket packet) { + System.out.println("Received udp message: " + msg); + } +} diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/ChunkMesh.java b/client/src/main/java/nl/andrewl/aos2_client/render/ChunkMesh.java index a852e30..98f0e44 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/render/ChunkMesh.java +++ b/client/src/main/java/nl/andrewl/aos2_client/render/ChunkMesh.java @@ -12,7 +12,7 @@ public class ChunkMesh { private final int vaoId; private final int eboId; - private int indiciesCount; + private int indexCount; private final int[] positionData; private final Chunk chunk; @@ -41,14 +41,14 @@ public class ChunkMesh { long start = System.nanoTime(); var meshData = ChunkMeshGenerator.generateMesh(chunk); double dur = (System.nanoTime() - start) / 1_000_000.0; - this.indiciesCount = meshData.indexBuffer().limit(); + this.indexCount = meshData.indexBuffer().limit(); // Print some debug information. System.out.printf( "Generated mesh for chunk (%d, %d, %d) in %.3f ms. %d vertices, %d indices.%n", chunk.getPosition().x, chunk.getPosition().y, chunk.getPosition().z, dur, meshData.vertexBuffer().limit() / 9, - indiciesCount + indexCount ); glBindBuffer(GL_ARRAY_BUFFER, vboId); @@ -80,7 +80,7 @@ public class ChunkMesh { public void draw() { glBindVertexArray(vaoId); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, eboId); - glDrawElements(GL_TRIANGLES, indiciesCount, GL_UNSIGNED_INT, 0); + glDrawElements(GL_TRIANGLES, indexCount, GL_UNSIGNED_INT, 0); } public void free() { diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/WindowUtils.java b/client/src/main/java/nl/andrewl/aos2_client/render/WindowUtils.java new file mode 100644 index 0000000..c5d0bbb --- /dev/null +++ b/client/src/main/java/nl/andrewl/aos2_client/render/WindowUtils.java @@ -0,0 +1,55 @@ +package nl.andrewl.aos2_client.render; + +import org.lwjgl.glfw.Callbacks; +import org.lwjgl.glfw.GLFWErrorCallback; +import org.lwjgl.opengl.GL; + +import static org.lwjgl.glfw.GLFW.*; +import static org.lwjgl.opengl.GL11.*; + +public class WindowUtils { + public static WindowInfo initUI() { + GLFWErrorCallback.createPrint(System.err).set(); + if (!glfwInit()) throw new IllegalStateException("Could not initialize GLFW."); + glfwDefaultWindowHints(); + glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); + glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); + + var vidMode = glfwGetVideoMode(glfwGetPrimaryMonitor()); + if (vidMode == null) throw new IllegalStateException("Could not get information about the primary monitory."); + long windowHandle = glfwCreateWindow(vidMode.width(), vidMode.height(), "Ace of Shades 2", glfwGetPrimaryMonitor(), 0); + if (windowHandle == 0) throw new RuntimeException("Failed to create GLFW window."); + + glfwSetKeyCallback(windowHandle, (window, key, scancode, action, mods) -> { + if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE) { + glfwSetWindowShouldClose(windowHandle, true); + } + }); + + glfwSetInputMode(windowHandle, GLFW_CURSOR, GLFW_CURSOR_DISABLED); + glfwSetInputMode(windowHandle, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE); + + glfwSetWindowPos(windowHandle, 0, 0); + glfwSetCursorPos(windowHandle, 0, 0); + + glfwMakeContextCurrent(windowHandle); + glfwSwapInterval(1); + glfwShowWindow(windowHandle); + + GL.createCapabilities(); +// GLUtil.setupDebugMessageCallback(System.out); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glEnable(GL_CULL_FACE); + glEnable(GL_DEPTH_TEST); + glCullFace(GL_BACK); + + return new WindowInfo(windowHandle, vidMode.width(), vidMode.height()); + } + + public static void clearUI(long windowHandle) { + Callbacks.glfwFreeCallbacks(windowHandle); + glfwDestroyWindow(windowHandle); + glfwTerminate(); + glfwSetErrorCallback(null).free(); + } +} diff --git a/core/pom.xml b/core/pom.xml index 390aea1..3425060 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -35,6 +35,12 @@ record-net v1.2.1 + + + net.openhft + zero-allocation-hashing + 0.15 + diff --git a/core/src/main/java/nl/andrewl/aos_core/Net.java b/core/src/main/java/nl/andrewl/aos_core/Net.java index d88c571..29bc5ab 100644 --- a/core/src/main/java/nl/andrewl/aos_core/Net.java +++ b/core/src/main/java/nl/andrewl/aos_core/Net.java @@ -1,6 +1,10 @@ package nl.andrewl.aos_core; +import nl.andrewl.aos_core.net.ChunkHashMessage; +import nl.andrewl.aos_core.net.ConnectAcceptMessage; +import nl.andrewl.aos_core.net.ConnectRejectMessage; import nl.andrewl.aos_core.net.ConnectRequestMessage; +import nl.andrewl.aos_core.net.udp.DatagramInit; import nl.andrewl.record_net.Message; import nl.andrewl.record_net.Serializer; import nl.andrewl.record_net.util.ExtendedDataInputStream; @@ -20,6 +24,10 @@ public final class Net { private static final Serializer serializer = new Serializer(); static { serializer.registerType(1, ConnectRequestMessage.class); + serializer.registerType(2, ConnectAcceptMessage.class); + serializer.registerType(3, ConnectRejectMessage.class); + serializer.registerType(4, DatagramInit.class); + serializer.registerType(5, ChunkHashMessage.class); } public static ExtendedDataInputStream getInputStream(InputStream in) { diff --git a/core/src/main/java/nl/andrewl/aos_core/model/Chunk.java b/core/src/main/java/nl/andrewl/aos_core/model/Chunk.java index d3dc5a5..1b916dd 100644 --- a/core/src/main/java/nl/andrewl/aos_core/model/Chunk.java +++ b/core/src/main/java/nl/andrewl/aos_core/model/Chunk.java @@ -1,5 +1,6 @@ package nl.andrewl.aos_core.model; +import net.openhft.hashing.LongHashFunction; import org.joml.Vector3f; import org.joml.Vector3i; @@ -104,6 +105,10 @@ public class Chunk { return sb.toString(); } + public long blockHash() { + return LongHashFunction.xx3(0).hashBytes(blocks); + } + public static Chunk random(Vector3i position, Random rand) { Chunk c = new Chunk(position); for (int i = 0; i < TOTAL_SIZE; i++) { diff --git a/core/src/main/java/nl/andrewl/aos_core/model/Player.java b/core/src/main/java/nl/andrewl/aos_core/model/Player.java index b51ce8c..408de4c 100644 --- a/core/src/main/java/nl/andrewl/aos_core/model/Player.java +++ b/core/src/main/java/nl/andrewl/aos_core/model/Player.java @@ -8,11 +8,33 @@ public class Player { private final Vector3f velocity; private final Vector2f orientation; private final String username; + private final int id; - public Player(String username) { + public Player(int id, String username) { this.position = new Vector3f(); this.velocity = new Vector3f(); this.orientation = new Vector2f(); + this.id = id; this.username = username; } + + public Vector3f getPosition() { + return position; + } + + public Vector3f getVelocity() { + return velocity; + } + + public Vector2f getOrientation() { + return orientation; + } + + public String getUsername() { + return username; + } + + public int getId() { + return id; + } } diff --git a/core/src/main/java/nl/andrewl/aos_core/net/ChunkHashMessage.java b/core/src/main/java/nl/andrewl/aos_core/net/ChunkHashMessage.java new file mode 100644 index 0000000..b043c10 --- /dev/null +++ b/core/src/main/java/nl/andrewl/aos_core/net/ChunkHashMessage.java @@ -0,0 +1,17 @@ +package nl.andrewl.aos_core.net; + +import nl.andrewl.record_net.Message; + +/** + * A message sent by the client, which contains a hash of a chunk, so that the + * server can determine if it's up-to-date, or if the server needs to send the + * latest chunk data to the user. + * @param cx The chunk x coordinate. + * @param cy The chunk y coordinate. + * @param cz The chunk z coordinate. + * @param hash The hash value of the chunk. + */ +public record ChunkHashMessage( + int cx, int cy, int cz, + long hash +) implements Message {} diff --git a/core/src/main/java/nl/andrewl/aos_core/net/ConnectAcceptMessage.java b/core/src/main/java/nl/andrewl/aos_core/net/ConnectAcceptMessage.java index 0f7f46a..5208148 100644 --- a/core/src/main/java/nl/andrewl/aos_core/net/ConnectAcceptMessage.java +++ b/core/src/main/java/nl/andrewl/aos_core/net/ConnectAcceptMessage.java @@ -2,4 +2,11 @@ package nl.andrewl.aos_core.net; import nl.andrewl.record_net.Message; -public record ConnectAcceptMessage () implements Message {} +/** + * The message that's sent by the server to indicate that a connecting client + * has been accepted and can join the server. + * @param clientId The client's id. + */ +public record ConnectAcceptMessage ( + int clientId +) implements Message {} diff --git a/core/src/main/java/nl/andrewl/aos_core/net/ConnectRejectMessage.java b/core/src/main/java/nl/andrewl/aos_core/net/ConnectRejectMessage.java index 5bed255..b96aa0c 100644 --- a/core/src/main/java/nl/andrewl/aos_core/net/ConnectRejectMessage.java +++ b/core/src/main/java/nl/andrewl/aos_core/net/ConnectRejectMessage.java @@ -2,4 +2,10 @@ package nl.andrewl.aos_core.net; import nl.andrewl.record_net.Message; -public record ConnectRejectMessage(String reason) implements Message {} +/** + * A message that's sent by the server when a connecting client is rejected. + * @param reason The reason for the rejection. + */ +public record ConnectRejectMessage( + String reason +) implements Message {} diff --git a/core/src/main/java/nl/andrewl/aos_core/net/ConnectRequestMessage.java b/core/src/main/java/nl/andrewl/aos_core/net/ConnectRequestMessage.java index 91d52e8..2e0f3c6 100644 --- a/core/src/main/java/nl/andrewl/aos_core/net/ConnectRequestMessage.java +++ b/core/src/main/java/nl/andrewl/aos_core/net/ConnectRequestMessage.java @@ -2,4 +2,4 @@ package nl.andrewl.aos_core.net; import nl.andrewl.record_net.Message; -public record ConnectRequestMessage(String username, int udpPort) implements Message {} +public record ConnectRequestMessage(String username) implements Message {} diff --git a/core/src/main/java/nl/andrewl/aos_core/net/TcpReceiver.java b/core/src/main/java/nl/andrewl/aos_core/net/TcpReceiver.java new file mode 100644 index 0000000..da207b1 --- /dev/null +++ b/core/src/main/java/nl/andrewl/aos_core/net/TcpReceiver.java @@ -0,0 +1,39 @@ +package nl.andrewl.aos_core.net; + +import nl.andrewl.aos_core.Net; +import nl.andrewl.record_net.Message; +import nl.andrewl.record_net.util.ExtendedDataInputStream; + +import java.io.EOFException; +import java.io.IOException; +import java.net.SocketException; +import java.util.function.Consumer; + +public class TcpReceiver implements Runnable { + private final ExtendedDataInputStream in; + private final Consumer messageConsumer; + + public TcpReceiver(ExtendedDataInputStream in, Consumer messageConsumer) { + this.in = in; + this.messageConsumer = messageConsumer; + } + + @Override + public void run() { + while (true) { + try { + Message msg = Net.read(in); + messageConsumer.accept(msg); + } catch (SocketException e) { + if (e.getMessage().equals("Socket closed")) { + return; + } + e.printStackTrace(); + } catch (EOFException e) { + return; + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} diff --git a/core/src/main/java/nl/andrewl/aos_core/net/UdpMessageHandler.java b/core/src/main/java/nl/andrewl/aos_core/net/UdpMessageHandler.java new file mode 100644 index 0000000..643d522 --- /dev/null +++ b/core/src/main/java/nl/andrewl/aos_core/net/UdpMessageHandler.java @@ -0,0 +1,10 @@ +package nl.andrewl.aos_core.net; + +import nl.andrewl.record_net.Message; + +import java.net.DatagramPacket; + +@FunctionalInterface +public interface UdpMessageHandler { + void handle(Message msg, DatagramPacket packet); +} diff --git a/core/src/main/java/nl/andrewl/aos_core/net/UdpReceiver.java b/core/src/main/java/nl/andrewl/aos_core/net/UdpReceiver.java new file mode 100644 index 0000000..27b3e44 --- /dev/null +++ b/core/src/main/java/nl/andrewl/aos_core/net/UdpReceiver.java @@ -0,0 +1,44 @@ +package nl.andrewl.aos_core.net; + +import nl.andrewl.aos_core.Net; +import nl.andrewl.record_net.Message; + +import java.io.EOFException; +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.SocketException; + +public class UdpReceiver implements Runnable { + public static final short MAX_PACKET_SIZE = 1400; + + private final DatagramSocket socket; + private final UdpMessageHandler handler; + + public UdpReceiver(DatagramSocket socket, UdpMessageHandler handler) { + this.socket = socket; + this.handler = handler; + } + + @Override + public void run() { + byte[] buffer = new byte[MAX_PACKET_SIZE]; + DatagramPacket packet = new DatagramPacket(buffer, MAX_PACKET_SIZE); + while (true) { + try { + socket.receive(packet); + Message msg = Net.read(buffer); + handler.handle(msg, packet); + } catch (SocketException e) { + if (e.getMessage().equals("Socket closed")) { + return; + } + e.printStackTrace(); + } catch (EOFException e) { + return; + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} diff --git a/core/src/main/java/nl/andrewl/aos_core/net/udp/ChunkUpdateMessage.java b/core/src/main/java/nl/andrewl/aos_core/net/udp/ChunkUpdateMessage.java new file mode 100644 index 0000000..e5b4750 --- /dev/null +++ b/core/src/main/java/nl/andrewl/aos_core/net/udp/ChunkUpdateMessage.java @@ -0,0 +1,19 @@ +package nl.andrewl.aos_core.net.udp; + +import nl.andrewl.record_net.Message; + +/** + * A message that's sent to clients when a block in a chunk is updated. + * @param cx The chunk x coordinate. + * @param cy The chunk y coordinate. + * @param cz The chunk z coordinate. + * @param lx The local x coordinate in the chunk. + * @param ly The local y coordinate in the chunk. + * @param lz The local z coordinate in the chunk. + * @param newBlock The new block data in the specified position. + */ +public record ChunkUpdateMessage( + int cx, int cy, int cz, + int lx, int ly, int lz, + byte newBlock +) implements Message {} diff --git a/core/src/main/java/nl/andrewl/aos_core/net/udp/DatagramInit.java b/core/src/main/java/nl/andrewl/aos_core/net/udp/DatagramInit.java index 670c5a7..80dbd19 100644 --- a/core/src/main/java/nl/andrewl/aos_core/net/udp/DatagramInit.java +++ b/core/src/main/java/nl/andrewl/aos_core/net/udp/DatagramInit.java @@ -2,4 +2,11 @@ package nl.andrewl.aos_core.net.udp; import nl.andrewl.record_net.Message; -public record DatagramInit() implements Message {} +/** + * The message that's sent initially by the client, and responded to by the + * server, when a client is establishing a UDP "connection" to the server. + * @param clientId The client's id. + */ +public record DatagramInit( + int clientId +) implements Message {} diff --git a/design/net.md b/design/net.md index a50d3c8..a29c024 100644 --- a/design/net.md +++ b/design/net.md @@ -8,9 +8,9 @@ When referring to the names of packets, we will assume a common package name of ### Player Connection This workflow is involved in the establishment of a connection between the client and server. -1. Player sends a `ConnectRequestMessage` via TCP, immediately upon opening a socket connection. It contains the player's desired `username`, and their `udpPort` that they will use to connect. -2. The server will respond with either a `ConnectRejectMessage` with a `reason` for the rejection, or a `ConnectAcceptMessage`. -3. If the player received an acceptance message, they will then send a `DatagramInit` to the server's UDP socket (on the same address/port). The player should keep sending such an init message until they receive a `DatagramInit` message echoed back as a response. The player should then stop sending init messages, and expect to begin receiving normal communication data through the datagram socket. +1. Player sends a `ConnectRequestMessage` via TCP, immediately upon opening a socket connection. It contains the player's desired `username`. +2. The server will respond with either a `ConnectRejectMessage` with a `reason` for the rejection, or a `ConnectAcceptMessage` containing the client's `clientId`. +3. If the player received an acceptance message, they will then send a `DatagramInit` to the server's UDP socket (on the same address/port) containing the `clientId` received in the `ConnectAcceptMessage`. The player should keep sending such an init message until they receive a `DatagramInit` message echoed back as a response. The player should then stop sending init messages, and expect to begin receiving normal communication data through the datagram socket. ### World Data A combination of TCP and UDP communication is used to ensure that all connected clients have the latest information about the state of the world. diff --git a/server/src/main/java/nl/andrewl/aos2_server/ClientHandler.java b/server/src/main/java/nl/andrewl/aos2_server/ClientCommunicationHandler.java similarity index 61% rename from server/src/main/java/nl/andrewl/aos2_server/ClientHandler.java rename to server/src/main/java/nl/andrewl/aos2_server/ClientCommunicationHandler.java index 15e1568..7000906 100644 --- a/server/src/main/java/nl/andrewl/aos2_server/ClientHandler.java +++ b/server/src/main/java/nl/andrewl/aos2_server/ClientCommunicationHandler.java @@ -1,85 +1,73 @@ package nl.andrewl.aos2_server; import nl.andrewl.aos_core.Net; +import nl.andrewl.aos_core.model.Player; +import nl.andrewl.aos_core.net.ConnectAcceptMessage; import nl.andrewl.aos_core.net.ConnectRejectMessage; import nl.andrewl.aos_core.net.ConnectRequestMessage; +import nl.andrewl.aos_core.net.TcpReceiver; import nl.andrewl.record_net.Message; import nl.andrewl.record_net.util.ExtendedDataInputStream; import nl.andrewl.record_net.util.ExtendedDataOutputStream; -import java.io.EOFException; import java.io.IOException; -import java.net.*; - -public class ClientHandler extends Thread { - private static int nextThreadId = 1; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.Socket; +public class ClientCommunicationHandler { private final Server server; private final Socket socket; private final DatagramSocket datagramSocket; private final ExtendedDataInputStream in; private final ExtendedDataOutputStream out; - private volatile boolean running; private InetAddress clientAddress; private int clientUdpPort; + private Player player; - public ClientHandler(Server server, Socket socket, DatagramSocket datagramSocket) throws IOException { - super("aos-client-handler-" + nextThreadId++); + public ClientCommunicationHandler(Server server, Socket socket, DatagramSocket datagramSocket) throws IOException { this.server = server; this.socket = socket; this.datagramSocket = datagramSocket; this.in = Net.getInputStream(socket.getInputStream()); this.out = Net.getOutputStream(socket.getOutputStream()); + establishConnection(); + new Thread(new TcpReceiver(in, this::handleTcpMessage)).start(); } public void shutdown() { - running = false; - } - - @Override - public void run() { - running = true; - establishConnection(); - while (running) { - try { - Message msg = Net.read(in); - } catch (SocketException e) { - if (e.getMessage().equals("Socket closed") | e.getMessage().equals("Connection reset")) { - shutdown(); - } else { - e.printStackTrace(); - } - } catch (EOFException e) { - shutdown(); - } catch (IOException e) { - e.printStackTrace(); - shutdown(); - } - } - } - - private void establishConnection() { try { - socket.setSoTimeout(1000); - } catch (SocketException e) { - throw new RuntimeException(e); + socket.close(); + } catch (IOException e) { + e.printStackTrace(); } + } + + public void setClientUdpPort(int port) { + this.clientUdpPort = port; + } + + private void handleTcpMessage(Message msg) { + System.out.println("Message received from client " + player.getUsername() + ": " + msg); + } + + private void establishConnection() throws IOException { + socket.setSoTimeout(1000); boolean connectionEstablished = false; int attempts = 0; while (!connectionEstablished && attempts < 100) { try { Message msg = Net.read(in); if (msg instanceof ConnectRequestMessage connectMsg) { + // Try to set the TCP timeout back to 0 now that we've got the correct request. + socket.setSoTimeout(0); this.clientAddress = socket.getInetAddress(); - this.clientUdpPort = connectMsg.udpPort(); System.out.println("Player connected: " + connectMsg.username()); connectionEstablished = true; - try { - socket.setSoTimeout(0); - } catch (SocketException e) { - throw new RuntimeException(e); - } + this.player = server.registerPlayer(this, connectMsg.username()); + Net.write(new ConnectAcceptMessage(player.getId()), out); } } catch (IOException e) { e.printStackTrace(); @@ -93,11 +81,11 @@ public class ClientHandler extends Thread { e.printStackTrace(); } System.out.println("Player couldn't connect after " + attempts + " attempts. Aborting."); - shutdown(); + socket.close(); } } - private void sendDatagramPacket(Message msg) { + public void sendDatagramPacket(Message msg) { try { sendDatagramPacket(Net.write(msg)); } catch (IOException e) { @@ -105,12 +93,12 @@ public class ClientHandler extends Thread { } } - private void sendDatagramPacket(byte[] data) { + public void sendDatagramPacket(byte[] data) { DatagramPacket packet = new DatagramPacket(data, data.length, clientAddress, clientUdpPort); sendDatagramPacket(packet); } - private void sendDatagramPacket(DatagramPacket packet) { + public void sendDatagramPacket(DatagramPacket packet) { try { packet.setAddress(clientAddress); packet.setPort(clientUdpPort); diff --git a/server/src/main/java/nl/andrewl/aos2_server/Server.java b/server/src/main/java/nl/andrewl/aos2_server/Server.java index 398a685..365f82e 100644 --- a/server/src/main/java/nl/andrewl/aos2_server/Server.java +++ b/server/src/main/java/nl/andrewl/aos2_server/Server.java @@ -1,22 +1,26 @@ package nl.andrewl.aos2_server; +import nl.andrewl.aos_core.model.Player; +import nl.andrewl.aos_core.net.UdpReceiver; +import nl.andrewl.aos_core.net.udp.DatagramInit; +import nl.andrewl.record_net.Message; + import java.io.IOException; -import java.net.DatagramSocket; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.SocketException; +import java.net.*; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Set; public class Server implements Runnable { private final ServerSocket serverSocket; private final DatagramSocket datagramSocket; private volatile boolean running; - private Set clientHandlers; - public static void main(String[] args) throws IOException { - new Server().run(); - } + private int nextClientId = 1; + private final Set clientHandlers; + private final Map players; + private final Map playerClientHandlers; public Server() throws IOException { this.serverSocket = new ServerSocket(24464, 5); @@ -24,22 +28,49 @@ public class Server implements Runnable { this.datagramSocket = new DatagramSocket(24464); this.datagramSocket.setReuseAddress(true); this.clientHandlers = new HashSet<>(); + this.players = new HashMap<>(); + this.playerClientHandlers = new HashMap<>(); } @Override public void run() { running = true; + new Thread(new UdpReceiver(datagramSocket, this::handleUdpMessage)).start(); System.out.println("Started AOS2-Server on TCP/UDP port " + serverSocket.getLocalPort() + "; now accepting connections."); while (running) { acceptClientConnection(); } + datagramSocket.close(); + for (var handler : clientHandlers) handler.shutdown(); + } + + public void handleUdpMessage(Message msg, DatagramPacket packet) { + // Echo any init message from known clients. + if (msg instanceof DatagramInit init) { + var handler = getHandler(init.clientId()); + if (handler != null) { + handler.setClientUdpPort(packet.getPort()); + handler.sendDatagramPacket(msg); + } + } + } + + public synchronized Player registerPlayer(ClientCommunicationHandler handler, String username) { + Player player = new Player(nextClientId++, username); + players.put(player.getId(), player); + playerClientHandlers.put(player.getId(), handler); + System.out.println("Registered player " + username + " with id " + player.getId()); + return player; + } + + public ClientCommunicationHandler getHandler(int id) { + return playerClientHandlers.get(id); } private void acceptClientConnection() { try { Socket clientSocket = serverSocket.accept(); - ClientHandler handler = new ClientHandler(this, clientSocket, datagramSocket); - handler.start(); + ClientCommunicationHandler handler = new ClientCommunicationHandler(this, clientSocket, datagramSocket); clientHandlers.add(handler); } catch (IOException e) { if (e instanceof SocketException && !this.running && e.getMessage().equalsIgnoreCase("Socket closed")) { @@ -48,4 +79,8 @@ public class Server implements Runnable { e.printStackTrace(); } } + + public static void main(String[] args) throws IOException { + new Server().run(); + } }