Compare commits

...
8 Commits
Author SHA1 Message Date
twoischnig d1542df493 Fix threading issues? 2026-04-26 00:54:27 +02:00
twoischnig da2a9431df New dll 2026-04-15 22:25:31 +02:00
twoischnig c198d8a727 Fix dead lobbies and other bugs 2026-04-15 22:23:07 +02:00
twoischnig 8862c6c8a6 1.0.6
Fix allocation garbage
2026-04-13 05:57:22 +02:00
twoischnig 57855e80e3 Fix unessesary memory allocation 2026-04-13 05:50:52 +02:00
twoischnig 5cd58eeec5 - 1.0.4 2024-07-11 22:49:49 +02:00
twoischnig 9e5a79fb68 Fixed typos 2024-07-11 22:35:16 +02:00
twoischnig 287e268df7 Removed Debug 2024-07-11 22:32:21 +02:00
10 changed files with 427 additions and 240 deletions

No files matched your search

@@ -66,7 +66,10 @@
"System.Memory": "4.5.5" "System.Memory": "4.5.5"
}, },
"runtime": { "runtime": {
"LobbyServerDto.dll": {} "LobbyServerDto.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.0.0"
}
} }
} }
} }
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -1,8 +1,8 @@
{ {
"name": "com.incobyte.lobbyclient", "name": "com.incobyte.lobbyclient",
"version": "1.0.3", "version": "1.0.8",
"displayName": "Game Lobby Client", "displayName": "Game Lobby Client",
"description": "Provides a client for the game lobvy server to list and join lobbies", "description": "Provides a client for the game lobby server to list and join lobbies",
"unity": "2022.3", "unity": "2022.3",
"keywords": [ "keywords": [
"nat punch", "nat punch",
+199 -66
View File
@@ -21,7 +21,7 @@ namespace Lobbies
UdpEchoServer udpEchoServer = new UdpEchoServer(); UdpEchoServer udpEchoServer = new UdpEchoServer();
private Dictionary<Guid, LobbyInfo> lobbyInformation = new Dictionary<Guid, LobbyInfo>(); private ConcurrentDictionary<Guid, LobbyInfo> lobbyInformation = new ConcurrentDictionary<Guid, LobbyInfo>();
private string? host; private string? host;
private int port; private int port;
private int connectionId; private int connectionId;
@@ -29,6 +29,8 @@ namespace Lobbies
public string? externalIp; public string? externalIp;
public int externalPort; public int externalPort;
private CancellationTokenRegistration connectionCancellationRegistration;
public void Connect(string host, int port, CancellationToken cancellationToken) public void Connect(string host, int port, CancellationToken cancellationToken)
{ {
this.host = host; this.host = host;
@@ -37,7 +39,8 @@ namespace Lobbies
try try
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
using var cts = cancellationToken.Register(() => connectionCancellationRegistration.Dispose();
connectionCancellationRegistration = cancellationToken.Register(() =>
{ {
tcpClient.Stop(); tcpClient.Stop();
}); });
@@ -56,24 +59,9 @@ namespace Lobbies
} }
} }
public IEnumerable<LobbyClientEvent> ReadEvents(int maxEvents) public bool TryReadEvent(out LobbyClientEvent? result)
{ {
if (events.Count > 0) return events.TryDequeue(out result);
{
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) public void HostLobby(Guid gameId, string name, int gameMode, int maxPlayerCount, string? password, int port)
@@ -102,8 +90,16 @@ namespace Lobbies
}; };
byte[] messageData = bufferRental.Rent(); byte[] messageData = bufferRental.Rent();
try
{
var len = lobbyCreate.Serialize(messageData); var len = lobbyCreate.Serialize(messageData);
await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); await tcpClient.Send(messageData, 0, len);
}
finally
{
bufferRental.Return(messageData);
}
}); });
} }
@@ -111,8 +107,11 @@ namespace Lobbies
{ {
_ = Task.Run(async () => { _ = Task.Run(async () => {
byte[]? passwordHash = null; byte[]? passwordHash = null;
if (!string.IsNullOrEmpty(password) && lobbyInformation.ContainsKey(lobbyId) && lobbyInformation[lobbyId].PasswordSalt != null)
passwordHash = PasswordHash.Hash(password, lobbyInformation[lobbyId].PasswordSalt!); if (!string.IsNullOrEmpty(password) &&
lobbyInformation.TryGetValue(lobbyId, out var lobby) &&
lobby.PasswordSalt != null)
passwordHash = PasswordHash.Hash(password, lobby.PasswordSalt!);
var lobbyCreate = new LobbyRequestHostInfo() var lobbyCreate = new LobbyRequestHostInfo()
{ {
@@ -120,9 +119,16 @@ namespace Lobbies
PasswordHash = passwordHash, PasswordHash = passwordHash,
}; };
byte[] messageData = bufferRental.Rent(); var messageData = bufferRental.Rent();
try
{
var len = lobbyCreate.Serialize(messageData); var len = lobbyCreate.Serialize(messageData);
await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); await tcpClient.Send(messageData, 0, len);
}
finally
{
bufferRental.Return(messageData);
}
}); });
} }
@@ -149,21 +155,48 @@ namespace Lobbies
Task.Run(() => Task.Run(() =>
{ {
byte[]? passwordHash = null; byte[]? passwordHash = null;
if (!string.IsNullOrEmpty(password) && lobbyInformation.ContainsKey(lobbyId) && lobbyInformation[lobbyId].PasswordSalt != null) if (!string.IsNullOrEmpty(password) &&
passwordHash = PasswordHash.Hash(password, lobbyInformation[lobbyId].PasswordSalt!); lobbyInformation.TryGetValue(lobbyId, out var lobby) &&
lobby.PasswordSalt != null)
{
passwordHash = PasswordHash.Hash(password, lobby.PasswordSalt);
}
if (!QueryExternalIpAndPort(sendUdpToGetExternalPortMappingCallback, out var ip, out var mappedPort))
{
events.Enqueue(new LobbyClientEvent
{
EventType = LobbyClientEventTypes.Failed,
EventData = new LobbyClientDisconnectReason
{
WasError = true,
ErrorMessage = "Could not determine external IP/port."
}
});
return;
}
QueryExternalIpAndPort(sendUdpToGetExternalPortMappingCallback);
var lobbyRequestNatPunch = new LobbyRequestNatPunch() var lobbyRequestNatPunch = new LobbyRequestNatPunch()
{ {
LobbyId = lobbyId, LobbyId = lobbyId,
PasswordHash = passwordHash, PasswordHash = passwordHash,
ClientIp = externalIp, ClientIp = ip,
ClientPort = externalPort ClientPort = mappedPort
}; };
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 () =>
{
try
{
await tcpClient.Send(messageData, 0, len);
}
finally
{
bufferRental.Return(messageData);
}
});
}); });
} }
@@ -179,31 +212,65 @@ namespace Lobbies
/// after that the udp client will be disposed.</param> /// after that the udp client will be disposed.</param>
public int RequestLobbyNatPunch(Guid lobbyId, string? password, int port = 0) public int RequestLobbyNatPunch(Guid lobbyId, string? password, int port = 0)
{ {
if(port < 0 && port > 65535) if(port < 0 || port > 65535)
throw new ArgumentOutOfRangeException(nameof(port)); throw new ArgumentOutOfRangeException(nameof(port));
var udpEchoServer = new UdpEchoServer();
udpEchoServer.Start(port);
Task.Run(() => Task.Run(() =>
{ {
byte[]? passwordHash = null; var udpEchoServer = new UdpEchoServer();
if (!string.IsNullOrEmpty(password) && lobbyInformation.ContainsKey(lobbyId) && lobbyInformation[lobbyId].PasswordSalt != null)
passwordHash = PasswordHash.Hash(password, lobbyInformation[lobbyId].PasswordSalt!); try
{
udpEchoServer.Start(port);
byte[]? passwordHash = null;
if (!string.IsNullOrEmpty(password) &&
lobbyInformation.TryGetValue(lobbyId, out var lobby) &&
lobby.PasswordSalt != null)
{
passwordHash = PasswordHash.Hash(password, lobby.PasswordSalt);
}
if (!QueryExternalIpAndPort(udpEchoServer.Send, out var ip, out var mappedPort))
{
events.Enqueue(new LobbyClientEvent
{
EventType = LobbyClientEventTypes.Failed,
EventData = new LobbyClientDisconnectReason
{
WasError = true,
ErrorMessage = "Could not determine external IP/port."
}
});
return;
}
QueryExternalIpAndPort(udpEchoServer.Send);
udpEchoServer.Dispose();
var lobbyRequestNatPunch = new LobbyRequestNatPunch() var lobbyRequestNatPunch = new LobbyRequestNatPunch()
{ {
LobbyId = lobbyId, LobbyId = lobbyId,
PasswordHash = passwordHash, PasswordHash = passwordHash,
ClientIp = externalIp, ClientIp = ip,
ClientPort = externalPort ClientPort = mappedPort
}; };
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 () =>
{
try
{
await tcpClient.Send(messageData, 0, len);
}
finally
{
bufferRental.Return(messageData);
}
});
}
finally
{
udpEchoServer.Dispose();
}
}); });
return udpEchoServer.Port; return udpEchoServer.Port;
@@ -233,7 +300,11 @@ namespace Lobbies
byte[] messageData = bufferRental.Rent(); byte[] messageData = bufferRental.Rent();
var len = lobbyUpdate.Serialize(messageData); var len = lobbyUpdate.Serialize(messageData);
_ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); }); _ = Task.Run(async () => {
try { await tcpClient.Send(messageData, 0, len); }
finally { bufferRental.Return(messageData); }
});
} }
public void CloseLobby() public void CloseLobby()
@@ -247,7 +318,9 @@ namespace Lobbies
byte[] messageData = bufferRental.Rent(); byte[] messageData = bufferRental.Rent();
var len = lobbyDelete.Serialize(messageData); var len = lobbyDelete.Serialize(messageData);
_ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); }); _ = Task.Run(async () => {
try { await tcpClient.Send(messageData, 0, len); } finally { bufferRental.Return(messageData); }
});
} }
public void ObserveLobbies(Guid gameId) public void ObserveLobbies(Guid gameId)
@@ -256,7 +329,7 @@ namespace Lobbies
byte[] messageData = bufferRental.Rent(); byte[] messageData = bufferRental.Rent();
var len = lobbiesObserve.Serialize(messageData); var len = lobbiesObserve.Serialize(messageData);
_ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); }); _ = Task.Run(async () => { try { await tcpClient.Send(messageData, 0, len); } finally { bufferRental.Return(messageData); } });
} }
public void StopObservingLobbies() public void StopObservingLobbies()
@@ -265,7 +338,7 @@ namespace Lobbies
byte[] messageData = bufferRental.Rent(); byte[] messageData = bufferRental.Rent();
var len = lobbiesStopObserve.Serialize(messageData); var len = lobbiesStopObserve.Serialize(messageData);
_ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); }); _ = Task.Run(async () => { try { await tcpClient.Send(messageData, 0, len); } finally { bufferRental.Return(messageData); } });
} }
public void NotifyLobbyNatPunchDone(int natPunchId, string externalIp, int externalPort) public void NotifyLobbyNatPunchDone(int natPunchId, string externalIp, int externalPort)
@@ -274,7 +347,7 @@ namespace Lobbies
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 () => { try { await tcpClient.Send(messageData, 0, len); } finally { bufferRental.Return(messageData); } });
} }
public Task TryDirectConnection(IPAddress[] ipAddressesToTry, int tryPort) public Task TryDirectConnection(IPAddress[] ipAddressesToTry, int tryPort)
@@ -348,26 +421,58 @@ namespace Lobbies
return new IPAddress[0]; return new IPAddress[0];
} }
public void QueryExternalIpAndPort(SendUdpMessageCallback sendUdpCallback) public bool QueryExternalIpAndPort(SendUdpMessageCallback sendUdpCallback, out string? ip, out int port)
{ {
ip = null;
port = 0;
byte[] messageData = bufferRental.Rent(); byte[] messageData = bufferRental.Rent();
try try
{ {
waitForExternalIp.Reset(); if (sendUdpCallback == null)
var queryExternalPortAndIp = new QueryExternalPortAndIp() { LobbyClientId = connectionId }; throw new ArgumentNullException(nameof(sendUdpCallback));
var len = queryExternalPortAndIp.Serialize(messageData);
var ip = GetIPsByName(host!, true, false).First();
var tries = 0;
do
{
sendUdpCallback(new IPEndPoint(ip, port), messageData, len);
}
while (!waitForExternalIp.WaitOne(100) && tries++ < 100);
}
catch
{
if (string.IsNullOrWhiteSpace(host))
return false;
if (connectionId <= 0)
return false;
var ips = GetIPsByName(host, true, false);
if (ips.Length == 0)
return false;
externalIp = null;
externalPort = 0;
waitForExternalIp.Reset();
var queryExternalPortAndIp = new QueryExternalPortAndIp
{
LobbyClientId = connectionId
};
int len = queryExternalPortAndIp.Serialize(messageData);
var remoteEndpoint = new IPEndPoint(ips[0], this.port);
for (int tries = 0; tries < 100; tries++)
{
sendUdpCallback(remoteEndpoint, messageData, len);
if (waitForExternalIp.WaitOne(100))
{
if (!string.IsNullOrWhiteSpace(externalIp) && externalPort > 0)
{
ip = externalIp;
port = externalPort;
return true;
}
return false;
}
}
return false;
} }
finally finally
{ {
@@ -432,7 +537,7 @@ namespace Lobbies
var lobbyDelete = LobbyDelete.Deserialize(data.Span); var lobbyDelete = LobbyDelete.Deserialize(data.Span);
if (lobbyDelete != null) if (lobbyDelete != null)
{ {
lobbyInformation.Remove(lobbyDelete.Id); lobbyInformation.TryRemove(lobbyDelete.Id, out var _);
events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.LobbyDelete, EventData = lobbyDelete }); events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.LobbyDelete, EventData = lobbyDelete });
} }
} }
@@ -486,19 +591,47 @@ namespace Lobbies
public IEnumerable<IPAddress> GatherLocalIpAddresses() public IEnumerable<IPAddress> GatherLocalIpAddresses()
{ {
foreach (NetworkInterface netInterface in NetworkInterface.GetAllNetworkInterfaces()) foreach (var netInterface in NetworkInterface.GetAllNetworkInterfaces())
{ {
IPInterfaceProperties ipProps = netInterface.GetIPProperties(); if (netInterface.OperationalStatus != OperationalStatus.Up)
continue;
foreach (UnicastIPAddressInformation addr in ipProps.UnicastAddresses) if (netInterface.NetworkInterfaceType == NetworkInterfaceType.Loopback)
continue;
// Optional: virtuelle Interfaces rausfiltern (je nach Bedarf)
if (netInterface.Description.Contains("Virtual", StringComparison.OrdinalIgnoreCase) ||
netInterface.Description.Contains("VMware", StringComparison.OrdinalIgnoreCase) ||
netInterface.Description.Contains("Docker", StringComparison.OrdinalIgnoreCase))
continue;
var ipProps = netInterface.GetIPProperties();
foreach (var addr in ipProps.UnicastAddresses)
{ {
yield return addr.Address; var ip = addr.Address;
// Nur IPv4
if (ip.AddressFamily != AddressFamily.InterNetwork)
continue;
// Loopback nochmal sicherheitshalber
if (IPAddress.IsLoopback(ip))
continue;
// Link-local (APIPA) raus (169.254.x.x)
if (ip.GetAddressBytes()[0] == 169 && ip.GetAddressBytes()[1] == 254)
continue;
yield return ip;
} }
} }
} }
public void Dispose() public void Dispose()
{ {
Stop();
connectionCancellationRegistration.Dispose();
waitForExternalIp.Dispose(); waitForExternalIp.Dispose();
tcpClient.Dispose(); tcpClient.Dispose();
udpEchoServer.Dispose(); udpEchoServer.Dispose();
+126 -87
View File
@@ -1,4 +1,6 @@
using System; using LobbyServerDto;
using System;
using System.IO;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -13,141 +15,170 @@ namespace Lobbies
internal delegate void DisconnectedEventArgs(bool clean, string error); internal delegate void DisconnectedEventArgs(bool clean, string error);
internal event DisconnectedEventArgs? Disconnected; internal event DisconnectedEventArgs? Disconnected;
internal delegate void ConnectedEventArgs(); internal delegate void ConnectedEventArgs();
internal event ConnectedEventArgs? Connected; internal event ConnectedEventArgs? Connected;
TcpClient? tcpClient; private const int HeaderSize = 4;
NetworkStream? networkStream; private const int MaxMessageSize = 4096;
CancellationTokenSource? cancellationTokenSource = new CancellationTokenSource(); private static readonly TimeSpan PingInterval = TimeSpan.FromSeconds(10);
bool running = false;
private TcpClient? tcpClient;
private NetworkStream? networkStream;
private CancellationTokenSource? cancellationTokenSource = new CancellationTokenSource();
private bool running = false;
private readonly BufferRental bufferRental = new BufferRental(MaxMessageSize);
private async Task ReadExact(NetworkStream stream, Memory<byte> buffer, int length, CancellationToken token)
{
int offset = 0;
while (offset < length)
{
int read = await stream.ReadAsync(buffer.Slice(offset), token);
if (read == 0)
throw new EndOfStreamException();
offset += read;
}
}
internal async Task Connect(string host, int port) internal async Task Connect(string host, int port)
{ {
bool wasError = false; bool cleanDisconnect = true;
string error = string.Empty; string error = string.Empty;
Task? pingTask = null;
try try
{ {
cancellationTokenSource!.Token.ThrowIfCancellationRequested(); if (cancellationTokenSource == null || cancellationTokenSource.IsCancellationRequested)
cancellationTokenSource = new CancellationTokenSource();
var token = cancellationTokenSource.Token;
token.ThrowIfCancellationRequested();
running = true; running = true;
tcpClient = new TcpClient(); tcpClient = new TcpClient();
using (cancellationTokenSource!.Token.Register(() => { tcpClient.Close(); }))
using (token.Register(() =>
{
try { tcpClient.Close(); } catch { }
}))
{ {
await tcpClient.ConnectAsync(host, port); await tcpClient.ConnectAsync(host, port);
} }
networkStream = tcpClient.GetStream(); networkStream = tcpClient.GetStream();
Memory<byte> buffer = new byte[4096];
Memory<byte> target = new byte[4096];
int currentOffset = 0;
int currentMessageRemainingLength = 0;
int currentMessageLength = 0;
bool validMessage = true;
int currentReadOffset = 0;
bool offsetSizeInt = false;
Connected?.Invoke(); Connected?.Invoke();
while (running) pingTask = Task.Run(() => PingLoop(token), token);
Memory<byte> buffer = new byte[MaxMessageSize];
while (running && !token.IsCancellationRequested)
{ {
int copyOffset = 0; await ReadExact(networkStream, buffer, HeaderSize, token);
int receivedBytes = currentReadOffset;
if (currentReadOffset < 4) int length = BitConverter.ToInt32(buffer.Span.Slice(0, HeaderSize));
{
receivedBytes += await networkStream.ReadAsync(buffer.Slice(currentReadOffset), cancellationTokenSource.Token) + currentReadOffset;
}
if (receivedBytes == 0 && running && !cancellationTokenSource.Token.IsCancellationRequested) if (length < 0)
{ throw new InvalidDataException("Negative message length received.");
throw new Exception("Connection lost!");
}
if (receivedBytes > 3 || (currentMessageRemainingLength > 0 && receivedBytes > currentMessageRemainingLength)) if (length > MaxMessageSize)
{ throw new InvalidDataException($"Message too large: {length} > {MaxMessageSize}.");
currentReadOffset = 0;
if (currentMessageLength == 0)
{
currentMessageRemainingLength = BitConverter.ToInt32(buffer.Span);
currentMessageLength = currentMessageRemainingLength;
receivedBytes -= 4;
copyOffset += 4;
offsetSizeInt = true;
}
else
offsetSizeInt = false;
var receivedCount = Math.Min(receivedBytes, currentMessageRemainingLength); if (length == 0)
receivedBytes -= receivedCount; continue;
copyOffset += receivedCount;
if (validMessage && currentOffset + receivedCount > 0) await ReadExact(networkStream, buffer, length, token);
{
if (currentOffset + receivedCount < target.Length)
buffer.Slice(offsetSizeInt ? 4 : 0, receivedCount).CopyTo(target.Slice(currentOffset));
else
validMessage = false;
}
currentOffset += receivedCount; DataReceived?.Invoke(length, buffer.Slice(0, length));
currentMessageRemainingLength -= receivedCount;
if (currentMessageRemainingLength <= 0)
{
if (validMessage)
DataReceived?.Invoke(currentMessageLength, target);
if (receivedBytes > 0)
{
buffer.Slice(copyOffset, receivedBytes).CopyTo(buffer);
currentReadOffset += receivedBytes;
}
currentOffset = 0;
currentMessageLength = 0;
currentMessageRemainingLength = 0;
validMessage = true;
} }
} }
else if (receivedBytes > 0) catch (OperationCanceledException)
{ {
currentReadOffset += receivedBytes; cleanDisconnect = true;
}
}
} }
catch (Exception e) catch (Exception e)
{ {
cleanDisconnect = false;
error = e.Message; error = e.Message;
} }
finally finally
{ {
wasError = running;
running = false; running = false;
networkStream?.Dispose(); try { cancellationTokenSource?.Cancel(); } catch { }
tcpClient?.Dispose();
if (pingTask != null)
{
try { await pingTask; } catch { }
}
try { networkStream?.Dispose(); } catch { }
try { tcpClient?.Close(); } catch { }
try { tcpClient?.Dispose(); } catch { }
tcpClient = null;
networkStream = null; networkStream = null;
tcpClient = null;
Disconnected?.Invoke(!wasError, error); Disconnected?.Invoke(cleanDisconnect, error);
}
}
private async Task PingLoop(CancellationToken token)
{
while (running && !token.IsCancellationRequested)
{
await Task.Delay(PingInterval, token);
if (!running || token.IsCancellationRequested)
break;
await SendPing(token);
}
}
private async Task SendPing(CancellationToken token)
{
if (!running || networkStream == null)
return;
try
{
await networkStream.WriteAsync(BitConverter.GetBytes(0), 0, 4, token);
}
catch
{
} }
} }
internal async Task Send(byte[] buffer, int offset, int count) internal async Task Send(byte[] buffer, int offset, int count)
{ {
byte[] frame = bufferRental.Rent();
try try
{ {
if (running && networkStream != null) if (!running || networkStream == null || cancellationTokenSource == null)
return;
if (count < 0 || count > MaxMessageSize)
throw new ArgumentOutOfRangeException(nameof(count));
if (offset < 0 || offset + count > buffer.Length)
throw new ArgumentOutOfRangeException(nameof(offset));
BitConverter.GetBytes(count).CopyTo(frame, 0);
Buffer.BlockCopy(buffer, offset, frame, HeaderSize, count);
await networkStream.WriteAsync(frame, 0, HeaderSize + count, cancellationTokenSource.Token);
}
catch
{ {
await networkStream.WriteAsync(BitConverter.GetBytes(count - offset), 0, 4, cancellationTokenSource!.Token);
await networkStream.WriteAsync(buffer, offset, count, cancellationTokenSource!.Token);
} }
finally
{
bufferRental.Return(frame);
} }
catch { }
} }
internal void Stop() internal void Stop()
@@ -158,9 +189,17 @@ namespace Lobbies
public void Dispose() public void Dispose()
{ {
cancellationTokenSource?.Cancel(); running = false;
tcpClient?.Dispose();
cancellationTokenSource?.Dispose(); try { cancellationTokenSource?.Cancel(); } catch { }
try { networkStream?.Dispose(); } catch { }
try { tcpClient?.Close(); } catch { }
try { tcpClient?.Dispose(); } catch { }
try { cancellationTokenSource?.Dispose(); } catch { }
networkStream = null;
tcpClient = null;
cancellationTokenSource = null;
} }
} }
} }
+4 -3
View File
@@ -3,7 +3,6 @@ using Lobbies;
using LobbyClientTest; using LobbyClientTest;
using LobbyServerDto; using LobbyServerDto;
using System.Net; using System.Net;
using System.Net.WebSockets;
Console.WriteLine("Starting lobby client v0.7!"); Console.WriteLine("Starting lobby client v0.7!");
var lobbyClient = new LobbyClient(); var lobbyClient = new LobbyClient();
@@ -25,8 +24,10 @@ _ = Task.Run(() =>
{ {
while (running) while (running)
{ {
foreach (var lobbyEvent in lobbyClient.ReadEvents(20)) while (lobbyClient.TryReadEvent(out var lobbyEvent))
{ {
if (lobbyEvent == null) continue;
switch (lobbyEvent.EventType) switch (lobbyEvent.EventType)
{ {
case LobbyClientEventTypes.LobbyJoinFailed: case LobbyClientEventTypes.LobbyJoinFailed:
@@ -169,7 +170,7 @@ _ = Task.Run(() =>
lobbyClient.QueryExternalIpAndPort((remoteEndpoint, messageData, messageLength) => lobbyClient.QueryExternalIpAndPort((remoteEndpoint, messageData, messageLength) =>
{ {
fakeGameHost.Send(remoteEndpoint, messageData, messageLength); fakeGameHost.Send(remoteEndpoint, messageData, messageLength);
}); }, out var ip, out var port);
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 < 32; z++) for (int z = 0; z < 32; z++)
+91 -76
View File
@@ -13,16 +13,27 @@ namespace LobbyServer
public delegate void ClientDisconnectedEventArgs(int clientId); public delegate void ClientDisconnectedEventArgs(int clientId);
public event ClientDisconnectedEventArgs? ClientDisconnected; public event ClientDisconnectedEventArgs? ClientDisconnected;
private const int HeaderSize = 4;
private const int MaxMessageSize = 4096;
private readonly BufferRental bufferRental = new BufferRental(MaxMessageSize);
internal class Client : IDisposable internal class Client : IDisposable
{ {
internal CancellationTokenSource? cancellationToken = null; internal CancellationTokenSource? cancellationToken = null;
internal NetworkStream? stream; internal NetworkStream? stream;
internal TcpClient? client; internal TcpClient? client;
internal DateTime lastSeenUtc = DateTime.UtcNow;
public void Dispose() public void Dispose()
{ {
cancellationToken?.Dispose(); try { stream?.Dispose(); } catch { }
try { client?.Close(); } catch { }
try { client?.Dispose(); } catch { }
try { cancellationToken?.Dispose(); } catch { }
cancellationToken = null; cancellationToken = null;
stream = null;
client = null;
} }
} }
@@ -38,6 +49,7 @@ namespace LobbyServer
cancellationToken.TryReset(); cancellationToken.TryReset();
running = true; running = true;
clientIdCounter = 0; clientIdCounter = 0;
_ = Task.Run(() => MonitorClients());
_ = Task.Run(() => Listener(port)); _ = Task.Run(() => Listener(port));
} }
@@ -55,7 +67,6 @@ namespace LobbyServer
catch catch
{ } { }
} }
activeClients.Clear ();
serverClosed.WaitOne(); serverClosed.WaitOne();
} }
@@ -93,127 +104,131 @@ namespace LobbyServer
public async Task Send(int clientId, byte[] buffer, int offset, int count) public async Task Send(int clientId, byte[] buffer, int offset, int count)
{ {
byte[] frame = bufferRental.Rent();
try try
{ {
if (activeClients.TryGetValue(clientId, out var lobbyClient) && lobbyClient.stream != null && lobbyClient.cancellationToken != null) if (activeClients.TryGetValue(clientId, out var lobbyClient) && lobbyClient.stream != null && lobbyClient.cancellationToken != null)
{ {
await lobbyClient.stream.WriteAsync(BitConverter.GetBytes(count - offset), 0, 4, lobbyClient.cancellationToken.Token); BitConverter.GetBytes(count).CopyTo(frame, 0);
await lobbyClient.stream.WriteAsync(buffer, offset, count, lobbyClient.cancellationToken.Token); Buffer.BlockCopy(buffer, offset, frame, HeaderSize, count);
await lobbyClient.stream.WriteAsync(frame, 0, HeaderSize + count, lobbyClient.cancellationToken.Token);
} }
} }
catch { } catch { }
finally
{
bufferRental.Return(frame);
}
}
private async Task ReadExact(NetworkStream stream, Memory<byte> buffer, int length, CancellationToken token)
{
int offset = 0;
while (offset < length)
{
int read = await stream.ReadAsync(buffer.Slice(offset), token);
if (read == 0)
throw new EndOfStreamException();
offset += read;
}
} }
private async Task ClientThread(TcpClient client) private async Task ClientThread(TcpClient client)
{ {
int myId = Interlocked.Increment(ref clientIdCounter); int myId = Interlocked.Increment(ref clientIdCounter);
Client? lobbyClient = null;
try try
{ {
await using NetworkStream stream = client.GetStream(); var stream = client.GetStream();
Memory<byte> buffer = new byte[4096]; Memory<byte> buffer = new byte[MaxMessageSize];
Memory<byte> target = new byte[4096];
using var lobbyClient = new Client
lobbyClient = new Client
{ {
cancellationToken = new CancellationTokenSource(), cancellationToken = new CancellationTokenSource(),
stream = stream, stream = stream,
client = client client = client,
lastSeenUtc = DateTime.UtcNow
}; };
activeClients.TryAdd(myId, lobbyClient); activeClients.TryAdd(myId, lobbyClient);
int currentOffset = 0;
int currentMessageRemainingLength = 0;
int currentMessageLength = 0;
bool validMessage = true;
bool offsetSizeInt = false;
int currentReadOffset = 0;
var lobbyClientConnectionInfo = new LobbyClientConnectionInfo { Id = myId }; var lobbyClientConnectionInfo = new LobbyClientConnectionInfo { Id = myId };
byte[] sendBuffer = new byte[128]; byte[] sendBuffer = new byte[128];
int sendLen = lobbyClientConnectionInfo.Serialize(sendBuffer); int sendLen = lobbyClientConnectionInfo.Serialize(sendBuffer);
await Send(myId, sendBuffer, 0, sendLen); await Send(myId, sendBuffer, 0, sendLen);
while (running) while (running && !lobbyClient.cancellationToken.Token.IsCancellationRequested)
{ {
int copyOffset = 0; await ReadExact(stream, buffer, HeaderSize, lobbyClient.cancellationToken.Token);
int receivedBytes = currentReadOffset; lobbyClient.lastSeenUtc = DateTime.UtcNow;
if (currentReadOffset < 4) int length = BitConverter.ToInt32(buffer.Span.Slice(0, HeaderSize));
{
receivedBytes += await stream.ReadAsync(buffer.Slice(currentReadOffset), lobbyClient.cancellationToken.Token) + currentReadOffset;
if (receivedBytes == 0)
throw new Exception("Connection lost!");
}
if (receivedBytes > 3 || (currentMessageRemainingLength > 0 && receivedBytes >= currentMessageRemainingLength)) if (length < 0)
{ throw new InvalidDataException("Negative message length received.");
currentReadOffset = 0;
if (currentMessageLength == 0)
{
currentMessageRemainingLength = BitConverter.ToInt32(buffer.Span);
currentMessageLength = currentMessageRemainingLength;
receivedBytes -= 4;
copyOffset += 4;
offsetSizeInt = true;
}
else
offsetSizeInt = false;
var receivedCount = Math.Min(receivedBytes, currentMessageRemainingLength); if (length > MaxMessageSize)
throw new InvalidDataException($"Message too large: {length} > {MaxMessageSize}.");
receivedBytes -= receivedCount; if (length == 0)
copyOffset += receivedCount; continue;
if (validMessage && currentOffset + receivedCount > 0) await ReadExact(stream, buffer, length, lobbyClient.cancellationToken.Token);
{
if (currentOffset + receivedCount < target.Length)
buffer.Slice(offsetSizeInt ? 4 : 0, receivedCount).CopyTo(target.Slice(currentOffset));
else
validMessage = false;
}
currentOffset += receivedCount; DataReceived?.Invoke(myId, length, buffer.Slice(0, length));
currentMessageRemainingLength -= receivedCount;
if (currentMessageRemainingLength <= 0)
{
if(validMessage)
DataReceived?.Invoke(myId, currentMessageLength, target);
if (receivedBytes > 0)
{
buffer.Slice(copyOffset, receivedBytes).CopyTo(buffer);
currentReadOffset += receivedBytes;
}
currentOffset = 0;
currentMessageLength = 0;
currentMessageRemainingLength = 0;
validMessage = true;
}
}
else if(receivedBytes > 0)
{
currentReadOffset += receivedBytes;
}
} }
} }
finally finally
{ {
activeClients.TryRemove(myId, out _);
lobbyClient?.Dispose();
ClientDisconnected?.Invoke(myId); ClientDisconnected?.Invoke(myId);
activeClients.TryRemove(myId, out var _);
client?.Dispose();
} }
} }
internal string? GetClientIp(int cliendId) private async Task MonitorClients()
{ {
try try
{ {
if (activeClients.TryGetValue(cliendId, out var client)) while (running)
{
var now = DateTime.UtcNow;
foreach (var kv in activeClients)
{
var id = kv.Key;
var client = kv.Value;
if ((now - client.lastSeenUtc) > TimeSpan.FromSeconds(30))
{
try
{
client.cancellationToken?.Cancel();
client.client?.Close();
}
catch { }
}
}
await Task.Delay(1000, cancellationToken.Token);
}
}
catch (OperationCanceledException)
{
}
}
internal string? GetClientIp(int cliendIp)
{
try
{
if (activeClients.TryGetValue(cliendIp, out var client))
{ {
return (client.client!.Client.RemoteEndPoint as IPEndPoint)!.Address.ToString(); return (client.client!.Client.RemoteEndPoint as IPEndPoint)!.Address.ToString();
} }
+1 -4
View File
@@ -1,11 +1,10 @@
using System.Net.Sockets; using System.Net.Sockets;
using System.Net;
using LobbyServerDto; using LobbyServerDto;
namespace LobbyServer namespace LobbyServer
{ {
/// <summary> /// <summary>
/// Small udp server to receive udp packets and report their remote ip and port to a event delegate /// Small udp server to receive udp packets and report their remote ip and port to an event delegate
/// </summary> /// </summary>
internal class UdpCandidateServer : IDisposable internal class UdpCandidateServer : IDisposable
{ {
@@ -36,14 +35,12 @@ namespace LobbyServer
var receiveResult = await serverSocket.ReceiveAsync(cancellationTokenSource.Token); var receiveResult = await serverSocket.ReceiveAsync(cancellationTokenSource.Token);
if(receiveResult.Buffer.Length > 0) if(receiveResult.Buffer.Length > 0)
{ {
Console.WriteLine($"{DateTime.Now}: [UdpCandidateServer] received packet");
Memory<byte> receivedData = new Memory<byte>(receiveResult.Buffer); Memory<byte> receivedData = new Memory<byte>(receiveResult.Buffer);
var identifier = LobbyMessageIdentifier.ReadLobbyMessageIdentifier(receivedData.Span); var identifier = LobbyMessageIdentifier.ReadLobbyMessageIdentifier(receivedData.Span);
switch (identifier) switch (identifier)
{ {
case QueryExternalPortAndIp.TypeId: case QueryExternalPortAndIp.TypeId:
var queryExternalPortAndIp = QueryExternalPortAndIp.Deserialize(receivedData.Span); var queryExternalPortAndIp = QueryExternalPortAndIp.Deserialize(receivedData.Span);
Console.WriteLine($"{DateTime.Now}: [UdpCandidateServer] received QueryExternalPortAndIp for lobby client id {queryExternalPortAndIp.LobbyClientId} from {receiveResult.RemoteEndPoint.Address.ToString()}:{receiveResult.RemoteEndPoint.Port}");
QueryIpAndPort?.Invoke(queryExternalPortAndIp.LobbyClientId, receiveResult.RemoteEndPoint.Address.ToString(), receiveResult.RemoteEndPoint.Port); QueryIpAndPort?.Invoke(queryExternalPortAndIp.LobbyClientId, receiveResult.RemoteEndPoint.Address.ToString(), receiveResult.RemoteEndPoint.Port);
break; break;
default: default:
-1
View File
@@ -1,5 +1,4 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Drawing;
namespace LobbyServerDto namespace LobbyServerDto
{ {