Build heartbeat chain
This commit is contained in:
1 parent
909718d8d5
commit
f946ebef1e
5 files changed
+168
-117
No files matched your search
@@ -1,7 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Numerics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Mistox.ResevedPackets;
|
||||
|
||||
@@ -13,15 +16,21 @@ namespace Mistox.Client {
|
||||
public string UserName { get; private set; } = ""; // My UserName
|
||||
public bool Alive { get; set; } = true; // Is the server running
|
||||
public bool isAuthenticated { get; private set; } = false; // Is the server authenticated
|
||||
public ConcurrentDictionary<IPEndPoint, mUDPClient> clientConnections { get; private set; } // UserName, Connection
|
||||
public ConcurrentDictionary<IPEndPoint, mUDPClient> clientConnections { get; private set; } = new(); // UserName, Connection
|
||||
public Vector3 Position { get; set; } = new Vector3(0, 0, 0); // My world position
|
||||
public float MaxNetworkRenderDistance { get; private set; } = 0; // The distance for connections
|
||||
|
||||
public mTCPClient ServerConnection {get; private set; } // Global TCP Server Connection
|
||||
public UdpClient ClientSocket { get; private set; } // Global UDP Sender
|
||||
public UDPReceiver UdpReceiver { get; private set; } // Global UDP Receiver
|
||||
|
||||
private readonly string ConnectionKey = ""; // Server Connection Key
|
||||
private readonly int Port = 0; // The port of the connected socket
|
||||
private readonly int HeartbeatInterval = 5000; // How often should we heartbeat
|
||||
|
||||
public MistoxClient(string userName, string connectionKey, IPEndPoint Server) {
|
||||
// Create and listen on UDP
|
||||
Port = Server.Port;
|
||||
ClientSocket = new UdpClient( Server.Port );
|
||||
UdpReceiver = new UDPReceiver( this );
|
||||
UserName = userName;
|
||||
@@ -33,15 +42,79 @@ namespace Mistox.Client {
|
||||
ServerConnection = new mTCPClient( this, Server );
|
||||
Console.WriteLine( "Connected to the server sucessfully" );
|
||||
Alive = true;
|
||||
} catch( Exception e ) {
|
||||
} catch ( Exception e ) {
|
||||
Console.WriteLine( "An error has occured with the connection to the server. Error { " );
|
||||
Console.WriteLine( e.ToString() );
|
||||
Console.WriteLine( "}" );
|
||||
Alive = false;
|
||||
}
|
||||
|
||||
// Start the heartbeat
|
||||
_ = RunAsync(Heartbeat);
|
||||
}
|
||||
|
||||
public async Task TryAuth() {
|
||||
async Task RunAsync(Func<Task> work) {
|
||||
try{
|
||||
await work();
|
||||
} catch (Exception ex) {
|
||||
Console.WriteLine($"Error during background packet processing: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Heartbeat
|
||||
/////////////////////////////
|
||||
|
||||
async Task Heartbeat() {
|
||||
while (Alive) {
|
||||
await SendServer(new HeartbeatPacket {
|
||||
Position = Position
|
||||
});
|
||||
Thread.Sleep(HeartbeatInterval);
|
||||
}
|
||||
}
|
||||
|
||||
void HeartbeatReceived(HeartbeatPacket packet) {
|
||||
// Build a HashSet of server IP addresses for O(1) removal checks
|
||||
var activeIps = new HashSet<IPAddress>(packet.ServerList.Count);
|
||||
for (int i = 0; i < packet.ServerList.Count; i++) {
|
||||
activeIps.Add(packet.ServerList[i].IpAddress);
|
||||
}
|
||||
|
||||
// Add users that joined
|
||||
foreach (UserTableObject cur in packet.ServerList) {
|
||||
IPEndPoint remoteHost = new IPEndPoint(cur.IpAddress, Port);
|
||||
if (!clientConnections.ContainsKey(remoteHost)) {
|
||||
mUDPClient newConnection = new mUDPClient(this, remoteHost);
|
||||
clientConnections[remoteHost] = newConnection;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove users that left
|
||||
foreach (IPEndPoint key in clientConnections.Keys) {
|
||||
if (!activeIps.Contains(key.Address)) {
|
||||
clientConnections.TryRemove(key, out _);
|
||||
}
|
||||
}
|
||||
|
||||
// Update distance status
|
||||
float maxDistanceSq = MaxNetworkRenderDistance * MaxNetworkRenderDistance;
|
||||
foreach (mUDPClient cur in clientConnections.Values) {
|
||||
float dx = Position.X - cur.Position.X;
|
||||
float dy = Position.Y - cur.Position.Y;
|
||||
float dz = Position.Z - cur.Position.Z;
|
||||
|
||||
// Fixed bug (dz * dz instead of dz + dz) & avoided Sqrt
|
||||
float distanceSq = (dx * dx) + (dy * dy) + (dz * dz);
|
||||
cur.WithinRange = distanceSq < maxDistanceSq;
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Auth
|
||||
/////////////////////////////
|
||||
|
||||
async Task TryAuth() {
|
||||
Console.WriteLine("Attempting to authenticate against the server...");
|
||||
await ServerConnection.Send(new UserAuthPacket {
|
||||
UserName = UserName,
|
||||
@@ -49,71 +122,18 @@ namespace Mistox.Client {
|
||||
});
|
||||
}
|
||||
|
||||
public async Task CheckAuthStatus(UserAuthPacket packet) {
|
||||
async Task CheckAuthStatus(UserAuthPacket packet) {
|
||||
if (packet.AuthSuccessful == 1) {
|
||||
isAuthenticated = true;
|
||||
}
|
||||
ServerName = packet.ServerName;
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Receive Methods
|
||||
/////////////////////////////
|
||||
|
||||
public void UserConnected(UserConnectedPacket packet) {
|
||||
IPEndPoint remoteUser = new IPEndPoint(packet.ClientIPAddress, packet.ClientPort);
|
||||
mUDPClient user = new mUDPClient(this, remoteUser) {
|
||||
UserName = packet.UserName
|
||||
};
|
||||
clientConnections[remoteUser] = user;
|
||||
InvokeOnUserConnected(user);
|
||||
}
|
||||
|
||||
public void UserDisconnected(UserDisconnectedPacket packet) {
|
||||
mUDPClient client = null;
|
||||
foreach(mUDPClient cur in clientConnections.Values) {
|
||||
if (cur.UserName.ToLower() == packet.UserName.ToLower()) {
|
||||
client = cur;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (client != null) {
|
||||
InvokeOnUserDisconnected( client );
|
||||
client.Alive = false;
|
||||
client.Dispose();
|
||||
clientConnections[client._remoteHost] = null;
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Events
|
||||
/////////////////////////////
|
||||
|
||||
public event Action<dynamic> onDataReceived;
|
||||
public void InvokeOnReceived(dynamic DataPacket, mUDPClient remote = null){
|
||||
switch (DataPacket as object) {
|
||||
case UserConnectedPacket packet:
|
||||
UserConnected(packet);
|
||||
break;
|
||||
case UserDisconnectedPacket packet:
|
||||
UserDisconnected(packet);
|
||||
break;
|
||||
case UserAuthPacket packet:
|
||||
Task.Run(async () => {
|
||||
try {
|
||||
await CheckAuthStatus(packet);
|
||||
} catch {}
|
||||
});
|
||||
break;
|
||||
case null:
|
||||
Console.WriteLine("Received a null packet.");
|
||||
break;
|
||||
default:
|
||||
onDataReceived?.Invoke(DataPacket);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// When (self) connects to the server
|
||||
public event Action onServerConnected;
|
||||
public void InvokeOnServerConnected(){
|
||||
Alive = true;
|
||||
@@ -125,28 +145,69 @@ namespace Mistox.Client {
|
||||
onServerConnected?.Invoke();
|
||||
}
|
||||
|
||||
// When any user connects to the server (self) hasn't seen yet
|
||||
public event Action<string> onUserJoined;
|
||||
public void InvokeOnUserJoined(string UserName){
|
||||
onUserJoined?.Invoke(UserName);
|
||||
}
|
||||
|
||||
// When user is within range of (self) and network rendering starts
|
||||
public event Action<mUDPClient> onUserConnected;
|
||||
public void InvokeOnUserConnected(mUDPClient ConnectedUser){
|
||||
onUserConnected?.Invoke(ConnectedUser);
|
||||
}
|
||||
|
||||
// When generic data is received
|
||||
public event Action<dynamic> onDataReceived;
|
||||
public void InvokeOnReceived(dynamic DataPacket, mUDPClient remote = null){
|
||||
switch (DataPacket as object) {
|
||||
case HeartbeatPacket packet:
|
||||
HeartbeatReceived(packet);
|
||||
break;
|
||||
case UserAuthPacket packet:
|
||||
_ = RunAsync(async () => {
|
||||
await CheckAuthStatus(packet);
|
||||
});
|
||||
break;
|
||||
case null:
|
||||
Console.WriteLine("Received a null packet.");
|
||||
break;
|
||||
default:
|
||||
onDataReceived?.Invoke(DataPacket);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// When user is no longer in range of (self) and network rendering stops
|
||||
public event Action<mUDPClient> onUserDisconnected;
|
||||
public void InvokeOnUserDisconnected(mUDPClient DisconnectedUser){
|
||||
onUserDisconnected?.Invoke(DisconnectedUser);
|
||||
}
|
||||
|
||||
// When any user disconnects from the server that isn't (self)
|
||||
public event Action<string> onUserLeft;
|
||||
public void InvokeOnUserLeft(string UserName){
|
||||
onUserLeft?.Invoke(UserName);
|
||||
}
|
||||
|
||||
// When (self) disconnects to the server
|
||||
public event Action onServerDisconnected;
|
||||
public void InvokeOnServerDisconnected(){
|
||||
Alive = false;
|
||||
onServerDisconnected?.Invoke();
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Send Methods
|
||||
/////////////////////////////
|
||||
|
||||
public async Task SendAllClients<Packet>(Packet data) {
|
||||
public async Task SendAllClientsInRange<Packet>(Packet data) {
|
||||
foreach( mUDPClient cur in clientConnections.Values ){
|
||||
if (cur.WithinRange) {
|
||||
await cur.Send( data );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendClient<Packet>(string UserName, Packet data) {
|
||||
foreach( mUDPClient cur in clientConnections.Values ) {
|
||||
|
||||
@@ -9,8 +9,6 @@ namespace Mistox.Server {
|
||||
|
||||
public class MistoxServer {
|
||||
|
||||
// Class Definitions
|
||||
|
||||
public string ServerName { get; private set; } = ""; // The name of the server
|
||||
public int MaxClients { get; set; } = 10; // The max players per server
|
||||
public float ConnectionRange { get; set; } = 40; // InGame distance for valid conections
|
||||
@@ -28,20 +26,18 @@ namespace Mistox.Server {
|
||||
Console.WriteLine( "The Server is initilized and waiting for clients to connect at port : " + ListenerAddress.Port );
|
||||
}
|
||||
|
||||
async Task ProcessAuthRequest(ServerConnection user, UserAuthPacket packet) {
|
||||
packet.ServerName = ServerName;
|
||||
if (packet.ConnectionKey == ConnectionKey) {
|
||||
packet.AuthSuccessful = 1;
|
||||
Connections.TryRemove(user.UserName, out _);
|
||||
user.UserName = packet.UserName;
|
||||
Connections[packet.UserName] = user;
|
||||
user.IsAuthenticated = true;
|
||||
} else {
|
||||
packet.AuthSuccessful = -1;
|
||||
async Task RunAsync(Func<Task> work) {
|
||||
try{
|
||||
await work();
|
||||
} catch (Exception ex) {
|
||||
Console.WriteLine($"Error during background packet processing: {ex.Message}");
|
||||
}
|
||||
await user.Send(packet);
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Heartbeat
|
||||
/////////////////////////////
|
||||
|
||||
async Task BuildServerList(ServerConnection user, HeartbeatPacket packet) {
|
||||
packet.ServerList = new List<UserTableObject>();
|
||||
foreach( ServerConnection cur in Connections.Values) {
|
||||
@@ -54,18 +50,32 @@ namespace Mistox.Server {
|
||||
await user.Send( packet );
|
||||
}
|
||||
|
||||
async Task RunAsync(Func<Task> work) {
|
||||
try{
|
||||
await work();
|
||||
} catch (Exception ex) {
|
||||
Console.WriteLine($"Error during background packet processing: {ex.Message}");
|
||||
/////////////////////////////
|
||||
// Auth
|
||||
/////////////////////////////
|
||||
|
||||
async Task ProcessAuthRequest(ServerConnection user, UserAuthPacket packet) {
|
||||
packet.ServerName = ServerName;
|
||||
if (packet.ConnectionKey == ConnectionKey) {
|
||||
packet.AuthSuccessful = 1;
|
||||
Connections.TryRemove(user.UserName, out _);
|
||||
user.UserName = packet.UserName;
|
||||
Connections[packet.UserName] = user;
|
||||
user.IsAuthenticated = true;
|
||||
InvokeUserConnected(user);
|
||||
} else {
|
||||
packet.AuthSuccessful = -1;
|
||||
}
|
||||
await user.Send(packet);
|
||||
}
|
||||
|
||||
// Functions
|
||||
/////////////////////////////
|
||||
// Events
|
||||
/////////////////////////////
|
||||
|
||||
public event Action<ServerConnection> onUserConnected;
|
||||
public void InvokeUserConnected( ServerConnection user ){
|
||||
Connections[user.UserName] = user;
|
||||
onUserConnected?.Invoke( user );
|
||||
}
|
||||
|
||||
@@ -78,6 +88,7 @@ namespace Mistox.Server {
|
||||
});
|
||||
break;
|
||||
case HeartbeatPacket packet when user.IsAuthenticated:
|
||||
user.Position = packet.Position;
|
||||
_ = RunAsync(async () => {
|
||||
await BuildServerList(user, packet);
|
||||
});
|
||||
@@ -93,9 +104,14 @@ namespace Mistox.Server {
|
||||
|
||||
public event Action<ServerConnection> onUserDisconnected;
|
||||
public void InvokeUserDisconnected( ServerConnection user ){
|
||||
Connections.Remove(user.UserName, out _);
|
||||
onUserDisconnected?.Invoke( user );
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Send Methods
|
||||
/////////////////////////////
|
||||
|
||||
public async Task SendAllClients<Packet>(Packet data) {
|
||||
foreach( ServerConnection cur in Connections.Values ) {
|
||||
await cur.Send(data);
|
||||
|
||||
@@ -26,7 +26,6 @@ namespace Mistox.Server {
|
||||
_client.NoDelay = true;
|
||||
_client.ReceiveTimeout = _parent.ConnectionTimeoutSeconds * 1000;
|
||||
|
||||
_parent.InvokeUserConnected( this );
|
||||
// Start the receive thread
|
||||
Task.Run(ReceiveThread);
|
||||
}
|
||||
@@ -93,7 +92,6 @@ namespace Mistox.Server {
|
||||
_sendLock.Dispose();
|
||||
_cts.Dispose();
|
||||
|
||||
_parent.Connections.TryRemove(UserName, out _);
|
||||
_parent.InvokeUserDisconnected(this);
|
||||
// Remove this item from the Dictionary on the Server for garbage collection
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ namespace Mistox.ResevedPackets {
|
||||
// Packet sent to all clients when a global position update occurrs
|
||||
[mPacket]
|
||||
public class HeartbeatPacket {
|
||||
public Vector3 Position { get; set; }
|
||||
public List<UserTableObject> ServerList { get; set; }
|
||||
}
|
||||
|
||||
|
||||
@@ -3,44 +3,19 @@ The server receive thread needs to push the user it was received from up the cha
|
||||
1. Allow the server to setup a server name Done
|
||||
2. Allow the client to setup a user name Done
|
||||
3. When a user connects add them to the Connections dictionary with a GUID. Done
|
||||
3. Have the client send the server a welcome packet Done
|
||||
4. Have the server use the welcome packet to fillout the user information on the server Done
|
||||
5. Respond to the client from the server with a server list of UDPEndpoints
|
||||
6. Instead of looping through a list of avalible users have a local user dictionary that can be used to lookup clients IPEndPoints
|
||||
4. Have the client send the server a welcome packet Done
|
||||
5. Have the server use the welcome packet to fillout the user information on the server Done
|
||||
6. Send the server the heartbeat location update every 5 seconds Done
|
||||
7. Respond to the client with a server list of UDPEndpoints and heartbeat locations Done
|
||||
8. Each client can figure out what connections it needs to make or drop via the list Done
|
||||
10. When the TCP connection drops update the heartbeat list so all the clients remove the connection Done
|
||||
11. Build a network cache table on the clients so obj's only send updates and not repeat packets
|
||||
|
||||
|
||||
Set the network bubble distance [ render distance ] on the server and pass to the clients
|
||||
Send a packet back to the client for if the lobby is full, notify them of why the connection failed
|
||||
|
||||
Test that the datapacket is formatted correctly. The packet size might be off from the actual packet size
|
||||
|
||||
Have each client update the ingame location to the server every 5 seconds [ Heartbeat ]
|
||||
For each user in the server calculate which other players are close enough for a connection
|
||||
So the server needs to hold a table of what clients a client is already connected to
|
||||
if a new player is now close enough update the client and the servers client table with that information
|
||||
have the clients udp together
|
||||
if a player goes out of the range update the clients that they no longer need the connection
|
||||
drop the player from the table and notify the end user they can close the connection
|
||||
|
||||
When user a joins a server
|
||||
The IP gets added to the servers master list
|
||||
The user needs to authenticate for username
|
||||
|
||||
when the server connection closes
|
||||
Notify all clients to drop the connections
|
||||
Remove the user from the server table
|
||||
|
||||
Use the Server TCP connection to notify all clients when a user disconnects
|
||||
|
||||
Only send updates across the network. Dont send same state events
|
||||
Need to setup a network buffer for Tagged Objects. ID
|
||||
|
||||
|
||||
Connection Flow:
|
||||
1. Connection to server is made working
|
||||
Server object is created working
|
||||
ServerConnection gets added to the Connections table working
|
||||
Server fires user connected event working
|
||||
|
||||
2. User sends an Auth Packet to server working
|
||||
Server responds with Auth sucess or fail and marks the users auth status in the server table. needs work
|
||||
After sucessfull auth share the users IPEndPoint with close connections not started
|
||||
|
||||
3. If the TCP connection closes not started
|
||||
1. If the TCP connection closes not started
|
||||
Disconnect the user and tell all clients that the user is gone not started
|
||||
Reference in new issue
Block a user