main
Thomas Woischnig 2023-11-29 20:28:43 +01:00
parent 2674eb2a10
commit 840218f119
25 changed files with 650 additions and 169 deletions

View File

@ -2,6 +2,9 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
@ -14,8 +17,21 @@ namespace Lobbies
TcpLobbbyClient tcpClient = new TcpLobbbyClient(); TcpLobbbyClient tcpClient = new TcpLobbbyClient();
private readonly ConcurrentQueue<LobbyClientEvent> events = new ConcurrentQueue<LobbyClientEvent>(); private readonly ConcurrentQueue<LobbyClientEvent> events = new ConcurrentQueue<LobbyClientEvent>();
BufferRental bufferRental = new BufferRental(4096); 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) public void Connect(string host, int port, CancellationToken cancellationToken)
{ {
this.host = host;
this.port = port;
try try
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
@ -30,9 +46,6 @@ namespace Lobbies
tcpClient.Disconnected -= TcpClient_Disconnected; tcpClient.Disconnected -= TcpClient_Disconnected;
tcpClient.Disconnected += TcpClient_Disconnected; tcpClient.Disconnected += TcpClient_Disconnected;
tcpClient.Connected -= TcpClient_Connected;
tcpClient.Connected += TcpClient_Connected;
_ = Task.Run(() => tcpClient.Connect(host, port)); _ = Task.Run(() => tcpClient.Connect(host, port));
} }
catch catch
@ -93,19 +106,25 @@ namespace Lobbies
_ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); }); _ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); });
} }
public void RequestLobbyNatPunch(Guid lobbyId, string? password, string? clientIp, int clientPort)
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() var lobbyRequestNatPunch = new LobbyRequestNatPunch()
{ {
LobbyId = lobbyId, LobbyId = lobbyId,
PasswordHash = string.IsNullOrEmpty(password) ? null : SHA256.HashData(Encoding.UTF8.GetBytes(password)), PasswordHash = string.IsNullOrEmpty(password) ? null : SHA256.HashData(Encoding.UTF8.GetBytes(password)),
ClientIp = clientIp, ClientIp = externalIp,
ClientPort = clientPort ClientPort = externalPort
}; };
byte[] messageData = bufferRental.Rent(); byte[] messageData = bufferRental.Rent();
var len = lobbyRequestNatPunch.Serialize(messageData); var len = lobbyRequestNatPunch.Serialize(messageData);
_ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(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) 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); }); _ = 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(); byte[] messageData = bufferRental.Rent();
var len = lobbyNatPunchDone.Serialize(messageData); var len = lobbyNatPunchDone.Serialize(messageData);
_ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(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) 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 } }); events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.Disconnected, EventData = new LobbyClientDisconnectReason { WasError = false, ErrorMessage = string.Empty } });
} }
private int PeekTypeId(ReadOnlySpan<byte> 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<byte> data) private void TcpClient_DataReceived(int dataLength, Memory<byte> data)
{ {
try try
{ {
if (dataLength > 0) 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: case LobbyInfo.TypeId:
{ {
var lobbyInfo = LobbyInfo.Deserialize(data.Span); var lobbyInfo = LobbyInfo.Deserialize(data.Span);
@ -260,6 +317,19 @@ namespace Lobbies
} }
} }
break; 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,6 +338,7 @@ namespace Lobbies
public void Dispose() public void Dispose()
{ {
waitForExternalIp.Dispose();
tcpClient.Dispose(); tcpClient.Dispose();
} }

View File

@ -1,10 +1,4 @@
using System; namespace Lobbies
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Lobbies
{ {
public class LobbyClientDisconnectReason public class LobbyClientDisconnectReason
{ {

View File

@ -1,27 +1,64 @@
using System; namespace Lobbies
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Lobbies
{ {
/// <summary>
/// Contains a lobby client event
/// </summary>
public class LobbyClientEvent public class LobbyClientEvent
{ {
/// <summary>
/// Identifier type of the event
/// </summary>
public LobbyClientEventTypes EventType { get; internal set; } public LobbyClientEventTypes EventType { get; internal set; }
/// <summary>
/// Optional data object, contains additional information for the event. See <see cref="LobbyClientEventTypes"/> for possible values.
/// </summary>
public object? EventData { get; internal set; } public object? EventData { get; internal set; }
} }
/// <summary>
/// The list of events that can occur.
/// </summary>
public enum LobbyClientEventTypes public enum LobbyClientEventTypes
{ {
/// <summary>
/// Client connected to lobby server. EventData is null.
/// </summary>
Connected, Connected,
/// <summary>
/// Client disconnected from lobby server graceful. EventData is <see cref="LobbyClientDisconnectReason"/>.
/// </summary>
Disconnected, Disconnected,
/// <summary>
/// Connection lost to lobby server. EventData is LobbyClientDisconnectReason.
/// </summary>
Failed, Failed,
/// <summary>
/// A lobby was added to the lobby list for the observed game id. EventData is <see cref="LobbyAdd"/> with the lobby data.
/// </summary>
LobbyAdd, LobbyAdd,
/// <summary>
/// A lobby was changed for the observed game id. EventData is <see cref="LobbyUpdate"/> with the lobby data.
/// </summary>
LobbyUpdate, LobbyUpdate,
/// <summary>
/// A lobby was removed from the lobby list for the observed game id. EventData is <see cref="LobbyDelete"/> with the lobby id.
/// </summary>
LobbyDelete, LobbyDelete,
/// <summary>
/// The game host shared his self known address. EventData is <see cref="LobbyHostInfo"/> with the games hosts self known address, most likely internal.
/// </summary>
LobbyHostInfo, LobbyHostInfo,
/// <summary>
/// A nat punch was requested by another LobbyClient to be performed by the host LobbyClient. EventData is <see cref="LobbyRequestNatPunch"/> with the clients seen external address to nat punch to.
/// </summary>
LobbyRequestNatPunch, LobbyRequestNatPunch,
/// <summary>
/// Game host has finished the nat punch. EventData is <see cref="LobbyRequestNatPunch"/> with the games hosts external address.
/// </summary>
LobbyNatPunchDone, LobbyNatPunchDone,
/// <summary>
/// Response to a query of our external ip and port seen by the lobby server. EventData is <see cref="SeenExternalIpAndPort"/> with the clients seen external address.
/// </summary>
ExternalIpAndPort
} }
} }

View File

@ -1,52 +1,67 @@
using System; using System.Net.Sockets;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Net; using System.Net;
using System.Text; using System.Text;
using System.Threading.Tasks; using static System.Runtime.InteropServices.JavaScript.JSType;
namespace LobbyClientTest namespace LobbyClientTest
{ {
internal class FakeGameHost internal class FakeGameHost : IDisposable
{ {
private Socket _socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); public bool isHost = false;
private const int bufSize = 8 * 1024; UdpClient? udpClient;
private State state = new State(); bool running = true;
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 int Server(int port) public int Server(int port)
{ {
_socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.ReuseAddress, true); udpClient = new UdpClient(port);
_socket.ExclusiveAddressUse = false;
_socket.Bind(new IPEndPoint(IPAddress.Any, port)); _ = Task.Run(() =>
{
Receive(); Receive();
return ((IPEndPoint)_socket.LocalEndPoint!).Port; });
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); 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() private void Receive()
{ {
_socket.BeginReceiveFrom(state.buffer, 0, bufSize, SocketFlags.None, ref epFrom, recv = (ar) => try
{ {
State so = (State)ar.AsyncState; IPEndPoint remoteEp = new IPEndPoint(IPAddress.Any, 0);
int bytes = _socket.EndReceiveFrom(ar, ref epFrom);
_socket.BeginReceiveFrom(so.buffer, 0, bufSize, SocketFlags.None, ref epFrom, recv, so); while (running)
Console.WriteLine($"Game {(isHost ? "host" : "client")} received: {epFrom.ToString()}: {bytes}, {Encoding.ASCII.GetString(so.buffer, 0, bytes)}"); {
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) if (isHost)
Send(epFrom, "Hello from Game Server!"); Send(remoteEp, "Hello from Game Server!");
}, state); }
}
catch
{
}
finally
{
}
}
public void Dispose()
{
running = false;
udpClient?.Dispose();
} }
} }
} }

View File

@ -4,19 +4,22 @@ using LobbyClientTest;
using LobbyServerDto; using LobbyServerDto;
using System.Net; using System.Net;
Console.WriteLine("Starting lobby client!"); Console.WriteLine("Starting lobby client v0.7!");
var lobbyClient = new LobbyClient(); var lobbyClient = new LobbyClient();
var cancellationTokenSource = new CancellationTokenSource(); var cancellationTokenSource = new CancellationTokenSource();
List<LobbyInfo> openLobbies = new List<LobbyInfo>(); List<LobbyInfo> openLobbies = new List<LobbyInfo>();
lobbyClient.Connect("localhost", 8088, cancellationTokenSource.Token); lobbyClient.Connect("lobby.incobyte.de" /*"localhost"*/, 8088, cancellationTokenSource.Token);
FakeGameHost fakeGameHost = new FakeGameHost(); FakeGameHost fakeGameHost = new FakeGameHost();
int myPort = fakeGameHost.Server(0); int myPort = fakeGameHost.Server(0);
string? myExternalIp = null;
int myExternalPort = -1;
IPEndPoint? hostInfo = null; IPEndPoint? hostInfo = null;
bool running = true; bool running = true;
bool connected = false;
_ = Task.Run(() => _ = Task.Run(() =>
{ {
@ -28,6 +31,7 @@ _ = Task.Run(() =>
{ {
case LobbyClientEventTypes.Connected: case LobbyClientEventTypes.Connected:
{ {
connected = true;
var p = Console.GetCursorPosition(); var p = Console.GetCursorPosition();
Console.SetCursorPosition(0, p.Top); Console.SetCursorPosition(0, p.Top);
Console.WriteLine("Lobby client connected!"); Console.WriteLine("Lobby client connected!");
@ -76,24 +80,56 @@ _ = Task.Run(() =>
Console.SetCursorPosition(0, p.Top); Console.SetCursorPosition(0, p.Top);
Console.WriteLine($"Host info for lobby {lobbyHostInfo!.LobbyId} is {lobbyHostInfo.HostIp}:{lobbyHostInfo.HostPort}!"); Console.WriteLine($"Host info for lobby {lobbyHostInfo!.LobbyId} is {lobbyHostInfo.HostIp}:{lobbyHostInfo.HostPort}!");
hostInfo = new IPEndPoint(IPAddress.Parse(lobbyHostInfo.HostIp!), lobbyHostInfo.HostPort); hostInfo = new IPEndPoint(IPAddress.Parse(lobbyHostInfo.HostIp!), lobbyHostInfo.HostPort);
Console.WriteLine($"Requesting nat punch!"); Console.WriteLine($"Requesting nat punch to me!");
lobbyClient.RequestLobbyNatPunch(lobbyHostInfo.LobbyId, null, null, myPort); lobbyClient.RequestLobbyNatPunch(lobbyHostInfo.LobbyId, null, (remoteEndpoint, messageBuffer, messageLength) => {
fakeGameHost.Send(remoteEndpoint, messageBuffer, messageLength);
});
Console.Write(">"); Console.Write(">");
} }
break; 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: case LobbyClientEventTypes.LobbyRequestNatPunch:
{ {
var lobbyRequestNatPunch = lobbyEvent.EventData as LobbyRequestNatPunch; var lobbyRequestNatPunch = lobbyEvent.EventData as LobbyRequestNatPunch;
var p = Console.GetCursorPosition(); var p = Console.GetCursorPosition();
Console.SetCursorPosition(0, p.Top); Console.SetCursorPosition(0, p.Top);
Console.WriteLine($"Nat punch requested to {lobbyRequestNatPunch!.ClientIp}:{lobbyRequestNatPunch.ClientPort}!"); 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));
Task.Run(() =>
{
lobbyClient.QueryExternalIpAndPort((remoteEndpoint, messageData, messageLength) => {
fakeGameHost.Send(remoteEndpoint, messageData, messageLength);
});
var ep = 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++) for (int z = 0; z < 32; z++)
{ {
fakeGameHost.Send(ep, "Nat Falcon Punch!"); fakeGameHost.Send(ep, "Nat Falcon Punch!");
} }
lobbyClient.NotifyLobbyNatPunchDone(lobbyRequestNatPunch.NatPunchId);
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(">"); Console.Write(">");
} }
break; break;
@ -104,7 +140,8 @@ _ = Task.Run(() =>
Console.SetCursorPosition(0, p.Top); Console.SetCursorPosition(0, p.Top);
Console.WriteLine($"Nat punch request done!"); Console.WriteLine($"Nat punch request done!");
Console.WriteLine($"Connecting game client!"); 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(">"); Console.Write(">");
} }
break; break;
@ -132,11 +169,18 @@ while (running)
switch (line) switch (line)
{ {
case "host": case "host":
{
if (!connected)
{
Console.WriteLine("Not connected yet!");
}
else
{ {
Console.WriteLine("Hosting game ..."); Console.WriteLine("Hosting game ...");
lobbyClient.HostLobby(GameGuids.NFS, "Hallo, Welt!", 1, 8, null, null, myPort); lobbyClient.HostLobby(GameGuids.NFS, "Hallo, Welt!", 1, 8, null, "127.0.0.1", myPort);
fakeGameHost.isHost = true; fakeGameHost.isHost = true;
} }
}
break; break;
case "host stop": case "host stop":
{ {
@ -146,6 +190,12 @@ while (running)
} }
break; break;
case "join": case "join":
{
if (!connected)
{
Console.WriteLine("Not connected yet!");
}
else
{ {
Console.WriteLine("Trying to join first lobby ..."); Console.WriteLine("Trying to join first lobby ...");
var firstLobby = openLobbies.FirstOrDefault(); var firstLobby = openLobbies.FirstOrDefault();
@ -158,6 +208,7 @@ while (running)
Console.WriteLine("Seeing no open lobby!"); Console.WriteLine("Seeing no open lobby!");
} }
} }
}
break; break;
case "observe": case "observe":
{ {

View File

@ -5,6 +5,7 @@ using System.Collections.Concurrent;
using var closing = new AutoResetEvent(false); using var closing = new AutoResetEvent(false);
using var tcpServer = new TcpServer(); using var tcpServer = new TcpServer();
using var udpServer = new UdpCandidateServer();
ConcurrentDictionary<Guid, Lobby> lobbiesById = new ConcurrentDictionary<Guid, Lobby>(); ConcurrentDictionary<Guid, Lobby> lobbiesById = new ConcurrentDictionary<Guid, Lobby>();
ConcurrentDictionary<Guid, List<Lobby>> lobbiesByGameId = new ConcurrentDictionary<Guid, List<Lobby>>(); ConcurrentDictionary<Guid, List<Lobby>> lobbiesByGameId = new ConcurrentDictionary<Guid, List<Lobby>>();
@ -14,35 +15,23 @@ ConcurrentDictionary<int, Guid> clientWatchingGameIdLobbies = new ConcurrentDict
ConcurrentDictionary<Guid, List<int>> clientsWatchingGameId = new ConcurrentDictionary<Guid, List<int>>(); ConcurrentDictionary<Guid, List<int>> clientsWatchingGameId = new ConcurrentDictionary<Guid, List<int>>();
BufferRental bufferRental = new BufferRental(4096); BufferRental bufferRental = new BufferRental(4096);
int PeekTypeId(ReadOnlySpan<byte> buffer) udpServer.QueryIpAndPort += (clientId, ip, port) =>
{ {
int typeId = 0; var messageData = bufferRental.Rent();
int shift = 0; var seenExternalIpAndPort = new SeenExternalIpAndPort() { Ip = ip, Port = port };
int offset = 0; var messageDataLength = seenExternalIpAndPort.Serialize(messageData);
byte b; _ = Task.Run(async () =>
do
{ {
{ await tcpServer.Send(clientId, messageData, 0, messageDataLength);
// Check for a corrupted stream. Read a max of 5 bytes. bufferRental.Return(messageData);
// 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;
}
tcpServer.DataReceived += (clientId, dataLength, data) => tcpServer.DataReceived += (clientId, dataLength, data) =>
{ {
if (dataLength > 0) if (dataLength > 0)
{ {
switch (PeekTypeId(data.Span)) switch (LobbyMessageIdentifier.ReadLobbyMessageIdentifier(data.Span))
{ {
case LobbyCreate.TypeId: case LobbyCreate.TypeId:
{ {
@ -433,8 +422,9 @@ Console.CancelKeyPress += (sender, args) =>
args.Cancel = true; args.Cancel = true;
}; };
Console.WriteLine($"{DateTime.Now}: Application started"); Console.WriteLine($"{DateTime.Now}: Application started v0.7");
udpServer.Start(8088);
tcpServer.Start(8088); tcpServer.Start(8088);
closing.WaitOne(); closing.WaitOne();

View File

@ -1,7 +1,7 @@
using System.Net.Sockets; using System.Net.Sockets;
using System.Net; using System.Net;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using LobbyServerDto;
namespace LobbyServer namespace LobbyServer
{ {
@ -131,6 +131,11 @@ namespace LobbyServer
bool offsetSizeInt = false; 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) while (running)
{ {
int copyOffset = 0; int copyOffset = 0;

View File

@ -0,0 +1,93 @@
using System.Net.Sockets;
using System.Net;
using LobbyServerDto;
namespace LobbyServer
{
/// <summary>
/// Small udp server to receive udp packets and report their remote ip and port to a event delegate
/// </summary>
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);
/// <summary>
/// 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
/// </summary>
public event QueryIpAndPortReceivedEventArgs? QueryIpAndPort;
/// <summary>
/// Listen to requests and fire events
/// </summary>
/// <param name="port">The port to listen on</param>
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<byte> receivedData = new Memory<byte>(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");
}
}
/// <summary>
/// Start udp listener
/// </summary>
/// <param name="port">The port to listen on</param>
public void Start(int port)
{
running = true;
_ = Task.Run(() => Listen(port));
}
/// <summary>
/// Stop udp listener
/// </summary>
public void Stop()
{
running = false;
cancellationTokenSource.Cancel();
}
public void Dispose()
{
if (!isDisposed)
{
cancellationTokenSource.Dispose();
isDisposed = true;
}
}
}
}

View File

@ -1,8 +1,15 @@
namespace LobbyServerDto namespace LobbyServerDto
{ {
/// <summary>
/// Used to subscribe to a game id by guid to observe it's list of open lobbies
/// </summary>
/// <remarks>A client can only be subscribed to one game id, sending multiple observe requests with different game ids will overwrite the previous</remarks>
[LobbyMessage] [LobbyMessage]
public partial class LobbiesObserve public partial class LobbiesObserve
{ {
/// <summary>
/// The game id to subscribe to
/// </summary>
public Guid GameId { get; set; } public Guid GameId { get; set; }
} }
} }

View File

@ -1,5 +1,8 @@
namespace LobbyServerDto namespace LobbyServerDto
{ {
/// <summary>
/// Stop observing and receiving updates on the lobby list
/// </summary>
[LobbyMessage] [LobbyMessage]
public partial class LobbiesStopObserve public partial class LobbiesStopObserve
{ {

View File

@ -0,0 +1,14 @@
namespace LobbyServerDto
{
/// <summary>
/// Send to lobby client after connection is established
/// </summary>
[LobbyMessage]
public partial class LobbyClientConnectionInfo
{
/// <summary>
/// Connection id on the lobby server
/// </summary>
public int Id { get; set; }
}
}

View File

@ -2,19 +2,47 @@
namespace LobbyServerDto namespace LobbyServerDto
{ {
/// <summary>
/// Create a lobby in the lobby list. The lobby will stay until either closed by the host or the host disconnects
/// </summary>
/// <remarks>A host can always have only one open lobby, if another lobby is already opened by the client it will be closed</remarks>
[LobbyMessage] [LobbyMessage]
public partial class LobbyCreate public partial class LobbyCreate
{ {
/// <summary>
/// Game Id for which game this lobby is for
/// </summary>
public Guid GameId { get; set; } public Guid GameId { get; set; }
/// <summary>
/// Display name of the lobby
/// </summary>
[MaxLength(64)] [MaxLength(64)]
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
/// <summary>
/// Game mode of lobby
/// </summary>
public int GameMode { get; set; } public int GameMode { get; set; }
/// <summary>
/// How many players are in the lobby
/// </summary>
public int PlayerCount { get; set; } public int PlayerCount { get; set; }
/// <summary>
/// Maximum players allowed in the lobby
/// </summary>
public int MaxPlayerCount { get; set; } public int MaxPlayerCount { get; set; }
/// <summary>
/// The hash of a password to protect the lobby. Only users with the password can request host information/nat punch.
/// </summary>
[MaxLength(26)] [MaxLength(26)]
public byte[]? PasswordHash { get; set; } public byte[]? PasswordHash { get; set; }
/// <summary>
/// The hosts ip. Used the the host information send to clients on their request.
/// </summary>
[MaxLength(32)] [MaxLength(32)]
public string? HostIp { get; set; } public string? HostIp { get; set; }
/// <summary>
/// The hosts port. Used the the host information send to clients on their request.
/// </summary>
public int HostPort { get; set; } public int HostPort { get; set; }
} }
} }

View File

@ -1,9 +1,19 @@
namespace LobbyServerDto namespace LobbyServerDto
{ {
/// <summary>
/// If send from host closes and deletes a lobby.
/// If received by client, the lobby with the Id parameter has been closed
/// </summary>
[LobbyMessage] [LobbyMessage]
public partial class LobbyDelete public partial class LobbyDelete
{ {
/// <summary>
/// If received by client, this contains the lobby id. Does not need to be set if the host sends this request.
/// </summary>
public Guid Id { get; set; } public Guid Id { get; set; }
/// <summary>
/// If received by client, this contains the game id. Does not need to be set if the host sends this request.
/// </summary>
public Guid GameId { get; set; } public Guid GameId { get; set; }
} }
} }

View File

@ -2,12 +2,24 @@
namespace LobbyServerDto namespace LobbyServerDto
{ {
/// <summary>
/// Send to client upon request, this contains the connection info supplied by the host.
/// </summary>
[LobbyMessage] [LobbyMessage]
public partial class LobbyHostInfo public partial class LobbyHostInfo
{ {
/// <summary>
/// Lobby id this information is for
/// </summary>
public Guid LobbyId { get; set; } public Guid LobbyId { get; set; }
/// <summary>
/// The hosts ip, this could be an internal address
/// </summary>
[MaxLength(32)] [MaxLength(32)]
public string? HostIp { get; set; } public string? HostIp { get; set; }
/// <summary>
/// The hosts port
/// </summary>
public int HostPort { get; set; } public int HostPort { get; set; }
} }
} }

View File

@ -1,13 +1,34 @@
namespace LobbyServerDto namespace LobbyServerDto
{ {
/// <summary>
/// Send when a lobby is created or updates, this contains the lobby information
/// </summary>
[LobbyMessage] [LobbyMessage]
public partial class LobbyInfo public partial class LobbyInfo
{ {
/// <summary>
/// Id of the lobby
/// </summary>
public Guid Id { get; set; } public Guid Id { get; set; }
/// <summary>
/// Displayname of the lobby
/// </summary>
public string? Name { get; set; } public string? Name { get; set; }
/// <summary>
/// Game mode of the lobby
/// </summary>
public int GameMode { get; set; } public int GameMode { get; set; }
/// <summary>
/// Count of currently joined players in the lobby
/// </summary>
public int PlayerCount { get; set; } public int PlayerCount { get; set; }
/// <summary>
/// Max players allowed in the lobby
/// </summary>
public int MaxPlayerCount { get; set; } public int MaxPlayerCount { get; set; }
/// <summary>
/// True if the lobby requires a password to gain access
/// </summary>
public bool PasswordProtected { get; set; } public bool PasswordProtected { get; set; }
} }
} }

View File

@ -0,0 +1,44 @@
namespace LobbyServerDto
{
/// <summary>
/// Used to read the message identifer from a received object
/// </summary>
public static class LobbyMessageIdentifier
{
/// <summary>
/// Read the message identifiere from the stream
/// </summary>
/// <param name="buffer"></param>
/// <returns>The message identifier or -1 if invalid</returns>
public static int ReadLobbyMessageIdentifier(ReadOnlySpan<byte> 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;
}
}
}

View File

@ -2,10 +2,29 @@
namespace LobbyServerDto namespace LobbyServerDto
{ {
/// <summary>
/// Send from host to lobby server and releyed to the client by lobby server if the nat punch is finished.
/// </summary>
[LobbyMessage] [LobbyMessage]
public partial class LobbyNatPunchDone public partial class LobbyNatPunchDone
{ {
/// <summary>
/// The lobby this nat punch was for
/// </summary>
public Guid LobbyId { get; set; } public Guid LobbyId { get; set; }
/// <summary>
/// Id of the nat punch that the lobby server requested from us in <see cref="LobbyRequestNatPunch"/>
/// </summary>
public int NatPunchId { get; set; } public int NatPunchId { get; set; }
/// <summary>
/// The external ip of the host
/// </summary>
[MaxLength(32)]
public string? ExternalIp { get; set; }
/// <summary>
/// The external port of the host
/// </summary>
public int ExternalPort { get; set; }
} }
} }

View File

@ -2,10 +2,19 @@
namespace LobbyServerDto namespace LobbyServerDto
{ {
/// <summary>
/// Request the host address for a lobby
/// </summary>
[LobbyMessage] [LobbyMessage]
public partial class LobbyRequestHostInfo public partial class LobbyRequestHostInfo
{ {
/// <summary>
/// Id of the lobby
/// </summary>
public Guid LobbyId { get; set; } public Guid LobbyId { get; set; }
/// <summary>
/// Hash of the password if the lobby requires one
/// </summary>
[MaxLength(26)] [MaxLength(26)]
public byte[]? PasswordHash { get; set; } public byte[]? PasswordHash { get; set; }
} }

View File

@ -2,16 +2,33 @@
namespace LobbyServerDto namespace LobbyServerDto
{ {
/// <summary>
/// Request a nat punch from the game host
/// </summary>
[LobbyMessage] [LobbyMessage]
public partial class LobbyRequestNatPunch public partial class LobbyRequestNatPunch
{ {
/// <summary>
/// Id of the lobby
/// </summary>
public Guid LobbyId { get; set; } public Guid LobbyId { get; set; }
/// <summary>
/// Password hash if the lobby is password protected
/// </summary>
[MaxLength(26)] [MaxLength(26)]
public byte[]? PasswordHash { get; set; } public byte[]? PasswordHash { get; set; }
/// <summary>
/// Our external ip
/// </summary>
public string? ClientIp { get; set; } public string? ClientIp { get; set; }
/// <summary>
/// Our external port
/// </summary>
public int ClientPort { get; set; } public int ClientPort { get; set; }
/// <summary>
/// Set by the lobby server, this will be the NatPunchId send to the host
/// </summary>
public int NatPunchId { get; set; } public int NatPunchId { get; set; }
} }
} }

View File

@ -2,16 +2,40 @@
namespace LobbyServerDto namespace LobbyServerDto
{ {
/// <summary>
/// If the host updates values of the lobby, it can send this request to update the lobby information on the lobby server
/// </summary>
[LobbyMessage] [LobbyMessage]
public partial class LobbyUpdate public partial class LobbyUpdate
{ {
/// <summary>
/// The displayname of the lobby
/// </summary>
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
/// <summary>
/// The game mode of the lobby
/// </summary>
public int GameMode { get; set; } public int GameMode { get; set; }
/// <summary>
/// Current count of players in the lobby
/// </summary>
public int PlayerCount { get; set; } public int PlayerCount { get; set; }
/// <summary>
/// Max players in the lobby
/// </summary>
public int MaxPlayerCount { get; set; } public int MaxPlayerCount { get; set; }
/// <summary>
/// The hash of the password required to enter this lobby
/// </summary>
public byte[]? PasswordHash { get; set; } public byte[]? PasswordHash { get; set; }
/// <summary>
/// The hosts ip
/// </summary>
[MaxLength(32)] [MaxLength(32)]
public string? HostIp { get; set; } public string? HostIp { get; set; }
/// <summary>
/// The hosts port
/// </summary>
public int HostPort { get; set; } public int HostPort { get; set; }
} }
} }

View File

@ -1,8 +1,14 @@
namespace LobbyServerDto namespace LobbyServerDto
{ {
/// <summary>
/// Send if a client requests information about a password protected lobby with an invalid password hash
/// </summary>
[LobbyMessage] [LobbyMessage]
public partial class LobbyWrongPassword public partial class LobbyWrongPassword
{ {
/// <summary>
/// Id of the lobby
/// </summary>
public Guid LobbyId { get; set; } public Guid LobbyId { get; set; }
} }
} }

View File

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

View File

@ -0,0 +1,15 @@

namespace LobbyServerDto
{
/// <summary>
/// Send from a client to the lobby server via udp to from the game port to retrieve the observerd external ip and port
/// </summary>
[LobbyMessage]
public partial class QueryExternalPortAndIp
{
/// <summary>
/// Lobby client id
/// </summary>
public int LobbyClientId { get; set; }
}
}

View File

@ -0,0 +1,22 @@

using System.ComponentModel.DataAnnotations;
namespace LobbyServerDto
{
/// <summary>
/// The ip and port seen by the lobby udp server by an <see cref="QueryExternalPortAndIp"/> request.
/// </summary>
[LobbyMessage]
public partial class SeenExternalIpAndPort
{
/// <summary>
/// The seen ip of the client
/// </summary>
[MaxLength(32)]
public string? Ip { get; set; }
/// <summary>
/// The seen port of the client
/// </summary>
public int Port { get; set; }
}
}

View File

@ -1,6 +1,9 @@
 
namespace LobbyServerDto namespace LobbyServerDto
{ {
/// <summary>
/// List of accepted game ips
/// </summary>
public static class GameGuids public static class GameGuids
{ {
public static Guid NFS = new Guid("0572706f-0fd9-46a9-8ea6-5a31a5363442"); public static Guid NFS = new Guid("0572706f-0fd9-46a9-8ea6-5a31a5363442");