{
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();
<Compile Include="TestHelper.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="UdpConnectionTests.cs" />
+ <Compile Include="MessageWriterTests.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Hazel\Hazel.csproj">
--- /dev/null
+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());
+ }
+ }
+}
}
}
+ [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()
--- /dev/null
+using System.IO;
+
+namespace Hazel
+{
+ ///
+ public static class BinaryReaderExtensions
+ {
+ ///
+ public static uint ReadPackedUInt32(this BinaryReader reader)
+ {
+ bool readMore = true;
+ int shift = 0;
+ uint output = 0;
+
+ while (readMore)
+ {
+ byte b = reader.ReadByte();
+ if (b >= 0x80)
+ {
+ readMore = true;
+ b ^= 0x80;
+ }
+ else
+ {
+ readMore = false;
+ }
+
+ output |= (uint)(b << shift);
+ shift += 7;
+ }
+
+ return output;
+ }
+
+ ///
+ public static int ReadPackedInt32(this BinaryReader reader)
+ {
+ return (int)reader.ReadPackedUInt32();
+ }
+
+ ///
+ public static byte[] ReadBytesAndSize(this BinaryReader reader)
+ {
+ int len = (int)reader.ReadPackedUInt32();
+ return reader.ReadBytes(len);
+ }
+ }
+}
\ No newline at end of file
+++ /dev/null
-using System.IO;
-
-namespace Hazel
-{
- ///
- public static class BinaryWriterExtensions
- {
- ///
- 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)
- {
- bool readMore = true;
- int shift = 0;
- uint output = 0;
-
- while (readMore)
- {
- byte b = reader.ReadByte();
- if (b >= 0x80)
- {
- readMore = true;
- b ^= 0x80;
- }
- else
- {
- readMore = false;
- }
-
- output |= (uint)(b << shift);
- shift += 7;
- }
-
- return output;
- }
-
- ///
- public static void WriteBytesFull(this BinaryWriter writer, byte[] bytes)
- {
- writer.WritePacked((uint)bytes.Length);
- writer.Write(bytes);
- }
-
- ///
- public static byte[] ReadBytesAndSize(this BinaryReader reader)
- {
- int len = (int)reader.ReadPackedUInt32();
- return reader.ReadBytes(len);
- }
- }
-}
\ No newline at end of file
/// </example>
public event EventHandler<DataReceivedEventArgs> DataReceived;
+ public event Action<byte[], int> DataSentRaw;
+ protected void InvokeDataSentRaw(byte[] data, int length)
+ {
+ this.DataSentRaw?.Invoke(data, length);
+ }
+
+ public event Action<byte[]> DataReceivedRaw;
+ protected void InvokeDataReceivedRaw(byte[] data)
+ {
+ this.DataReceivedRaw?.Invoke(data);
+ }
+
/// <summary>
/// Called when the end point disconnects or an error occurs.
/// </summary>
/// </remarks>
public abstract void Connect(byte[] bytes = null, int timeout = 5000);
+
+ /// <summary>
+ /// Connects the connection to a server and begins listening.
+ /// </summary>
+ /// <param name="bytes">The bytes of data to send in the handshake.</param>
+ /// <param name="timeout">The number of milliseconds to wait before giving up on the connect attempt.</param>
+ /// <remarks>
+ /// 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
+ /// <see cref="HazelException"/> if there is a problem connecting.
+ /// </remarks>
+ public abstract void ConnectAsync(byte[] bytes = null, int timeout = 5000);
+
/// <summary>
/// Invokes the DataReceived event.
/// </summary>
<WarningLevel>4</WarningLevel>
<DocumentationFile>
</DocumentationFile>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<DocumentationFile>bin\Release\Hazel.XML</DocumentationFile>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup>
<SignAssembly>true</SignAssembly>
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
- <Compile Include="BinaryWriterExtensions.cs" />
+ <Compile Include="BinaryReaderExtensions.cs" />
<Compile Include="Connection.cs" />
<Compile Include="ConnectionEndPoint.cs" />
<Compile Include="ConnectionListener.cs" />
using System;
+using System.Collections.Generic;
using System.IO;
+using System.Text;
namespace Hazel
{
private static readonly ObjectPool<MessageWriter> objectPool = new ObjectPool<MessageWriter>(() => 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<int> messageStarts = new Stack<int>();
+
///
public MessageWriter(int bufferSize)
{
this.Buffer = new byte[bufferSize];
- this.Stream = new MemoryStream(this.Buffer, true);
- this.Writer = new BinaryWriter(this.Stream);
}
///
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;
}
}
}
/// <inheritdoc />
protected override void WriteBytesToConnection(byte[] bytes, int length)
{
+ InvokeDataSentRaw(bytes, length);
+
lock (stateLock)
{
if (State != ConnectionState.Connected && State != ConnectionState.Connecting)
/// <inheritdoc />
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.");
+ }
+ }
+
+ /// <inheritdoc />
+ public override void ConnectAsync(byte[] bytes = null, int timeout = 5000)
{
lock (stateLock)
{
State = ConnectionState.Connecting;
}
-
+
//Begin listening
try
{
//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.");
- }
}
/// <summary>
/// </summary>
volatile bool hasReceivedSomething = false;
- /// <summary>
- /// The total time it has taken reliable packets to make a round trip.
- /// </summary>
- long totalRoundTime = 0;
-
- /// <summary>
- /// The number of reliable messages that have been sent.
- /// </summary>
- long totalReliableMessages = 0;
+ object PingLock = new object();
/// <summary>
/// Returns the average ping to this endpoint.
/// 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.
/// </remarks>
- 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;
/// <summary>
/// The maximum times a message should be resent before marking the endpoint as disconnected.
Trace.WriteLine("Resend.");
},
- resendTimeout > 0 ? resendTimeout : (AveragePing != 0 ? (int)AveragePing * 4 : 200),
+ resendTimeout > 0 ? resendTimeout : (AveragePingMs != 0 ? (int)AveragePingMs * 4 : 200),
ackCallback
);
//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();
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;
}
}
/// <param name="buffer">The buffer containing the bytes received.</param>
protected internal void HandleReceive(byte[] buffer)
{
+ InvokeDataReceivedRaw(buffer);
+
//Inform keepalive not to send for a while
ResetKeepAliveTimer();
/// <inheritdoc />
protected override void WriteBytesToConnection(byte[] bytes, int length)
{
+ InvokeDataSentRaw(bytes, length);
+
lock (stateLock)
{
if (State != ConnectionState.Connected)
throw new HazelException("Cannot manually connect a UdpServerConnection, did you mean to use UdpClientConnection?");
}
+ /// <inheritdoc />
+ /// <remarks>
+ /// This will always throw a HazelException.
+ /// </remarks>
+ public override void ConnectAsync(byte[] bytes = null, int timeout = 5000)
+ {
+ throw new HazelException("Cannot manually connect a UdpServerConnection, did you mean to use UdpClientConnection?");
+ }
+
/// <inheritdoc />
protected override void HandleDisconnect(HazelException e = null)
{