From be3f2d89b8b211f54402d1db14bbb4a70c83e8c5 Mon Sep 17 00:00:00 2001 From: Forest Date: Thu, 19 Jul 2018 17:27:29 -0700 Subject: [PATCH] Lots of good stuff. Should have committed earlier --- Hazel.UnitTests/BroadcastTests.cs | 4 +- Hazel.UnitTests/Hazel.UnitTests.csproj | 1 + Hazel.UnitTests/MessageWriterTests.cs | 114 ++++++++++++ Hazel.UnitTests/UdpConnectionTests.cs | 26 +++ ...xtensions.cs => BinaryReaderExtensions.cs} | 23 +-- Hazel/Connection.cs | 25 +++ Hazel/Hazel.csproj | 4 +- Hazel/MessageWriter.cs | 172 +++++++++++++++--- Hazel/Udp/UdpClientConnection.cs | 30 +-- Hazel/Udp/UdpConnection.Reliable.cs | 30 +-- Hazel/Udp/UdpConnection.cs | 15 +- Hazel/Udp/UdpServerConnection.cs | 11 ++ 12 files changed, 362 insertions(+), 93 deletions(-) create mode 100644 Hazel.UnitTests/MessageWriterTests.cs rename Hazel/{BinaryWriterExtensions.cs => BinaryReaderExtensions.cs} (60%) diff --git a/Hazel.UnitTests/BroadcastTests.cs b/Hazel.UnitTests/BroadcastTests.cs index 53b3139..d916446 100644 --- a/Hazel.UnitTests/BroadcastTests.cs +++ b/Hazel.UnitTests/BroadcastTests.cs @@ -15,8 +15,8 @@ namespace Hazel.UnitTests { const string TestData = "pwerowerower"; - using (UdpBroadcaster caster = new UdpBroadcaster(47777)) - using (UdpBroadcastListener listener = new UdpBroadcastListener(47777)) + using (UdpBroadcaster caster = new UdpBroadcaster(4777)) + using (UdpBroadcastListener listener = new UdpBroadcastListener(4777)) { listener.StartListen(); diff --git a/Hazel.UnitTests/Hazel.UnitTests.csproj b/Hazel.UnitTests/Hazel.UnitTests.csproj index 73dbb50..b14bdd5 100644 --- a/Hazel.UnitTests/Hazel.UnitTests.csproj +++ b/Hazel.UnitTests/Hazel.UnitTests.csproj @@ -58,6 +58,7 @@ + diff --git a/Hazel.UnitTests/MessageWriterTests.cs b/Hazel.UnitTests/MessageWriterTests.cs new file mode 100644 index 0000000..571808a --- /dev/null +++ b/Hazel.UnitTests/MessageWriterTests.cs @@ -0,0 +1,114 @@ +using System; +using System.IO; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Hazel.UnitTests +{ + [TestClass] + public class MessageWriterTests + { + [TestMethod] + public void WriteProperInt() + { + const int Test1 = int.MaxValue; + const int Test2 = int.MinValue; + + var msg = new MessageWriter(128); + msg.Write(Test1); + msg.Write(Test2); + + Assert.AreEqual(8, msg.Length); + Assert.AreEqual(msg.Length, msg.Position); + + using (MemoryStream m = new MemoryStream(msg.Buffer, 0, msg.Length)) + using (BinaryReader reader = new BinaryReader(m)) + { + Assert.AreEqual(Test1, reader.ReadInt32()); + Assert.AreEqual(Test2, reader.ReadInt32()); + } + } + + [TestMethod] + public void WriteProperBool() + { + const bool Test1 = true; + const bool Test2 = false; + + var msg = new MessageWriter(128); + msg.Write(Test1); + msg.Write(Test2); + + Assert.AreEqual(2, msg.Length); + Assert.AreEqual(msg.Length, msg.Position); + + using (MemoryStream m = new MemoryStream(msg.Buffer, 0, msg.Length)) + using (BinaryReader reader = new BinaryReader(m)) + { + Assert.AreEqual(Test1, reader.ReadBoolean()); + Assert.AreEqual(Test2, reader.ReadBoolean()); + } + } + + [TestMethod] + public void WriteProperString() + { + const string Test1 = "Hello"; + string Test2 = new string(' ', 1024); + var msg = new MessageWriter(2048); + msg.Write(Test1); + msg.Write(Test2); + + Assert.AreEqual(msg.Length, msg.Position); + + using (MemoryStream m = new MemoryStream(msg.Buffer, 0, msg.Length)) + using (BinaryReader reader = new BinaryReader(m)) + { + Assert.AreEqual(Test1, reader.ReadString()); + Assert.AreEqual(Test2, reader.ReadString()); + } + } + + [TestMethod] + public void WriteProperFloat() + { + const float Test1 = 12.34f; + + var msg = new MessageWriter(2048); + msg.Write(Test1); + + Assert.AreEqual(msg.Length, msg.Position); + + using (MemoryStream m = new MemoryStream(msg.Buffer, 0, msg.Length)) + using (BinaryReader reader = new BinaryReader(m)) + { + Assert.AreEqual(Test1, reader.ReadSingle()); + } + } + + [TestMethod] + public void WritesMessageLength() + { + var msg = new MessageWriter(2048); + msg.StartMessage(1); + msg.Write(65534); + msg.EndMessage(); + + Assert.AreEqual(2 + 1 + 4, msg.Position); + Assert.AreEqual(msg.Length, msg.Position); + + using (MemoryStream m = new MemoryStream(msg.Buffer, 0, msg.Length)) + using (BinaryReader reader = new BinaryReader(m)) + { + Assert.AreEqual(4, reader.ReadUInt16()); // Length After Type and Target + Assert.AreEqual(1, reader.ReadByte()); // Type + Assert.AreEqual(65534, reader.ReadInt32()); // Content + } + } + + [TestMethod] + public void GetLittleEndian() + { + Assert.IsTrue(MessageWriter.IsLittleEndian()); + } + } +} diff --git a/Hazel.UnitTests/UdpConnectionTests.cs b/Hazel.UnitTests/UdpConnectionTests.cs index 2adcd83..4ac8045 100644 --- a/Hazel.UnitTests/UdpConnectionTests.cs +++ b/Hazel.UnitTests/UdpConnectionTests.cs @@ -53,6 +53,32 @@ namespace Hazel.UnitTests } } + [TestMethod] + public void UdpUnreliableMessageSendTest() + { + using (UdpConnectionListener listener = new UdpConnectionListener(new NetworkEndPoint(IPAddress.Any, 4296, IPMode.IPv4))) + using (UdpConnection connection = new UdpClientConnection(new NetworkEndPoint(IPAddress.Loopback, 4296, IPMode.IPv4))) + { + listener.NewConnection += delegate (object sender, NewConnectionEventArgs e) + { + e.Connection.DataReceived += delegate (object s, DataReceivedEventArgs evt) + { + Assert.IsTrue(Enumerable.SequenceEqual(evt.Bytes, new byte[] { 1, 2, 3, 4, 5, 6 })); + }; + }; + + listener.Start(); + connection.Connect(); + + for (int i = 0; i < 4; ++i) + { + var msg = MessageWriter.Get(SendOption.None); + msg.Write(new byte[] { 1, 2, 3, 4, 5, 6 }); + connection.Send(msg); + msg.Recycle(); + } + } + } [TestMethod] public void UdpUnreliableDataSubsetSendTest() diff --git a/Hazel/BinaryWriterExtensions.cs b/Hazel/BinaryReaderExtensions.cs similarity index 60% rename from Hazel/BinaryWriterExtensions.cs rename to Hazel/BinaryReaderExtensions.cs index d5aeb22..332f236 100644 --- a/Hazel/BinaryWriterExtensions.cs +++ b/Hazel/BinaryReaderExtensions.cs @@ -3,24 +3,8 @@ namespace Hazel { /// - public static class BinaryWriterExtensions + public static class BinaryReaderExtensions { - /// - public static void WritePacked(this BinaryWriter writer, uint value) - { - do - { - byte b = (byte)(value & 0xFF); - if (value >= 0x80) - { - b |= 0x80; - } - - writer.Write(b); - value >>= 7; - } while (value > 0); - } - /// public static uint ReadPackedUInt32(this BinaryReader reader) { @@ -49,10 +33,9 @@ namespace Hazel } /// - public static void WriteBytesFull(this BinaryWriter writer, byte[] bytes) + public static int ReadPackedInt32(this BinaryReader reader) { - writer.WritePacked((uint)bytes.Length); - writer.Write(bytes); + return (int)reader.ReadPackedUInt32(); } /// diff --git a/Hazel/Connection.cs b/Hazel/Connection.cs index 751795a..0f12223 100644 --- a/Hazel/Connection.cs +++ b/Hazel/Connection.cs @@ -50,6 +50,18 @@ namespace Hazel /// public event EventHandler DataReceived; + public event Action DataSentRaw; + protected void InvokeDataSentRaw(byte[] data, int length) + { + this.DataSentRaw?.Invoke(data, length); + } + + public event Action DataReceivedRaw; + protected void InvokeDataReceivedRaw(byte[] data) + { + this.DataReceivedRaw?.Invoke(data); + } + /// /// Called when the end point disconnects or an error occurs. /// @@ -201,6 +213,19 @@ namespace Hazel /// public abstract void Connect(byte[] bytes = null, int timeout = 5000); + + /// + /// Connects the connection to a server and begins listening. + /// + /// The bytes of data to send in the handshake. + /// The number of milliseconds to wait before giving up on the connect attempt. + /// + /// Calling Connect makes the connection attempt to connect to the end point that's specified in the + /// constructor. This method will block until the connection attempt completes and will throw a + /// if there is a problem connecting. + /// + public abstract void ConnectAsync(byte[] bytes = null, int timeout = 5000); + /// /// Invokes the DataReceived event. /// diff --git a/Hazel/Hazel.csproj b/Hazel/Hazel.csproj index 24e67e9..1172bb0 100644 --- a/Hazel/Hazel.csproj +++ b/Hazel/Hazel.csproj @@ -23,6 +23,7 @@ 4 + true pdbonly @@ -32,6 +33,7 @@ prompt 4 bin\Release\Hazel.XML + true true @@ -49,7 +51,7 @@ - + diff --git a/Hazel/MessageWriter.cs b/Hazel/MessageWriter.cs index 83902d9..3508688 100644 --- a/Hazel/MessageWriter.cs +++ b/Hazel/MessageWriter.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Text; namespace Hazel { @@ -10,19 +12,17 @@ namespace Hazel private static readonly ObjectPool objectPool = new ObjectPool(() => new MessageWriter(BufferSize)); internal byte[] Buffer; - internal MemoryStream Stream; - public readonly BinaryWriter Writer; + public int Length; + public int Position; public SendOption SendOption { get; private set; } - private long lastMessageStart; - + private Stack messageStarts = new Stack(); + /// public MessageWriter(int bufferSize) { this.Buffer = new byte[bufferSize]; - this.Stream = new MemoryStream(this.Buffer, true); - this.Writer = new BinaryWriter(this.Stream); } /// @@ -30,57 +30,169 @@ namespace Hazel public static MessageWriter Get(SendOption sendOption = SendOption.None) { var output = objectPool.GetObject(); - output.SendOption = sendOption; + output.Clear(sendOption); + return output; + } + + public bool HasBytes(int expected) + { + if (this.SendOption == SendOption.None) + { + return this.Length > 1 + expected; + } + + return this.Length > 3 + expected; + } + + /// + public void StartMessage(byte typeFlag) + { + messageStarts.Push(this.Position); + this.Position += 2; // Skip for size + this.Write(typeFlag); + } + + /// + public void EndMessage() + { + var lastMessageStart = messageStarts.Pop(); + ushort length = (ushort)(this.Position - lastMessageStart - 3); // Minus length and type byte + this.Buffer[lastMessageStart] = (byte)length; + this.Buffer[lastMessageStart + 1] = (byte)(length >> 8); + } + + /// + public void CancelMessage() + { + this.Position = this.messageStarts.Pop(); + } + + public void Clear(SendOption sendOption) + { + this.Position = this.Length = 0; + this.SendOption = sendOption; + + this.Buffer[0] = (byte)sendOption; switch (sendOption) { case SendOption.None: - output.Buffer[0] = (byte)sendOption; - output.Stream.Position = 1; // Type + this.Length = this.Position = 1; break; case SendOption.Reliable: - output.Buffer[0] = (byte)sendOption; - output.Stream.Position = 3; // Type + ID + this.Length = this.Position = 3; break; case SendOption.FragmentedReliable: throw new NotImplementedException("Sry bruh"); } - - return output; } /// - public void StartMessage(byte typeFlag, uint targetObjId) + public void Recycle() { - this.lastMessageStart = this.Stream.Position; - this.Stream.Position = this.lastMessageStart + 2; + this.Position = this.Length = 0; + objectPool.PutObject(this); + } - this.Writer.Write(typeFlag); - this.Writer.WritePacked(targetObjId); + #region WriteMethods + public void Write(bool value) + { + this.Buffer[this.Position++] = (byte)(value ? 1 : 0); + if (this.Position > this.Length) this.Length = this.Position; } - /// - public void EndMessage() + public void Write(byte value) { - this.Writer.Flush(); + this.Buffer[this.Position++] = value; + if (this.Position > this.Length) this.Length = this.Position; + } - ushort length = (ushort)(this.Stream.Position - this.lastMessageStart); - this.Buffer[this.lastMessageStart] = (byte)(length >> 8); - this.Buffer[this.lastMessageStart + 1] = (byte)(length & 0xFF); + public void Write(short value) + { + this.Buffer[this.Position++] = (byte)value; + this.Buffer[this.Position++] = (byte)(value >> 8); + if (this.Position > this.Length) this.Length = this.Position; + } + + public void Write(int value) + { + this.Buffer[this.Position++] = (byte)value; + this.Buffer[this.Position++] = (byte)(value >> 8); + this.Buffer[this.Position++] = (byte)(value >> 16); + this.Buffer[this.Position++] = (byte)(value >> 24); + if (this.Position > this.Length) this.Length = this.Position; + } + + public unsafe void Write(float value) + { + fixed (byte* ptr = &this.Buffer[this.Position]) + { + byte* valuePtr = (byte*)&value; + + *ptr = *valuePtr; + *(ptr + 1) = *(valuePtr + 1); + *(ptr + 2) = *(valuePtr + 2); + *(ptr + 3) = *(valuePtr + 3); + } + + this.Position += 4; + if (this.Position > this.Length) this.Length = this.Position; + } + + public void Write(string value) + { + var bytes = UTF8Encoding.UTF8.GetBytes(value); + this.WritePacked(bytes.Length); + this.Write(bytes); + } + + public void WriteBytesFull(byte[] bytes) + { + this.WritePacked((uint)bytes.Length); + this.Write(bytes); + } + + public void Write(byte[] bytes) + { + Array.Copy(bytes, 0, this.Buffer, this.Position, bytes.Length); + this.Position += bytes.Length; + if (this.Position > this.Length) this.Length = this.Position; } /// - public void CancelMessage() + public void WritePacked(int value) { - this.Writer.Flush(); - this.Stream.Position = this.lastMessageStart; + this.WritePacked((uint)value); } /// - public void Recycle() + public void WritePacked(uint value) { - this.Writer.Flush(); - objectPool.PutObject(this); + do + { + byte b = (byte)(value & 0xFF); + if (value >= 0x80) + { + b |= 0x80; + } + + this.Write(b); + value >>= 7; + } while (value > 0); + } + #endregion + + public unsafe static bool IsLittleEndian() + { + byte b; + unsafe + { + int i = 1; + byte* bp = (byte*)&i; + b = *bp; + } + + return b == 1; } } } diff --git a/Hazel/Udp/UdpClientConnection.cs b/Hazel/Udp/UdpClientConnection.cs index 4ea5c88..1bd5a79 100644 --- a/Hazel/Udp/UdpClientConnection.cs +++ b/Hazel/Udp/UdpClientConnection.cs @@ -56,6 +56,8 @@ namespace Hazel.Udp /// protected override void WriteBytesToConnection(byte[] bytes, int length) { + InvokeDataSentRaw(bytes, length); + lock (stateLock) { if (State != ConnectionState.Connected && State != ConnectionState.Connecting) @@ -104,6 +106,22 @@ namespace Hazel.Udp /// public override void Connect(byte[] bytes = null, int timeout = 5000) + { + this.ConnectAsync(bytes, timeout); + + //Wait till hello packet is acknowledged and the state is set to Connected + bool timedOut = !WaitOnConnect(timeout); + + //If we timed out raise an exception + if (timedOut) + { + Dispose(); + throw new HazelException("Connection attempt timed out."); + } + } + + /// + public override void ConnectAsync(byte[] bytes = null, int timeout = 5000) { lock (stateLock) { @@ -112,7 +130,7 @@ namespace Hazel.Udp State = ConnectionState.Connecting; } - + //Begin listening try { @@ -148,16 +166,6 @@ namespace Hazel.Udp //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 SendHello(bytes, () => { lock (stateLock) State = ConnectionState.Connected; }); - - //Wait till hello packet is acknowledged and the state is set to Connected - bool timedOut = !WaitOnConnect(timeout); - - //If we timed out raise an exception - if (timedOut) - { - Dispose(); - throw new HazelException("Connection attempt timed out."); - } } /// diff --git a/Hazel/Udp/UdpConnection.Reliable.cs b/Hazel/Udp/UdpConnection.Reliable.cs index 1a1a3ff..e1c98e5 100644 --- a/Hazel/Udp/UdpConnection.Reliable.cs +++ b/Hazel/Udp/UdpConnection.Reliable.cs @@ -53,15 +53,7 @@ namespace Hazel.Udp /// volatile bool hasReceivedSomething = false; - /// - /// The total time it has taken reliable packets to make a round trip. - /// - long totalRoundTime = 0; - - /// - /// The number of reliable messages that have been sent. - /// - long totalReliableMessages = 0; + object PingLock = new object(); /// /// Returns the average ping to this endpoint. @@ -70,17 +62,7 @@ namespace Hazel.Udp /// This returns the average ping for a one-way trip as calculated from the reliable packets that have been sent /// and acknowledged by the endpoint. /// - public double AveragePing - { - get - { - long t = Interlocked.Read(ref totalReliableMessages); - if (t == 0) - return 0; - else - return Interlocked.Read(ref totalRoundTime) / t / 2; - } - } + public volatile float AveragePingMs = 500; /// /// The maximum times a message should be resent before marking the endpoint as disconnected. @@ -235,7 +217,7 @@ namespace Hazel.Udp Trace.WriteLine("Resend."); }, - resendTimeout > 0 ? resendTimeout : (AveragePing != 0 ? (int)AveragePing * 4 : 200), + resendTimeout > 0 ? resendTimeout : (AveragePingMs != 0 ? (int)AveragePingMs * 4 : 200), ackCallback ); @@ -394,8 +376,10 @@ namespace Hazel.Udp //Add to average ping packet.Stopwatch.Stop(); - Interlocked.Add(ref totalRoundTime, packet.Stopwatch.Elapsed.Milliseconds); - Interlocked.Increment(ref totalReliableMessages); + lock (PingLock) + { + this.AveragePingMs = this.AveragePingMs * .7f + (float)packet.Stopwatch.Elapsed.TotalMilliseconds * .3f; + } packet.Recycle(); diff --git a/Hazel/Udp/UdpConnection.cs b/Hazel/Udp/UdpConnection.cs index 54d4c99..19434ea 100644 --- a/Hazel/Udp/UdpConnection.cs +++ b/Hazel/Udp/UdpConnection.cs @@ -36,25 +36,26 @@ namespace Hazel.Udp if (State != ConnectionState.Connected) throw new InvalidOperationException("Could not send data as this Connection is not connected. Did you disconnect?"); + byte[] buffer = new byte[msg.Length]; + Buffer.BlockCopy(msg.Buffer, 0, buffer, 0, msg.Length); //Inform keepalive not to send for a while ResetKeepAliveTimer(); - int length = (int)msg.Stream.Length; switch (msg.SendOption) { case SendOption.Reliable: - AttachReliableID(msg.Buffer, 1, length); - WriteBytesToConnection(msg.Buffer, length); - Statistics.LogReliableSend(length - 3, length); + AttachReliableID(buffer, 1, buffer.Length); + WriteBytesToConnection(buffer, buffer.Length); + Statistics.LogReliableSend(buffer.Length - 3, buffer.Length); break; case SendOption.FragmentedReliable: throw new NotImplementedException("Not yet"); default: - WriteBytesToConnection(msg.Buffer, length); - Statistics.LogUnreliableSend(length - 1, length);; + WriteBytesToConnection(buffer, buffer.Length); + Statistics.LogUnreliableSend(buffer.Length - 1, buffer.Length);; break; } } @@ -159,6 +160,8 @@ namespace Hazel.Udp /// The buffer containing the bytes received. protected internal void HandleReceive(byte[] buffer) { + InvokeDataReceivedRaw(buffer); + //Inform keepalive not to send for a while ResetKeepAliveTimer(); diff --git a/Hazel/Udp/UdpServerConnection.cs b/Hazel/Udp/UdpServerConnection.cs index 1f47e6f..3bfae30 100644 --- a/Hazel/Udp/UdpServerConnection.cs +++ b/Hazel/Udp/UdpServerConnection.cs @@ -47,6 +47,8 @@ namespace Hazel.Udp /// protected override void WriteBytesToConnection(byte[] bytes, int length) { + InvokeDataSentRaw(bytes, length); + lock (stateLock) { if (State != ConnectionState.Connected) @@ -65,6 +67,15 @@ namespace Hazel.Udp throw new HazelException("Cannot manually connect a UdpServerConnection, did you mean to use UdpClientConnection?"); } + /// + /// + /// This will always throw a HazelException. + /// + public override void ConnectAsync(byte[] bytes = null, int timeout = 5000) + { + throw new HazelException("Cannot manually connect a UdpServerConnection, did you mean to use UdpClientConnection?"); + } + /// protected override void HandleDisconnect(HazelException e = null) { -- 2.39.5