diff --git a/Assets/NetworkLobbyClient/Runtime/LobbyClient.dll b/Assets/NetworkLobbyClient/Runtime/LobbyClient.dll index 1752f7d..fe2de95 100644 Binary files a/Assets/NetworkLobbyClient/Runtime/LobbyClient.dll and b/Assets/NetworkLobbyClient/Runtime/LobbyClient.dll differ diff --git a/Assets/NetworkLobbyClient/Runtime/LobbyServerDto.dll b/Assets/NetworkLobbyClient/Runtime/LobbyServerDto.dll index 735b14d..b069a44 100644 Binary files a/Assets/NetworkLobbyClient/Runtime/LobbyServerDto.dll and b/Assets/NetworkLobbyClient/Runtime/LobbyServerDto.dll differ diff --git a/Assets/NetworkLobbyClient/package.json b/Assets/NetworkLobbyClient/package.json index 0dfb399..a2d0fa7 100644 --- a/Assets/NetworkLobbyClient/package.json +++ b/Assets/NetworkLobbyClient/package.json @@ -1,6 +1,6 @@ { "name": "com.incobyte.lobbyclient", - "version": "1.0.7", + "version": "1.0.8", "displayName": "Game Lobby Client", "description": "Provides a client for the game lobby server to list and join lobbies", "unity": "2022.3", diff --git a/LobbyClient/TcpClient.cs b/LobbyClient/TcpClient.cs index edad351..25ce41d 100644 --- a/LobbyClient/TcpClient.cs +++ b/LobbyClient/TcpClient.cs @@ -1,4 +1,5 @@ -using System; +using LobbyServerDto; +using System; using System.IO; using System.Net.Sockets; using System.Threading; @@ -25,7 +26,20 @@ namespace Lobbies private NetworkStream? networkStream; private CancellationTokenSource? cancellationTokenSource = new CancellationTokenSource(); private bool running = false; - private readonly SemaphoreSlim sendLock = new SemaphoreSlim(1, 1); + private readonly BufferRental bufferRental = new BufferRental(MaxMessageSize); + private async Task ReadExact(NetworkStream stream, Memory 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) { @@ -60,81 +74,26 @@ namespace Lobbies pingTask = Task.Run(() => PingLoop(token), token); Memory buffer = new byte[MaxMessageSize]; - Memory target = new byte[MaxMessageSize]; - - int bufferedBytes = 0; - int currentMessageLength = -1; - int currentMessageOffset = 0; while (running && !token.IsCancellationRequested) { - if (bufferedBytes == buffer.Length) - throw new InvalidDataException("Receive buffer overflow."); + await ReadExact(networkStream, buffer, HeaderSize, token); - int bytesRead = await networkStream.ReadAsync(buffer.Slice(bufferedBytes), token); - if (bytesRead == 0) - break; + int length = BitConverter.ToInt32(buffer.Span.Slice(0, HeaderSize)); - bufferedBytes += bytesRead; + if (length < 0) + throw new InvalidDataException("Negative message length received."); - while (true) - { - if (currentMessageLength < 0) - { - if (bufferedBytes < HeaderSize) - break; + if (length > MaxMessageSize) + throw new InvalidDataException($"Message too large: {length} > {MaxMessageSize}."); - currentMessageLength = BitConverter.ToInt32(buffer.Span.Slice(0, HeaderSize)); + if (length == 0) + continue; - if (currentMessageLength < 0) - throw new InvalidDataException("Negative message length received."); + await ReadExact(networkStream, buffer, length, token); - if (currentMessageLength > MaxMessageSize) - throw new InvalidDataException($"Message too large: {currentMessageLength} > {MaxMessageSize}."); - - if (bufferedBytes > HeaderSize) - buffer.Slice(HeaderSize, bufferedBytes - HeaderSize).CopyTo(buffer); - - bufferedBytes -= HeaderSize; - currentMessageOffset = 0; - - if (currentMessageLength == 0) - { - currentMessageLength = -1; - continue; - } - } - - int remainingMessageBytes = currentMessageLength - currentMessageOffset; - if (remainingMessageBytes <= 0) - { - DataReceived?.Invoke(currentMessageLength, target.Slice(0, currentMessageLength)); - currentMessageLength = -1; - currentMessageOffset = 0; - continue; - } - - if (bufferedBytes == 0) - break; - - int chunkSize = Math.Min(bufferedBytes, remainingMessageBytes); - - buffer.Slice(0, chunkSize).CopyTo(target.Slice(currentMessageOffset)); - currentMessageOffset += chunkSize; - - if (bufferedBytes > chunkSize) - buffer.Slice(chunkSize, bufferedBytes - chunkSize).CopyTo(buffer); - - bufferedBytes -= chunkSize; - - if (currentMessageOffset == currentMessageLength) - { - DataReceived?.Invoke(currentMessageLength, target.Slice(0, currentMessageLength)); - currentMessageLength = -1; - currentMessageOffset = 0; - } - } - } + DataReceived?.Invoke(length, buffer.Slice(0, length)); + } } catch (OperationCanceledException) { @@ -185,37 +144,42 @@ namespace Lobbies if (!running || networkStream == null) return; - await sendLock.WaitAsync(token); try { await networkStream.WriteAsync(BitConverter.GetBytes(0), 0, 4, token); } - finally + catch { - sendLock.Release(); } } internal async Task Send(byte[] buffer, int offset, int count) { + byte[] frame = bufferRental.Rent(); try { if (!running || networkStream == null || cancellationTokenSource == null) return; - await sendLock.WaitAsync(cancellationTokenSource.Token); - try - { - await networkStream.WriteAsync(BitConverter.GetBytes(count), 0, 4, cancellationTokenSource.Token); - await networkStream.WriteAsync(buffer, offset, count, cancellationTokenSource.Token); - } - finally - { - sendLock.Release(); - } + 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 { } - } + catch + { + } + finally + { + bufferRental.Return(frame); + } + } internal void Stop() { diff --git a/LobbyClientTest/Program.cs b/LobbyClientTest/Program.cs index 83c51d5..f737bc6 100644 --- a/LobbyClientTest/Program.cs +++ b/LobbyClientTest/Program.cs @@ -3,7 +3,6 @@ using Lobbies; using LobbyClientTest; using LobbyServerDto; using System.Net; -using System.Net.WebSockets; Console.WriteLine("Starting lobby client v0.7!"); var lobbyClient = new LobbyClient(); diff --git a/LobbyServer/TcpLobbyServer.cs b/LobbyServer/TcpLobbyServer.cs index 4a407af..94b7127 100644 --- a/LobbyServer/TcpLobbyServer.cs +++ b/LobbyServer/TcpLobbyServer.cs @@ -13,13 +13,17 @@ namespace LobbyServer public delegate void ClientDisconnectedEventArgs(int clientId); 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 CancellationTokenSource? cancellationToken = null; internal NetworkStream? stream; internal TcpClient? client; internal DateTime lastSeenUtc = DateTime.UtcNow; - + public void Dispose() { try { stream?.Dispose(); } catch { } @@ -100,15 +104,36 @@ namespace LobbyServer public async Task Send(int clientId, byte[] buffer, int offset, int count) { + byte[] frame = bufferRental.Rent(); try { if (activeClients.TryGetValue(clientId, out var lobbyClient) && lobbyClient.stream != null && lobbyClient.cancellationToken != null) - { - await lobbyClient.stream.WriteAsync(BitConverter.GetBytes(count), 0, 4, lobbyClient.cancellationToken.Token); - await lobbyClient.stream.WriteAsync(buffer, offset, count, lobbyClient.cancellationToken.Token); + { + BitConverter.GetBytes(count).CopyTo(frame, 0); + Buffer.BlockCopy(buffer, offset, frame, HeaderSize, count); + + await lobbyClient.stream.WriteAsync(frame, 0, HeaderSize + count, lobbyClient.cancellationToken.Token); } } catch { } + finally + { + bufferRental.Return(frame); + } + } + + private async Task ReadExact(NetworkStream stream, Memory 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) @@ -120,8 +145,8 @@ namespace LobbyServer { var stream = client.GetStream(); - Memory buffer = new byte[4096]; - Memory target = new byte[4096]; + Memory buffer = new byte[MaxMessageSize]; + lobbyClient = new Client { @@ -133,83 +158,31 @@ namespace LobbyServer activeClients.TryAdd(myId, lobbyClient); - int bufferedBytes = 0; - int currentMessageLength = -1; - int currentMessageOffset = 0; - bool validMessage = true; - var lobbyClientConnectionInfo = new LobbyClientConnectionInfo { Id = myId }; byte[] sendBuffer = new byte[128]; int sendLen = lobbyClientConnectionInfo.Serialize(sendBuffer); await Send(myId, sendBuffer, 0, sendLen); - + while (running && !lobbyClient.cancellationToken.Token.IsCancellationRequested) { - int bytesRead = await stream.ReadAsync(buffer.Slice(bufferedBytes), lobbyClient.cancellationToken.Token); - if (bytesRead == 0) - break; - - bufferedBytes += bytesRead; + await ReadExact(stream, buffer, HeaderSize, lobbyClient.cancellationToken.Token); lobbyClient.lastSeenUtc = DateTime.UtcNow; - while (true) - { - if (currentMessageLength < 0) - { - if (bufferedBytes < 4) - break; + int length = BitConverter.ToInt32(buffer.Span.Slice(0, HeaderSize)); - currentMessageLength = BitConverter.ToInt32(buffer.Span.Slice(0, 4)); + if (length < 0) + throw new InvalidDataException("Negative message length received."); - if (currentMessageLength < 0) - throw new InvalidDataException("Negative message length received."); + if (length > MaxMessageSize) + throw new InvalidDataException($"Message too large: {length} > {MaxMessageSize}."); - buffer.Slice(4, bufferedBytes - 4).CopyTo(buffer); - bufferedBytes -= 4; - currentMessageOffset = 0; - validMessage = currentMessageLength <= target.Length; - - if (currentMessageLength == 0) - { - currentMessageLength = -1; - continue; - } - } - - if (bufferedBytes == 0) - break; - - int chunkSize = Math.Min(bufferedBytes, currentMessageLength - currentMessageOffset); - - if (chunkSize > 0) - { - if (validMessage) - { - buffer.Slice(0, chunkSize).CopyTo(target.Slice(currentMessageOffset)); - } - - currentMessageOffset += chunkSize; - - buffer.Slice(chunkSize, bufferedBytes - chunkSize).CopyTo(buffer); - bufferedBytes -= chunkSize; - } - - if (currentMessageOffset < currentMessageLength) - break; - - if (validMessage) - { - DataReceived?.Invoke(myId, currentMessageLength, target.Slice(0, currentMessageLength)); - } - - currentMessageLength = -1; - currentMessageOffset = 0; - validMessage = true; - } - - if (bufferedBytes == buffer.Length && currentMessageLength < 0) - throw new InvalidDataException("Receive buffer overflow while waiting for message header."); - } + if (length == 0) + continue; + + await ReadExact(stream, buffer, length, lobbyClient.cancellationToken.Token); + + DataReceived?.Invoke(myId, length, buffer.Slice(0, length)); + } } finally { diff --git a/LobbyServer/UdpCandidateServer.cs b/LobbyServer/UdpCandidateServer.cs index d77535d..9ef9495 100644 --- a/LobbyServer/UdpCandidateServer.cs +++ b/LobbyServer/UdpCandidateServer.cs @@ -1,11 +1,10 @@ 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 + /// Small udp server to receive udp packets and report their remote ip and port to an event delegate /// internal class UdpCandidateServer : IDisposable { diff --git a/LobbyServerDto/BufferRental.cs b/LobbyServerDto/BufferRental.cs index 2792ca9..231f300 100644 --- a/LobbyServerDto/BufferRental.cs +++ b/LobbyServerDto/BufferRental.cs @@ -1,5 +1,4 @@ using System.Collections.Concurrent; -using System.Drawing; namespace LobbyServerDto {