From 7ad176bb1cb0756b9afe8d8f557cddb0aa6956d1 Mon Sep 17 00:00:00 2001 From: Nana Axel Date: Wed, 22 Feb 2017 21:33:52 +0100 Subject: [PATCH 1/7] (re)Formatting code --- src/Alchemy/WebSocketServer.cs | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/Alchemy/WebSocketServer.cs b/src/Alchemy/WebSocketServer.cs index 3f9a61e..8b0494c 100644 --- a/src/Alchemy/WebSocketServer.cs +++ b/src/Alchemy/WebSocketServer.cs @@ -37,7 +37,8 @@ static WebSocketServer() CleanupThread.Name = "WebSocketServer Cleanup Thread"; CleanupThread.Start(); - for(int i = 0; i < ClientThreads.Length; i++){ + for (int i = 0; i < ClientThreads.Length; i++) + { ClientThreads[i] = new Thread(HandleClientThread); ClientThreads[i].Name = "WebSocketServer Client Thread #" + (i + 1); ClientThreads[i].Start(); @@ -67,7 +68,8 @@ private static void HandleClientThread() client.SetupContext(context); } - lock(CurrentConnections){ + lock (CurrentConnections) + { CurrentConnections.Add(context); } } @@ -89,7 +91,7 @@ private static void HandleContextCleanupThread() foreach (var connection in currentConnections) { if (cancellation.IsCancellationRequested) break; - + if (!connection.Connected) { lock (CurrentConnections) @@ -153,7 +155,7 @@ private static void HandleContextCleanupThread() /// /// Initializes a new instance of the class. /// - public WebSocketServer(int listenPort = 0, IPAddress listenAddress = null) : base(listenPort, listenAddress) {} + public WebSocketServer(int listenPort = 0, IPAddress listenAddress = null) : base(listenPort, listenAddress) { } /// /// Gets or sets the origin host. @@ -252,7 +254,7 @@ protected override void OnRunClient(object data) /// The Async result. private void DoReceive(IAsyncResult result) { - var context = (Context) result.AsyncState; + var context = (Context)result.AsyncState; context.Reset(); try { @@ -274,6 +276,7 @@ private void DoReceive(IAsyncResult result) context.ReceiveReady.Release(); } } + private void SetupContext(Context _context) { _context.ReceiveEventArgs.UserToken = _context; @@ -282,6 +285,7 @@ private void SetupContext(Context _context) StartReceive(_context); } + private void StartReceive(Context _context) { try @@ -295,7 +299,7 @@ private void StartReceive(Context _context) ReceiveEventArgs_Completed(_context.Connection.Client, _context.ReceiveEventArgs); } } - catch (SocketException ex) + catch (SocketException) { //logger.Error("SocketException in ReceieveAsync", ex); _context.Disconnect(); @@ -309,15 +313,18 @@ private void StartReceive(Context _context) } catch (OperationCanceledException) { } } + void ReceiveEventArgs_Completed(object sender, SocketAsyncEventArgs e) { var context = (Context)e.UserToken; context.Reset(); if (e.SocketError != SocketError.Success) { - //logger.Error("Socket Error: " + e.SocketError.ToString()); + //logger.Error("Socket Error: " + e.SocketError.ToString()); context.ReceivedByteCount = 0; - } else { + } + else + { context.ReceivedByteCount = e.BytesTransferred; } @@ -326,17 +333,19 @@ void ReceiveEventArgs_Completed(object sender, SocketAsyncEventArgs e) context.Handler.HandleRequest(context); context.ReceiveReady.Release(); StartReceive(context); - } else { + } + else + { context.Disconnect(); context.ReceiveReady.Release(); } } - - public void Dispose() + + public new void Dispose() { cancellation.Cancel(); base.Dispose(); Handler.Instance.Dispose(); - } + } } } From 6289b860e01d2939e1d69f5788384d4564066670 Mon Sep 17 00:00:00 2001 From: Nana Axel Date: Wed, 22 Feb 2017 21:35:40 +0100 Subject: [PATCH 2/7] Add paths for Linux environment --- packages/repositories.config | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/repositories.config b/packages/repositories.config index 5619e18..258e42c 100644 --- a/packages/repositories.config +++ b/packages/repositories.config @@ -1,5 +1,7 @@  + + \ No newline at end of file From cb181a793d2a0cbfd0bfe33d360a1ff466087677 Mon Sep 17 00:00:00 2001 From: Nana Axel Date: Wed, 22 Feb 2017 21:38:04 +0100 Subject: [PATCH 3/7] Fix file name --- src/Alchemy/Alchemy.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Alchemy/Alchemy.csproj b/src/Alchemy/Alchemy.csproj index 1fbc435..dc5558f 100644 --- a/src/Alchemy/Alchemy.csproj +++ b/src/Alchemy/Alchemy.csproj @@ -80,7 +80,7 @@ - + From 248ccffe8cfbe84bcf17d5c16dc07de30449a04e Mon Sep 17 00:00:00 2001 From: Nana Axel Date: Wed, 22 Feb 2017 21:42:12 +0100 Subject: [PATCH 4/7] StartReceive() only if a connection is still alive --- src/Alchemy/WebSocketServer.cs | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/Alchemy/WebSocketServer.cs b/src/Alchemy/WebSocketServer.cs index 8b0494c..1fd5136 100644 --- a/src/Alchemy/WebSocketServer.cs +++ b/src/Alchemy/WebSocketServer.cs @@ -285,33 +285,36 @@ private void SetupContext(Context _context) StartReceive(_context); } - + private void StartReceive(Context _context) { - try + if (_context.Connected) { - if (_context.ReceiveReady.Wait(TimeOut, cancellation.Token)) + try { - try + if (_context.ReceiveReady.Wait(TimeOut, cancellation.Token)) { - if (!_context.Connection.Client.ReceiveAsync(_context.ReceiveEventArgs)) + try + { + if (!_context.Connection.Client.ReceiveAsync(_context.ReceiveEventArgs)) + { + ReceiveEventArgs_Completed(_context.Connection.Client, _context.ReceiveEventArgs); + } + } + catch (SocketException) { - ReceiveEventArgs_Completed(_context.Connection.Client, _context.ReceiveEventArgs); + //logger.Error("SocketException in ReceieveAsync", ex); + _context.Disconnect(); } } - catch (SocketException) + else { - //logger.Error("SocketException in ReceieveAsync", ex); + //logger.Error("Timeout waiting for ReceiveReady"); _context.Disconnect(); } } - else - { - //logger.Error("Timeout waiting for ReceiveReady"); - _context.Disconnect(); - } + catch (OperationCanceledException) { } } - catch (OperationCanceledException) { } } void ReceiveEventArgs_Completed(object sender, SocketAsyncEventArgs e) @@ -341,7 +344,7 @@ void ReceiveEventArgs_Completed(object sender, SocketAsyncEventArgs e) } } - public new void Dispose() + public void Dispose() { cancellation.Cancel(); base.Dispose(); From 4bffbd3fd7b743edd0ef3f64761d8cbd356ea532 Mon Sep 17 00:00:00 2001 From: Nana Axel Date: Fri, 24 Feb 2017 13:15:05 +0100 Subject: [PATCH 5/7] Code re-Formatting --- src/Alchemy/WebSocketClient.cs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/Alchemy/WebSocketClient.cs b/src/Alchemy/WebSocketClient.cs index 2472078..3751680 100644 --- a/src/Alchemy/WebSocketClient.cs +++ b/src/Alchemy/WebSocketClient.cs @@ -60,7 +60,8 @@ static WebSocketClient() NewClients = new Queue(); ContextMapping = new Dictionary(); - for(int i = 0; i < ClientThreads.Length; i++){ + for (int i = 0; i < ClientThreads.Length; i++) + { ClientThreads[i] = new Thread(HandleClientThread); ClientThreads[i].Start(); } @@ -108,7 +109,7 @@ public WebSocketClient(string path) public void Connect() { if (_client != null) return; - + try { ReadyState = ReadyStates.CONNECTING; @@ -142,7 +143,7 @@ protected void OnRunClient(IAsyncResult result) { _client.EndConnect(result); } - catch (Exception ex) + catch (Exception) { Disconnect(); connectError = true; @@ -192,7 +193,7 @@ private void SetupContext(Context context) { ReceiveEventArgs_Completed(_context.Connection.Client, _context.ReceiveEventArgs); } - + if (!IsAuthenticated) { @@ -236,7 +237,7 @@ void ReceiveEventArgs_Completed(object sender, SocketAsyncEventArgs e) private void Authenticate() { - _handshake = new ClientHandshake { Version = "8", Origin = Origin, Host = _host, Key = GenerateKey(), ResourcePath = _path, SubProtocols = SubProtocols}; + _handshake = new ClientHandshake { Version = "8", Origin = Origin, Host = _host, Key = GenerateKey(), ResourcePath = _path, SubProtocols = SubProtocols }; _client.Client.Send(Encoding.UTF8.GetBytes(_handshake.ToString())); } @@ -264,7 +265,7 @@ private bool CheckAuthenticationResponse(Context context) } } - if(String.IsNullOrEmpty(CurrentProtocol)) + if (String.IsNullOrEmpty(CurrentProtocol)) { return false; } @@ -305,7 +306,7 @@ private void ReceiveData(Context context) private void DoReceive(IAsyncResult result) { - var context = (Context) result.AsyncState; + var context = (Context)result.AsyncState; context.Reset(); try @@ -335,7 +336,7 @@ private static String GenerateKey() for (var index = 0; index < bytes.Length; index++) { - bytes[index] = (byte) random.Next(0, 255); + bytes[index] = (byte)random.Next(0, 255); } return Convert.ToBase64String(bytes); @@ -369,12 +370,12 @@ public void Send(byte[] data) { _context.UserContext.Send(data); } - + public void Dispose() { cancellation.Cancel(); Handler.Instance.Dispose(); } - + } -} +} From 3777769b84a698a1bbf1974d385271acafd25c0d Mon Sep 17 00:00:00 2001 From: Nana Axel Date: Fri, 24 Feb 2017 13:30:24 +0100 Subject: [PATCH 6/7] Implement server behaviours The WebSocketServer can now handle different events for different routes. --- src/Alchemy/Alchemy.csproj | 1 + src/Alchemy/Classes/ServerBehaviour.cs | 36 ++++++++++ src/Alchemy/WebSocketServer.cs | 94 +++++++++++++++++++++++--- 3 files changed, 123 insertions(+), 8 deletions(-) create mode 100644 src/Alchemy/Classes/ServerBehaviour.cs diff --git a/src/Alchemy/Alchemy.csproj b/src/Alchemy/Alchemy.csproj index dc5558f..583ee79 100644 --- a/src/Alchemy/Alchemy.csproj +++ b/src/Alchemy/Alchemy.csproj @@ -71,6 +71,7 @@ + diff --git a/src/Alchemy/Classes/ServerBehaviour.cs b/src/Alchemy/Classes/ServerBehaviour.cs new file mode 100644 index 0000000..fd1fdbf --- /dev/null +++ b/src/Alchemy/Classes/ServerBehaviour.cs @@ -0,0 +1,36 @@ +namespace Alchemy.Classes +{ + /// + /// The server behaviour base class. + /// + public abstract class ServerBehaviour + { + /// + /// The event triggered when the user is connected. + /// + /// The current user context. + public virtual void OnConnected(UserContext context) + { } + + /// + /// The event triggered when the server receive data. + /// + /// The current user context. + public virtual void OnReceive(UserContext context) + { } + + /// + /// The event triggered when the server send data. + /// + /// The current user context. + public virtual void OnSend(UserContext context) + { } + + /// + /// The event triggered when the user is disconnected. + /// + /// The current user context. + public virtual void OnDisconnect(UserContext context) + { } + } +} \ No newline at end of file diff --git a/src/Alchemy/WebSocketServer.cs b/src/Alchemy/WebSocketServer.cs index 1fd5136..eedb42a 100644 --- a/src/Alchemy/WebSocketServer.cs +++ b/src/Alchemy/WebSocketServer.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using System.Net; +using System.Text.RegularExpressions; using System.Net.Sockets; using System.Threading; using Alchemy.Classes; @@ -120,15 +122,21 @@ private static void HandleContextCleanupThread() /// /// These are the default OnEvent delegates for the server. By default, all new UserContexts will use these events. - /// It is up to you whether you want to replace them at runtime or even manually set the events differently per connection in OnReceive. /// public OnEventDelegate OnConnect = x => { }; - public OnEventDelegate OnConnected = x => { }; public OnEventDelegate OnDisconnect = x => { }; public OnEventDelegate OnReceive = x => { }; public OnEventDelegate OnSend = x => { }; + /// + /// These are events passed to the context to manage server behaviours. + /// + private OnEventDelegate _onConnected = x => { }; + private OnEventDelegate _onDisconnect = x => { }; + private OnEventDelegate _onReceive = x => { }; + private OnEventDelegate _onSend = x => { }; + /// /// Enables or disables the Flash Access Policy Server(APServer). /// This is used when you would like your app to only listen on a single port rather than 2. @@ -152,10 +160,65 @@ private static void HandleContextCleanupThread() private string _destination = String.Empty; private string _origin = String.Empty; + private Dictionary _behaviours = new Dictionary(); + /// /// Initializes a new instance of the class. /// - public WebSocketServer(int listenPort = 0, IPAddress listenAddress = null) : base(listenPort, listenAddress) { } + public WebSocketServer(int listenPort = 0, IPAddress listenAddress = null) : base(listenPort, listenAddress) + { + _onConnected = (context) => + { + OnConnected(context); + var keys = _behaviours.Keys.Where(k => Regex.IsMatch(context.RequestPath, $"^{k}$")); + if (keys.Count() > 0) + { + foreach (var k in keys) + { + _behaviours[k].OnConnected(context); + } + } + }; + + _onReceive = (context) => + { + OnReceive(context); + var keys = _behaviours.Keys.Where(k => Regex.IsMatch(context.RequestPath, $"^{k}$")); + if (keys.Count() > 0) + { + foreach (var k in keys) + { + _behaviours[k].OnReceive(context); + } + } + }; + + _onSend = (context) => + { + OnSend(context); + var keys = _behaviours.Keys.Where(k => Regex.IsMatch(context.RequestPath, $"^{k}$")); + if (keys.Count() > 0) + { + foreach (var k in keys) + { + _behaviours[k].OnSend(context); + } + } + }; + + _onDisconnect = (context) => + { + OnDisconnect(context); + var keys = _behaviours.Keys.Where(k => Regex.IsMatch(context.RequestPath, $"^{k}$")); + if (keys.Count() > 0) + { + foreach (var k in keys) + { + _behaviours[k].OnDisconnect(context); + } + } + }; + } /// /// Gets or sets the origin host. @@ -189,6 +252,21 @@ public string Destination } } + /// + /// Add a new server behaviour. + /// + /// The request path to apply the behaviour. Can be a . + /// The to use when the user connect using the + public void AddServerBehaviour(string path, ServerBehaviour behaviour) + { + string key = path.TrimEnd('/'); + + if (!_behaviours.ContainsKey(key)) + { + _behaviours.Add(path, behaviour); + } + } + /// /// Starts this instance. /// @@ -230,10 +308,10 @@ protected override void OnRunClient(object data) context.UserContext.ClientAddress = context.Connection.Client.RemoteEndPoint; context.UserContext.SetOnConnect(OnConnect); - context.UserContext.SetOnConnected(OnConnected); - context.UserContext.SetOnDisconnect(OnDisconnect); - context.UserContext.SetOnSend(OnSend); - context.UserContext.SetOnReceive(OnReceive); + context.UserContext.SetOnConnected(_onConnected); + context.UserContext.SetOnDisconnect(_onDisconnect); + context.UserContext.SetOnSend(_onSend); + context.UserContext.SetOnReceive(_onReceive); context.BufferSize = BufferSize; context.UserContext.OnConnect(); @@ -344,7 +422,7 @@ void ReceiveEventArgs_Completed(object sender, SocketAsyncEventArgs e) } } - public void Dispose() + public new void Dispose() { cancellation.Cancel(); base.Dispose(); From 95cfc1d2f88094a49a338dfe6d098016c6ba6378 Mon Sep 17 00:00:00 2001 From: Nana Axel Date: Fri, 24 Feb 2017 13:49:04 +0100 Subject: [PATCH 7/7] Automatic support for sub protocols --- src/Alchemy/WebSocketClient.cs | 5 ++++- test/Integration/Alchemy/Alchemy.csproj | 12 ++++++------ test/Unit/Alchemy/Alchemy.csproj | 15 +++++++++------ 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/Alchemy/WebSocketClient.cs b/src/Alchemy/WebSocketClient.cs index 3751680..63cad57 100644 --- a/src/Alchemy/WebSocketClient.cs +++ b/src/Alchemy/WebSocketClient.cs @@ -98,9 +98,12 @@ private static void HandleClientThread() } public WebSocketClient(string path) { - var r = new Regex("^(wss?)://(.*)\\:([0-9]*)/(.*)$"); + var r = new Regex("^(\\w+)://(.*)\\:([0-9]*)/(.*)$"); var matches = r.Match(path); + if (matches.Groups[1].Value != "ws" || matches.Groups[1].Value != "wss") + SubProtocols = new string[] {matches.Groups[1].Value}; + _host = matches.Groups[2].Value; _port = Int32.Parse(matches.Groups[3].Value); _path = matches.Groups[4].Value; diff --git a/test/Integration/Alchemy/Alchemy.csproj b/test/Integration/Alchemy/Alchemy.csproj index a57c2e2..b1d0800 100644 --- a/test/Integration/Alchemy/Alchemy.csproj +++ b/test/Integration/Alchemy/Alchemy.csproj @@ -53,12 +53,6 @@ - - - {45486CDE-86A3-4769-952F-E0821BF79493} - Alchemy %28src\Alchemy%29 - - @@ -70,4 +64,10 @@ --> + + + {D7B6AB15-5986-4FDC-ADA8-9EF14DF8F26D} + Alchemy + + \ No newline at end of file diff --git a/test/Unit/Alchemy/Alchemy.csproj b/test/Unit/Alchemy/Alchemy.csproj index 4a475d6..119ca61 100644 --- a/test/Unit/Alchemy/Alchemy.csproj +++ b/test/Unit/Alchemy/Alchemy.csproj @@ -40,6 +40,8 @@ true true false + 4 + false bin\x64\Release\ @@ -50,18 +52,13 @@ prompt false false + 4 - - - {45486CDE-86A3-4769-952F-E0821BF79493} - Alchemy %28src\Alchemy%29 - - ..\..\..\packages\NUnit.2.5.10.11092\lib\nunit.framework.dll @@ -84,4 +81,10 @@ --> + + + {D7B6AB15-5986-4FDC-ADA8-9EF14DF8F26D} + Alchemy + + \ No newline at end of file