Added successful client connection flow.

This commit is contained in:
Andrew Lalis 2022-07-06 20:20:15 +02:00
parent 1bf7074b76
commit 682f9f9bc2
20 changed files with 514 additions and 155 deletions

View File

@ -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);
}
// 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);
}
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));
private InetAddress serverAddress;
private int serverPort;
private String username;
private CommunicationHandler communicationHandler;
private volatile boolean running;
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);
public Client(InetAddress serverAddress, int serverPort, String username) {
this.serverAddress = serverAddress;
this.serverPort = serverPort;
this.username = username;
this.communicationHandler = new CommunicationHandler();
}
chunkRenderer.free();
Callbacks.glfwFreeCallbacks(windowHandle);
glfwDestroyWindow(windowHandle);
glfwTerminate();
glfwSetErrorCallback(null).free();
@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!");
}
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);
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());
}
}

View File

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

View File

@ -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() {

View File

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

View File

@ -35,6 +35,12 @@
<artifactId>record-net</artifactId>
<version>v1.2.1</version>
</dependency>
<!-- https://github.com/OpenHFT/Zero-Allocation-Hashing -->
<dependency>
<groupId>net.openhft</groupId>
<artifactId>zero-allocation-hashing</artifactId>
<version>0.15</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
<dependency>

View File

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

View File

@ -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++) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Message> messageConsumer;
public TcpReceiver(ExtendedDataInputStream in, Consumer<Message> 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();
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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();
socket.close();
} catch (IOException e) {
e.printStackTrace();
shutdown();
}
}
}
private void establishConnection() {
try {
socket.setSoTimeout(1000);
} catch (SocketException e) {
throw new RuntimeException(e);
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);

View File

@ -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<ClientHandler> clientHandlers;
public static void main(String[] args) throws IOException {
new Server().run();
}
private int nextClientId = 1;
private final Set<ClientCommunicationHandler> clientHandlers;
private final Map<Integer, Player> players;
private final Map<Integer, ClientCommunicationHandler> 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();
}
}