From f906b010db30a9cfa27d0c03c43e41a3678ebddb Mon Sep 17 00:00:00 2001 From: AeonLucid Date: Mon, 19 Oct 2020 16:18:49 +0200 Subject: [PATCH] Re-add Hazel client --- .../Net/Messages/C2S/Message00HostGameC2S.cs | 4 + .../Impostor.Client.App.csproj | 16 ++ src/Impostor.Client.App/Program.cs | 70 +++++ src/Impostor.Client/Impostor.Client.csproj | 12 + src/Impostor.Hazel/Connection.cs | 10 +- src/Impostor.Hazel/NetworkConnection.cs | 6 +- src/Impostor.Hazel/Udp/UdpClientConnection.cs | 252 ++++++++++++++++++ src/Impostor.Hazel/Udp/UdpConnection.cs | 27 +- src/Impostor.Hazel/Udp/UdpServerConnection.cs | 17 +- .../Net/Manager/GameManager.cs | 2 +- src/Impostor.sln | 24 ++ 11 files changed, 413 insertions(+), 27 deletions(-) create mode 100644 src/Impostor.Client.App/Impostor.Client.App.csproj create mode 100644 src/Impostor.Client.App/Program.cs create mode 100644 src/Impostor.Client/Impostor.Client.csproj create mode 100644 src/Impostor.Hazel/Udp/UdpClientConnection.cs diff --git a/src/Impostor.Api/Net/Messages/C2S/Message00HostGameC2S.cs b/src/Impostor.Api/Net/Messages/C2S/Message00HostGameC2S.cs index 7245db2..1ee8b76 100644 --- a/src/Impostor.Api/Net/Messages/C2S/Message00HostGameC2S.cs +++ b/src/Impostor.Api/Net/Messages/C2S/Message00HostGameC2S.cs @@ -7,12 +7,16 @@ namespace Impostor.Api.Net.Messages.C2S { public static void Serialize(IMessageWriter writer, GameOptionsData gameOptionsData) { + writer.StartMessage(MessageFlags.HostGame); + using (var memory = new MemoryStream()) using (var writerBin = new BinaryWriter(memory)) { gameOptionsData.Serialize(writerBin, GameOptionsData.LatestVersion); writer.WriteBytesAndSize(memory.ToArray()); } + + writer.EndMessage(); } public static GameOptionsData Deserialize(IMessageReader reader) diff --git a/src/Impostor.Client.App/Impostor.Client.App.csproj b/src/Impostor.Client.App/Impostor.Client.App.csproj new file mode 100644 index 0000000..886e26e --- /dev/null +++ b/src/Impostor.Client.App/Impostor.Client.App.csproj @@ -0,0 +1,16 @@ + + + + Exe + net5.0 + + + + + + + + + + + diff --git a/src/Impostor.Client.App/Program.cs b/src/Impostor.Client.App/Program.cs new file mode 100644 index 0000000..f5f1cf3 --- /dev/null +++ b/src/Impostor.Client.App/Program.cs @@ -0,0 +1,70 @@ +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Hazel; +using Hazel.Udp; +using Impostor.Api.Innersloth; +using Impostor.Api.Net.Messages; +using Impostor.Api.Net.Messages.C2S; +using Serilog; + +namespace Impostor.Client.App +{ + internal static class Program + { + private static readonly ManualResetEvent QuitEvent = new ManualResetEvent(false); + + private static async Task Main(string[] args) + { + Log.Logger = new LoggerConfiguration() + .WriteTo.Console() + .CreateLogger(); + + var writeHandshake = MessageWriter.Get(MessageType.Reliable); + + writeHandshake.Write(50516550); + writeHandshake.Write("AeonLucid"); + + var writeGameCreate = MessageWriter.Get(MessageType.Reliable); + + Message00HostGameC2S.Serialize(writeGameCreate, new GameOptionsData + { + MaxPlayers = 4, + NumImpostors = 2 + }); + + using (var connection = new UdpClientConnection(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 22023))) + { + var e = new ManualResetEvent(false); + + // Register events. + connection.DataReceived = DataReceived; + connection.Disconnected = Disconnected; + + // Connect and send handshake. + await connection.ConnectAsync(writeHandshake.ToByteArray(false)); + Log.Information("Connected."); + + // Create a game. + await connection.Send(writeGameCreate); + Log.Information("Requested game creation."); + + e.WaitOne(); + } + } + + private static ValueTask DataReceived(DataReceivedEventArgs e) + { + Log.Information("Received data."); + return default; + } + + private static ValueTask Disconnected(DisconnectedEventArgs e) + { + Log.Information("Disconnected: " + e.Reason); + QuitEvent.Set(); + return default; + } + } +} \ No newline at end of file diff --git a/src/Impostor.Client/Impostor.Client.csproj b/src/Impostor.Client/Impostor.Client.csproj new file mode 100644 index 0000000..28b6ed3 --- /dev/null +++ b/src/Impostor.Client/Impostor.Client.csproj @@ -0,0 +1,12 @@ + + + + net5.0 + + + + + + + + diff --git a/src/Impostor.Hazel/Connection.cs b/src/Impostor.Hazel/Connection.cs index c2ab579..6b6c87e 100644 --- a/src/Impostor.Hazel/Connection.cs +++ b/src/Impostor.Hazel/Connection.cs @@ -155,21 +155,13 @@ namespace Hazel /// /// public abstract ValueTask SendBytes(byte[] bytes, MessageType messageType = MessageType.Unreliable); - - /// - /// Connects the connection to a server and begins listening. - /// This method blocks and may thrown if there is a problem connecting. - /// - /// The bytes of data to send in the handshake. - /// The number of milliseconds to wait before giving up on the connect attempt. - public abstract void Connect(byte[] bytes = null, int timeout = 5000); /// /// Connects the connection to a server and begins listening. /// This method does not block. /// /// The bytes of data to send in the handshake. - public abstract void ConnectAsync(byte[] bytes = null); + public abstract ValueTask ConnectAsync(byte[] bytes = null); /// /// Invokes the DataReceived event. diff --git a/src/Impostor.Hazel/NetworkConnection.cs b/src/Impostor.Hazel/NetworkConnection.cs index 68966ba..fc1c320 100644 --- a/src/Impostor.Hazel/NetworkConnection.cs +++ b/src/Impostor.Hazel/NetworkConnection.cs @@ -52,14 +52,14 @@ namespace Hazel /// /// Sends a disconnect message to the end point. /// - protected abstract bool SendDisconnect(MessageWriter writer); + protected abstract ValueTask SendDisconnect(MessageWriter writer); /// /// Called when the socket has been disconnected at the remote host. /// protected async ValueTask DisconnectRemote(string reason, IMessageReader reader) { - if (this.SendDisconnect(null)) + if (await SendDisconnect(null)) { try { @@ -107,7 +107,7 @@ namespace Hazel /// public override async ValueTask Disconnect(string reason, MessageWriter writer = null) { - if (this.SendDisconnect(writer)) + if (await SendDisconnect(writer)) { try { diff --git a/src/Impostor.Hazel/Udp/UdpClientConnection.cs b/src/Impostor.Hazel/Udp/UdpClientConnection.cs new file mode 100644 index 0000000..1bb5722 --- /dev/null +++ b/src/Impostor.Hazel/Udp/UdpClientConnection.cs @@ -0,0 +1,252 @@ +using System; +using System.Buffers; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using Impostor.Api.Net.Messages; +using Serilog; + +namespace Hazel.Udp +{ + /// + /// Represents a client's connection to a server that uses the UDP protocol. + /// + /// + public sealed class UdpClientConnection : UdpConnection + { + private static readonly ILogger Logger = Log.ForContext(); + + /// + /// The socket we're connected via. + /// + private readonly UdpClient _socket; + + private readonly Timer _reliablePacketTimer; + private readonly SemaphoreSlim _connectWaitLock; + private readonly MemoryPool _pool; + private readonly Channel _channel; + private Task _listenTask; + private Task _handleTask; + + /// + /// Creates a new UdpClientConnection. + /// + /// A to connect to. + public UdpClientConnection(IPEndPoint remoteEndPoint, IPMode ipMode = IPMode.IPv4) : base(null) + { + EndPoint = remoteEndPoint; + RemoteEndPoint = remoteEndPoint; + IPMode = ipMode; + + _socket = new UdpClient + { + DontFragment = false + }; + + _reliablePacketTimer = new Timer(ManageReliablePacketsInternal, null, 100, Timeout.Infinite); + _connectWaitLock = new SemaphoreSlim(1, 1); + _pool = MemoryPool.Shared; + _channel = Channel.CreateUnbounded(new UnboundedChannelOptions + { + SingleReader = true, + SingleWriter = true + }); + } + + ~UdpClientConnection() + { + Dispose(false); + } + + private async void ManageReliablePacketsInternal(object state) + { + await ManageReliablePackets(); + + try + { + _reliablePacketTimer.Change(100, Timeout.Infinite); + } + catch + { + // ignored + } + } + + /// + protected override ValueTask WriteBytesToConnection(byte[] bytes, int length) + { + return WriteBytesToConnectionReal(bytes, length); + } + + private async ValueTask WriteBytesToConnectionReal(byte[] bytes, int length) + { + try + { + await _socket.SendAsync(bytes, length); + } + catch (NullReferenceException) { } + catch (ObjectDisposedException) + { + // Already disposed and disconnected... + } + catch (SocketException ex) + { + await DisconnectInternal(HazelInternalErrors.SocketExceptionSend, "Could not send data as a SocketException occurred: " + ex.Message); + } + } + + /// + public override async ValueTask ConnectAsync(byte[] bytes = null) + { + State = ConnectionState.Connecting; + + try + { + _socket.Connect(RemoteEndPoint); + } + catch (SocketException e) + { + State = ConnectionState.NotConnected; + throw new HazelException("A SocketException occurred while binding to the port.", e); + } + + try + { + _listenTask = ListenAsync(); + } + catch (ObjectDisposedException) + { + // If the socket's been disposed then we can just end there but make sure we're in NotConnected state. + // If we end up here I'm really lost... + State = ConnectionState.NotConnected; + return; + } + catch (SocketException e) + { + Dispose(); + throw new HazelException("A SocketException occurred while initiating a receive operation.", e); + } + + // Write bytes to the server to tell it hi (and to punch a hole in our NAT, if present) + // When acknowledged set the state to connected + await SendHello(bytes, () => + { + State = ConnectionState.Connected; + InitializeKeepAliveTimer(); + }); + + await _connectWaitLock.WaitAsync(TimeSpan.FromSeconds(10)); + } + + private async Task ListenAsync() + { + // Start packet handler. + await StartAsync(); + + // Listen. + while (State != ConnectionState.NotConnected) + { + UdpReceiveResult data; + + try + { + data = await _socket.ReceiveAsync(); + } + catch (SocketException e) + { + await DisconnectInternal(HazelInternalErrors.SocketExceptionReceive, "Socket exception while reading data: " + e.Message); + return; + } + catch (Exception) + { + return; + } + + if (data.Buffer.Length == 0) + { + await DisconnectInternal(HazelInternalErrors.ReceivedZeroBytes, "Received 0 bytes"); + return; + } + + await HandleAsync(data.Buffer); + } + } + + private async ValueTask HandleAsync(ReadOnlyMemory memory) + { + // Rent memory. + var dest = _pool.Rent(memory.Length); + + // Copy data. + memory.CopyTo(dest.Memory); + + try + { + // Write to client. + await Pipeline.Writer.WriteAsync(new MessageData(dest, memory.Length)); + } + catch (ChannelClosedException) + { + // Clean up. + dest.Dispose(); + } + } + + protected override void SetState(ConnectionState state) + { + if (state == ConnectionState.Connected) + { + _connectWaitLock.Release(); + } + } + + /// + /// Sends a disconnect message to the end point. + /// You may include optional disconnect data. The SendOption must be unreliable. + /// + protected override async ValueTask SendDisconnect(MessageWriter data = null) + { + lock (this) + { + if (_state == ConnectionState.NotConnected) return false; + _state = ConnectionState.NotConnected; + } + + var bytes = EmptyDisconnectBytes; + if (data != null && data.Length > 0) + { + if (data.SendOption != MessageType.Unreliable) + { + throw new ArgumentException("Disconnect messages can only be unreliable."); + } + + bytes = data.ToByteArray(true); + bytes[0] = (byte)UdpSendOption.Disconnect; + } + + try + { + await _socket.SendAsync(bytes, bytes.Length, RemoteEndPoint); + } + catch { } + + return true; + } + + /// + protected override void Dispose(bool disposing) + { + State = ConnectionState.NotConnected; + + try { _socket.Close(); } catch { } + try { _socket.Dispose(); } catch { } + + _reliablePacketTimer.Dispose(); + _connectWaitLock.Dispose(); + + base.Dispose(disposing); + } + } +} \ No newline at end of file diff --git a/src/Impostor.Hazel/Udp/UdpConnection.cs b/src/Impostor.Hazel/Udp/UdpConnection.cs index c33ffb3..39de8a7 100644 --- a/src/Impostor.Hazel/Udp/UdpConnection.cs +++ b/src/Impostor.Hazel/Udp/UdpConnection.cs @@ -187,7 +187,10 @@ namespace Hazel.Udp _isFirst = false; // Slice 4 bytes to get handshake data. - await _listener.InvokeNewConnection(message.Slice(4), this); + if (_listener != null) + { + await _listener.InvokeNewConnection(message.Slice(4), this); + } } switch (message.Buffer.Span[0]) @@ -256,6 +259,28 @@ namespace Hazel.Udp Statistics.LogUnreliableSend(length, bytes.Length); } + + /// + /// Sends a hello packet to the remote endpoint. + /// + /// + /// The callback to invoke when the hello packet is acknowledged. + protected ValueTask SendHello(byte[] bytes, Action acknowledgeCallback) + { + //First byte of handshake is version indicator so add data after + byte[] actualBytes; + if (bytes == null) + { + actualBytes = new byte[1]; + } + else + { + actualBytes = new byte[bytes.Length + 1]; + Buffer.BlockCopy(bytes, 0, actualBytes, 1, bytes.Length); + } + + return HandleSend(actualBytes, (byte)UdpSendOption.Hello, acknowledgeCallback); + } /// protected override void Dispose(bool disposing) diff --git a/src/Impostor.Hazel/Udp/UdpServerConnection.cs b/src/Impostor.Hazel/Udp/UdpServerConnection.cs index c95cdb2..d6aa107 100644 --- a/src/Impostor.Hazel/Udp/UdpServerConnection.cs +++ b/src/Impostor.Hazel/Udp/UdpServerConnection.cs @@ -47,16 +47,7 @@ namespace Hazel.Udp /// /// This will always throw a HazelException. /// - public override void Connect(byte[] bytes = null, int timeout = 5000) - { - throw new InvalidOperationException("Cannot manually connect a UdpServerConnection, did you mean to use UdpClientConnection?"); - } - - /// - /// - /// This will always throw a HazelException. - /// - public override void ConnectAsync(byte[] bytes = null) + public override ValueTask ConnectAsync(byte[] bytes = null) { throw new InvalidOperationException("Cannot manually connect a UdpServerConnection, did you mean to use UdpClientConnection?"); } @@ -64,11 +55,11 @@ namespace Hazel.Udp /// /// Sends a disconnect message to the end point. /// - protected override bool SendDisconnect(MessageWriter data = null) + protected override ValueTask SendDisconnect(MessageWriter data = null) { lock (this) { - if (this._state != ConnectionState.Connected) return false; + if (this._state != ConnectionState.Connected) return ValueTask.FromResult(false); this._state = ConnectionState.NotConnected; } @@ -87,7 +78,7 @@ namespace Hazel.Udp } catch { } - return true; + return ValueTask.FromResult(true); } protected override void Dispose(bool disposing) diff --git a/src/Impostor.Server/Net/Manager/GameManager.cs b/src/Impostor.Server/Net/Manager/GameManager.cs index d05eac8..c71a56d 100644 --- a/src/Impostor.Server/Net/Manager/GameManager.cs +++ b/src/Impostor.Server/Net/Manager/GameManager.cs @@ -51,7 +51,7 @@ namespace Impostor.Server.Net.Manager } _nodeLocator.Save(gameCodeStr, _publicIp); - _logger.LogDebug("Created game with code {0} ({1}).", game.Code, gameCode); + _logger.LogDebug("Created game with code {0}.", game.Code); await _eventManager.CallAsync(new GameCreatedEvent(game)); diff --git a/src/Impostor.sln b/src/Impostor.sln index e621271..3d60abb 100644 --- a/src/Impostor.sln +++ b/src/Impostor.sln @@ -27,6 +27,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Plugins.Debugger", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Hazel", "Impostor.Hazel\Impostor.Hazel.csproj", "{671B753B-31AE-4C36-AD71-09CF00FA17CA}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "client", "client", "{9F1919B0-915B-4749-9944-697DF7E7F67F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Client", "Impostor.Client\Impostor.Client.csproj", "{BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Client.App", "Impostor.Client.App\Impostor.Client.App.csproj", "{3DF86F12-7099-44F6-B98B-A148213D60B1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -103,6 +109,22 @@ Global {671B753B-31AE-4C36-AD71-09CF00FA17CA}.Release|Any CPU.Build.0 = Release|Any CPU {671B753B-31AE-4C36-AD71-09CF00FA17CA}.Release|x86.ActiveCfg = Release|Any CPU {671B753B-31AE-4C36-AD71-09CF00FA17CA}.Release|x86.Build.0 = Release|Any CPU + {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Debug|x86.ActiveCfg = Debug|Any CPU + {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Debug|x86.Build.0 = Debug|Any CPU + {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Release|Any CPU.Build.0 = Release|Any CPU + {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Release|x86.ActiveCfg = Release|Any CPU + {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Release|x86.Build.0 = Release|Any CPU + {3DF86F12-7099-44F6-B98B-A148213D60B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3DF86F12-7099-44F6-B98B-A148213D60B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3DF86F12-7099-44F6-B98B-A148213D60B1}.Debug|x86.ActiveCfg = Debug|Any CPU + {3DF86F12-7099-44F6-B98B-A148213D60B1}.Debug|x86.Build.0 = Debug|Any CPU + {3DF86F12-7099-44F6-B98B-A148213D60B1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3DF86F12-7099-44F6-B98B-A148213D60B1}.Release|Any CPU.Build.0 = Release|Any CPU + {3DF86F12-7099-44F6-B98B-A148213D60B1}.Release|x86.ActiveCfg = Release|Any CPU + {3DF86F12-7099-44F6-B98B-A148213D60B1}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -116,5 +138,7 @@ Global {7C3EB599-2292-4532-B280-D5BED1094DD4} = {94FAED42-15BB-40CF-BE64-5D5C3B4ABC8F} {82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69} = {94FAED42-15BB-40CF-BE64-5D5C3B4ABC8F} {ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7} = {36AA9913-E6EA-4A6C-90E6-2FD3CC2E3124} + {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB} = {9F1919B0-915B-4749-9944-697DF7E7F67F} + {3DF86F12-7099-44F6-B98B-A148213D60B1} = {9F1919B0-915B-4749-9944-697DF7E7F67F} EndGlobalSection EndGlobal -- 2.39.5