]> git.deb.at Git - rhonda/impostor.hazel.git/commitdiff
Added a listener with a fixed number of threads. Still testing how that works in...
authorForest <forest@innersloth.com>
Wed, 15 Jan 2020 21:08:15 +0000 (13:08 -0800)
committerForest <forest@innersloth.com>
Wed, 15 Jan 2020 21:08:15 +0000 (13:08 -0800)
Hazel.UnitTests/UPnPTests.cs
Hazel/Connection.cs
Hazel/FewerThreads/HazelThreadPool.cs [new file with mode: 0644]
Hazel/FewerThreads/UdpConnectionListener2.cs [new file with mode: 0644]
Hazel/FewerThreads/UdpServerConnection2.cs [new file with mode: 0644]
Hazel/Hazel.csproj
Hazel/UPnP/ILogger.cs
Hazel/UPnP/UPnPHelper.cs

index 460740d6f5a7f9492611e58ef5b480fce3e6a999..dfe80fee52020f624f70805645145f77d695dbc1 100644 (file)
@@ -30,12 +30,12 @@ namespace Hazel.UnitTests
     {
         public static readonly ILogger Instance = new Logger();
 
-        public void LogError(string msg)
+        public void WriteError(string msg)
         {
             Console.WriteLine(msg);
         }
 
-        public void LogInfo(string msg)
+        public void WriteInfo(string msg)
         {
             Console.WriteLine(msg);
         }
index f06a15f4742291d84939eb932c69f9f3666dc3bc..b5bfe334c7ee1c26854cc0b383235c04a0fb8d33 100644 (file)
@@ -164,7 +164,6 @@ namespace Hazel
         /// <param name="timeout">The number of milliseconds to wait before giving up on the connect attempt.</param>
         public abstract void Connect(byte[] bytes = null, int timeout = 5000);
 
-
         /// <summary>
         ///     Connects the connection to a server and begins listening.
         ///     This method does not block.
@@ -185,11 +184,15 @@ namespace Hazel
         /// </remarks>
         protected void InvokeDataReceived(MessageReader msg, SendOption sendOption)
         {
-            //Make a copy to avoid race condition between null check and invocation
+            // Make a copy to avoid race condition between null check and invocation
             Action<DataReceivedEventArgs> handler = DataReceived;
             if (handler != null)
             {
-                handler(new DataReceivedEventArgs(this, msg, sendOption));
+                try
+                {
+                    handler(new DataReceivedEventArgs(this, msg, sendOption));
+                }
+                catch { }
             }
             else
             {
@@ -209,12 +212,18 @@ namespace Hazel
         /// </remarks>
         protected void InvokeDisconnected(string e, MessageReader reader)
         {
-            //Make a copy to avoid race condition between null check and invocation
+            // Make a copy to avoid race condition between null check and invocation
             EventHandler<DisconnectedEventArgs> handler = Disconnected;
             if (handler != null)
             {
                 DisconnectedEventArgs args = new DisconnectedEventArgs(e, reader);
-                handler.Invoke(this, args);
+                try
+                {
+                    handler(this, args);
+                }
+                catch
+                {
+                }
             }
         }
 
diff --git a/Hazel/FewerThreads/HazelThreadPool.cs b/Hazel/FewerThreads/HazelThreadPool.cs
new file mode 100644 (file)
index 0000000..fb36b00
--- /dev/null
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Hazel
+{
+    internal class HazelThreadPool
+    {
+        private Thread[] threads;
+
+        public HazelThreadPool(int numThreads, ThreadStart action)
+        {
+            this.threads = new Thread[numThreads];
+            for (int i = 0; i < this.threads.Length; ++i)
+            {
+                this.threads[i] = new Thread(action);
+            }
+        }
+
+        public void Start()
+        {
+            for (int i = 0; i < this.threads.Length; ++i)
+            {
+                this.threads[i].Start();
+            }
+        }
+
+        public void Join()
+        {
+            for (int i = 0; i < this.threads.Length; ++i)
+            {
+                var thread = this.threads[i];
+                try
+                {
+                    thread.Join();
+                }
+                catch { }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/Hazel/FewerThreads/UdpConnectionListener2.cs b/Hazel/FewerThreads/UdpConnectionListener2.cs
new file mode 100644 (file)
index 0000000..52d65da
--- /dev/null
@@ -0,0 +1,320 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading;
+
+namespace Hazel.Udp.FewerThreads
+{
+    /// <summary>
+    ///     Listens for new UDP connections and creates UdpConnections for them.
+    /// </summary>
+    /// <inheritdoc />
+    public class UdpConnectionListener2 : IDisposable
+    {
+        private struct SendMessageInfo
+        {
+            public byte[] Buffer;
+            public EndPoint Recipient;
+        }
+
+        private struct ReceiveMessageInfo
+        {
+            public MessageReader Message;
+            public EndPoint Sender;
+        }
+
+        private const int SendReceiveBufferSize = 1024 * 1024;
+        private const int BufferSize = ushort.MaxValue;
+
+        public event Action<NewConnectionEventArgs> NewConnection;
+
+        /// <summary>
+        /// A callback for early connection rejection. 
+        /// * Return false to reject connection.
+        /// * A null response is ok, we just won't send anything.
+        /// </summary>
+        public AcceptConnectionCheck AcceptConnection;
+        public delegate bool AcceptConnectionCheck(IPEndPoint endPoint, byte[] input, out byte[] response);
+
+        private Socket socket;
+        private ILogger Logger;
+
+        public IPEndPoint EndPoint { get; }
+        public IPMode IPMode { get; }
+
+        private Thread reliablePacketThread;
+        private Thread receiveThread;
+        private Thread sendThread;
+        private HazelThreadPool processThreads;
+
+        private ConcurrentDictionary<EndPoint, UdpServerConnection2> allConnections = new ConcurrentDictionary<EndPoint, UdpServerConnection2>();
+
+        private Queue<ReceiveMessageInfo> receiveQueue = new Queue<ReceiveMessageInfo>();
+        private Queue<SendMessageInfo> sendQueue = new Queue<SendMessageInfo>();
+
+        public int ConnectionCount { get { return this.allConnections.Count; } }
+        public int SendQueueLength { get { lock(this.sendQueue) return this.sendQueue.Count; } }
+        public int ReceiveQueueLength { get { lock (this.receiveQueue) return this.receiveQueue.Count; } }
+
+        private bool isActive;
+
+        public UdpConnectionListener2(IPEndPoint endPoint, ILogger logger, IPMode ipMode = IPMode.IPv4)
+        {
+            this.Logger = logger;
+            this.EndPoint = endPoint;
+            this.IPMode = ipMode;
+
+            this.socket = UdpConnection.CreateSocket(this.IPMode);
+            this.socket.Blocking = false;
+
+            this.socket.ReceiveBufferSize = SendReceiveBufferSize;
+            this.socket.SendBufferSize = SendReceiveBufferSize;
+
+            this.reliablePacketThread = new Thread(ManageReliablePackets);
+            this.sendThread = new Thread(SendLoop);
+            this.receiveThread = new Thread(ReceiveLoop);
+            this.processThreads = new HazelThreadPool(4, ProcessingLoop);
+        }
+
+        ~UdpConnectionListener2()
+        {
+            this.Dispose(false);
+        }
+        
+        private void ManageReliablePackets()
+        {
+            while (this.isActive)
+            {
+                foreach (var kvp in this.allConnections)
+                {
+                    var sock = kvp.Value;
+                    sock.ManageReliablePackets();
+                }
+
+                Thread.Sleep(100);
+            }
+        }
+
+        public void Start()
+        {
+            try
+            {
+                socket.Bind(EndPoint);
+            }
+            catch (SocketException e)
+            {
+                throw new HazelException("Could not start listening as a SocketException occurred", e);
+            }
+
+            this.isActive = true;
+            this.reliablePacketThread.Start();
+            this.sendThread.Start();
+            this.receiveThread.Start();
+            this.processThreads.Start();
+        }
+
+        private void ReceiveLoop()
+        {
+            while (this.isActive)
+            {
+                if (this.socket.Poll(Timeout.Infinite, SelectMode.SelectRead))
+                {
+                    EndPoint remoteEP = new IPEndPoint(IPMode == IPMode.IPv4 ? IPAddress.Any : IPAddress.IPv6Any, this.EndPoint.Port);
+                    MessageReader message = MessageReader.GetSized(BufferSize);
+                    try
+                    {
+                        message.Length = socket.ReceiveFrom(message.Buffer, 0, message.Buffer.Length, SocketFlags.None, ref remoteEP);
+                    }
+                    catch (SocketException sx)
+                    {
+                        message.Recycle();
+                        this.Logger.WriteError("Socket Ex in StartListening: " + sx.Message);
+                        continue;
+                    }
+                    catch (Exception ex)
+                    {
+                        message.Recycle();
+                        this.Logger.WriteError("Stopped due to: " + ex.Message);
+                        return;
+                    }
+
+                    lock (this.receiveQueue)
+                    {
+                        this.receiveQueue.Enqueue(new ReceiveMessageInfo() { Message = message, Sender = remoteEP });
+                        Monitor.Pulse(this.receiveQueue);
+                    }
+                }
+            }
+        }
+
+        private void ProcessingLoop()
+        {
+            while (this.isActive)
+            {
+                ReceiveMessageInfo msg;
+                lock (this.receiveQueue)
+                {
+                    if (this.receiveQueue.Count == 0)
+                    {
+                        Monitor.Wait(this.receiveQueue);
+
+                        if (this.receiveQueue.Count == 0)
+                        {
+                            continue;
+                        }
+                    }
+
+                    msg = this.receiveQueue.Dequeue();
+                }
+
+                try
+                {
+                    this.ReadCallback(msg.Message, msg.Sender);
+                }
+                catch
+                {
+                }
+            }
+        }
+
+        private void SendLoop()
+        {
+            while (this.isActive)
+            {
+                SendMessageInfo msg;
+                lock (this.sendQueue)
+                {
+                    if (this.sendQueue.Count == 0)
+                    {
+                        Monitor.Wait(this.sendQueue);
+
+                        if (this.sendQueue.Count == 0)
+                        {
+                            continue;
+                        }
+                    }
+
+                    msg = this.sendQueue.Dequeue();
+                }
+
+                try
+                {
+                    this.socket.SendTo(msg.Buffer, 0, msg.Buffer.Length, SocketFlags.None, msg.Recipient);
+                }
+                catch { }
+            }
+        }
+
+        void ReadCallback(MessageReader message, EndPoint remoteEndPoint)
+        {
+            int bytesReceived = message.Length;
+            bool aware = true;
+            bool isHello = message.Buffer[0] == (byte)UdpSendOption.Hello;
+
+            // If we're aware of this connection use the one already
+            // If this is a new client then connect with them!
+            UdpServerConnection2 connection;
+            if (!this.allConnections.TryGetValue(remoteEndPoint, out connection))
+            {
+                lock (this.allConnections)
+                {
+                    if (!this.allConnections.TryGetValue(remoteEndPoint, out connection))
+                    {
+                        // Check for malformed connection attempts
+                        if (!isHello)
+                        {
+                            message.Recycle();
+                            return;
+                        }
+
+                        if (AcceptConnection != null)
+                        {
+                            if (!AcceptConnection((IPEndPoint)remoteEndPoint, message.Buffer, out var response))
+                            {
+                                message.Recycle();
+                                if (response != null)
+                                {
+                                    SendDataRaw(response, remoteEndPoint);
+                                }
+
+                                return;
+                            }
+                        }
+
+                        aware = false;
+                        connection = new UdpServerConnection2(this, (IPEndPoint)remoteEndPoint, this.IPMode);
+                        if (!this.allConnections.TryAdd(remoteEndPoint, connection))
+                        {
+                            throw new HazelException("Failed to add a connection. This should never happen.");
+                        }
+                    }
+                }
+            }
+
+            //Inform the connection of the buffer (new connections need to send an ack back to client)
+            connection.HandleReceive(message, bytesReceived);
+            
+            //If it's a new connection invoke the NewConnection event.
+            if (!aware)
+            {
+                // Skip header and hello byte;
+                message.Offset = 4;
+                message.Length = bytesReceived - 4;
+                message.Position = 0;
+                this.NewConnection?.Invoke(new NewConnectionEventArgs(message, connection));
+            }
+            else if (isHello)
+            {
+                message.Recycle();
+            }
+        }
+
+        internal void SendDataRaw(byte[] response, EndPoint remoteEndPoint)
+        {
+            lock (this.sendQueue)
+            {
+                this.sendQueue.Enqueue(new SendMessageInfo() { Buffer = response, Recipient = remoteEndPoint });
+                Monitor.Pulse(this.sendQueue);
+            }
+        }
+
+        /// <summary>
+        ///     Removes a virtual connection from the list.
+        /// </summary>
+        /// <param name="endPoint">The endpoint of the virtual connection.</param>
+        internal bool RemoveConnectionTo(EndPoint endPoint)
+        {
+            return this.allConnections.TryRemove(endPoint, out var conn);
+        }
+
+        /// <inheritdoc />
+        protected virtual void Dispose(bool disposing)
+        {
+            foreach (var kvp in this.allConnections)
+            {
+                kvp.Value.Dispose();
+            }
+
+            try { this.socket.Shutdown(SocketShutdown.Both); } catch { }
+            try { this.socket.Close(); } catch { }
+            try { this.socket.Dispose(); } catch { }
+
+            this.isActive = false;
+
+            lock (this.sendQueue) Monitor.PulseAll(this.sendQueue);
+            lock (this.receiveQueue) Monitor.PulseAll(this.receiveQueue);
+
+            this.reliablePacketThread.Join();
+            this.sendThread.Join();
+            this.receiveThread.Join();
+            this.processThreads.Join();
+        }
+
+        public void Dispose()
+        {
+            this.Dispose(true);
+        }
+    }
+}
diff --git a/Hazel/FewerThreads/UdpServerConnection2.cs b/Hazel/FewerThreads/UdpServerConnection2.cs
new file mode 100644 (file)
index 0000000..fd5ca4f
--- /dev/null
@@ -0,0 +1,101 @@
+using System;
+using System.Net;
+
+namespace Hazel.Udp.FewerThreads
+{
+    /// <summary>
+    ///     Represents a servers's connection to a client that uses the UDP protocol.
+    /// </summary>
+    /// <inheritdoc/>
+    internal sealed class UdpServerConnection2 : UdpConnection
+    {
+        /// <summary>
+        ///     The connection listener that we use the socket of.
+        /// </summary>
+        /// <remarks>
+        ///     Udp server connections utilize the same socket in the listener for sends/receives, this is the listener that 
+        ///     created this connection and is hence the listener this conenction sends and receives via.
+        /// </remarks>
+        public UdpConnectionListener2 Listener { get; private set; }
+
+        /// <summary>
+        ///     Creates a UdpConnection for the virtual connection to the endpoint.
+        /// </summary>
+        /// <param name="listener">The listener that created this connection.</param>
+        /// <param name="endPoint">The endpoint that we are connected to.</param>
+        /// <param name="IPMode">The IPMode we are connected using.</param>
+        internal UdpServerConnection2(UdpConnectionListener2 listener, IPEndPoint endPoint, IPMode IPMode)
+            : base()
+        {
+            this.Listener = listener;
+            this.RemoteEndPoint = endPoint;
+            this.EndPoint = endPoint;
+            this.IPMode = IPMode;
+
+            State = ConnectionState.Connected;
+            this.InitializeKeepAliveTimer();
+        }
+
+        /// <inheritdoc />
+        protected override void WriteBytesToConnection(byte[] bytes, int length)
+        {
+            if (bytes.Length != length) throw new ArgumentException("I made an assumption here. I hope you see this error.");
+
+            Listener.SendDataRaw(bytes, RemoteEndPoint);
+        }
+
+        /// <inheritdoc />
+        /// <remarks>
+        ///     This will always throw a HazelException.
+        /// </remarks>
+        public override void Connect(byte[] bytes = null, int timeout = 5000)
+        {
+            throw new InvalidOperationException("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 InvalidOperationException("Cannot manually connect a UdpServerConnection, did you mean to use UdpClientConnection?");
+        }
+
+        /// <summary>
+        ///     Sends a disconnect message to the end point.
+        /// </summary>
+        protected override bool SendDisconnect(MessageWriter data = null)
+        {
+            if (!Listener.RemoveConnectionTo(RemoteEndPoint)) return false;
+            this._state = ConnectionState.NotConnected;
+            
+            var bytes = EmptyDisconnectBytes;
+            if (data != null && data.Length > 0)
+            {
+                if (data.SendOption != SendOption.None) throw new ArgumentException("Disconnect messages can only be unreliable.");
+
+                bytes = data.ToByteArray(true);
+                bytes[0] = (byte)UdpSendOption.Disconnect;
+            }
+
+            try
+            {
+                Listener.SendDataRaw(bytes, RemoteEndPoint);
+            }
+            catch { }
+
+            return true;
+        }
+
+        protected override void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                SendDisconnect();
+            }
+
+            base.Dispose(disposing);
+        }
+    }
+}
index 7e81a65230acdb6812abd1649bb9dca5b52f1ae3..e249199d5deb5e4903b21f50c79503884cac6aaa 100644 (file)
@@ -72,6 +72,9 @@
     <Compile Include="ConnectionState.cs" />
     <Compile Include="DataReceivedEventArgs.cs" />
     <Compile Include="DisconnectedEventArgs.cs" />
+    <Compile Include="FewerThreads\HazelThreadPool.cs" />
+    <Compile Include="FewerThreads\UdpConnectionListener2.cs" />
+    <Compile Include="FewerThreads\UdpServerConnection2.cs" />
     <Compile Include="HazelException.cs" />
     <Compile Include="IPMode.cs" />
     <Compile Include="IRecyclable.cs" />
index 3a217e17030b17e23e05c7fa703fcfcb72fec8a6..0f89e9c6da37ff51cf26e959e6305d83faa106e5 100644 (file)
@@ -4,11 +4,24 @@ using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 
-namespace Hazel.UPnP
+namespace Hazel
 {
     public interface ILogger
     {
-        void LogInfo(string msg);
-        void LogError(string msg);
+        void WriteError(string msg);
+        void WriteInfo(string msg);
+    }
+
+    public class NullLogger : ILogger
+    {
+        public static readonly NullLogger Instance = new NullLogger();
+
+        public void WriteError(string msg)
+        {
+        }
+
+        public void WriteInfo(string msg)
+        {
+        }
     }
 }
index 506ac705ab43146bafeaeb08dd131680e1aacd68..771709ee9f4ec28f903b63ff02bc8501a7f87006 100644 (file)
@@ -77,7 +77,7 @@ namespace Hazel.UPnP
             }
             catch(Exception e)
             {
-                this.logger.LogInfo("Exception listening for UPnP: " + e.Message);
+                this.logger.WriteInfo("Exception listening for UPnP: " + e.Message);
             }
         }
 
@@ -133,7 +133,7 @@ namespace Hazel.UPnP
 
             byte[] buffer = System.Text.Encoding.UTF8.GetBytes(str);
 
-            this.logger.LogInfo("Attempting UPnP discovery");
+            this.logger.WriteInfo("Attempting UPnP discovery");
 
             socket.SendTo(buffer, new IPEndPoint(NetUtility.GetBroadcastAddress(), 1900));
         }
@@ -166,14 +166,14 @@ namespace Hazel.UPnP
                 }
 
                 serviceUrl = CombineUrls(resp, node.Value);
-                this.logger.LogInfo("UPnP service ready");
+                this.logger.WriteInfo("UPnP service ready");
                 Status = UPnPStatus.Available;
                 discoveryComplete.Set();
                 return true;
             }
             catch (Exception e)
             {
-                this.logger.LogError("Exception while parsing UPnP Service URL: " + e.Message);
+                this.logger.WriteError("Exception while parsing UPnP Service URL: " + e.Message);
                 return false;
             }
         }
@@ -252,12 +252,12 @@ namespace Hazel.UPnP
                     "</u:AddPortMapping>",
                     "AddPortMapping");
 
-                this.logger.LogInfo("Sent UPnP port forward request.");
+                this.logger.WriteInfo("Sent UPnP port forward request.");
                 return true;
             }
             catch (Exception ex)
             {
-                this.logger.LogError("UPnP port forward failed: " + ex.Message);
+                this.logger.WriteError("UPnP port forward failed: " + ex.Message);
                 return false;
             }
         }