diff --git a/LobbyClient/LobbyClient.cs b/LobbyClient/LobbyClient.cs index fae50c3..e6a7ec6 100644 --- a/LobbyClient/LobbyClient.cs +++ b/LobbyClient/LobbyClient.cs @@ -2,6 +2,9 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; using System.Security.Cryptography; using System.Text; using System.Threading; @@ -14,8 +17,21 @@ namespace Lobbies TcpLobbbyClient tcpClient = new TcpLobbbyClient(); private readonly ConcurrentQueue events = new ConcurrentQueue(); BufferRental bufferRental = new BufferRental(4096); + + AutoResetEvent waitForExternalIp = new AutoResetEvent(false); + + 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(); @@ -30,9 +46,6 @@ namespace Lobbies tcpClient.Disconnected -= TcpClient_Disconnected; tcpClient.Disconnected += TcpClient_Disconnected; - tcpClient.Connected -= TcpClient_Connected; - tcpClient.Connected += TcpClient_Connected; - _ = Task.Run(() => tcpClient.Connect(host, port)); } catch @@ -93,19 +106,25 @@ namespace Lobbies _ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); }); } - public void RequestLobbyNatPunch(Guid lobbyId, string? password, string? clientIp, int clientPort) - { - var lobbyRequestNatPunch = new LobbyRequestNatPunch() - { - LobbyId = lobbyId, - PasswordHash = string.IsNullOrEmpty(password) ? null : SHA256.HashData(Encoding.UTF8.GetBytes(password)), - ClientIp = clientIp, - ClientPort = clientPort - }; - byte[] messageData = bufferRental.Rent(); - var len = lobbyRequestNatPunch.Serialize(messageData); - _ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); }); + public delegate void SendUdpMessageCallback(IPEndPoint remoteEndpoint, byte[] messageBuffer, int messageLength); + public void RequestLobbyNatPunch(Guid lobbyId, string? password, SendUdpMessageCallback sendUdpCallback) + { + Task.Run(() => + { + QueryExternalIpAndPort(sendUdpCallback); + var lobbyRequestNatPunch = new LobbyRequestNatPunch() + { + LobbyId = lobbyId, + PasswordHash = string.IsNullOrEmpty(password) ? null : SHA256.HashData(Encoding.UTF8.GetBytes(password)), + 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); }); + }); } public void UpdateLobby(string name, int gameMode, int maxPlayerCount, int playerCount, string? password, string? ip, int port) @@ -156,23 +175,75 @@ namespace Lobbies _ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); }); } - public void NotifyLobbyNatPunchDone(int natPunchId) + public void NotifyLobbyNatPunchDone(int natPunchId, string externalIp, int externalPort) { - var lobbyNatPunchDone = new LobbyNatPunchDone() { NatPunchId = natPunchId }; + 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 void Stop() + public static IPAddress[] GetIPsByName(string hostName, bool ip4Wanted, bool ip6Wanted) { - tcpClient.Stop(); + // 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]; } - private void TcpClient_Connected() + public void QueryExternalIpAndPort(SendUdpMessageCallback sendUdpCallback) { - events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.Connected, EventData = null }); + 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) @@ -183,38 +254,24 @@ namespace Lobbies events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.Disconnected, EventData = new LobbyClientDisconnectReason { WasError = false, ErrorMessage = string.Empty } }); } - private int PeekTypeId(ReadOnlySpan buffer) - { - int typeId = 0; - int shift = 0; - int offset = 0; - byte b; - do - { - { - // Check for a corrupted stream. Read a max of 5 bytes. - // In a future version, add a DataFormatException. - if (shift == 5 * 7) // 5 bytes max per Int32, shift += 7 - throw new FormatException("Format_Bad7BitInt32"); - - // ReadByte handles end of stream cases for us. - b = buffer[offset++]; - typeId |= (b & 0x7F) << shift; - shift += 7; - } - } while ((b & 0x80) != 0); - - return typeId; - } - private void TcpClient_DataReceived(int dataLength, Memory data) { try { if (dataLength > 0) { - switch (PeekTypeId(data.Span)) + switch (LobbyMessageIdentifier.ReadLobbyMessageIdentifier(data.Span)) { + 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); @@ -260,6 +317,19 @@ namespace Lobbies } } 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; } } } @@ -268,8 +338,9 @@ namespace Lobbies public void Dispose() { - tcpClient.Dispose(); + waitForExternalIp.Dispose(); + tcpClient.Dispose(); } - + } } diff --git a/LobbyClient/LobbyClientDisconnectReason.cs b/LobbyClient/LobbyClientDisconnectReason.cs index dd7c851..5862ba3 100644 --- a/LobbyClient/LobbyClientDisconnectReason.cs +++ b/LobbyClient/LobbyClientDisconnectReason.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Lobbies +namespace Lobbies { public class LobbyClientDisconnectReason { diff --git a/LobbyClient/LobbyClientEvent.cs b/LobbyClient/LobbyClientEvent.cs index 1a7f21c..b4a5770 100644 --- a/LobbyClient/LobbyClientEvent.cs +++ b/LobbyClient/LobbyClientEvent.cs @@ -1,27 +1,64 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Lobbies +namespace Lobbies { + /// + /// Contains a lobby client event + /// public class LobbyClientEvent { + /// + /// Identifier type of the event + /// public LobbyClientEventTypes EventType { get; internal set; } + /// + /// Optional data object, contains additional information for the event. See for possible values. + /// public object? EventData { get; internal set; } } + /// + /// The list of events that can occur. + /// public enum LobbyClientEventTypes { + /// + /// Client connected to lobby server. EventData is null. + /// Connected, + /// + /// Client disconnected from lobby server graceful. EventData is . + /// Disconnected, + /// + /// Connection lost to lobby server. EventData is LobbyClientDisconnectReason. + /// Failed, + /// + /// A lobby was added to the lobby list for the observed game id. EventData is with the lobby data. + /// LobbyAdd, + /// + /// A lobby was changed for the observed game id. EventData is with the lobby data. + /// LobbyUpdate, + /// + /// A lobby was removed from the lobby list for the observed game id. EventData is with the lobby id. + /// LobbyDelete, + /// + /// The game host shared his self known address. EventData is with the games hosts self known address, most likely internal. + /// LobbyHostInfo, + /// + /// A nat punch was requested by another LobbyClient to be performed by the host LobbyClient. EventData is with the clients seen external address to nat punch to. + /// LobbyRequestNatPunch, + /// + /// Game host has finished the nat punch. EventData is with the games hosts external address. + /// LobbyNatPunchDone, + /// + /// Response to a query of our external ip and port seen by the lobby server. EventData is with the clients seen external address. + /// + ExternalIpAndPort } } diff --git a/LobbyClientTest/FakeGameHost.cs b/LobbyClientTest/FakeGameHost.cs index f829bea..1fd813e 100644 --- a/LobbyClientTest/FakeGameHost.cs +++ b/LobbyClientTest/FakeGameHost.cs @@ -1,52 +1,67 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Sockets; +using System.Net.Sockets; using System.Net; using System.Text; -using System.Threading.Tasks; +using static System.Runtime.InteropServices.JavaScript.JSType; namespace LobbyClientTest { - internal class FakeGameHost + internal class FakeGameHost : IDisposable { - private Socket _socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - private const int bufSize = 8 * 1024; - private State state = new State(); - private EndPoint epFrom = new IPEndPoint(IPAddress.Any, 0); - private AsyncCallback recv = null; - public bool isHost; - public class State - { - public byte[] buffer = new byte[bufSize]; - } + public bool isHost = false; + UdpClient? udpClient; + bool running = true; public int Server(int port) { - _socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.ReuseAddress, true); - _socket.ExclusiveAddressUse = false; - _socket.Bind(new IPEndPoint(IPAddress.Any, port)); - Receive(); - return ((IPEndPoint)_socket.LocalEndPoint!).Port; + udpClient = new UdpClient(port); + + _ = Task.Run(() => + { + Receive(); + }); + + return ((IPEndPoint)udpClient.Client.LocalEndPoint!).Port; } - public void Send(EndPoint ep, string text) + public void Send(IPEndPoint ep, string text) { byte[] data = Encoding.ASCII.GetBytes(text); - _socket.SendTo(data, 0, data.Length, SocketFlags.None, ep); + udpClient?.Send(data, data.Length, ep); + } + + public void Send(IPEndPoint ep, byte[] message, int length) + { + udpClient?.Send(message, length, ep); } private void Receive() { - _socket.BeginReceiveFrom(state.buffer, 0, bufSize, SocketFlags.None, ref epFrom, recv = (ar) => + try { - State so = (State)ar.AsyncState; - int bytes = _socket.EndReceiveFrom(ar, ref epFrom); - _socket.BeginReceiveFrom(so.buffer, 0, bufSize, SocketFlags.None, ref epFrom, recv, so); - Console.WriteLine($"Game {(isHost ? "host" : "client")} received: {epFrom.ToString()}: {bytes}, {Encoding.ASCII.GetString(so.buffer, 0, bytes)}"); - if (isHost) - Send(epFrom, "Hello from Game Server!"); - }, state); + IPEndPoint remoteEp = new IPEndPoint(IPAddress.Any, 0); + + while (running) + { + var data = udpClient!.Receive(ref remoteEp); + Console.WriteLine($"Game {(isHost ? "host" : "client")} received: {remoteEp.ToString()}: {data.Length}, {Encoding.ASCII.GetString(data, 0, data.Length)}"); + if (isHost) + Send(remoteEp, "Hello from Game Server!"); + } + } + catch + { + + } + finally + { + + } + } + + public void Dispose() + { + running = false; + udpClient?.Dispose(); } } } diff --git a/LobbyClientTest/Program.cs b/LobbyClientTest/Program.cs index 84039f2..4d22e13 100644 --- a/LobbyClientTest/Program.cs +++ b/LobbyClientTest/Program.cs @@ -4,30 +4,34 @@ using LobbyClientTest; using LobbyServerDto; using System.Net; -Console.WriteLine("Starting lobby client!"); +Console.WriteLine("Starting lobby client v0.7!"); var lobbyClient = new LobbyClient(); var cancellationTokenSource = new CancellationTokenSource(); List openLobbies = new List(); -lobbyClient.Connect("localhost", 8088, cancellationTokenSource.Token); +lobbyClient.Connect("lobby.incobyte.de" /*"localhost"*/, 8088, cancellationTokenSource.Token); FakeGameHost fakeGameHost = new FakeGameHost(); int myPort = fakeGameHost.Server(0); +string? myExternalIp = null; +int myExternalPort = -1; IPEndPoint? hostInfo = null; bool running = true; +bool connected = false; _ = Task.Run(() => { while (running) { foreach (var lobbyEvent in lobbyClient.ReadEvents(20)) - { + { switch (lobbyEvent.EventType) { case LobbyClientEventTypes.Connected: { + connected = true; var p = Console.GetCursorPosition(); Console.SetCursorPosition(0, p.Top); Console.WriteLine("Lobby client connected!"); @@ -76,24 +80,56 @@ _ = Task.Run(() => Console.SetCursorPosition(0, p.Top); Console.WriteLine($"Host info for lobby {lobbyHostInfo!.LobbyId} is {lobbyHostInfo.HostIp}:{lobbyHostInfo.HostPort}!"); hostInfo = new IPEndPoint(IPAddress.Parse(lobbyHostInfo.HostIp!), lobbyHostInfo.HostPort); - Console.WriteLine($"Requesting nat punch!"); - lobbyClient.RequestLobbyNatPunch(lobbyHostInfo.LobbyId, null, null, myPort); + Console.WriteLine($"Requesting nat punch to me!"); + lobbyClient.RequestLobbyNatPunch(lobbyHostInfo.LobbyId, null, (remoteEndpoint, messageBuffer, messageLength) => { + fakeGameHost.Send(remoteEndpoint, messageBuffer, messageLength); + }); Console.Write(">"); } break; + case LobbyClientEventTypes.ExternalIpAndPort: + { + var seenExternalIpAndPort = lobbyEvent.EventData as SeenExternalIpAndPort; + if (seenExternalIpAndPort != null) + { + myExternalIp = seenExternalIpAndPort.Ip; + myExternalPort = seenExternalIpAndPort.Port; + + var p = Console.GetCursorPosition(); + Console.SetCursorPosition(0, p.Top); + Console.WriteLine($"Received my external ip {seenExternalIpAndPort!.Ip}:{seenExternalIpAndPort.Port}"); + Console.Write(">"); + + connected = true; + } + } + break; case LobbyClientEventTypes.LobbyRequestNatPunch: { var lobbyRequestNatPunch = lobbyEvent.EventData as LobbyRequestNatPunch; var p = Console.GetCursorPosition(); Console.SetCursorPosition(0, p.Top); Console.WriteLine($"Nat punch requested to {lobbyRequestNatPunch!.ClientIp}:{lobbyRequestNatPunch.ClientPort}!"); - //NatPuncher.NatPunch(new IPEndPoint(IPAddress.Any, myPort), new IPEndPoint(IPAddress.Parse(lobbyRequestNatPunch.ClientIp!), lobbyRequestNatPunch.ClientPort)); - var ep = new IPEndPoint(IPAddress.Parse(lobbyRequestNatPunch.ClientIp!), lobbyRequestNatPunch.ClientPort); - for (int z = 0; z < 16; z++) + + Task.Run(() => { - fakeGameHost.Send(ep, "Nat Falcon Punch!"); - } - lobbyClient.NotifyLobbyNatPunchDone(lobbyRequestNatPunch.NatPunchId); + lobbyClient.QueryExternalIpAndPort((remoteEndpoint, messageData, messageLength) => { + fakeGameHost.Send(remoteEndpoint, messageData, messageLength); + }); + + var ep = new IPEndPoint(IPAddress.Parse(lobbyRequestNatPunch.ClientIp!), lobbyRequestNatPunch.ClientPort); + for (int z = 0; z < 32; z++) + { + fakeGameHost.Send(ep, "Nat Falcon Punch!"); + } + + lobbyClient.NotifyLobbyNatPunchDone(lobbyRequestNatPunch.NatPunchId, lobbyClient.externalIp!, lobbyClient.externalPort); + var p = Console.GetCursorPosition(); + Console.SetCursorPosition(0, p.Top); + Console.WriteLine($"Nat punch done!"); + Console.Write(">"); + }); + Console.Write(">"); } break; @@ -104,7 +140,8 @@ _ = Task.Run(() => Console.SetCursorPosition(0, p.Top); Console.WriteLine($"Nat punch request done!"); Console.WriteLine($"Connecting game client!"); - fakeGameHost.Send(hostInfo!, "Hello from Game Client!"); + + fakeGameHost.Send(new IPEndPoint(IPAddress.Parse(lobbyNatPunchDone!.ExternalIp!), lobbyNatPunchDone.ExternalPort), "Hello from Game Client!"); Console.Write(">"); } break; @@ -133,9 +170,16 @@ while (running) { case "host": { - Console.WriteLine("Hosting game ..."); - lobbyClient.HostLobby(GameGuids.NFS, "Hallo, Welt!", 1, 8, null, null, myPort); - fakeGameHost.isHost = true; + if (!connected) + { + Console.WriteLine("Not connected yet!"); + } + else + { + Console.WriteLine("Hosting game ..."); + lobbyClient.HostLobby(GameGuids.NFS, "Hallo, Welt!", 1, 8, null, "127.0.0.1", myPort); + fakeGameHost.isHost = true; + } } break; case "host stop": @@ -147,15 +191,22 @@ while (running) break; case "join": { - Console.WriteLine("Trying to join first lobby ..."); - var firstLobby = openLobbies.FirstOrDefault(); - if (firstLobby != null) + if (!connected) { - lobbyClient.RequestLobbyHostInfo(firstLobby.Id, null); + Console.WriteLine("Not connected yet!"); } else { - Console.WriteLine("Seeing no open lobby!"); + Console.WriteLine("Trying to join first lobby ..."); + var firstLobby = openLobbies.FirstOrDefault(); + if (firstLobby != null) + { + lobbyClient.RequestLobbyHostInfo(firstLobby.Id, null); + } + else + { + Console.WriteLine("Seeing no open lobby!"); + } } } break; diff --git a/LobbyServer/Program.cs b/LobbyServer/Program.cs index f239ef7..61e4dc6 100644 --- a/LobbyServer/Program.cs +++ b/LobbyServer/Program.cs @@ -5,6 +5,7 @@ using System.Collections.Concurrent; using var closing = new AutoResetEvent(false); using var tcpServer = new TcpServer(); +using var udpServer = new UdpCandidateServer(); ConcurrentDictionary lobbiesById = new ConcurrentDictionary(); ConcurrentDictionary> lobbiesByGameId = new ConcurrentDictionary>(); @@ -14,35 +15,23 @@ ConcurrentDictionary clientWatchingGameIdLobbies = new ConcurrentDict ConcurrentDictionary> clientsWatchingGameId = new ConcurrentDictionary>(); BufferRental bufferRental = new BufferRental(4096); -int PeekTypeId(ReadOnlySpan buffer) -{ - int typeId = 0; - int shift = 0; - int offset = 0; - byte b; - do +udpServer.QueryIpAndPort += (clientId, ip, port) => +{ + var messageData = bufferRental.Rent(); + var seenExternalIpAndPort = new SeenExternalIpAndPort() { Ip = ip, Port = port }; + var messageDataLength = seenExternalIpAndPort.Serialize(messageData); + _ = Task.Run(async () => { - { - // Check for a corrupted stream. Read a max of 5 bytes. - // In a future version, add a DataFormatException. - if (shift == 5 * 7) // 5 bytes max per Int32, shift += 7 - throw new FormatException("Format_Bad7BitInt32"); - - // ReadByte handles end of stream cases for us. - b = buffer[offset++]; - typeId |= (b & 0x7F) << shift; - shift += 7; - } - } while ((b & 0x80) != 0); - - return typeId; -} + await tcpServer.Send(clientId, messageData, 0, messageDataLength); + bufferRental.Return(messageData); + }); +}; tcpServer.DataReceived += (clientId, dataLength, data) => { if (dataLength > 0) { - switch (PeekTypeId(data.Span)) + switch (LobbyMessageIdentifier.ReadLobbyMessageIdentifier(data.Span)) { case LobbyCreate.TypeId: { @@ -433,8 +422,9 @@ Console.CancelKeyPress += (sender, args) => args.Cancel = true; }; -Console.WriteLine($"{DateTime.Now}: Application started"); +Console.WriteLine($"{DateTime.Now}: Application started v0.7"); +udpServer.Start(8088); tcpServer.Start(8088); closing.WaitOne(); diff --git a/LobbyServer/TcpLobbyServer.cs b/LobbyServer/TcpLobbyServer.cs index 3cecf05..55e4977 100644 --- a/LobbyServer/TcpLobbyServer.cs +++ b/LobbyServer/TcpLobbyServer.cs @@ -1,7 +1,7 @@ using System.Net.Sockets; using System.Net; using System.Collections.Concurrent; - +using LobbyServerDto; namespace LobbyServer { @@ -129,7 +129,12 @@ namespace LobbyServer int currentMessageLength = 0; bool validMessage = true; bool offsetSizeInt = false; - int currentReadOffset = 0; + int currentReadOffset = 0; + + var lobbyClientConnectionInfo = new LobbyClientConnectionInfo { Id = myId }; + byte[] sendBuffer = new byte[128]; + int sendLen = lobbyClientConnectionInfo.Serialize(sendBuffer); + await Send(myId, sendBuffer, 0, sendLen); while (running) { diff --git a/LobbyServer/UdpCandidateServer.cs b/LobbyServer/UdpCandidateServer.cs new file mode 100644 index 0000000..8dd8f7e --- /dev/null +++ b/LobbyServer/UdpCandidateServer.cs @@ -0,0 +1,93 @@ +using System.Net.Sockets; +using System.Net; +using LobbyServerDto; + +namespace LobbyServer +{ + /// + /// Small udp server to receive udp packets and report their remote ip and port to a event delegate + /// + internal class UdpCandidateServer : IDisposable + { + private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + private bool running = false; + private bool isDisposed = false; + + public delegate void QueryIpAndPortReceivedEventArgs(int clientId, string ip, int port); + /// + /// If a valid request for a ip and port query comes in call this event with the seen remote ip and port for a lobby server client id + /// + public event QueryIpAndPortReceivedEventArgs? QueryIpAndPort; + + /// + /// Listen to requests and fire events + /// + /// The port to listen on + private async void Listen(int port) + { + try + { + cancellationTokenSource.Token.ThrowIfCancellationRequested(); + + using var serverSocket = new UdpClient(port, AddressFamily.InterNetwork); + Console.WriteLine($"{DateTime.Now}: [UdpCandidateServer] started"); + while (running) + { + var receiveResult = await serverSocket.ReceiveAsync(cancellationTokenSource.Token); + if(receiveResult.Buffer.Length > 0) + { + Memory receivedData = new Memory(receiveResult.Buffer); + switch (LobbyMessageIdentifier.ReadLobbyMessageIdentifier(receivedData.Span)) + { + case QueryExternalPortAndIp.TypeId: + var queryExternalPortAndIp = QueryExternalPortAndIp.Deserialize(receivedData.Span); + QueryIpAndPort?.Invoke(queryExternalPortAndIp.LobbyClientId, receiveResult.RemoteEndPoint.Address.ToString(), receiveResult.RemoteEndPoint.Port); + break; + } + } + } + } + catch when (cancellationTokenSource.IsCancellationRequested) //Cancel requested + { + Console.WriteLine($"{DateTime.Now}: [UdpCandidateServer] canceled"); + } + catch(Exception e) + { + Console.WriteLine($"{DateTime.Now}: [UdpCandidateServer] exception: {e}"); + throw; + } + finally + { + Console.WriteLine($"{DateTime.Now}: [UdpCandidateServer] stopped"); + } + } + + /// + /// Start udp listener + /// + /// The port to listen on + public void Start(int port) + { + running = true; + _ = Task.Run(() => Listen(port)); + } + + /// + /// Stop udp listener + /// + public void Stop() + { + running = false; + cancellationTokenSource.Cancel(); + } + + public void Dispose() + { + if (!isDisposed) + { + cancellationTokenSource.Dispose(); + isDisposed = true; + } + } + } +} diff --git a/LobbyServerDto/LobbiesObserve.cs b/LobbyServerDto/LobbiesObserve.cs index d09caf6..37242fb 100644 --- a/LobbyServerDto/LobbiesObserve.cs +++ b/LobbyServerDto/LobbiesObserve.cs @@ -1,8 +1,15 @@ namespace LobbyServerDto { + /// + /// Used to subscribe to a game id by guid to observe it's list of open lobbies + /// + /// A client can only be subscribed to one game id, sending multiple observe requests with different game ids will overwrite the previous [LobbyMessage] public partial class LobbiesObserve { + /// + /// The game id to subscribe to + /// public Guid GameId { get; set; } } } \ No newline at end of file diff --git a/LobbyServerDto/LobbiesStopObserve.cs b/LobbyServerDto/LobbiesStopObserve.cs index 13d62af..f5a76b4 100644 --- a/LobbyServerDto/LobbiesStopObserve.cs +++ b/LobbyServerDto/LobbiesStopObserve.cs @@ -1,5 +1,8 @@ namespace LobbyServerDto { + /// + /// Stop observing and receiving updates on the lobby list + /// [LobbyMessage] public partial class LobbiesStopObserve { diff --git a/LobbyServerDto/LobbyClientConnectionInfo.cs b/LobbyServerDto/LobbyClientConnectionInfo.cs new file mode 100644 index 0000000..1914e55 --- /dev/null +++ b/LobbyServerDto/LobbyClientConnectionInfo.cs @@ -0,0 +1,14 @@ +namespace LobbyServerDto +{ + /// + /// Send to lobby client after connection is established + /// + [LobbyMessage] + public partial class LobbyClientConnectionInfo + { + /// + /// Connection id on the lobby server + /// + public int Id { get; set; } + } +} diff --git a/LobbyServerDto/LobbyCreate.cs b/LobbyServerDto/LobbyCreate.cs index f67c0ce..7893dc0 100644 --- a/LobbyServerDto/LobbyCreate.cs +++ b/LobbyServerDto/LobbyCreate.cs @@ -2,19 +2,47 @@ namespace LobbyServerDto { + /// + /// Create a lobby in the lobby list. The lobby will stay until either closed by the host or the host disconnects + /// + /// A host can always have only one open lobby, if another lobby is already opened by the client it will be closed [LobbyMessage] public partial class LobbyCreate { + /// + /// Game Id for which game this lobby is for + /// public Guid GameId { get; set; } + /// + /// Display name of the lobby + /// [MaxLength(64)] public string Name { get; set; } = string.Empty; + /// + /// Game mode of lobby + /// public int GameMode { get; set; } + /// + /// How many players are in the lobby + /// public int PlayerCount { get; set; } + /// + /// Maximum players allowed in the lobby + /// public int MaxPlayerCount { get; set; } + /// + /// The hash of a password to protect the lobby. Only users with the password can request host information/nat punch. + /// [MaxLength(26)] public byte[]? PasswordHash { get; set; } + /// + /// The hosts ip. Used the the host information send to clients on their request. + /// [MaxLength(32)] public string? HostIp { get; set; } + /// + /// The hosts port. Used the the host information send to clients on their request. + /// public int HostPort { get; set; } } } \ No newline at end of file diff --git a/LobbyServerDto/LobbyDelete.cs b/LobbyServerDto/LobbyDelete.cs index 613efa2..f686560 100644 --- a/LobbyServerDto/LobbyDelete.cs +++ b/LobbyServerDto/LobbyDelete.cs @@ -1,9 +1,19 @@ namespace LobbyServerDto { + /// + /// If send from host closes and deletes a lobby. + /// If received by client, the lobby with the Id parameter has been closed + /// [LobbyMessage] public partial class LobbyDelete { + /// + /// If received by client, this contains the lobby id. Does not need to be set if the host sends this request. + /// public Guid Id { get; set; } + /// + /// If received by client, this contains the game id. Does not need to be set if the host sends this request. + /// public Guid GameId { get; set; } } } \ No newline at end of file diff --git a/LobbyServerDto/LobbyHostInfo.cs b/LobbyServerDto/LobbyHostInfo.cs index 5c1364f..cbed3aa 100644 --- a/LobbyServerDto/LobbyHostInfo.cs +++ b/LobbyServerDto/LobbyHostInfo.cs @@ -2,12 +2,24 @@ namespace LobbyServerDto { + /// + /// Send to client upon request, this contains the connection info supplied by the host. + /// [LobbyMessage] public partial class LobbyHostInfo { + /// + /// Lobby id this information is for + /// public Guid LobbyId { get; set; } + /// + /// The hosts ip, this could be an internal address + /// [MaxLength(32)] public string? HostIp { get; set; } + /// + /// The hosts port + /// public int HostPort { get; set; } } } \ No newline at end of file diff --git a/LobbyServerDto/LobbyInfo.cs b/LobbyServerDto/LobbyInfo.cs index b7a9c91..22d3a39 100644 --- a/LobbyServerDto/LobbyInfo.cs +++ b/LobbyServerDto/LobbyInfo.cs @@ -1,13 +1,34 @@ namespace LobbyServerDto { + /// + /// Send when a lobby is created or updates, this contains the lobby information + /// [LobbyMessage] public partial class LobbyInfo { + /// + /// Id of the lobby + /// public Guid Id { get; set; } + /// + /// Displayname of the lobby + /// public string? Name { get; set; } + /// + /// Game mode of the lobby + /// public int GameMode { get; set; } + /// + /// Count of currently joined players in the lobby + /// public int PlayerCount { get; set; } + /// + /// Max players allowed in the lobby + /// public int MaxPlayerCount { get; set; } + /// + /// True if the lobby requires a password to gain access + /// public bool PasswordProtected { get; set; } } } diff --git a/LobbyServerDto/LobbyMessageIdentifier.cs b/LobbyServerDto/LobbyMessageIdentifier.cs new file mode 100644 index 0000000..93443c2 --- /dev/null +++ b/LobbyServerDto/LobbyMessageIdentifier.cs @@ -0,0 +1,44 @@ +namespace LobbyServerDto +{ + /// + /// Used to read the message identifer from a received object + /// + public static class LobbyMessageIdentifier + { + /// + /// Read the message identifiere from the stream + /// + /// + /// The message identifier or -1 if invalid + public static int ReadLobbyMessageIdentifier(ReadOnlySpan buffer) + { + if(buffer.Length == 0) + return -1; + + int typeId = 0; + int shift = 0; + int offset = 0; + byte currentByte; + do + { + if (offset < buffer.Length) + { + // Check for a corrupted stream. Read a max of 5 bytes. + // In a future version, add a DataFormatException. + if (shift == 5 * 7) // 5 bytes max per Int32, shift += 7 + return -1; + + // ReadByte handles end of stream cases for us. + currentByte = buffer[offset++]; + typeId |= (currentByte & 0x7F) << shift; + shift += 7; + } + else + return -1; + + } while ((currentByte & 0x80) != 0); + + return typeId; + } + } +} diff --git a/LobbyServerDto/LobbyNatPunchDone.cs b/LobbyServerDto/LobbyNatPunchDone.cs index 946d636..061fc1e 100644 --- a/LobbyServerDto/LobbyNatPunchDone.cs +++ b/LobbyServerDto/LobbyNatPunchDone.cs @@ -2,10 +2,29 @@ namespace LobbyServerDto { + /// + /// Send from host to lobby server and releyed to the client by lobby server if the nat punch is finished. + /// [LobbyMessage] public partial class LobbyNatPunchDone { + /// + /// The lobby this nat punch was for + /// public Guid LobbyId { get; set; } + /// + /// Id of the nat punch that the lobby server requested from us in + /// public int NatPunchId { get; set; } + + /// + /// The external ip of the host + /// + [MaxLength(32)] + public string? ExternalIp { get; set; } + /// + /// The external port of the host + /// + public int ExternalPort { get; set; } } } \ No newline at end of file diff --git a/LobbyServerDto/LobbyRequestHostInfo.cs b/LobbyServerDto/LobbyRequestHostInfo.cs index 5d57a09..656487c 100644 --- a/LobbyServerDto/LobbyRequestHostInfo.cs +++ b/LobbyServerDto/LobbyRequestHostInfo.cs @@ -2,10 +2,19 @@ namespace LobbyServerDto { + /// + /// Request the host address for a lobby + /// [LobbyMessage] public partial class LobbyRequestHostInfo { + /// + /// Id of the lobby + /// public Guid LobbyId { get; set; } + /// + /// Hash of the password if the lobby requires one + /// [MaxLength(26)] public byte[]? PasswordHash { get; set; } } diff --git a/LobbyServerDto/LobbyRequestNatPunch.cs b/LobbyServerDto/LobbyRequestNatPunch.cs index 4f54e7c..4affca6 100644 --- a/LobbyServerDto/LobbyRequestNatPunch.cs +++ b/LobbyServerDto/LobbyRequestNatPunch.cs @@ -2,16 +2,33 @@ namespace LobbyServerDto { + /// + /// Request a nat punch from the game host + /// [LobbyMessage] public partial class LobbyRequestNatPunch { + /// + /// Id of the lobby + /// public Guid LobbyId { get; set; } + /// + /// Password hash if the lobby is password protected + /// [MaxLength(26)] public byte[]? PasswordHash { get; set; } + /// + /// Our external ip + /// public string? ClientIp { get; set; } + /// + /// Our external port + /// public int ClientPort { get; set; } - + /// + /// Set by the lobby server, this will be the NatPunchId send to the host + /// public int NatPunchId { get; set; } } } \ No newline at end of file diff --git a/LobbyServerDto/LobbyUpdate.cs b/LobbyServerDto/LobbyUpdate.cs index 4cc276b..2dbceab 100644 --- a/LobbyServerDto/LobbyUpdate.cs +++ b/LobbyServerDto/LobbyUpdate.cs @@ -2,16 +2,40 @@ namespace LobbyServerDto { + /// + /// If the host updates values of the lobby, it can send this request to update the lobby information on the lobby server + /// [LobbyMessage] public partial class LobbyUpdate { + /// + /// The displayname of the lobby + /// public string Name { get; set; } = string.Empty; + /// + /// The game mode of the lobby + /// public int GameMode { get; set; } + /// + /// Current count of players in the lobby + /// public int PlayerCount { get; set; } + /// + /// Max players in the lobby + /// public int MaxPlayerCount { get; set; } + /// + /// The hash of the password required to enter this lobby + /// public byte[]? PasswordHash { get; set; } + /// + /// The hosts ip + /// [MaxLength(32)] public string? HostIp { get; set; } + /// + /// The hosts port + /// public int HostPort { get; set; } } } \ No newline at end of file diff --git a/LobbyServerDto/LobbyWrongPassword.cs b/LobbyServerDto/LobbyWrongPassword.cs index 6cd18ad..450edf1 100644 --- a/LobbyServerDto/LobbyWrongPassword.cs +++ b/LobbyServerDto/LobbyWrongPassword.cs @@ -1,8 +1,14 @@ namespace LobbyServerDto { + /// + /// Send if a client requests information about a password protected lobby with an invalid password hash + /// [LobbyMessage] public partial class LobbyWrongPassword { + /// + /// Id of the lobby + /// public Guid LobbyId { get; set; } } } \ No newline at end of file diff --git a/LobbyServerDto/NatPuncher.cs b/LobbyServerDto/NatPuncher.cs deleted file mode 100644 index 46fe96e..0000000 --- a/LobbyServerDto/NatPuncher.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Net; -using System.Net.Sockets; - - -namespace LobbyServerDto -{ - public class NatPuncher - { - public static void NatPunch(IPEndPoint localEndpoint, IPEndPoint remoteEndpoint) - { - try - { - using Socket socket = new Socket(SocketType.Dgram, ProtocolType.Udp); - socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.ReuseAddress, true); - socket.ExclusiveAddressUse = false; - socket.Bind(localEndpoint); - - for (int i = 0; i < 16; i++) - { - socket.SendTo(new byte[] { }, remoteEndpoint); - } - } - catch - { - throw; - } - } - } -} diff --git a/LobbyServerDto/QueryExternalPortAndIp.cs b/LobbyServerDto/QueryExternalPortAndIp.cs new file mode 100644 index 0000000..24c1e1d --- /dev/null +++ b/LobbyServerDto/QueryExternalPortAndIp.cs @@ -0,0 +1,15 @@ + +namespace LobbyServerDto +{ + /// + /// Send from a client to the lobby server via udp to from the game port to retrieve the observerd external ip and port + /// + [LobbyMessage] + public partial class QueryExternalPortAndIp + { + /// + /// Lobby client id + /// + public int LobbyClientId { get; set; } + } +} diff --git a/LobbyServerDto/SeenExternalIpAndPort.cs b/LobbyServerDto/SeenExternalIpAndPort.cs new file mode 100644 index 0000000..ac8accf --- /dev/null +++ b/LobbyServerDto/SeenExternalIpAndPort.cs @@ -0,0 +1,22 @@ + +using System.ComponentModel.DataAnnotations; + +namespace LobbyServerDto +{ + /// + /// The ip and port seen by the lobby udp server by an request. + /// + [LobbyMessage] + public partial class SeenExternalIpAndPort + { + /// + /// The seen ip of the client + /// + [MaxLength(32)] + public string? Ip { get; set; } + /// + /// The seen port of the client + /// + public int Port { get; set; } + } +} diff --git a/LobbyServerDto/ValidGameGuids.cs b/LobbyServerDto/ValidGameGuids.cs index aca426e..0bdef15 100644 --- a/LobbyServerDto/ValidGameGuids.cs +++ b/LobbyServerDto/ValidGameGuids.cs @@ -1,6 +1,9 @@  namespace LobbyServerDto { + /// + /// List of accepted game ips + /// public static class GameGuids { public static Guid NFS = new Guid("0572706f-0fd9-46a9-8ea6-5a31a5363442");