LobbyServer/LobbyClient/LobbyClient.cs

506 lines
22 KiB
C#

using LobbyServerDto;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
namespace Lobbies
{
public class LobbyClient : IDisposable
{
TcpLobbbyClient tcpClient = new TcpLobbbyClient();
private readonly ConcurrentQueue<LobbyClientEvent> events = new ConcurrentQueue<LobbyClientEvent>();
BufferRental bufferRental = new BufferRental(4096);
AutoResetEvent waitForExternalIp = new AutoResetEvent(false);
UdpEchoServer udpEchoServer = new UdpEchoServer();
private Dictionary<Guid, LobbyInfo> lobbyInformation = new Dictionary<Guid, LobbyInfo>();
private string? host;
private int port;
private int connectionId;
public string? externalIp;
public int externalPort;
public void Connect(string host, int port, CancellationToken cancellationToken)
{
this.host = host;
this.port = port;
try
{
cancellationToken.ThrowIfCancellationRequested();
using var cts = cancellationToken.Register(() =>
{
tcpClient.Stop();
});
tcpClient.DataReceived -= TcpClient_DataReceived;
tcpClient.DataReceived += TcpClient_DataReceived;
tcpClient.Disconnected -= TcpClient_Disconnected;
tcpClient.Disconnected += TcpClient_Disconnected;
_ = Task.Run(() => tcpClient.Connect(host, port));
}
catch
{
}
}
public IEnumerable<LobbyClientEvent> ReadEvents(int maxEvents)
{
if (events.Count > 0)
{
maxEvents = Math.Min(maxEvents, events.Count);
while (maxEvents > 0)
{
if(events.TryDequeue(out var _event))
{
yield return _event;
}
else
{
break;
}
}
}
}
public void HostLobby(Guid gameId, string name, int gameMode, int maxPlayerCount, string? password, int port)
{
udpEchoServer.Start(0);
byte[]? hash = null, salt = null;
if(!string.IsNullOrEmpty(password))
{
(hash, salt) = PasswordHash.Hash(password);
}
var lobbyCreate = new LobbyCreate()
{
GameId = gameId,
Name = name,
GameMode = gameMode,
MaxPlayerCount = maxPlayerCount,
PlayerCount = 0,
PasswordHash = hash,
PasswordSalt = salt,
HostIps = GatherLocalIpAddresses().ToArray(),
HostPort = port,
HostTryPort = udpEchoServer.Port
};
byte[] messageData = bufferRental.Rent();
var len = lobbyCreate.Serialize(messageData);
_ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); });
}
public void RequestLobbyHostInfo(Guid lobbyId, string? password)
{
byte[]? passwordHash = null;
if(!string.IsNullOrEmpty(password) && lobbyInformation.ContainsKey(lobbyId) && lobbyInformation[lobbyId].PasswordSalt != null)
passwordHash = PasswordHash.Hash(password, lobbyInformation[lobbyId].PasswordSalt!);
var lobbyCreate = new LobbyRequestHostInfo()
{
LobbyId = lobbyId,
PasswordHash = passwordHash,
};
byte[] messageData = bufferRental.Rent();
var len = lobbyCreate.Serialize(messageData);
_ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); });
}
/// <summary>
/// This callback is called if the nat punch requires the game server/client udp sockets sends a packet to a server so that
/// the external mapped port by the firewall can be seen by the external point.
/// </summary>
/// <param name="remoteEndpoint">The endpoint to send data to</param>
/// <param name="messageBuffer">The message byte buffer to send</param>
/// <param name="messageLength">Length of the data to send</param>
public delegate void SendUdpMessageCallback(IPEndPoint remoteEndpoint, byte[] messageBuffer, int messageLength);
/// <summary>
/// This function requests a nat punch for this client from the game host. First the external port for our game client is detected by
/// sending a udp packet to the lobby server who then forwards the nat punch request to the host with the seen external ip and port.
/// The game host then sends a udp packet to the client to open it's nat bridge and when done sends a event to the client via the lobby server.
/// The client receives the external port and ip seen by the lobby server for the host and can connect to the host.
/// </summary>
/// <param name="lobbyId">The lobby to request a nat punch for</param>
/// <param name="password">Optional password of lobby</param>
/// <param name="sendUdpToGetExternalPortMappingCallback">A callback to send udp data if you have a udp game client ready and bound</param>
public void RequestLobbyNatPunch(Guid lobbyId, string? password, SendUdpMessageCallback sendUdpToGetExternalPortMappingCallback)
{
byte[]? passwordHash = null;
if (!string.IsNullOrEmpty(password) && lobbyInformation.ContainsKey(lobbyId) && lobbyInformation[lobbyId].PasswordSalt != null)
passwordHash = PasswordHash.Hash(password, lobbyInformation[lobbyId].PasswordSalt!);
Task.Run(() =>
{
QueryExternalIpAndPort(sendUdpToGetExternalPortMappingCallback);
var lobbyRequestNatPunch = new LobbyRequestNatPunch()
{
LobbyId = lobbyId,
PasswordHash = passwordHash,
ClientIp = externalIp,
ClientPort = externalPort
};
byte[] messageData = bufferRental.Rent();
var len = lobbyRequestNatPunch.Serialize(messageData);
_ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); });
});
}
/// <summary>
/// This function requests a nat punch for this client from the game host. First the external port for our game client is detected by
/// sending a udp packet to the lobby server who then forwards the nat punch request to the host with the seen external ip and port.
/// The game host then sends a udp packet to the client to open it's nat bridge and when done sends a event to the client via the lobby server.
/// The client receives the external port and ip seen by the lobby server for the host and can connect to the host.
/// </summary>
/// <param name="lobbyId">The lobby to request a nat punch for</param>
/// <param name="password">Optional password of lobby</param>
/// <param name="port">The port the game client will later use. We create a udp socket on it and send a packet to the lobby server to get the firewall to map that port for a short period of time
/// after that the udp client will be disposed.</param>
public int RequestLobbyNatPunch(Guid lobbyId, string? password, int port = 0)
{
if(port < 0 && port > 65535)
throw new ArgumentOutOfRangeException(nameof(port));
byte[]? passwordHash = null;
if (!string.IsNullOrEmpty(password) && lobbyInformation.ContainsKey(lobbyId) && lobbyInformation[lobbyId].PasswordSalt != null)
passwordHash = PasswordHash.Hash(password, lobbyInformation[lobbyId].PasswordSalt!);
var udpEchoServer = new UdpEchoServer();
udpEchoServer.Start(port);
Task.Run(() =>
{
QueryExternalIpAndPort(udpEchoServer.Send);
udpEchoServer.Dispose();
var lobbyRequestNatPunch = new LobbyRequestNatPunch()
{
LobbyId = lobbyId,
PasswordHash = passwordHash,
ClientIp = externalIp,
ClientPort = externalPort
};
byte[] messageData = bufferRental.Rent();
var len = lobbyRequestNatPunch.Serialize(messageData);
_ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); });
});
return udpEchoServer.Port;
}
public void UpdateLobby(string name, int gameMode, int maxPlayerCount, int playerCount, string? password, int port)
{
byte[]? hash = null, salt = null;
if (!string.IsNullOrEmpty(password))
{
(hash, salt) = PasswordHash.Hash(password);
}
var lobbyUpdate = new LobbyUpdate()
{
Name = name,
GameMode = gameMode,
MaxPlayerCount = maxPlayerCount,
PlayerCount = playerCount,
PasswordHash = hash,
PasswordSalt = salt,
HostIps = GatherLocalIpAddresses().ToArray(),
HostPort = port,
HostTryPort = udpEchoServer.Port
};
byte[] messageData = bufferRental.Rent();
var len = lobbyUpdate.Serialize(messageData);
_ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); });
}
public void CloseLobby()
{
udpEchoServer.Stop();
var lobbyDelete = new LobbyDelete()
{
};
byte[] messageData = bufferRental.Rent();
var len = lobbyDelete.Serialize(messageData);
_ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); });
}
public void ObserveLobbies(Guid gameId)
{
var lobbiesObserve = new LobbiesObserve() { GameId = gameId };
byte[] messageData = bufferRental.Rent();
var len = lobbiesObserve.Serialize(messageData);
_ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); });
}
public void StopObservingLobbies()
{
var lobbiesStopObserve = new LobbiesStopObserve() { };
byte[] messageData = bufferRental.Rent();
var len = lobbiesStopObserve.Serialize(messageData);
_ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); });
}
public void NotifyLobbyNatPunchDone(int natPunchId, string externalIp, int externalPort)
{
var lobbyNatPunchDone = new LobbyNatPunchDone() { NatPunchId = natPunchId, ExternalIp = externalIp, ExternalPort = externalPort };
byte[] messageData = bufferRental.Rent();
var len = lobbyNatPunchDone.Serialize(messageData);
_ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); });
}
public Task TryDirectConnection(IPAddress[] ipAddressesToTry, int tryPort)
{
return Task.Run(() =>
{
string? error = string.Empty;
IPAddress? ret = null;
try
{
using (var waitForIpEvent = new AutoResetEvent(false))
using (var udpEchoClient = new UdpEchoServer())
{
udpEchoClient.Reached += (ep) =>
{
ret = ep.Address;
try
{
waitForIpEvent.Set();
udpEchoClient.Stop();
}
catch { }
};
udpEchoClient.Start(0);
foreach (var ip in ipAddressesToTry)
{
udpEchoClient.CheckConnectionPossible(new IPEndPoint(ip, tryPort));
}
if(!waitForIpEvent.WaitOne(500))
error = $"Timeout";
}
}
catch (Exception ex)
{
error = $"Exception: {ex}";
}
events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.DirectConnectionTestComplete, EventData = new DirectConnectionTestResult { DirectConnectionPossible = ret != null, IPAddress = ret, ErrorMessage = error } });
});
}
public static IPAddress[] GetIPsByName(string hostName, bool ip4Wanted, bool ip6Wanted)
{
// Check if the hostname is already an IPAddress
IPAddress? outIpAddress;
if (IPAddress.TryParse(hostName, out outIpAddress) == true)
return new IPAddress[] { outIpAddress };
//<----------
IPAddress[] addresslist = Dns.GetHostAddresses(hostName);
if (addresslist == null || addresslist.Length == 0)
return new IPAddress[0];
//<----------
if (ip4Wanted && ip6Wanted)
return addresslist;
//<----------
if (ip4Wanted)
return addresslist.Where(o => o.AddressFamily == AddressFamily.InterNetwork).ToArray();
//<----------
if (ip6Wanted)
return addresslist.Where(o => o.AddressFamily == AddressFamily.InterNetworkV6).ToArray();
//<----------
return new IPAddress[0];
}
public void QueryExternalIpAndPort(SendUdpMessageCallback sendUdpCallback)
{
byte[] messageData = bufferRental.Rent();
try
{
waitForExternalIp.Reset();
var queryExternalPortAndIp = new QueryExternalPortAndIp() { LobbyClientId = connectionId };
var len = queryExternalPortAndIp.Serialize(messageData);
var ip = GetIPsByName(host!, true, false).First();
do
{
sendUdpCallback(new IPEndPoint(ip, port), messageData, len);
}
while (!waitForExternalIp.WaitOne(100));
}
catch
{
}
finally
{
bufferRental.Return(messageData);
}
}
public void Stop()
{
waitForExternalIp.Set();
tcpClient.Stop();
}
private void TcpClient_Disconnected(bool clean, string error)
{
if(!clean)
events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.Failed, EventData = new LobbyClientDisconnectReason { WasError = true, ErrorMessage = error } });
else
events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.Disconnected, EventData = new LobbyClientDisconnectReason { WasError = false, ErrorMessage = string.Empty } });
}
private void TcpClient_DataReceived(int dataLength, Memory<byte> data)
{
try
{
if (dataLength > 0)
{
switch (LobbyMessageIdentifier.ReadLobbyMessageIdentifier(data.Span))
{
case LobbyNotFound.TypeId:
{
events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.LobbyJoinFailed, EventData = new LobbyJoinFailReason { Reason = LobbyJoinFailReason.FailReasons.LobbyNotFound, ErrorMessage = string.Empty } });
}
break;
case LobbyWrongPassword.TypeId:
{
events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.LobbyJoinFailed, EventData = new LobbyJoinFailReason { Reason = LobbyJoinFailReason.FailReasons.WrongPassword, ErrorMessage = string.Empty } });
}
break;
case LobbyClientConnectionInfo.TypeId:
{
var lobbyClientConnectionInfo = LobbyClientConnectionInfo.Deserialize(data.Span);
if (lobbyClientConnectionInfo != null)
{
connectionId = lobbyClientConnectionInfo.Id;
events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.Connected, EventData = null });
}
}
break;
case LobbyInfo.TypeId:
{
var lobbyInfo = LobbyInfo.Deserialize(data.Span);
if (lobbyInfo != null)
{
lobbyInformation[lobbyInfo.Id] = lobbyInfo;
events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.LobbyUpdate, EventData = lobbyInfo });
}
}
break;
case LobbyDelete.TypeId:
{
var lobbyDelete = LobbyDelete.Deserialize(data.Span);
if (lobbyDelete != null)
{
lobbyInformation.Remove(lobbyDelete.Id);
events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.LobbyDelete, EventData = lobbyDelete });
}
}
break;
case LobbyHostInfo.TypeId:
{
var lobbyHostInfo = LobbyHostInfo.Deserialize(data.Span);
if (lobbyHostInfo != null)
{
events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.LobbyHostInfo, EventData = lobbyHostInfo });
}
}
break;
case LobbyRequestNatPunch.TypeId:
{
var lobbyRequestNatPunch = LobbyRequestNatPunch.Deserialize(data.Span);
if (lobbyRequestNatPunch != null)
{
events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.LobbyRequestNatPunch, EventData = lobbyRequestNatPunch });
}
}
break;
case LobbyNatPunchDone.TypeId:
{
var lobbyNatPunchDone = LobbyNatPunchDone.Deserialize(data.Span);
if (lobbyNatPunchDone != null)
{
events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.LobbyNatPunchDone, EventData = lobbyNatPunchDone });
}
}
break;
case SeenExternalIpAndPort.TypeId:
{
var seenExternalIpAndPort = SeenExternalIpAndPort.Deserialize(data.Span);
if (seenExternalIpAndPort != null)
{
externalIp = seenExternalIpAndPort.Ip;
externalPort = seenExternalIpAndPort.Port;
waitForExternalIp.Set();
events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.ExternalIpAndPort, EventData = seenExternalIpAndPort });
}
}
break;
}
}
}
catch { }
}
public IEnumerable<IPAddress> GatherLocalIpAddresses()
{
foreach (NetworkInterface netInterface in NetworkInterface.GetAllNetworkInterfaces())
{
IPInterfaceProperties ipProps = netInterface.GetIPProperties();
foreach (UnicastIPAddressInformation addr in ipProps.UnicastAddresses)
{
yield return addr.Address;
}
}
}
public void Dispose()
{
waitForExternalIp.Dispose();
tcpClient.Dispose();
udpEchoServer.Dispose();
}
}
}