From 8e73eefa9b5904ab2b0e798ef646606386c9316f Mon Sep 17 00:00:00 2001 From: Viktor Nikolaev Date: Fri, 1 Mar 2019 09:50:22 +0300 Subject: [PATCH 01/34] Support for Server Name Indication --- WebSocket.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/WebSocket.cs b/WebSocket.cs index d051288..9d6b0da 100644 --- a/WebSocket.cs +++ b/WebSocket.cs @@ -566,6 +566,9 @@ async Task ConnectAsync(Uri uri, WebSocketOptions options, Action options.IgnoreCertificateErrors || RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || sslPolicyErrors == SslPolicyErrors.None, userCertificateSelectionCallback: (sender, host, certificates, certificate, issuers) => this.Certificate ); - await (stream as SslStream).AuthenticateAsClientAsync(targetHost: uri.Host).WithCancellationToken(this._processingCTS.Token).ConfigureAwait(false); + await (stream as SslStream).AuthenticateAsClientAsync(targetHost: sniHost ?? uri.Host).WithCancellationToken(this._processingCTS.Token).ConfigureAwait(false); Events.Log.ConnectionSecured(id); this._logger.Log(LogLevel.Trace, LogLevel.Debug, $"The connection successfully secured ({id} @ {endpoint})"); @@ -610,7 +613,7 @@ async Task ConnectAsync(Uri uri, WebSocketOptions options, Action Date: Sun, 3 Mar 2019 00:42:01 +0700 Subject: [PATCH 02/34] Improvement: add try/catch on all .SendAsync methods --- WebSocket.cs | 107 +++++++++++++++++++++++++++++++------ WebSocketImplementation.cs | 29 +++++----- WebSocketWrapper.cs | 15 +++--- 3 files changed, 116 insertions(+), 35 deletions(-) diff --git a/WebSocket.cs b/WebSocket.cs index d051288..8232051 100644 --- a/WebSocket.cs +++ b/WebSocket.cs @@ -982,10 +982,28 @@ async Task ReceiveAsync(ManagedWebSocket websocket) /// true if this message is a standalone message (this is the norm), false if it is a multi-part message (and true for the last message) /// The cancellation token /// - public Task SendAsync(Guid id, ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken = default(CancellationToken)) - => this._websockets.TryGetValue(id, out ManagedWebSocket websocket) - ? websocket.SendAsync(buffer, messageType, endOfMessage, cancellationToken) - : Task.FromException(new InformationNotFoundException($"WebSocket connection with identity \"{id}\" is not found")); + public async Task SendAsync(Guid id, ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken = default(CancellationToken)) + { + ManagedWebSocket websocket = null; + try + { + if (this._websockets.TryGetValue(id, out websocket)) + await websocket.SendAsync(buffer, messageType, endOfMessage, cancellationToken).ConfigureAwait(false); + else + throw new InformationNotFoundException($"WebSocket connection with identity \"{id}\" is not found"); + } + catch (Exception ex) + { + try + { + this.ErrorHandler?.Invoke(websocket, ex); + } + catch (Exception e) + { + this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); + } + } + } /// /// Sends the message to a WebSocket connection @@ -995,10 +1013,28 @@ async Task ReceiveAsync(ManagedWebSocket websocket) /// true if this message is a standalone message (this is the norm), false if it is a multi-part message (and true for the last message) /// The cancellation token /// - public Task SendAsync(Guid id, string message, bool endOfMessage, CancellationToken cancellationToken = default(CancellationToken)) - => this._websockets.TryGetValue(id, out ManagedWebSocket websocket) - ? websocket.SendAsync(message, endOfMessage, cancellationToken) - : Task.FromException(new InformationNotFoundException($"WebSocket connection with identity \"{id}\" is not found")); + public async Task SendAsync(Guid id, string message, bool endOfMessage, CancellationToken cancellationToken = default(CancellationToken)) + { + ManagedWebSocket websocket = null; + try + { + if (this._websockets.TryGetValue(id, out websocket)) + await websocket.SendAsync(message, endOfMessage, cancellationToken).ConfigureAwait(false); + else + throw new InformationNotFoundException($"WebSocket connection with identity \"{id}\" is not found"); + } + catch (Exception ex) + { + try + { + this.ErrorHandler?.Invoke(websocket, ex); + } + catch (Exception e) + { + this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); + } + } + } /// /// Sends the message to a WebSocket connection @@ -1008,10 +1044,28 @@ async Task ReceiveAsync(ManagedWebSocket websocket) /// true if this message is a standalone message (this is the norm), false if it is a multi-part message (and true for the last message) /// The cancellation token /// - public Task SendAsync(Guid id, byte[] message, bool endOfMessage, CancellationToken cancellationToken = default(CancellationToken)) - => this._websockets.TryGetValue(id, out ManagedWebSocket websocket) - ? websocket.SendAsync(message, endOfMessage, cancellationToken) - : Task.FromException(new InformationNotFoundException($"WebSocket connection with identity \"{id}\" is not found")); + public async Task SendAsync(Guid id, byte[] message, bool endOfMessage, CancellationToken cancellationToken = default(CancellationToken)) + { + ManagedWebSocket websocket = null; + try + { + if (this._websockets.TryGetValue(id, out websocket)) + await websocket.SendAsync(message, endOfMessage, cancellationToken).ConfigureAwait(false); + else + throw new InformationNotFoundException($"WebSocket connection with identity \"{id}\" is not found"); + } + catch (Exception ex) + { + try + { + this.ErrorHandler?.Invoke(websocket, ex); + } + catch (Exception e) + { + this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); + } + } + } /// /// Sends the message to the WebSocket connections that matched with the predicate @@ -1022,8 +1076,25 @@ async Task ReceiveAsync(ManagedWebSocket websocket) /// true if this message is a standalone message (this is the norm), false if it is a multi-part message (and true for the last message) /// The cancellation token /// - public Task SendAsync(Func predicate, ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken = default(CancellationToken)) => this.GetWebSockets(predicate).ToList().ForEachAsync((connection, token) - => connection.SendAsync(buffer.Clone(), messageType, endOfMessage, token), cancellationToken); + public Task SendAsync(Func predicate, ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken = default(CancellationToken)) + => this.GetWebSockets(predicate).ForEachAsync(async (websocket, token) => + { + try + { + await websocket.SendAsync(buffer.Clone(), messageType, endOfMessage, token).ConfigureAwait(false); + } + catch (Exception ex) + { + try + { + this.ErrorHandler?.Invoke(websocket, ex); + } + catch (Exception e) + { + this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); + } + } + }, cancellationToken); /// /// Sends the message to the WebSocket connections that matched with the predicate @@ -1295,7 +1366,7 @@ await Task.WhenAll( protected bool _disposing = false, _disposed = false; - internal virtual async Task DisposeAsync(WebSocketCloseStatus closeStatus = WebSocketCloseStatus.EndpointUnavailable, string closeStatusDescription = "Service is unavailable", CancellationToken cancellationToken = default(CancellationToken), Action onCompleted = null) + internal virtual async Task DisposeAsync(WebSocketCloseStatus closeStatus = WebSocketCloseStatus.EndpointUnavailable, string closeStatusDescription = "Service is unavailable", CancellationToken cancellationToken = default(CancellationToken), Action onDisposed = null) { if (!this._disposing && !this._disposed) { @@ -1303,7 +1374,11 @@ await Task.WhenAll( Events.Log.WebSocketDispose(this.ID, this.State); if (this.State == WebSocketState.Open) await this.CloseOutputTimeoutAsync(closeStatus, closeStatusDescription, null, () => Events.Log.WebSocketDisposeCloseTimeout(this.ID, this.State), ex => Events.Log.WebSocketDisposeError(this.ID, this.State, ex.ToString())).ConfigureAwait(false); - onCompleted?.Invoke(); + try + { + onDisposed?.Invoke(); + } + catch { } this._disposed = true; this._disposing = false; } diff --git a/WebSocketImplementation.cs b/WebSocketImplementation.cs index 77071f5..3d73c12 100644 --- a/WebSocketImplementation.cs +++ b/WebSocketImplementation.cs @@ -86,13 +86,13 @@ public WebSocketImplementation(Guid id, bool isClient, Func recycl /// /// Puts data on the wire /// - /// + /// /// /// - async Task PutOnTheWireAsync(MemoryStream data, CancellationToken cancellationToken) + async Task PutOnTheWireAsync(MemoryStream stream, CancellationToken cancellationToken) { // add into queue - this._buffers.Enqueue(data.ToArraySegment()); + this._buffers.Enqueue(stream.ToArraySegment()); // check pending write operations if (this._writting) @@ -341,13 +341,14 @@ public override async Task SendAsync(ArraySegment buffer, WebSocketMessage } // send - using (var stream = this._recycledStreamFactory()) - { - stream.Write(opCode, buffer, endOfMessage, this.IsClient); - Events.Log.SendingFrame(this.ID, opCode, endOfMessage, buffer.Count, false); - await this.PutOnTheWireAsync(stream, cancellationToken).ConfigureAwait(false); - this._isContinuationFrame = !endOfMessage; - } + if (this._state == WebSocketState.Open) + using (var stream = this._recycledStreamFactory()) + { + stream.Write(opCode, buffer, endOfMessage, this.IsClient); + Events.Log.SendingFrame(this.ID, opCode, endOfMessage, buffer.Count, false); + await this.PutOnTheWireAsync(stream, cancellationToken).ConfigureAwait(false); + this._isContinuationFrame = !endOfMessage; + } } /// @@ -428,11 +429,15 @@ public override void Abort() this._processingCTS.Cancel(); } - internal override Task DisposeAsync(WebSocketCloseStatus closeStatus = WebSocketCloseStatus.EndpointUnavailable, string closeStatusDescription = "Service is unavailable", CancellationToken cancellationToken = default(CancellationToken), Action onCompleted = null) + internal override Task DisposeAsync(WebSocketCloseStatus closeStatus = WebSocketCloseStatus.EndpointUnavailable, string closeStatusDescription = "Service is unavailable", CancellationToken cancellationToken = default(CancellationToken), Action onDisposed = null) => base.DisposeAsync(closeStatus, closeStatusDescription, cancellationToken, () => { this.Close(); - onCompleted?.Invoke(); + try + { + onDisposed?.Invoke(); + } + catch { } }); internal override void Close() diff --git a/WebSocketWrapper.cs b/WebSocketWrapper.cs index 5369258..d51404e 100644 --- a/WebSocketWrapper.cs +++ b/WebSocketWrapper.cs @@ -126,20 +126,21 @@ public override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string c public override void Abort() => this._websocket.Abort(); - internal override Task DisposeAsync(WebSocketCloseStatus closeStatus = WebSocketCloseStatus.EndpointUnavailable, string closeStatusDescription = "Service is unavailable", CancellationToken cancellationToken = default(CancellationToken), Action onCompleted = null) + internal override Task DisposeAsync(WebSocketCloseStatus closeStatus = WebSocketCloseStatus.EndpointUnavailable, string closeStatusDescription = "Service is unavailable", CancellationToken cancellationToken = default(CancellationToken), Action onDisposed = null) => base.DisposeAsync(closeStatus, closeStatusDescription, cancellationToken, () => { this.Close(); - onCompleted?.Invoke(); + try + { + onDisposed?.Invoke(); + } + catch { } }); internal override void Close() { - if (!this._disposing && !this._disposed) - { - if ("System.Net.WebSockets.ManagedWebSocket".Equals($"{this._websocket.GetType()}")) - this._websocket.Dispose(); - } + if (!this._disposing && !this._disposed && "System.Net.WebSockets.ManagedWebSocket".Equals($"{this._websocket.GetType()}")) + this._websocket.Dispose(); } ~WebSocketWrapper() From 787fd8686d22b5193a67108e81a636cd553fc0ac Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Wed, 6 Mar 2019 17:11:36 +0700 Subject: [PATCH 03/34] Upgrade to latest components --- VIEApps.Components.WebSockets.csproj | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/VIEApps.Components.WebSockets.csproj b/VIEApps.Components.WebSockets.csproj index 2e44917..5aaf8d1 100644 --- a/VIEApps.Components.WebSockets.csproj +++ b/VIEApps.Components.WebSockets.csproj @@ -9,14 +9,14 @@ VIEApps NGX WebSockets false ../VIEApps.Components.snk - 10.2.1903.1 - 10.2.1903.1 - v10.2.netstandard-2+rev:2019.03.01-latest.components-ignore.cert.errors + 10.2.1903.3 + 10.2.1903.3 + v10.2.netstandard-2+rev:2019.03.06-latest.components VIEApps NGX VIEApps.net false VIEApps.Components.WebSockets - 10.2.1903.1 + 10.2.1903.3 VIEApps NGX WebSockets High performance WebSocket on .NET Standard 2.0 (both server and client - standalone or wrapper of System.Net.WebSockets.WebSocket) VIEApps.net @@ -24,7 +24,7 @@ LICENSE.md ..\ websocket;websockets;websocket-client;websocket-server;websocket-wrapper;vieapps;vieapps.components - Upgrade to latest components, add options to ignore remote certificate errors (support for client certificates) + Upgrade to latest components https://github.com/vieapps/Components.Utility/raw/master/logo.png https://github.com/vieapps/Components.WebSockets https://github.com/vieapps/Components.WebSockets @@ -39,7 +39,7 @@ - + \ No newline at end of file From c34f96b4fb80cf978e1f4c9805b25c8b7440cb58 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Tue, 12 Mar 2019 23:08:59 +0700 Subject: [PATCH 04/34] Improvements: add locking mechanims, check disposed objects when sending, ... --- PingPong.cs | 4 ++-- README.md | 4 ++-- VIEApps.Components.WebSockets.csproj | 2 +- WebSocket.cs | 6 +++--- WebSocketImplementation.cs | 27 +++++++++++++++++++-------- WebSocketWrapper.cs | 24 ++++++++++++++++++------ 6 files changed, 45 insertions(+), 22 deletions(-) diff --git a/PingPong.cs b/PingPong.cs index c57ca0d..b707ccb 100644 --- a/PingPong.cs +++ b/PingPong.cs @@ -17,7 +17,7 @@ internal class PongEventArgs : EventArgs /// /// The data extracted from a Pong WebSocket frame /// - public ArraySegment Payload { get; private set; } + public ArraySegment Payload { get; } /// /// Initialises a new instance of the PongEventArgs class @@ -41,7 +41,7 @@ internal interface IPingPongManager /// /// Sends a ping frame /// - /// The payload (must be 125 bytes of less) + /// The payload (must be 125 bytes or less) /// The cancellation token Task SendPingAsync(ArraySegment payload, CancellationToken cancellation = default(CancellationToken)); } diff --git a/README.md b/README.md index 21933dd..572fef3 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ Use the **StopListen** method to stop the listener. ### WebSocket server with Secure WebSockets (wss://) Enabling secure connections requires two things: -- Pointing certificate to an x509 certificate that containing a public and private key. +- Pointing certificate to an X.509 certificate that containing a public and private key. - Using the scheme **wss** instead of **ws** (or **https** instead of **http**) on all clients ```csharp @@ -149,7 +149,7 @@ websocket.StartListen(); Want to have a free SSL certificate? Take a look at [Let's Encrypt](https://letsencrypt.org/). -Special: A simple tool named [lets-encrypt-win-simple](https://github.com/PKISharp/win-acme) will help your IIS works with Let's Encrypt very well. +Special: A simple tool named [win-acme](https://github.com/PKISharp/win-acme) will help your IIS works with Let's Encrypt very well. ### SubProtocol Negotiation diff --git a/VIEApps.Components.WebSockets.csproj b/VIEApps.Components.WebSockets.csproj index 5aaf8d1..02681c2 100644 --- a/VIEApps.Components.WebSockets.csproj +++ b/VIEApps.Components.WebSockets.csproj @@ -11,7 +11,7 @@ ../VIEApps.Components.snk 10.2.1903.3 10.2.1903.3 - v10.2.netstandard-2+rev:2019.03.06-latest.components + v10.2.netstandard-2+rev:2019.03.12-disposed.objects VIEApps NGX VIEApps.net false diff --git a/WebSocket.cs b/WebSocket.cs index 8232051..44262fc 100644 --- a/WebSocket.cs +++ b/WebSocket.cs @@ -1243,6 +1243,7 @@ public void Dispose() /// public abstract class ManagedWebSocket : System.Net.WebSockets.WebSocket { + protected bool _disposing = false, _disposed = false; #region Properties /// @@ -1362,9 +1363,8 @@ await Task.WhenAll( /// /// Cleans up unmanaged resources (will send a close frame if the connection is still open) /// - public override void Dispose() => this.DisposeAsync().Wait(4321); - - protected bool _disposing = false, _disposed = false; + public override void Dispose() + => this.DisposeAsync().Wait(4321); internal virtual async Task DisposeAsync(WebSocketCloseStatus closeStatus = WebSocketCloseStatus.EndpointUnavailable, string closeStatusDescription = "Service is unavailable", CancellationToken cancellationToken = default(CancellationToken), Action onDisposed = null) { diff --git a/WebSocketImplementation.cs b/WebSocketImplementation.cs index 3d73c12..8913cb8 100644 --- a/WebSocketImplementation.cs +++ b/WebSocketImplementation.cs @@ -25,7 +25,9 @@ internal class WebSocketImplementation : ManagedWebSocket WebSocketMessageType _continuationFrameMessageType = WebSocketMessageType.Binary; WebSocketCloseStatus? _closeStatus; string _closeStatusDescription; - bool _isContinuationFrame, _writting = false; + bool _isContinuationFrame = false; + readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); + bool _pending = false; readonly string _subProtocol; readonly CancellationTokenSource _processingCTS; readonly ConcurrentQueue> _buffers = new ConcurrentQueue>(); @@ -91,19 +93,22 @@ public WebSocketImplementation(Guid id, bool isClient, Func recycl /// async Task PutOnTheWireAsync(MemoryStream stream, CancellationToken cancellationToken) { - // add into queue + // add into queue and check pending operations this._buffers.Enqueue(stream.ToArraySegment()); - - // check pending write operations - if (this._writting) + if (this._pending) { Events.Log.PendingOperations(this.ID); - Logger.Log(LogLevel.Debug, LogLevel.Warning, $"Pending operations => {this._buffers.Count:#,##0} ({this.ID} @ {this.RemoteEndPoint})"); + Logger.Log(LogLevel.Debug, LogLevel.Warning, $"#{Thread.CurrentThread.ManagedThreadId} Pendings => {this._buffers.Count:#,##0} ({this.ID} @ {this.RemoteEndPoint})"); return; } + // check disposed + if (this._disposing || this._disposed) + throw new ObjectDisposedException("WebSocketImplementation"); + // put data to wire - this._writting = true; + this._pending = true; + await this._lock.WaitAsync(cancellationToken).ConfigureAwait(false); try { while (this._buffers.Count > 0) @@ -116,7 +121,8 @@ async Task PutOnTheWireAsync(MemoryStream stream, CancellationToken cancellation } finally { - this._writting = false; + this._pending = false; + this._lock.Release(); } } @@ -438,6 +444,11 @@ public override void Abort() onDisposed?.Invoke(); } catch { } + try + { + this._lock.Dispose(); + } + catch { } }); internal override void Close() diff --git a/WebSocketWrapper.cs b/WebSocketWrapper.cs index d51404e..1df1327 100644 --- a/WebSocketWrapper.cs +++ b/WebSocketWrapper.cs @@ -17,7 +17,8 @@ internal class WebSocketWrapper : ManagedWebSocket #region Properties readonly System.Net.WebSockets.WebSocket _websocket = null; readonly ConcurrentQueue, WebSocketMessageType, bool>> _buffers = new ConcurrentQueue, WebSocketMessageType, bool>>(); - bool _sending = false; + readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); + bool _pending = false; /// /// Gets the state that indicates the reason why the remote endpoint initiated the close handshake @@ -73,17 +74,22 @@ public override Task ReceiveAsync(ArraySegment buf /// public override async Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) { - // add into queue and check pending write operations + // add into queue and check pending operations this._buffers.Enqueue(new Tuple, WebSocketMessageType, bool>(buffer, messageType, endOfMessage)); - if (this._sending) + if (this._pending) { Events.Log.PendingOperations(this.ID); - Logger.Log(LogLevel.Debug, LogLevel.Warning, $"Pending operations => {this._buffers.Count:#,##0} ({this.ID} @ {this.RemoteEndPoint})"); + Logger.Log(LogLevel.Debug, LogLevel.Warning, $"#{Thread.CurrentThread.ManagedThreadId} Pendings => {this._buffers.Count:#,##0} ({this.ID} @ {this.RemoteEndPoint})"); return; } + // check disposed + if (this._disposing || this._disposed) + throw new ObjectDisposedException("WebSocketImplementation"); + // put data to wire - this._sending = true; + this._pending = true; + await this._lock.WaitAsync(cancellationToken).ConfigureAwait(false); try { while (this.State == WebSocketState.Open && this._buffers.Count > 0) @@ -96,7 +102,8 @@ public override async Task SendAsync(ArraySegment buffer, WebSocketMessage } finally { - this._sending = false; + this._pending = false; + this._lock.Release(); } } @@ -135,6 +142,11 @@ public override void Abort() onDisposed?.Invoke(); } catch { } + try + { + this._lock.Dispose(); + } + catch { } }); internal override void Close() From ef5a36fd6d3aee690df38480998bc600f5c58bc1 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Sat, 16 Mar 2019 17:30:00 +0700 Subject: [PATCH 05/34] Code refactoring --- VIEApps.Components.WebSockets.csproj | 2 +- WebSocket.cs | 271 ++++++++++++++++----------- WebSocketHelper.cs | 30 +-- WebSocketImplementation.cs | 12 +- WebSocketWrapper.cs | 12 +- 5 files changed, 184 insertions(+), 143 deletions(-) diff --git a/VIEApps.Components.WebSockets.csproj b/VIEApps.Components.WebSockets.csproj index 02681c2..7f39d76 100644 --- a/VIEApps.Components.WebSockets.csproj +++ b/VIEApps.Components.WebSockets.csproj @@ -11,7 +11,7 @@ ../VIEApps.Components.snk 10.2.1903.3 10.2.1903.3 - v10.2.netstandard-2+rev:2019.03.12-disposed.objects + v10.2.netstandard-2+rev:2019.03.15-disposed.objects.headers VIEApps NGX VIEApps.net false diff --git a/WebSocket.cs b/WebSocket.cs index 44262fc..e966745 100644 --- a/WebSocket.cs +++ b/WebSocket.cs @@ -197,7 +197,8 @@ public void StartListen(int port = 46429, X509Certificate2 certificate = null, A } catch (Exception ex) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); } return; } @@ -214,23 +215,27 @@ public void StartListen(int port = 46429, X509Certificate2 certificate = null, A this._tcpListener.Server.SetKeepAliveInterval(); this._tcpListener.Start(1024); - var platform = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? "Windows" - : RuntimeInformation.IsOSPlatform(OSPlatform.Linux) - ? "Linux" - : "macOS"; - platform += $" ({RuntimeInformation.FrameworkDescription.Trim()}) - SSL: {this.Certificate != null}"; - if (this.Certificate != null) - platform += $" ({this.Certificate.GetNameInfo(X509NameType.DnsName, false)} :: Issued by {this.Certificate.GetNameInfo(X509NameType.DnsName, true)})"; + if (this._logger.IsEnabled(LogLevel.Debug)) + { + var platform = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? "Windows" + : RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + ? "Linux" + : "macOS"; + platform += $" ({RuntimeInformation.FrameworkDescription.Trim()}) - SSL: {this.Certificate != null}"; + if (this.Certificate != null) + platform += $" ({this.Certificate.GetNameInfo(X509NameType.DnsName, false)} :: Issued by {this.Certificate.GetNameInfo(X509NameType.DnsName, true)})"; + this._logger.LogInformation($"The listener is started (listening port: {this.Port})\r\nPlatform: {platform}\r\nPowered by {WebSocketHelper.AgentName} v{this.GetType().Assembly.GetVersion()}"); + } - this._logger.LogInformation($"The listener is started (listening port: {this.Port})\r\nPlatform: {platform}\r\nPowered by {WebSocketHelper.AgentName} {this.GetType().Assembly.GetVersion()}"); try { onSuccess?.Invoke(); } catch (Exception ex) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); } // listen for incoming connection requests @@ -239,26 +244,30 @@ public void StartListen(int port = 46429, X509Certificate2 certificate = null, A catch (SocketException ex) { var message = $"Error occurred while listening on port \"{this.Port}\". Make sure another application is not running and consuming this port."; - this._logger.Log(LogLevel.Debug, LogLevel.Error, message, ex); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, message, ex); try { onFailure?.Invoke(new ListenerSocketException(message, ex)); } catch (Exception e) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); } } catch (Exception ex) { - this._logger.Log(LogLevel.Debug, LogLevel.Error, $"Got an unexpected error while listening: {ex.Message}", ex); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Got an unexpected error while listening: {ex.Message}", ex); try { onFailure?.Invoke(ex); } catch (Exception e) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); } } } @@ -326,7 +335,7 @@ async Task ListenAsync() { this.StopListen(false); if (ex is OperationCanceledException || ex is TaskCanceledException || ex is ObjectDisposedException || ex is SocketException || ex is IOException) - this._logger.LogInformation($"The listener is stopped {(this._logger.IsEnabled(LogLevel.Debug) ? $"({ex.GetType()})" : "")}"); + this._logger.LogDebug($"The listener is stopped {(this._logger.IsEnabled(LogLevel.Debug) ? $"({ex.GetType()})" : "")}"); else this._logger.LogError($"The listener is stopped ({ex.Message})", ex); } @@ -349,7 +358,8 @@ async Task AcceptClientAsync(TcpClient tcpClient) try { Events.Log.AttemptingToSecureConnection(id); - this._logger.Log(LogLevel.Trace, LogLevel.Debug, $"Attempting to secure the connection ({id} @ {endpoint})"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"Attempting to secure the connection ({id} @ {endpoint})"); stream = new SslStream(tcpClient.GetStream(), false); await (stream as SslStream).AuthenticateAsServerAsync( @@ -360,7 +370,8 @@ async Task AcceptClientAsync(TcpClient tcpClient) ).WithCancellationToken(this._listeningCTS.Token).ConfigureAwait(false); Events.Log.ConnectionSecured(id); - this._logger.Log(LogLevel.Trace, LogLevel.Debug, $"The connection successfully secured ({id} @ {endpoint})"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"The connection successfully secured ({id} @ {endpoint})"); } catch (OperationCanceledException) { @@ -377,15 +388,18 @@ async Task AcceptClientAsync(TcpClient tcpClient) else { Events.Log.ConnectionNotSecured(id); - this._logger.Log(LogLevel.Trace, LogLevel.Debug, $"Use insecured connection ({id} @ {endpoint})"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"Use insecured connection ({id} @ {endpoint})"); stream = tcpClient.GetStream(); } // parse request - this._logger.Log(LogLevel.Trace, LogLevel.Debug, $"The connection is opened, then parse the request ({id} @ {endpoint})"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"The connection is opened, then parse the request ({id} @ {endpoint})"); var header = await stream.ReadHeaderAsync(this._listeningCTS.Token).ConfigureAwait(false); - this._logger.Log(LogLevel.Trace, LogLevel.Debug, $"Handshake request ({id} @ {endpoint}) => \r\n{header.Trim()}"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"Handshake request ({id} @ {endpoint}) => \r\n{header.Trim()}"); var isWebSocketRequest = false; var path = string.Empty; @@ -400,7 +414,8 @@ async Task AcceptClientAsync(TcpClient tcpClient) // verify request if (!isWebSocketRequest) { - this._logger.Log(LogLevel.Trace, LogLevel.Debug, $"The request contains no WebSocket upgrade request, then ignore ({id} @ {endpoint})"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"The request contains no WebSocket upgrade request, then ignore ({id} @ {endpoint})"); stream.Close(); tcpClient.Close(); return; @@ -412,20 +427,17 @@ async Task AcceptClientAsync(TcpClient tcpClient) KeepAliveInterval = this.KeepAliveInterval }; Events.Log.AcceptWebSocketStarted(id); - this._logger.Log(LogLevel.Trace, LogLevel.Debug, $"The request has requested an upgrade to WebSocket protocol, negotiating WebSocket handshake ({id} @ {endpoint})"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"The request has requested an upgrade to WebSocket protocol, negotiating WebSocket handshake ({id} @ {endpoint})"); try { // check the version (support version 13 and above) match = new Regex("Sec-WebSocket-Version: (.*)").Match(header); - if (match.Success) - { - var secWebSocketVersion = match.Groups[1].Value.Trim().CastAs(); - if (secWebSocketVersion < 13) - throw new VersionNotSupportedException($"WebSocket Version {secWebSocketVersion} is not supported, must be 13 or above"); - } - else + if (!match.Success || !Int32.TryParse(match.Groups[1].Value, out int version)) throw new VersionNotSupportedException("Unable to find \"Sec-WebSocket-Version\" in the upgrade request"); + else if (version < 13) + throw new VersionNotSupportedException($"WebSocket Version {version} is not supported, must be 13 or above"); // get the request key match = new Regex("Sec-WebSocket-Key: (.*)").Match(header); @@ -454,7 +466,8 @@ async Task AcceptClientAsync(TcpClient tcpClient) Events.Log.SendingHandshake(id, handshake); await stream.WriteHeaderAsync(handshake, this._listeningCTS.Token).ConfigureAwait(false); Events.Log.HandshakeSent(id, handshake); - this._logger.Log(LogLevel.Trace, LogLevel.Debug, $"Handshake response ({id} @ {endpoint}) => \r\n{handshake.Trim()}"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"Handshake response ({id} @ {endpoint}) => \r\n{handshake.Trim()}"); } catch (VersionNotSupportedException ex) { @@ -470,7 +483,8 @@ async Task AcceptClientAsync(TcpClient tcpClient) } Events.Log.ServerHandshakeSuccess(id); - this._logger.Log(LogLevel.Trace, LogLevel.Debug, $"WebSocket handshake response has been sent, the stream is ready ({id} @ {endpoint})"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"WebSocket handshake response has been sent, the stream is ready ({id} @ {endpoint})"); // update the connected WebSocket connection match = new Regex("Sec-WebSocket-Extensions: (.*)").Match(header); @@ -483,26 +497,11 @@ async Task AcceptClientAsync(TcpClient tcpClient) ? match.Groups[1].Value.Trim() : string.Empty; - websocket = new WebSocketImplementation(id, false, this._recycledStreamFactory, stream, options, new Uri($"ws{(this.Certificate != null ? "s" : "")}://{host}{path}"), endpoint, tcpClient.Client.LocalEndPoint); - - match = new Regex("User-Agent: (.*)").Match(header); - websocket.Extra["User-Agent"] = match.Success - ? match.Groups[1].Value.Trim() - : string.Empty; - - match = new Regex("Referer: (.*)").Match(header); - if (match.Success) - websocket.Extra["Referer"] = match.Groups[1].Value.Trim(); - else - { - match = new Regex("Origin: (.*)").Match(header); - websocket.Extra["Referer"] = match.Success - ? match.Groups[1].Value.Trim() - : string.Empty; - } - // add into the collection + websocket = new WebSocketImplementation(id, false, this._recycledStreamFactory, stream, options, new Uri($"ws{(this.Certificate != null ? "s" : "")}://{host}{path}"), endpoint, tcpClient.Client.LocalEndPoint, header.ToDictionary()); await this.AddWebSocketAsync(websocket).ConfigureAwait(false); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"The server WebSocket connection was successfully established ({websocket.ID} @ {websocket.RemoteEndPoint})\r\n- URI: {websocket.RequestUri}\r\n- Headers:\r\n\t{websocket.Headers.ToString("\r\n\t", kvp => $"{kvp.Key}: {kvp.Value}")}"); // callback try @@ -511,7 +510,8 @@ async Task AcceptClientAsync(TcpClient tcpClient) } catch (Exception e) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); } // receive messages @@ -525,14 +525,16 @@ async Task AcceptClientAsync(TcpClient tcpClient) } else { - this._logger.Log(LogLevel.Debug, LogLevel.Error, $"Error occurred while accepting an incoming connection request: {ex.Message}", ex); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while accepting an incoming connection request: {ex.Message}", ex); try { this.ErrorHandler?.Invoke(websocket, ex); } catch (Exception e) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); } } } @@ -542,7 +544,9 @@ async Task AcceptClientAsync(TcpClient tcpClient) #region Connect to remote endpoints as client async Task ConnectAsync(Uri uri, WebSocketOptions options, Action onSuccess = null, Action onFailure = null) { - this._logger.Log(LogLevel.Trace, LogLevel.Debug, $"Attempting to connect ({uri})"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"Attempting to connect ({uri})"); + try { // connect the TCP client @@ -565,7 +569,8 @@ async Task ConnectAsync(Uri uri, WebSocketOptions options, Action options.IgnoreCertificateErrors || RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || sslPolicyErrors == SslPolicyErrors.None, + userCertificateValidationCallback: (sender, certificate, chain, sslPolicyErrors) => sslPolicyErrors == SslPolicyErrors.None || options.IgnoreCertificateErrors || RuntimeInformation.IsOSPlatform(OSPlatform.Linux), userCertificateSelectionCallback: (sender, host, certificates, certificate, issuers) => this.Certificate ); await (stream as SslStream).AuthenticateAsClientAsync(targetHost: uri.Host).WithCancellationToken(this._processingCTS.Token).ConfigureAwait(false); Events.Log.ConnectionSecured(id); - this._logger.Log(LogLevel.Trace, LogLevel.Debug, $"The connection successfully secured ({id} @ {endpoint})"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"The connection successfully secured ({id} @ {endpoint})"); } catch (OperationCanceledException) { @@ -601,12 +608,15 @@ async Task ConnectAsync(Uri uri, WebSocketOptions options, Action \r\n{handshake.Trim()}"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"Handshake request ({id} @ {endpoint}) => \r\n{handshake.Trim()}"); // read response Events.Log.ReadingResponse(id); @@ -635,7 +646,8 @@ async Task ConnectAsync(Uri uri, WebSocketOptions options, Action \r\n{response.Trim()}"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"Handshake response ({id} @ {endpoint}) => \r\n{response.Trim()}"); } catch (Exception ex) { @@ -671,13 +683,14 @@ async Task ConnectAsync(Uri uri, WebSocketOptions options, Action $"{kvp.Key}: {kvp.Value}")}"); // callback try @@ -697,7 +712,8 @@ async Task ConnectAsync(Uri uri, WebSocketOptions options, Action {ex.Message}", ex); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); } // receive messages @@ -709,14 +725,16 @@ async Task ConnectAsync(Uri uri, WebSocketOptions options, Action {e.Message}", e); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); } } } @@ -769,33 +787,18 @@ public void Connect(string location, Action onSuccess, Action< /// The original request URI of the WebSocket connection /// The remote endpoint of the WebSocket connection /// The local endpoint of the WebSocket connection - /// The string that presents the user agent of the client that made this request to the WebSocket connection - /// The string that presents the url referer of the client that made this request to the WebSocket connection - /// The string that presents the headers of the client that made this request to the WebSocket connection - /// The string that presents the cookies of the client that made this request to the WebSocket connection + /// The collection that presents the headers of the client that made this request to the WebSocket connection /// The action to fire when the WebSocket connection is wrap success /// A task that run the receiving process when wrap successful or an exception when failed - public Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, EndPoint remoteEndPoint = null, EndPoint localEndPoint = null, string userAgent = null, string urlReferer = null, string headers = null, string cookies = null, Action onSuccess = null) + public Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, EndPoint remoteEndPoint = null, EndPoint localEndPoint = null, Dictionary headers = null, Action onSuccess = null) { try { // create - var websocket = new WebSocketWrapper(webSocket, requestUri, remoteEndPoint, localEndPoint); - - if (!string.IsNullOrWhiteSpace(userAgent)) - websocket.Extra["User-Agent"] = userAgent; - - if (!string.IsNullOrWhiteSpace(urlReferer)) - websocket.Extra["Referer"] = urlReferer; - - if (!string.IsNullOrWhiteSpace(headers)) - websocket.Extra["Headers"] = headers; - - if (!string.IsNullOrWhiteSpace(cookies)) - websocket.Extra["Cookies"] = cookies; - + var websocket = new WebSocketWrapper(webSocket, requestUri, remoteEndPoint, localEndPoint, headers); this.AddWebSocket(websocket); - this._logger.Log(LogLevel.Trace, LogLevel.Debug, $"Wrap a WebSocket connection [{webSocket.GetType()}] successful ({websocket.ID} @ {websocket.RemoteEndPoint}"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"Wrap a WebSocket connection [{webSocket.GetType()}] successful ({websocket.ID} @ {websocket.RemoteEndPoint})\r\n- URI: {websocket.RequestUri}\r\n- Headers:\r\n\t{websocket.Headers.ToString("\r\n\t", kvp => $"{kvp.Key}: {kvp.Value}")}"); // callback try @@ -805,7 +808,8 @@ public Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, } catch (Exception ex) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); } // receive messages @@ -813,11 +817,22 @@ public Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, } catch (Exception ex) { - this._logger.Log(LogLevel.Debug, LogLevel.Error, $"Unable to wrap a WebSocket connection [{webSocket.GetType()}]: {ex.Message}", ex); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Unable to wrap a WebSocket connection [{webSocket.GetType()}]: {ex.Message}", ex); return Task.FromException(new WrapWebSocketFailedException($"Unable to wrap a WebSocket connection [{webSocket.GetType()}]", ex)); } } + /// + /// Wraps a WebSocket connection of ASP.NET / ASP.NET Core and acts like a WebSocket server + /// + /// The WebSocket connection of ASP.NET / ASP.NET Core + /// The original request URI of the WebSocket connection + /// The remote endpoint of the WebSocket connection + /// A task that run the receiving process when wrap successful or an exception when failed + public Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, EndPoint remoteEndPoint) + => this.WrapAsync(webSocket, requestUri, remoteEndPoint, null, new Dictionary(), null); + /// /// Wraps a WebSocket connection of ASP.NET / ASP.NET Core and acts like a WebSocket server /// @@ -827,10 +842,13 @@ public Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, /// The local endpoint of the WebSocket connection /// The string that presents the user agent of the client that made this request to the WebSocket connection /// The string that presents the url referer of the client that made this request to the WebSocket connection + /// The string that presents the headers of the client that made this request to the WebSocket connection + /// The string that presents the cookies of the client that made this request to the WebSocket connection /// The action to fire when the WebSocket connection is wrap success /// A task that run the receiving process when wrap successful or an exception when failed - public Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, string userAgent, string urlReferer, Action onSuccess) - => this.WrapAsync(webSocket, requestUri, remoteEndPoint, localEndPoint, userAgent, urlReferer, null, null, onSuccess); + [Obsolete] + public Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, string userAgent, string urlReferer, string headers, string cookies, Action onSuccess = null) + => this.WrapAsync(webSocket, requestUri, remoteEndPoint, localEndPoint, (headers ?? "").ToDictionary(), onSuccess); /// /// Wraps a WebSocket connection of ASP.NET / ASP.NET Core and acts like a WebSocket server @@ -838,9 +856,14 @@ public Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, /// The WebSocket connection of ASP.NET / ASP.NET Core /// The original request URI of the WebSocket connection /// The remote endpoint of the WebSocket connection + /// The local endpoint of the WebSocket connection + /// The string that presents the user agent of the client that made this request to the WebSocket connection + /// The string that presents the url referer of the client that made this request to the WebSocket connection + /// The action to fire when the WebSocket connection is wrap success /// A task that run the receiving process when wrap successful or an exception when failed - public Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, EndPoint remoteEndPoint) - => this.WrapAsync(webSocket, requestUri, remoteEndPoint, null, null, null, null, null, null); + [Obsolete] + public Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, string userAgent, string urlReferer, Action onSuccess) + => this.WrapAsync(webSocket, requestUri, remoteEndPoint, localEndPoint, new Dictionary(), onSuccess); #endregion #region Receive messages @@ -875,21 +898,27 @@ async Task ReceiveAsync(ManagedWebSocket websocket) } catch (Exception e) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); } if (ex is OperationCanceledException || ex is TaskCanceledException || ex is ObjectDisposedException || ex is WebSocketException || ex is SocketException || ex is IOException) - this._logger.Log(LogLevel.Trace, LogLevel.Debug, $"Stop receiving process when got an error: {ex.Message} ({ex.GetType().GetTypeName(true)})"); + { + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"Stop receiving process when got an error: {ex.Message} ({ex.GetType().GetTypeName(true)})"); + } else { - this._logger.Log(LogLevel.Debug, LogLevel.Error, closeStatusDescription, ex); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, closeStatusDescription, ex); try { this.ErrorHandler?.Invoke(websocket, ex); } catch (Exception e) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); } } return; @@ -898,7 +927,8 @@ async Task ReceiveAsync(ManagedWebSocket websocket) // message to close if (result.MessageType == WebSocketMessageType.Close) { - this._logger.Log(LogLevel.Trace, LogLevel.Debug, $"The remote endpoint is initiated to close - Status: {result.CloseStatus} - Description: {result.CloseStatusDescription ?? "N/A"} ({websocket.ID} @ {websocket.RemoteEndPoint})"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"The remote endpoint is initiated to close - Status: {result.CloseStatus} - Description: {result.CloseStatusDescription ?? "N/A"} ({websocket.ID} @ {websocket.RemoteEndPoint})"); this.CloseWebSocket(websocket); try { @@ -906,7 +936,8 @@ async Task ReceiveAsync(ManagedWebSocket websocket) } catch (Exception ex) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); } return; } @@ -915,7 +946,8 @@ async Task ReceiveAsync(ManagedWebSocket websocket) if (result.Count > WebSocketHelper.ReceiveBufferSize) { var message = $"WebSocket frame cannot exceed buffer size of {WebSocketHelper.ReceiveBufferSize:#,##0} bytes"; - this._logger.Log(LogLevel.Debug, LogLevel.Debug, $"Close the connection because {message} ({websocket.ID} @ {websocket.RemoteEndPoint})"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"Close the connection because {message} ({websocket.ID} @ {websocket.RemoteEndPoint})"); await websocket.CloseAsync(WebSocketCloseStatus.MessageTooBig, $"{message}, send multiple frames instead.", CancellationToken.None).ConfigureAwait(false); this.CloseWebSocket(websocket); @@ -926,7 +958,8 @@ async Task ReceiveAsync(ManagedWebSocket websocket) } catch (Exception ex) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); } return; } @@ -934,14 +967,16 @@ async Task ReceiveAsync(ManagedWebSocket websocket) // got a message if (result.Count > 0) { - this._logger.Log(LogLevel.Trace, LogLevel.Debug, $"A message was received - Type: {result.MessageType} - End of message: {result.EndOfMessage} - Length: {result.Count:#,##0} ({websocket.ID} @ {websocket.RemoteEndPoint})"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"A message was received - Type: {result.MessageType} - End of message: {result.EndOfMessage} - Length: {result.Count:#,##0} ({websocket.ID} @ {websocket.RemoteEndPoint})"); try { this.MessageReceivedHandler?.Invoke(websocket, result, buffer.Take(result.Count).ToArray()); } catch (Exception ex) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); } } @@ -960,7 +995,8 @@ async Task ReceiveAsync(ManagedWebSocket websocket) } catch (Exception ex) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); } return; } @@ -1000,7 +1036,8 @@ async Task ReceiveAsync(ManagedWebSocket websocket) } catch (Exception e) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); } } } @@ -1031,7 +1068,8 @@ async Task ReceiveAsync(ManagedWebSocket websocket) } catch (Exception e) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); } } } @@ -1062,7 +1100,8 @@ async Task ReceiveAsync(ManagedWebSocket websocket) } catch (Exception e) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); } } } @@ -1091,7 +1130,8 @@ async Task ReceiveAsync(ManagedWebSocket websocket) } catch (Exception e) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); } } }, cancellationToken); @@ -1286,6 +1326,14 @@ public abstract class ManagedWebSocket : System.Net.WebSockets.WebSocket /// public Dictionary Extra { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + /// + /// Gets the header information of the WebSocket connection + /// + public Dictionary Headers + { + get => this.Extra.TryGetValue("Headers", out object headers) && headers is Dictionary ? headers as Dictionary : new Dictionary(); + } + /// /// Gets the state to include the full exception (with stack trace) in the close response when an exception is encountered and the WebSocket connection is closed /// @@ -1364,7 +1412,7 @@ await Task.WhenAll( /// Cleans up unmanaged resources (will send a close frame if the connection is still open) /// public override void Dispose() - => this.DisposeAsync().Wait(4321); + => this.DisposeAsync().GetAwaiter().GetResult(); internal virtual async Task DisposeAsync(WebSocketCloseStatus closeStatus = WebSocketCloseStatus.EndpointUnavailable, string closeStatusDescription = "Service is unavailable", CancellationToken cancellationToken = default(CancellationToken), Action onDisposed = null) { @@ -1372,8 +1420,7 @@ public override void Dispose() { this._disposing = true; Events.Log.WebSocketDispose(this.ID, this.State); - if (this.State == WebSocketState.Open) - await this.CloseOutputTimeoutAsync(closeStatus, closeStatusDescription, null, () => Events.Log.WebSocketDisposeCloseTimeout(this.ID, this.State), ex => Events.Log.WebSocketDisposeError(this.ID, this.State, ex.ToString())).ConfigureAwait(false); + await Task.WhenAll(this.State == WebSocketState.Open ? this.CloseOutputTimeoutAsync(closeStatus, closeStatusDescription, null, () => Events.Log.WebSocketDisposeCloseTimeout(this.ID, this.State), ex => Events.Log.WebSocketDisposeError(this.ID, this.State, ex.ToString())) : Task.CompletedTask).ConfigureAwait(false); try { onDisposed?.Invoke(); diff --git a/WebSocketHelper.cs b/WebSocketHelper.cs index f663cce..b9126f4 100644 --- a/WebSocketHelper.cs +++ b/WebSocketHelper.cs @@ -74,35 +74,25 @@ public static Func GetRecyclableMemoryStreamFactory() public static Task WriteHeaderAsync(this Stream stream, string header, CancellationToken cancellationToken = default(CancellationToken)) => stream.WriteAsync((header.Trim() + "\r\n\r\n").ToArraySegment(), cancellationToken); - /// - /// Computes a WebSocket accept key from a given key - /// - /// The WebSocket request key - /// A WebSocket accept key - public static string ComputeAcceptKey(this string key) + internal static string ComputeAcceptKey(this string key) => (key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").GetHash("SHA1").ToBase64(); - /// - /// Negotiates sub-protocol - /// - /// - /// - /// - public static string NegotiateSubProtocol(this IEnumerable requestedSubProtocols, IEnumerable supportedSubProtocols) + internal static string NegotiateSubProtocol(this IEnumerable requestedSubProtocols, IEnumerable supportedSubProtocols) => requestedSubProtocols == null || supportedSubProtocols == null || !requestedSubProtocols.Any() || !supportedSubProtocols.Any() ? null : requestedSubProtocols.Intersect(supportedSubProtocols).FirstOrDefault() ?? throw new SubProtocolNegotiationFailedException("Unable to negotiate a sub-protocol"); - /// - /// Set keep-alive interval to something more reasonable (because the TCP keep-alive default values of Windows are huge ~7200s) - /// - /// - /// - /// - public static void SetKeepAliveInterval(this Socket socket, uint keepaliveInterval = 60000, uint retryInterval = 10000) + internal static void SetKeepAliveInterval(this Socket socket, uint keepaliveInterval = 60000, uint retryInterval = 10000) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) socket.IOControl(IOControlCode.KeepAliveValues, ((uint)1).ToBytes().Concat(keepaliveInterval.ToBytes(), retryInterval.ToBytes()), null); } + + internal static Dictionary ToDictionary(this string @string) + => string.IsNullOrWhiteSpace(@string) + ? new Dictionary(StringComparer.OrdinalIgnoreCase) + : @string.Replace("\r", "").ToList("\n") + .Where(header => header.IndexOf(":") > 0) + .ToDictionary(header => header.Left(header.IndexOf(":")).Trim(), header => header.Right(header.Length - header.IndexOf(":") - 1).Trim(), StringComparer.OrdinalIgnoreCase); } } \ No newline at end of file diff --git a/WebSocketImplementation.cs b/WebSocketImplementation.cs index 8913cb8..cda357e 100644 --- a/WebSocketImplementation.cs +++ b/WebSocketImplementation.cs @@ -6,6 +6,7 @@ using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; +using System.Collections.Generic; using System.Collections.Concurrent; using Microsoft.Extensions.Logging; using net.vieapps.Components.WebSockets.Exceptions; @@ -60,7 +61,7 @@ internal class WebSocketImplementation : ManagedWebSocket protected override bool IncludeExceptionInCloseResponse { get; } #endregion - public WebSocketImplementation(Guid id, bool isClient, Func recycledStreamFactory, Stream stream, WebSocketOptions options, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint) + public WebSocketImplementation(Guid id, bool isClient, Func recycledStreamFactory, Stream stream, WebSocketOptions options, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, Dictionary headers) { if (options.KeepAliveInterval.Ticks < 0) throw new ArgumentException("KeepAliveInterval must be Zero or positive", nameof(options)); @@ -72,6 +73,7 @@ public WebSocketImplementation(Guid id, bool isClient, Func recycl this.RequestUri = requestUri; this.RemoteEndPoint = remoteEndPoint; this.LocalEndPoint = localEndPoint; + this.Extra["Headers"] = headers ?? new Dictionary(StringComparer.OrdinalIgnoreCase); this._recycledStreamFactory = recycledStreamFactory ?? WebSocketHelper.GetRecyclableMemoryStreamFactory(); this._stream = stream; @@ -93,6 +95,10 @@ public WebSocketImplementation(Guid id, bool isClient, Func recycl /// async Task PutOnTheWireAsync(MemoryStream stream, CancellationToken cancellationToken) { + // check disposed + if (this._disposed) + throw new ObjectDisposedException("WebSocketImplementation"); + // add into queue and check pending operations this._buffers.Enqueue(stream.ToArraySegment()); if (this._pending) @@ -102,10 +108,6 @@ async Task PutOnTheWireAsync(MemoryStream stream, CancellationToken cancellation return; } - // check disposed - if (this._disposing || this._disposed) - throw new ObjectDisposedException("WebSocketImplementation"); - // put data to wire this._pending = true; await this._lock.WaitAsync(cancellationToken).ConfigureAwait(false); diff --git a/WebSocketWrapper.cs b/WebSocketWrapper.cs index 1df1327..0d1ab58 100644 --- a/WebSocketWrapper.cs +++ b/WebSocketWrapper.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using System.Collections.Concurrent; +using System.Collections.Generic; using Microsoft.Extensions.Logging; using net.vieapps.Components.Utility; #endregion @@ -46,13 +47,14 @@ internal class WebSocketWrapper : ManagedWebSocket protected override bool IncludeExceptionInCloseResponse { get; } = false; #endregion - public WebSocketWrapper(System.Net.WebSockets.WebSocket websocket, Uri requestUri, EndPoint remoteEndPoint = null, EndPoint localEndPoint = null) + public WebSocketWrapper(System.Net.WebSockets.WebSocket websocket, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, Dictionary headers) { this._websocket = websocket; this.ID = Guid.NewGuid(); this.RequestUri = requestUri; this.RemoteEndPoint = remoteEndPoint; this.LocalEndPoint = localEndPoint; + this.Extra["Headers"] = headers ?? new Dictionary(StringComparer.OrdinalIgnoreCase); } /// @@ -74,6 +76,10 @@ public override Task ReceiveAsync(ArraySegment buf /// public override async Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) { + // check disposed + if (this._disposed) + throw new ObjectDisposedException("WebSocketWrapper"); + // add into queue and check pending operations this._buffers.Enqueue(new Tuple, WebSocketMessageType, bool>(buffer, messageType, endOfMessage)); if (this._pending) @@ -83,10 +89,6 @@ public override async Task SendAsync(ArraySegment buffer, WebSocketMessage return; } - // check disposed - if (this._disposing || this._disposed) - throw new ObjectDisposedException("WebSocketImplementation"); - // put data to wire this._pending = true; await this._lock.WaitAsync(cancellationToken).ConfigureAwait(false); From a0da85fca7e089aa8921a360062b789372efb2f4 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Tue, 26 Mar 2019 09:27:15 +0700 Subject: [PATCH 06/34] Improvement: add support of dual IP modes (v4 & v6) --- VIEApps.Components.WebSockets.csproj | 2 +- WebSocket.cs | 45 +++++++++++----------------- WebSocketHelper.cs | 12 +++++++- 3 files changed, 30 insertions(+), 29 deletions(-) diff --git a/VIEApps.Components.WebSockets.csproj b/VIEApps.Components.WebSockets.csproj index 7f39d76..54f18d7 100644 --- a/VIEApps.Components.WebSockets.csproj +++ b/VIEApps.Components.WebSockets.csproj @@ -11,7 +11,7 @@ ../VIEApps.Components.snk 10.2.1903.3 10.2.1903.3 - v10.2.netstandard-2+rev:2019.03.15-disposed.objects.headers + v10.2.netstandard-2+rev:2019.03.25-dual.IP.modes VIEApps NGX VIEApps.net false diff --git a/WebSocket.cs b/WebSocket.cs index e966745..fa59a4e 100644 --- a/WebSocket.cs +++ b/WebSocket.cs @@ -39,11 +39,6 @@ public class WebSocket : IDisposable TcpListener _tcpListener = null; bool _disposing = false, _disposed = false; - /// - /// Gets the listening port of the listener - /// - public int Port { get; private set; } = 46429; - /// /// Gets or sets the SSL certificate for securing connections /// @@ -203,17 +198,16 @@ public void StartListen(int port = 46429, X509Certificate2 certificate = null, A return; } - // listen + // set X.509 certificate + this.Certificate = certificate ?? this.Certificate; + + // open the listener and listen for incoming requests try { // open the listener - this.Port = port > IPEndPoint.MinPort && port < IPEndPoint.MaxPort ? port : 46429; - this.Certificate = certificate ?? this.Certificate; - - this._tcpListener = new TcpListener(IPAddress.Any, this.Port); - this._tcpListener.Server.NoDelay = this.NoDelay; - this._tcpListener.Server.SetKeepAliveInterval(); - this._tcpListener.Start(1024); + this._tcpListener = new TcpListener(IPAddress.IPv6Any, port > IPEndPoint.MinPort && port < IPEndPoint.MaxPort ? port : 46429); + this._tcpListener.Server.SetOptions(this.NoDelay, true); + this._tcpListener.Start(512); if (this._logger.IsEnabled(LogLevel.Debug)) { @@ -225,9 +219,10 @@ public void StartListen(int port = 46429, X509Certificate2 certificate = null, A platform += $" ({RuntimeInformation.FrameworkDescription.Trim()}) - SSL: {this.Certificate != null}"; if (this.Certificate != null) platform += $" ({this.Certificate.GetNameInfo(X509NameType.DnsName, false)} :: Issued by {this.Certificate.GetNameInfo(X509NameType.DnsName, true)})"; - this._logger.LogInformation($"The listener is started (listening port: {this.Port})\r\nPlatform: {platform}\r\nPowered by {WebSocketHelper.AgentName} v{this.GetType().Assembly.GetVersion()}"); + this._logger.LogInformation($"The listener is started => {this._tcpListener.Server.LocalEndPoint}\r\nPlatform: {platform}\r\nPowered by {WebSocketHelper.AgentName} v{this.GetType().Assembly.GetVersion()}"); } + // callback when success try { onSuccess?.Invoke(); @@ -243,7 +238,7 @@ public void StartListen(int port = 46429, X509Certificate2 certificate = null, A } catch (SocketException ex) { - var message = $"Error occurred while listening on port \"{this.Port}\". Make sure another application is not running and consuming this port."; + var message = $"Error occurred while listening on port \"{(port > IPEndPoint.MinPort && port < IPEndPoint.MaxPort ? port : 46429)}\". Make sure another application is not running and consuming this port."; if (this._logger.IsEnabled(LogLevel.Debug)) this._logger.Log(LogLevel.Error, message, ex); try @@ -325,11 +320,7 @@ async Task ListenAsync() try { while (!this._listeningCTS.IsCancellationRequested) - { - var tcpClient = await this._tcpListener.AcceptTcpClientAsync().WithCancellationToken(this._listeningCTS.Token).ConfigureAwait(false); - tcpClient.Client.SetKeepAliveInterval(); - this.AcceptClient(tcpClient); - } + this.AcceptClient(await this._tcpListener.AcceptTcpClientAsync().WithCancellationToken(this._listeningCTS.Token).ConfigureAwait(false)); } catch (Exception ex) { @@ -349,10 +340,12 @@ async Task AcceptClientAsync(TcpClient tcpClient) ManagedWebSocket websocket = null; try { - var id = Guid.NewGuid(); - var endpoint = tcpClient.Client.RemoteEndPoint; + // set optins + tcpClient.Client.SetOptions(this.NoDelay); // get stream + var id = Guid.NewGuid(); + var endpoint = tcpClient.Client.RemoteEndPoint; Stream stream = null; if (this.Certificate != null) try @@ -551,11 +544,9 @@ async Task ConnectAsync(Uri uri, WebSocketOptions options, Action requestedSu ? null : requestedSubProtocols.Intersect(supportedSubProtocols).FirstOrDefault() ?? throw new SubProtocolNegotiationFailedException("Unable to negotiate a sub-protocol"); - internal static void SetKeepAliveInterval(this Socket socket, uint keepaliveInterval = 60000, uint retryInterval = 10000) + internal static void SetOptions(this Socket socket, bool noDelay = true, bool dualMode = false, uint keepaliveInterval = 60000, uint retryInterval = 10000) { + // general options + socket.NoDelay = noDelay; + if (dualMode) + { + socket.DualMode = true; + socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, false); + socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1); + } + + // specifict options (only avalable when running on Windows) if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) socket.IOControl(IOControlCode.KeepAliveValues, ((uint)1).ToBytes().Concat(keepaliveInterval.ToBytes(), retryInterval.ToBytes()), null); } From 43f7c7fb6a5e6475536e756e9f57c632d63e38ea Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Mon, 1 Apr 2019 22:02:21 +0700 Subject: [PATCH 07/34] Upgrade to latest components to release new nuget version --- VIEApps.Components.WebSockets.csproj | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/VIEApps.Components.WebSockets.csproj b/VIEApps.Components.WebSockets.csproj index 54f18d7..757bc9a 100644 --- a/VIEApps.Components.WebSockets.csproj +++ b/VIEApps.Components.WebSockets.csproj @@ -9,14 +9,14 @@ VIEApps NGX WebSockets false ../VIEApps.Components.snk - 10.2.1903.3 - 10.2.1903.3 - v10.2.netstandard-2+rev:2019.03.25-dual.IP.modes + 10.2.1904.1 + 10.2.1904.1 + v10.2.netstandard-2+rev:2019.04.01-dual.IP.modes VIEApps NGX VIEApps.net false VIEApps.Components.WebSockets - 10.2.1903.3 + 10.2.1904.1 VIEApps NGX WebSockets High performance WebSocket on .NET Standard 2.0 (both server and client - standalone or wrapper of System.Net.WebSockets.WebSocket) VIEApps.net @@ -24,7 +24,7 @@ LICENSE.md ..\ websocket;websockets;websocket-client;websocket-server;websocket-wrapper;vieapps;vieapps.components - Upgrade to latest components + Do some improvement: dual IP modes, locking mechanims, extra headers, ... https://github.com/vieapps/Components.Utility/raw/master/logo.png https://github.com/vieapps/Components.WebSockets https://github.com/vieapps/Components.WebSockets @@ -39,7 +39,7 @@ - + \ No newline at end of file From 08841acf6a86793cabaa21a657f5ddf0f0710be5 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Tue, 9 Apr 2019 03:17:57 +0700 Subject: [PATCH 08/34] Add options to build custom ping/pong payload --- PingPong.cs | 112 ++++++++--------------------- WebSocket.cs | 140 +++++++++++++++++++++++++++---------- WebSocketImplementation.cs | 31 +++----- WebSocketOptions.cs | 33 ++++++--- WebSocketWrapper.cs | 2 +- 5 files changed, 168 insertions(+), 150 deletions(-) diff --git a/PingPong.cs b/PingPong.cs index b707ccb..fc6ac6a 100644 --- a/PingPong.cs +++ b/PingPong.cs @@ -9,86 +9,40 @@ namespace net.vieapps.Components.WebSockets { - /// - /// Pong EventArgs - /// - internal class PongEventArgs : EventArgs - { - /// - /// The data extracted from a Pong WebSocket frame - /// - public ArraySegment Payload { get; } - - /// - /// Initialises a new instance of the PongEventArgs class - /// - /// The pong payload must be 125 bytes or less (can be zero bytes) - public PongEventArgs(ArraySegment payload) => this.Payload = payload; - } - - // -------------------------------------------------- - - /// - /// Ping Pong Manager used to facilitate ping pong WebSocket messages - /// - internal interface IPingPongManager - { - /// - /// Raised when a Pong frame is received - /// - event EventHandler Pong; - - /// - /// Sends a ping frame - /// - /// The payload (must be 125 bytes or less) - /// The cancellation token - Task SendPingAsync(ArraySegment payload, CancellationToken cancellation = default(CancellationToken)); - } - - // -------------------------------------------------- - - /// - /// Ping Pong Manager used to facilitate ping pong WebSocket messages - /// - internal class PingPongManager : IPingPongManager + internal class PingPongManager { readonly WebSocketImplementation _websocket; - readonly Task _pingTask; readonly CancellationToken _cancellationToken; - readonly Stopwatch _stopwatch; - long _pingSentTicks; + readonly Action _onPong; + readonly Func _getPongPayload; + readonly Func _getPingPayload; + long _pingTimestamp = 0; - /// - /// Raised when a Pong frame is received - /// - public event EventHandler Pong; - - /// - /// Initialises a new instance of the PingPongManager to facilitate ping pong WebSocket messages. - /// - /// The WebSocket instance used to listen to ping messages and send pong messages - /// The token used to cancel a pending ping send AND the automatic sending of ping messages if KeepAliveInterval is positive - public PingPongManager(WebSocketImplementation websocket, CancellationToken cancellationToken) + public PingPongManager(WebSocketImplementation websocket, WebSocketOptions options, CancellationToken cancellationToken) { this._websocket = websocket; - this._websocket.Pong += this.DoPong; this._cancellationToken = cancellationToken; - this._stopwatch = Stopwatch.StartNew(); - this._pingTask = Task.Run(this.DoPingAsync); + this._getPongPayload = options.GetPongPayload; + this._onPong = options.OnPong; + if (this._websocket.KeepAliveInterval != TimeSpan.Zero) + { + this._getPingPayload = options.GetPingPayload; + Task.Run(this.SendPingAsync).ConfigureAwait(false); + } + } + + public void OnPong(byte[] pong) + { + this._pingTimestamp = 0; + this._onPong?.Invoke(this._websocket, pong); } - /// - /// Sends a ping frame - /// - /// The payload (must be 125 bytes of less) - /// The cancellation token - public Task SendPingAsync(ArraySegment payload, CancellationToken cancellationToken = default(CancellationToken)) - => this._websocket.SendPingAsync(payload, cancellationToken); + public Task SendPongAsync(byte[] ping) + => this._websocket.SendPongAsync((this._getPongPayload?.Invoke(this._websocket, ping) ?? ping).ToArraySegment(), this._cancellationToken); - async Task DoPingAsync() + public async Task SendPingAsync() { - Events.Log.PingPongManagerStarted(this._websocket.ID, (int)this._websocket.KeepAliveInterval.TotalSeconds); + Events.Log.PingPongManagerStarted(this._websocket.ID, this._websocket.KeepAliveInterval.TotalSeconds.CastAs()); try { while (!this._cancellationToken.IsCancellationRequested) @@ -97,30 +51,22 @@ async Task DoPingAsync() if (this._websocket.State != WebSocketState.Open) break; - if (this._pingSentTicks != 0) + if (this._pingTimestamp != 0) { Events.Log.KeepAliveIntervalExpired(this._websocket.ID, (int)this._websocket.KeepAliveInterval.TotalSeconds); - await this._websocket.CloseAsync(WebSocketCloseStatus.NormalClosure, $"No Pong message received in response to a Ping after KeepAliveInterval ({this._websocket.KeepAliveInterval})", this._cancellationToken).ConfigureAwait(false); + await this._websocket.CloseAsync(WebSocketCloseStatus.NormalClosure, $"No PONG message received in response to a PING message after keep-alive-interval ({this._websocket.KeepAliveInterval})", this._cancellationToken).ConfigureAwait(false); break; } - this._pingSentTicks = this._stopwatch.Elapsed.Ticks; - await this.SendPingAsync(this._pingSentTicks.ToArraySegment(), this._cancellationToken).ConfigureAwait(false); + this._pingTimestamp = DateTime.Now.ToUnixTimestamp(); + await this._websocket.SendPingAsync((this._getPingPayload?.Invoke(this._websocket) ?? this._pingTimestamp.ToBytes()).ToArraySegment(), this._cancellationToken).ConfigureAwait(false); } } - catch (OperationCanceledException) + catch (Exception) { - // normal, do nothing + // do nothing } Events.Log.PingPongManagerEnded(this._websocket.ID); } - - protected virtual void OnPong(PongEventArgs args) => this.Pong?.Invoke(this, args); - - void DoPong(object sender, PongEventArgs arg) - { - this._pingSentTicks = 0; - this.OnPong(arg); - } } } \ No newline at end of file diff --git a/WebSocket.cs b/WebSocket.cs index fa59a4e..6ee6276 100644 --- a/WebSocket.cs +++ b/WebSocket.cs @@ -40,27 +40,27 @@ public class WebSocket : IDisposable bool _disposing = false, _disposed = false; /// - /// Gets or sets the SSL certificate for securing connections + /// Gets or Sets the SSL certificate for securing connections /// public X509Certificate2 Certificate { get; set; } = null; /// - /// Gets or sets the SSL protocol for securing connections with SSL Certificate + /// Gets or Sets the SSL protocol for securing connections with SSL Certificate /// public SslProtocols SslProtocol { get; set; } = SslProtocols.Tls; /// - /// Gets or sets the collection of supported sub-protocol + /// Gets or Sets the collection of supported sub-protocol /// public IEnumerable SupportedSubProtocols { get; set; } = new string[0]; /// - /// Gets or sets keep-alive interval (seconds) for sending ping messages from server + /// Gets or Sets the keep-alive interval for sending ping messages from server /// public TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(60); /// - /// Gets or sets a value that specifies whether the listener is disable the Nagle algorithm or not (default is true - means disable for better performance) + /// Gets or Sets a value that specifies whether the listener is disable the Nagle algorithm or not (default is true - means disable for better performance) /// /// /// Set to true to send a message immediately with the least amount of latency (typical usage for chat) @@ -71,9 +71,9 @@ public class WebSocket : IDisposable public bool NoDelay { get; set; } = true; /// - /// Gets or sets await interval (miliseconds) while receiving messages + /// Gets or Sets await interval between two rounds of receiving messages /// - public int AwaitInterval { get; set; } = 0; + public TimeSpan ReceivingAwaitInterval { get; set; } = TimeSpan.Zero; #endregion #region Event Handlers @@ -181,7 +181,10 @@ public static string AgentName /// The SSL Certificate to secure connections /// Action to fire when start successful /// Action to fire when failed to start - public void StartListen(int port = 46429, X509Certificate2 certificate = null, Action onSuccess = null, Action onFailure = null) + /// The function to get the custom 'PING' playload to send a 'PING' message + /// The function to get the custom 'PONG' playload to response to a 'PING' message + /// The action to fire when a 'PONG' message has been sent + public void StartListen(int port = 46429, X509Certificate2 certificate = null, Action onSuccess = null, Action onFailure = null, Func getPingPayload = null, Func getPongPayload = null, Action onPong = null) { // check if (this._tcpListener != null) @@ -234,7 +237,7 @@ public void StartListen(int port = 46429, X509Certificate2 certificate = null, A } // listen for incoming connection requests - this.Listen(); + this.Listen(getPingPayload, getPongPayload, onPong); } catch (SocketException ex) { @@ -267,6 +270,18 @@ public void StartListen(int port = 46429, X509Certificate2 certificate = null, A } } + /// + /// Starts to listen for client requests as a WebSocket server + /// + /// The port for listening + /// Action to fire when start successful + /// Action to fire when failed to start + /// The function to get the custom 'PING' playload to send a 'PING' message + /// The function to get the custom 'PONG' playload to response to a 'PING' message + /// The action to fire when a 'PONG' message has been sent + public void StartListen(int port, Action onSuccess, Action onFailure, Func getPingPayload, Func getPongPayload, Action onPong) + => this.StartListen(port, null, onSuccess, onFailure, getPingPayload, getPongPayload, onPong); + /// /// Starts to listen for client requests as a WebSocket server /// @@ -274,14 +289,24 @@ public void StartListen(int port = 46429, X509Certificate2 certificate = null, A /// Action to fire when start successful /// Action to fire when failed to start public void StartListen(int port, Action onSuccess, Action onFailure) - => this.StartListen(port, null, onSuccess, onFailure); + => this.StartListen(port, onSuccess, onFailure, null, null, null); + + /// + /// Starts to listen for client requests as a WebSocket server + /// + /// The port for listening + /// The function to get the custom 'PING' playload to send a 'PING' message + /// The function to get the custom 'PONG' playload to response to a 'PING' message + /// The action to fire when a 'PONG' message has been sent + public void StartListen(int port, Func getPingPayload, Func getPongPayload, Action onPong) + => this.StartListen(port, null, null, getPingPayload, getPongPayload, onPong); /// /// Starts to listen for client requests as a WebSocket server /// /// The port for listening public void StartListen(int port) - => this.StartListen(port, null, null); + => this.StartListen(port, null, null, null); /// /// Stops listen @@ -309,18 +334,18 @@ public void StopListen(bool cancelPendings = true) } } - Task Listen() + Task Listen(Func getPingPayload, Func getPongPayload, Action onPong) { this._listeningCTS = CancellationTokenSource.CreateLinkedTokenSource(this._processingCTS.Token); - return this.ListenAsync(); + return this.ListenAsync(getPingPayload, getPongPayload, onPong); } - async Task ListenAsync() + async Task ListenAsync(Func getPingPayload, Func getPongPayload, Action onPong) { try { while (!this._listeningCTS.IsCancellationRequested) - this.AcceptClient(await this._tcpListener.AcceptTcpClientAsync().WithCancellationToken(this._listeningCTS.Token).ConfigureAwait(false)); + this.AcceptClient(await this._tcpListener.AcceptTcpClientAsync().WithCancellationToken(this._listeningCTS.Token).ConfigureAwait(false), getPingPayload, getPongPayload, onPong); } catch (Exception ex) { @@ -332,10 +357,10 @@ async Task ListenAsync() } } - void AcceptClient(TcpClient tcpClient) - => Task.Run(() => this.AcceptClientAsync(tcpClient)).ConfigureAwait(false); + void AcceptClient(TcpClient tcpClient, Func getPingPayload, Func getPongPayload, Action onPong) + => Task.Run(() => this.AcceptClientAsync(tcpClient, getPingPayload, getPongPayload, onPong)).ConfigureAwait(false); - async Task AcceptClientAsync(TcpClient tcpClient) + async Task AcceptClientAsync(TcpClient tcpClient, Func getPingPayload, Func getPongPayload, Action onPong) { ManagedWebSocket websocket = null; try @@ -415,14 +440,18 @@ async Task AcceptClientAsync(TcpClient tcpClient) } // accept the request - var options = new WebSocketOptions - { - KeepAliveInterval = this.KeepAliveInterval - }; Events.Log.AcceptWebSocketStarted(id); if (this._logger.IsEnabled(LogLevel.Trace)) this._logger.Log(LogLevel.Debug, $"The request has requested an upgrade to WebSocket protocol, negotiating WebSocket handshake ({id} @ {endpoint})"); + var options = new WebSocketOptions + { + KeepAliveInterval = this.KeepAliveInterval.Ticks < 0 ? TimeSpan.FromSeconds(60) : this.KeepAliveInterval, + GetPingPayload = getPingPayload, + GetPongPayload = getPongPayload, + OnPong = onPong + }; + try { // check the version (support version 13 and above) @@ -441,7 +470,7 @@ async Task AcceptClientAsync(TcpClient tcpClient) // negotiate subprotocol match = new Regex("Sec-WebSocket-Protocol: (.*)").Match(header); options.SubProtocol = match.Success - ? match.Groups[1].Value.Trim().Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries).NegotiateSubProtocol(this.SupportedSubProtocols) + ? match.Groups[1].Value?.Trim().Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries).NegotiateSubProtocol(this.SupportedSubProtocols) : null; // handshake @@ -653,7 +682,7 @@ async Task ConnectAsync(Uri uri, WebSocketOptions options, Action 0) + if (this.ReceivingAwaitInterval.Ticks > 0) try { - await Task.Delay(this.AwaitInterval, this._processingCTS.Token).ConfigureAwait(false); + await Task.Delay(this.ReceivingAwaitInterval, this._processingCTS.Token).ConfigureAwait(false); } catch { @@ -1317,14 +1346,6 @@ public abstract class ManagedWebSocket : System.Net.WebSockets.WebSocket /// public Dictionary Extra { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); - /// - /// Gets the header information of the WebSocket connection - /// - public Dictionary Headers - { - get => this.Extra.TryGetValue("Headers", out object headers) && headers is Dictionary ? headers as Dictionary : new Dictionary(); - } - /// /// Gets the state to include the full exception (with stack trace) in the close response when an exception is encountered and the WebSocket connection is closed /// @@ -1431,5 +1452,54 @@ internal virtual void Close() { } } #endregion + #region Extra information + /// + /// Sets the value of a specified key of the extra information + /// + /// + /// + /// + public void Set(string key, T value) + => this.Extra[key] = value; + + /// + /// Gets the value of a specified key from the extra information + /// + /// + /// + /// + /// + public T Get(string key, T @default = default(T)) + => this.Extra.TryGetValue(key, out object value) && value != null && value is T + ? (T)value + : @default; + + /// + /// Removes the value of a specified key from the extra information + /// + /// + /// + public bool Remove(string key) + => this.Extra.Remove(key); + + /// + /// Removes the value of a specified key from the extra information + /// + /// + /// + /// + /// + public bool Remove(string key, out T value) + { + value = this.Get(key); + return this.Remove(key); + } + + /// + /// Gets the header information of the WebSocket connection + /// + public Dictionary Headers => this.Get("Headers", new Dictionary()); + #endregion + } } \ No newline at end of file diff --git a/WebSocketImplementation.cs b/WebSocketImplementation.cs index cda357e..3db331f 100644 --- a/WebSocketImplementation.cs +++ b/WebSocketImplementation.cs @@ -1,6 +1,7 @@ #region Related components using System; using System.Net; +using System.Linq; using System.IO; using System.IO.Compression; using System.Net.WebSockets; @@ -21,7 +22,7 @@ internal class WebSocketImplementation : ManagedWebSocket #region Properties readonly Func _recycledStreamFactory; readonly Stream _stream; - readonly IPingPongManager _pingpongManager; + readonly PingPongManager _pingpongManager; WebSocketState _state; WebSocketMessageType _continuationFrameMessageType = WebSocketMessageType.Binary; WebSocketCloseStatus? _closeStatus; @@ -33,8 +34,6 @@ internal class WebSocketImplementation : ManagedWebSocket readonly CancellationTokenSource _processingCTS; readonly ConcurrentQueue> _buffers = new ConcurrentQueue>(); - public event EventHandler Pong; - /// /// Gets the state that indicates the reason why the remote endpoint initiated the close handshake /// @@ -73,18 +72,14 @@ public WebSocketImplementation(Guid id, bool isClient, Func recycl this.RequestUri = requestUri; this.RemoteEndPoint = remoteEndPoint; this.LocalEndPoint = localEndPoint; - this.Extra["Headers"] = headers ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + this.Set("Headers", headers); this._recycledStreamFactory = recycledStreamFactory ?? WebSocketHelper.GetRecyclableMemoryStreamFactory(); this._stream = stream; this._state = WebSocketState.Open; this._subProtocol = options.SubProtocol; this._processingCTS = new CancellationTokenSource(); - - if (this.KeepAliveInterval == TimeSpan.Zero) - Events.Log.KeepAliveIntervalZero(this.ID); - else - this._pingpongManager = new PingPongManager(this, this._processingCTS.Token); + this._pingpongManager = new PingPongManager(this, options, this._processingCTS.Token); } /// @@ -183,11 +178,11 @@ public override async Task ReceiveAsync(ArraySegment(buffer.Array, buffer.Offset, frame.Count), cts.Token).ConfigureAwait(false); + await this._pingpongManager.SendPongAsync(buffer.Take(frame.Count).ToArray()).ConfigureAwait(false); break; case WebSocketOpCode.Pong: - this.Pong?.Invoke(this, new PongEventArgs(new ArraySegment(buffer.Array, frame.Count, buffer.Offset))); + this._pingpongManager.OnPong(buffer.Take(frame.Count).ToArray()); break; case WebSocketOpCode.Text: @@ -260,24 +255,18 @@ async Task RespondToCloseFrameAsync(WebSocketFrame frame return new WebSocketReceiveResult(frame.Count, WebSocketMessageType.Close, frame.IsFinBitSet, frame.CloseStatus, frame.CloseStatusDescription); } - /// - /// Called when a Pong frame is received - /// - /// - protected virtual void OnPong(PongEventArgs args) => this.Pong?.Invoke(this, args); - /// /// Calls this when got ping messages (pong payload must be 125 bytes or less, pong should contain the same payload as the ping) /// /// /// /// - async Task SendPongAsync(ArraySegment payload, CancellationToken cancellationToken) + public async Task SendPongAsync(ArraySegment payload, CancellationToken cancellationToken) { // exceeded max length if (payload.Count > 125) { - var ex = new BufferOverflowException($"Max pong message size is 125 bytes, exceeded: {payload.Count}"); + var ex = new BufferOverflowException($"Max PONG message size is 125 bytes, exceeded: {payload.Count}"); await this.CloseOutputTimeoutAsync(WebSocketCloseStatus.ProtocolError, ex.Message, ex).ConfigureAwait(false); throw ex; } @@ -294,7 +283,7 @@ async Task SendPongAsync(ArraySegment payload, CancellationToken cancellat } catch (Exception ex) { - await this.CloseOutputTimeoutAsync(WebSocketCloseStatus.EndpointUnavailable, "Unable to send Pong response", ex).ConfigureAwait(false); + await this.CloseOutputTimeoutAsync(WebSocketCloseStatus.EndpointUnavailable, "Unable to send PONG response", ex).ConfigureAwait(false); throw; } } @@ -308,7 +297,7 @@ async Task SendPongAsync(ArraySegment payload, CancellationToken cancellat public async Task SendPingAsync(ArraySegment payload, CancellationToken cancellationToken) { if (payload.Count > 125) - throw new BufferOverflowException($"Max ping message size is 125 bytes, exceeded: {payload.Count}"); + throw new BufferOverflowException($"Max PING message size is 125 bytes, exceeded: {payload.Count}"); if (this._state == WebSocketState.Open) using (var stream = this._recycledStreamFactory()) diff --git a/WebSocketOptions.cs b/WebSocketOptions.cs index 9633d70..8c72eff 100644 --- a/WebSocketOptions.cs +++ b/WebSocketOptions.cs @@ -12,26 +12,24 @@ public class WebSocketOptions /// Gets or sets how often to send ping requests to the remote endpoint /// /// - /// This is done to prevent proxy servers from closing your connection - /// The default is TimeSpan.Zero meaning that it is disabled. + /// This is done to prevent proxy servers from closing your connection, the default is TimeSpan.Zero meaning that it is disabled. /// WebSocket servers usually send ping messages so it is not normally necessary for the client to send them (hence the TimeSpan.Zero default) - /// You can manually control ping pong messages using the PingPongManager class. - /// If you do that it is advisible to set this KeepAliveInterval to zero + /// You can manually control ping pong messages using the PingPongManager class. If you do that it is advisible to set this KeepAliveInterval to zero. /// public TimeSpan KeepAliveInterval { get; set; } = TimeSpan.Zero; /// - /// Gets or sets the sub-protocol (Sec-WebSocket-Protocol) + /// Gets or Sets the sub-protocol (Sec-WebSocket-Protocol) /// public string SubProtocol { get; set; } /// - /// Gets or sets the extensions (Sec-WebSocket-Extensions) + /// Gets or Sets the extensions (Sec-WebSocket-Extensions) /// public string Extensions { get; set; } /// - /// Gets or sets state to send a message immediately or not + /// Gets or Sets state to send a message immediately or not /// /// /// Set to true to send a message immediately with the least amount of latency (typical usage for chat) @@ -42,12 +40,12 @@ public class WebSocketOptions public bool NoDelay { get; set; } = true; /// - /// Gets or sets the additional headers + /// Gets or Sets the additional headers /// public Dictionary AdditionalHeaders { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); /// - /// Gets or sets the state to include the full exception (with stack trace) in the close response when an exception is encountered and the WebSocket connection is closed + /// Gets or Sets the state to include the full exception (with stack trace) in the close response when an exception is encountered and the WebSocket connection is closed /// /// /// The default is false @@ -55,11 +53,26 @@ public class WebSocketOptions public bool IncludeExceptionInCloseResponse { get; set; } = false; /// - /// Gets or sets whether remote certificate errors should be ignored + /// Gets or Sets whether remote certificate errors should be ignored /// /// /// The default is false /// public bool IgnoreCertificateErrors { get; set; } = false; + + /// + /// Gets or Sets the function to prepare the custom 'PING' playload to send a 'PING' message + /// + public Func GetPingPayload { get; set; } + + /// + /// Gets or Sets the function to prepare the custom 'PONG' playload to response to a 'PING' message + /// + public Func GetPongPayload { get; set; } + + /// + /// Gets or Sets the action to fire when a 'PONG' message has been sent + /// + public Action OnPong { get; set; } } } \ No newline at end of file diff --git a/WebSocketWrapper.cs b/WebSocketWrapper.cs index 0d1ab58..7c9792c 100644 --- a/WebSocketWrapper.cs +++ b/WebSocketWrapper.cs @@ -54,7 +54,7 @@ public WebSocketWrapper(System.Net.WebSockets.WebSocket websocket, Uri requestUr this.RequestUri = requestUri; this.RemoteEndPoint = remoteEndPoint; this.LocalEndPoint = localEndPoint; - this.Extra["Headers"] = headers ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + this.Set("Headers", headers); } /// From 52b1cc9895297c1f09c3f52a61949e89bdcfefb5 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Tue, 9 Apr 2019 09:55:05 +0700 Subject: [PATCH 09/34] Add options to build custom ping/pong payload --- PingPong.cs | 2 +- WebSocket.cs | 24 ++++++++++++++++-------- WebSocketImplementation.cs | 27 ++++++++++++--------------- 3 files changed, 29 insertions(+), 24 deletions(-) diff --git a/PingPong.cs b/PingPong.cs index fc6ac6a..2ba1bbb 100644 --- a/PingPong.cs +++ b/PingPong.cs @@ -54,7 +54,7 @@ public async Task SendPingAsync() if (this._pingTimestamp != 0) { Events.Log.KeepAliveIntervalExpired(this._websocket.ID, (int)this._websocket.KeepAliveInterval.TotalSeconds); - await this._websocket.CloseAsync(WebSocketCloseStatus.NormalClosure, $"No PONG message received in response to a PING message after keep-alive-interval ({this._websocket.KeepAliveInterval})", this._cancellationToken).ConfigureAwait(false); + await this._websocket.CloseAsync(WebSocketCloseStatus.NormalClosure, $"No PONG message received in response to a PING message after keep-alive interval ({this._websocket.KeepAliveInterval})", this._cancellationToken).ConfigureAwait(false); break; } diff --git a/WebSocket.cs b/WebSocket.cs index 6ee6276..587b2e8 100644 --- a/WebSocket.cs +++ b/WebSocket.cs @@ -40,22 +40,22 @@ public class WebSocket : IDisposable bool _disposing = false, _disposed = false; /// - /// Gets or Sets the SSL certificate for securing connections + /// Gets or Sets the SSL certificate for securing connections (server) /// - public X509Certificate2 Certificate { get; set; } = null; + public X509Certificate2 Certificate { get; set; } /// - /// Gets or Sets the SSL protocol for securing connections with SSL Certificate + /// Gets or Sets the SSL protocol for securing connections with SSL Certificate (server) /// public SslProtocols SslProtocol { get; set; } = SslProtocols.Tls; /// - /// Gets or Sets the collection of supported sub-protocol + /// Gets or Sets the collection of supported sub-protocol (server) /// public IEnumerable SupportedSubProtocols { get; set; } = new string[0]; /// - /// Gets or Sets the keep-alive interval for sending ping messages from server + /// Gets or Sets the keep-alive interval for sending ping messages (server) /// public TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(60); @@ -134,12 +134,20 @@ public Action OnMessageReceive } #endregion + /// + /// Creates new an instance of the centralized WebSocket + /// + /// The cancellation token + public WebSocket(CancellationToken cancellationToken) + : this(null, cancellationToken) { } + /// /// Creates new an instance of the centralized WebSocket /// /// The logger factory /// The cancellation token - public WebSocket(ILoggerFactory loggerFactory, CancellationToken cancellationToken) : this(loggerFactory, null, cancellationToken) { } + public WebSocket(ILoggerFactory loggerFactory, CancellationToken cancellationToken) + : this(loggerFactory, null, cancellationToken) { } /// /// Creates new an instance of the centralized WebSocket @@ -799,7 +807,7 @@ public void Connect(string location, Action onSuccess, Action< => this.Connect(location, null, onSuccess, onFailure); #endregion - #region Wrap a WebSocket connection of ASP.NET / ASP.NET Core + #region Wrap a WebSocket connection /// /// Wraps a WebSocket connection of ASP.NET / ASP.NET Core and acts like a WebSocket server /// @@ -1190,7 +1198,7 @@ async Task AddWebSocketAsync(ManagedWebSocket websocket) if (!this.AddWebSocket(websocket)) { if (websocket != null) - await Task.Delay(UtilityService.GetRandomNumber(123, 456)).ConfigureAwait(false); + await Task.Delay(UtilityService.GetRandomNumber(123, 456), this._processingCTS.Token).ConfigureAwait(false); return this.AddWebSocket(websocket); } return true; diff --git a/WebSocketImplementation.cs b/WebSocketImplementation.cs index 3db331f..db190ae 100644 --- a/WebSocketImplementation.cs +++ b/WebSocketImplementation.cs @@ -16,7 +16,7 @@ namespace net.vieapps.Components.WebSockets { - internal class WebSocketImplementation : ManagedWebSocket + public class WebSocketImplementation : ManagedWebSocket { #region Properties @@ -24,7 +24,7 @@ internal class WebSocketImplementation : ManagedWebSocket readonly Stream _stream; readonly PingPongManager _pingpongManager; WebSocketState _state; - WebSocketMessageType _continuationFrameMessageType = WebSocketMessageType.Binary; + WebSocketMessageType _continuationMessageType = WebSocketMessageType.Binary; WebSocketCloseStatus? _closeStatus; string _closeStatusDescription; bool _isContinuationFrame = false; @@ -60,15 +60,12 @@ internal class WebSocketImplementation : ManagedWebSocket protected override bool IncludeExceptionInCloseResponse { get; } #endregion - public WebSocketImplementation(Guid id, bool isClient, Func recycledStreamFactory, Stream stream, WebSocketOptions options, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, Dictionary headers) + internal WebSocketImplementation(Guid id, bool isClient, Func recycledStreamFactory, Stream stream, WebSocketOptions options, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, Dictionary headers) { - if (options.KeepAliveInterval.Ticks < 0) - throw new ArgumentException("KeepAliveInterval must be Zero or positive", nameof(options)); - this.ID = id; this.IsClient = isClient; this.IncludeExceptionInCloseResponse = options.IncludeExceptionInCloseResponse; - this.KeepAliveInterval = options.KeepAliveInterval; + this.KeepAliveInterval = options.KeepAliveInterval.Ticks < 0 ? TimeSpan.FromSeconds(60) : options.KeepAliveInterval; this.RequestUri = requestUri; this.RemoteEndPoint = remoteEndPoint; this.LocalEndPoint = localEndPoint; @@ -148,27 +145,27 @@ public override async Task ReceiveAsync(ArraySegment ReceiveAsync(ArraySegment Date: Thu, 11 Apr 2019 08:49:53 +0700 Subject: [PATCH 10/34] Improvement: custom ping/pong payload --- README.md | 4 ++-- VIEApps.Components.WebSockets.csproj | 12 ++++++------ WebSocketImplementation.cs | 25 +++++++++++++++++-------- WebSocketWrapper.cs | 11 +++++++++-- 4 files changed, 34 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 572fef3..c64f366 100644 --- a/README.md +++ b/README.md @@ -117,8 +117,8 @@ var websocket = new WebSocket And this class has some methods for working on both side of client and server role: ```csharp -void Connect(Uri uri, WebSocketOptions options, Action onSuccess, Action onFailed); -void StartListen(int port, X509Certificate2 certificate, Action onSuccess, Action onFailed); +void Connect(Uri uri, WebSocketOptions options, Action onSuccess, Action onFailure); +void StartListen(int port, X509Certificate2 certificate, Action onSuccess, Action onFailure, Func getPingPayload, Func getPongPayload, Action onPong); void StopListen(); ``` diff --git a/VIEApps.Components.WebSockets.csproj b/VIEApps.Components.WebSockets.csproj index 757bc9a..6d2c844 100644 --- a/VIEApps.Components.WebSockets.csproj +++ b/VIEApps.Components.WebSockets.csproj @@ -9,14 +9,14 @@ VIEApps NGX WebSockets false ../VIEApps.Components.snk - 10.2.1904.1 - 10.2.1904.1 - v10.2.netstandard-2+rev:2019.04.01-dual.IP.modes + 10.2.1904.3 + 10.2.1904.3 + v10.2.netstandard-2+rev:2019.04.11-custom.ping.pong VIEApps NGX VIEApps.net false VIEApps.Components.WebSockets - 10.2.1904.1 + 10.2.1904.3 VIEApps NGX WebSockets High performance WebSocket on .NET Standard 2.0 (both server and client - standalone or wrapper of System.Net.WebSockets.WebSocket) VIEApps.net @@ -24,7 +24,7 @@ LICENSE.md ..\ websocket;websockets;websocket-client;websocket-server;websocket-wrapper;vieapps;vieapps.components - Do some improvement: dual IP modes, locking mechanims, extra headers, ... + Improvement: custom ping/pong payload, dual IP modes, locking mechanims, extra headers, ... https://github.com/vieapps/Components.Utility/raw/master/logo.png https://github.com/vieapps/Components.WebSockets https://github.com/vieapps/Components.WebSockets @@ -39,7 +39,7 @@ - + \ No newline at end of file diff --git a/WebSocketImplementation.cs b/WebSocketImplementation.cs index db190ae..29e4f7a 100644 --- a/WebSocketImplementation.cs +++ b/WebSocketImplementation.cs @@ -16,23 +16,24 @@ namespace net.vieapps.Components.WebSockets { - public class WebSocketImplementation : ManagedWebSocket + internal class WebSocketImplementation : ManagedWebSocket { #region Properties readonly Func _recycledStreamFactory; readonly Stream _stream; readonly PingPongManager _pingpongManager; + readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); + readonly string _subProtocol; + readonly CancellationTokenSource _processingCTS; + readonly ConcurrentQueue> _buffers = new ConcurrentQueue>(); + readonly ILogger _logger; WebSocketState _state; WebSocketMessageType _continuationMessageType = WebSocketMessageType.Binary; WebSocketCloseStatus? _closeStatus; string _closeStatusDescription; bool _isContinuationFrame = false; - readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); bool _pending = false; - readonly string _subProtocol; - readonly CancellationTokenSource _processingCTS; - readonly ConcurrentQueue> _buffers = new ConcurrentQueue>(); /// /// Gets the state that indicates the reason why the remote endpoint initiated the close handshake @@ -60,7 +61,7 @@ public class WebSocketImplementation : ManagedWebSocket protected override bool IncludeExceptionInCloseResponse { get; } #endregion - internal WebSocketImplementation(Guid id, bool isClient, Func recycledStreamFactory, Stream stream, WebSocketOptions options, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, Dictionary headers) + public WebSocketImplementation(Guid id, bool isClient, Func recycledStreamFactory, Stream stream, WebSocketOptions options, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, Dictionary headers) { this.ID = id; this.IsClient = isClient; @@ -77,6 +78,7 @@ internal WebSocketImplementation(Guid id, bool isClient, Func recy this._subProtocol = options.SubProtocol; this._processingCTS = new CancellationTokenSource(); this._pingpongManager = new PingPongManager(this, options, this._processingCTS.Token); + this._logger = Logger.CreateLogger(); } /// @@ -89,14 +91,19 @@ async Task PutOnTheWireAsync(MemoryStream stream, CancellationToken cancellation { // check disposed if (this._disposed) - throw new ObjectDisposedException("WebSocketImplementation"); + { + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.LogWarning($"Object disposed => {this.ID}"); + throw new ObjectDisposedException($"WebSocketImplementation => {this.ID}"); + } // add into queue and check pending operations this._buffers.Enqueue(stream.ToArraySegment()); if (this._pending) { Events.Log.PendingOperations(this.ID); - Logger.Log(LogLevel.Debug, LogLevel.Warning, $"#{Thread.CurrentThread.ManagedThreadId} Pendings => {this._buffers.Count:#,##0} ({this.ID} @ {this.RemoteEndPoint})"); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.LogWarning($"#{Thread.CurrentThread.ManagedThreadId} Pendings => {this._buffers.Count:#,##0} ({this.ID} @ {this.RemoteEndPoint})"); return; } @@ -211,6 +218,8 @@ public override async Task ReceiveAsync(ArraySegment {ex.Message}"); throw ex; } } diff --git a/WebSocketWrapper.cs b/WebSocketWrapper.cs index 7c9792c..3ac143f 100644 --- a/WebSocketWrapper.cs +++ b/WebSocketWrapper.cs @@ -19,6 +19,7 @@ internal class WebSocketWrapper : ManagedWebSocket readonly System.Net.WebSockets.WebSocket _websocket = null; readonly ConcurrentQueue, WebSocketMessageType, bool>> _buffers = new ConcurrentQueue, WebSocketMessageType, bool>>(); readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); + readonly ILogger _logger; bool _pending = false; /// @@ -50,6 +51,7 @@ internal class WebSocketWrapper : ManagedWebSocket public WebSocketWrapper(System.Net.WebSockets.WebSocket websocket, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, Dictionary headers) { this._websocket = websocket; + this._logger = Logger.CreateLogger(); this.ID = Guid.NewGuid(); this.RequestUri = requestUri; this.RemoteEndPoint = remoteEndPoint; @@ -78,14 +80,19 @@ public override async Task SendAsync(ArraySegment buffer, WebSocketMessage { // check disposed if (this._disposed) - throw new ObjectDisposedException("WebSocketWrapper"); + { + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.LogWarning($"Object disposed => {this.ID}"); + throw new ObjectDisposedException($"WebSocketWrapper => {this.ID}"); + } // add into queue and check pending operations this._buffers.Enqueue(new Tuple, WebSocketMessageType, bool>(buffer, messageType, endOfMessage)); if (this._pending) { Events.Log.PendingOperations(this.ID); - Logger.Log(LogLevel.Debug, LogLevel.Warning, $"#{Thread.CurrentThread.ManagedThreadId} Pendings => {this._buffers.Count:#,##0} ({this.ID} @ {this.RemoteEndPoint})"); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.LogWarning($"#{Thread.CurrentThread.ManagedThreadId} Pendings => {this._buffers.Count:#,##0} ({this.ID} @ {this.RemoteEndPoint})"); return; } From 0d587991c5ee8edad945d26e652af5a0ccf8d4fa Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Wed, 1 May 2019 07:21:13 +0700 Subject: [PATCH 11/34] Upgrade to latest components --- PingPong.cs | 5 +---- README.md | 9 +++++--- VIEApps.Components.WebSockets.csproj | 12 +++++------ WebSocket.cs | 32 ---------------------------- 4 files changed, 13 insertions(+), 45 deletions(-) diff --git a/PingPong.cs b/PingPong.cs index 2ba1bbb..1364dd4 100644 --- a/PingPong.cs +++ b/PingPong.cs @@ -62,10 +62,7 @@ public async Task SendPingAsync() await this._websocket.SendPingAsync((this._getPingPayload?.Invoke(this._websocket) ?? this._pingTimestamp.ToBytes()).ToArraySegment(), this._cancellationToken).ConfigureAwait(false); } } - catch (Exception) - { - // do nothing - } + catch { } Events.Log.PingPongManagerEnded(this._websocket.ID); } } diff --git a/README.md b/README.md index c64f366..3bb5741 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,9 @@ public EndPoint LocalEndPoint { get; } // Extra information public Dictionary Extra { get; } + +// Headers information +public Dictionary Headers { get; } ``` ## Fly on the sky with Event-liked driven @@ -135,7 +138,7 @@ Use the **StopListen** method to stop the listener. ### WebSocket server with Secure WebSockets (wss://) Enabling secure connections requires two things: -- Pointing certificate to an X.509 certificate that containing a public and private key. +- Pointing certificate to an x509 certificate that containing a public and private key. - Using the scheme **wss** instead of **ws** (or **https** instead of **http**) on all clients ```csharp @@ -185,7 +188,7 @@ When integrate this component with your app that hosted by ASP.NET / ASP.NET Cor then the method **WrapAsync** is here to help. This method will return a task that run a process for receiving messages from this WebSocket connection. ```csharp -Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, string userAgent, string urlReferrer, string headers, string cookies, Action onSuccess); +Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, Dictionary headers, Action onSuccess); ``` And might be you need an extension method to wrap an existing WebSocket connection, then take a look at some lines of code below: @@ -316,7 +319,7 @@ bool CloseWebSocket(ManagedWebSocket websocket, WebSocketCloseStatus closeStatus Our prefers: - [Microsoft.Extensions.Logging.Console](https://www.nuget.org/packages/Microsoft.Extensions.Logging.Console): live logs -- [Serilog.Extensions.Logging.File](https://www.nuget.org/packages/Serilog.Extensions.Logging.File): rolling log files (by date) - high performance, and very simple to use +- [Serilog.Extensions.Logging.File](https://www.nuget.org/packages/Serilog.Extensions.Logging.File): rolling log files (by hour or date) - high performance, and very simple to use ### Namespaces diff --git a/VIEApps.Components.WebSockets.csproj b/VIEApps.Components.WebSockets.csproj index 6d2c844..9d18376 100644 --- a/VIEApps.Components.WebSockets.csproj +++ b/VIEApps.Components.WebSockets.csproj @@ -9,14 +9,14 @@ VIEApps NGX WebSockets false ../VIEApps.Components.snk - 10.2.1904.3 - 10.2.1904.3 - v10.2.netstandard-2+rev:2019.04.11-custom.ping.pong + 10.2.1905.1 + 10.2.1905.1 + v10.2.netstandard-2+rev:2019.05.01-latest.components VIEApps NGX VIEApps.net false VIEApps.Components.WebSockets - 10.2.1904.3 + 10.2.1905.1 VIEApps NGX WebSockets High performance WebSocket on .NET Standard 2.0 (both server and client - standalone or wrapper of System.Net.WebSockets.WebSocket) VIEApps.net @@ -24,7 +24,7 @@ LICENSE.md ..\ websocket;websockets;websocket-client;websocket-server;websocket-wrapper;vieapps;vieapps.components - Improvement: custom ping/pong payload, dual IP modes, locking mechanims, extra headers, ... + Upgrade to latest components https://github.com/vieapps/Components.Utility/raw/master/logo.png https://github.com/vieapps/Components.WebSockets https://github.com/vieapps/Components.WebSockets @@ -39,7 +39,7 @@ - + \ No newline at end of file diff --git a/WebSocket.cs b/WebSocket.cs index 587b2e8..e46d6e4 100644 --- a/WebSocket.cs +++ b/WebSocket.cs @@ -860,38 +860,6 @@ public Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, /// A task that run the receiving process when wrap successful or an exception when failed public Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, EndPoint remoteEndPoint) => this.WrapAsync(webSocket, requestUri, remoteEndPoint, null, new Dictionary(), null); - - /// - /// Wraps a WebSocket connection of ASP.NET / ASP.NET Core and acts like a WebSocket server - /// - /// The WebSocket connection of ASP.NET / ASP.NET Core - /// The original request URI of the WebSocket connection - /// The remote endpoint of the WebSocket connection - /// The local endpoint of the WebSocket connection - /// The string that presents the user agent of the client that made this request to the WebSocket connection - /// The string that presents the url referer of the client that made this request to the WebSocket connection - /// The string that presents the headers of the client that made this request to the WebSocket connection - /// The string that presents the cookies of the client that made this request to the WebSocket connection - /// The action to fire when the WebSocket connection is wrap success - /// A task that run the receiving process when wrap successful or an exception when failed - [Obsolete] - public Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, string userAgent, string urlReferer, string headers, string cookies, Action onSuccess = null) - => this.WrapAsync(webSocket, requestUri, remoteEndPoint, localEndPoint, (headers ?? "").ToDictionary(), onSuccess); - - /// - /// Wraps a WebSocket connection of ASP.NET / ASP.NET Core and acts like a WebSocket server - /// - /// The WebSocket connection of ASP.NET / ASP.NET Core - /// The original request URI of the WebSocket connection - /// The remote endpoint of the WebSocket connection - /// The local endpoint of the WebSocket connection - /// The string that presents the user agent of the client that made this request to the WebSocket connection - /// The string that presents the url referer of the client that made this request to the WebSocket connection - /// The action to fire when the WebSocket connection is wrap success - /// A task that run the receiving process when wrap successful or an exception when failed - [Obsolete] - public Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, string userAgent, string urlReferer, Action onSuccess) - => this.WrapAsync(webSocket, requestUri, remoteEndPoint, localEndPoint, new Dictionary(), onSuccess); #endregion #region Receive messages From 9c943f658b86dd7fa42e8f5ddd5abd9a93a4c9a4 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Tue, 7 May 2019 03:01:36 +0700 Subject: [PATCH 12/34] Close websocket --- VIEApps.Components.WebSockets.csproj | 2 +- WebSocket.cs | 53 +++++++++++++++++++++++----- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/VIEApps.Components.WebSockets.csproj b/VIEApps.Components.WebSockets.csproj index 9d18376..c524d8d 100644 --- a/VIEApps.Components.WebSockets.csproj +++ b/VIEApps.Components.WebSockets.csproj @@ -11,7 +11,7 @@ ../VIEApps.Components.snk 10.2.1905.1 10.2.1905.1 - v10.2.netstandard-2+rev:2019.05.01-latest.components + v10.2.netstandard-2+rev:2019.05.07-closing VIEApps NGX VIEApps.net false diff --git a/WebSocket.cs b/WebSocket.cs index e46d6e4..05d7f5c 100644 --- a/WebSocket.cs +++ b/WebSocket.cs @@ -887,7 +887,7 @@ async Task ReceiveAsync(ManagedWebSocket websocket) closeStatusDescription = websocket.IsClient ? "Disconnected" : "Service is unavailable"; } - this.CloseWebSocket(websocket, closeStatus, closeStatusDescription); + await this.CloseWebSocketAsync(websocket, closeStatus, closeStatusDescription).ConfigureAwait(false); try { this.ConnectionBrokenHandler?.Invoke(websocket); @@ -925,7 +925,7 @@ async Task ReceiveAsync(ManagedWebSocket websocket) { if (this._logger.IsEnabled(LogLevel.Trace)) this._logger.Log(LogLevel.Debug, $"The remote endpoint is initiated to close - Status: {result.CloseStatus} - Description: {result.CloseStatusDescription ?? "N/A"} ({websocket.ID} @ {websocket.RemoteEndPoint})"); - this.CloseWebSocket(websocket); + await this.CloseWebSocketAsync(websocket).ConfigureAwait(false); try { this.ConnectionBrokenHandler?.Invoke(websocket); @@ -946,7 +946,7 @@ async Task ReceiveAsync(ManagedWebSocket websocket) this._logger.Log(LogLevel.Debug, $"Close the connection because {message} ({websocket.ID} @ {websocket.RemoteEndPoint})"); await websocket.CloseAsync(WebSocketCloseStatus.MessageTooBig, $"{message}, send multiple frames instead.", CancellationToken.None).ConfigureAwait(false); - this.CloseWebSocket(websocket); + await this.CloseWebSocketAsync(websocket).ConfigureAwait(false); try { this.ConnectionBrokenHandler?.Invoke(websocket); @@ -984,7 +984,7 @@ async Task ReceiveAsync(ManagedWebSocket websocket) } catch { - this.CloseWebSocket(websocket, websocket.IsClient ? WebSocketCloseStatus.NormalClosure : WebSocketCloseStatus.EndpointUnavailable, websocket.IsClient ? "Disconnected" : "Service is unavailable"); + await this.CloseWebSocketAsync(websocket, websocket.IsClient ? WebSocketCloseStatus.NormalClosure : WebSocketCloseStatus.EndpointUnavailable, websocket.IsClient ? "Disconnected" : "Service is unavailable").ConfigureAwait(false); try { this.ConnectionBrokenHandler?.Invoke(websocket); @@ -1199,15 +1199,28 @@ public IEnumerable GetWebSockets(Func /// The close status to use /// A description of why we are closing /// - bool CloseWebsocket(ManagedWebSocket websocket, WebSocketCloseStatus closeStatus, string closeStatusDescription) + async Task CloseWebsocketAsync(ManagedWebSocket websocket, WebSocketCloseStatus closeStatus, string closeStatusDescription) { if (websocket.State == WebSocketState.Open) - Task.Run(() => websocket.DisposeAsync(closeStatus, closeStatusDescription)).ConfigureAwait(false); + await websocket.DisposeAsync(closeStatus, closeStatusDescription).ConfigureAwait(false); else websocket.Close(); return true; } + /// + /// Closes the WebSocket connection and remove from the centralized collections + /// + /// The WebSocket connection to close + /// The close status to use + /// A description of why we are closing + /// + bool CloseWebsocket(ManagedWebSocket websocket, WebSocketCloseStatus closeStatus, string closeStatusDescription) + { + Task.Run(() => this.CloseWebsocketAsync(websocket, closeStatus, closeStatusDescription)).ConfigureAwait(false); + return true; + } + /// /// Closes the WebSocket connection and remove from the centralized collections /// @@ -1216,10 +1229,22 @@ bool CloseWebsocket(ManagedWebSocket websocket, WebSocketCloseStatus closeStatus /// A description of why we are closing /// true if closed and destroyed public bool CloseWebSocket(Guid id, WebSocketCloseStatus closeStatus = WebSocketCloseStatus.EndpointUnavailable, string closeStatusDescription = "Service is unavailable") - => this._websockets.TryRemove(id, out ManagedWebSocket websocket) + => this._websockets.TryRemove(id, out var websocket) ? this.CloseWebsocket(websocket, closeStatus, closeStatusDescription) : false; + /// + /// Closes the WebSocket connection and remove from the centralized collections + /// + /// The identity of a WebSocket connection to close + /// The close status to use + /// A description of why we are closing + /// true if closed and destroyed + public Task CloseWebSocketAsync(Guid id, WebSocketCloseStatus closeStatus = WebSocketCloseStatus.EndpointUnavailable, string closeStatusDescription = "Service is unavailable") + => this._websockets.TryRemove(id, out var websocket) + ? this.CloseWebsocketAsync(websocket, closeStatus, closeStatusDescription) + : Task.FromResult(false); + /// /// Closes the WebSocket connection and remove from the centralized collections /// @@ -1230,7 +1255,19 @@ public bool CloseWebSocket(Guid id, WebSocketCloseStatus closeStatus = WebSocket public bool CloseWebSocket(ManagedWebSocket websocket, WebSocketCloseStatus closeStatus = WebSocketCloseStatus.EndpointUnavailable, string closeStatusDescription = "Service is unavailable") => websocket == null ? false - : this.CloseWebsocket(this._websockets.TryRemove(websocket.ID, out ManagedWebSocket webSocket) ? webSocket : websocket, closeStatus, closeStatusDescription); + : this.CloseWebsocket(this._websockets.TryRemove(websocket.ID, out var webSocket) ? webSocket : websocket, closeStatus, closeStatusDescription); + + /// + /// Closes the WebSocket connection and remove from the centralized collections + /// + /// The WebSocket connection to close + /// The close status to use + /// A description of why we are closing + /// true if closed and destroyed + public Task CloseWebSocketAsync(ManagedWebSocket websocket, WebSocketCloseStatus closeStatus = WebSocketCloseStatus.EndpointUnavailable, string closeStatusDescription = "Service is unavailable") + => websocket == null + ? Task.FromResult(false) + : this.CloseWebsocketAsync(this._websockets.TryRemove(websocket.ID, out var webSocket) ? webSocket : websocket, closeStatus, closeStatusDescription); #endregion #region Dispose From c19fdab4f8a3a7c682b1a1e13dd73371771af952 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Tue, 7 May 2019 11:13:23 +0700 Subject: [PATCH 13/34] Improvement: add async close methods --- VIEApps.Components.WebSockets.csproj | 10 +++++----- WebSocketHelper.cs | 8 ++++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/VIEApps.Components.WebSockets.csproj b/VIEApps.Components.WebSockets.csproj index c524d8d..29e5281 100644 --- a/VIEApps.Components.WebSockets.csproj +++ b/VIEApps.Components.WebSockets.csproj @@ -9,14 +9,14 @@ VIEApps NGX WebSockets false ../VIEApps.Components.snk - 10.2.1905.1 - 10.2.1905.1 + 10.2.1905.2 + 10.2.1905.2 v10.2.netstandard-2+rev:2019.05.07-closing VIEApps NGX VIEApps.net false VIEApps.Components.WebSockets - 10.2.1905.1 + 10.2.1905.2 VIEApps NGX WebSockets High performance WebSocket on .NET Standard 2.0 (both server and client - standalone or wrapper of System.Net.WebSockets.WebSocket) VIEApps.net @@ -24,7 +24,7 @@ LICENSE.md ..\ websocket;websockets;websocket-client;websocket-server;websocket-wrapper;vieapps;vieapps.components - Upgrade to latest components + Improvement: add async close methods https://github.com/vieapps/Components.Utility/raw/master/logo.png https://github.com/vieapps/Components.WebSockets https://github.com/vieapps/Components.WebSockets @@ -39,7 +39,7 @@ - + \ No newline at end of file diff --git a/WebSocketHelper.cs b/WebSocketHelper.cs index 9e2d450..32e2088 100644 --- a/WebSocketHelper.cs +++ b/WebSocketHelper.cs @@ -98,11 +98,15 @@ internal static void SetOptions(this Socket socket, bool noDelay = true, bool du socket.IOControl(IOControlCode.KeepAliveValues, ((uint)1).ToBytes().Concat(keepaliveInterval.ToBytes(), retryInterval.ToBytes()), null); } - internal static Dictionary ToDictionary(this string @string) - => string.IsNullOrWhiteSpace(@string) + internal static Dictionary ToDictionary(this string @string, Action> onPreCompleted = null) + { + var dictionary = string.IsNullOrWhiteSpace(@string) ? new Dictionary(StringComparer.OrdinalIgnoreCase) : @string.Replace("\r", "").ToList("\n") .Where(header => header.IndexOf(":") > 0) .ToDictionary(header => header.Left(header.IndexOf(":")).Trim(), header => header.Right(header.Length - header.IndexOf(":") - 1).Trim(), StringComparer.OrdinalIgnoreCase); + onPreCompleted?.Invoke(dictionary); + return dictionary; + } } } \ No newline at end of file From 21a05728618745e96c57be2b50c9d1299ff9f8da Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Wed, 22 May 2019 14:53:32 +0700 Subject: [PATCH 14/34] Improvement: OS name --- VIEApps.Components.WebSockets.csproj | 12 ++++++------ WebSocket.cs | 25 ++++++++++++++----------- WebSocketImplementation.cs | 4 ++-- WebSocketWrapper.cs | 4 ++-- 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/VIEApps.Components.WebSockets.csproj b/VIEApps.Components.WebSockets.csproj index 29e5281..b24c2de 100644 --- a/VIEApps.Components.WebSockets.csproj +++ b/VIEApps.Components.WebSockets.csproj @@ -9,14 +9,14 @@ VIEApps NGX WebSockets false ../VIEApps.Components.snk - 10.2.1905.2 - 10.2.1905.2 - v10.2.netstandard-2+rev:2019.05.07-closing + 10.2.1906.1 + 10.2.1906.1 + v10.2.netstandard-2+rev:2019.05.23-extensions VIEApps NGX VIEApps.net false VIEApps.Components.WebSockets - 10.2.1905.2 + 10.2.1906.1 VIEApps NGX WebSockets High performance WebSocket on .NET Standard 2.0 (both server and client - standalone or wrapper of System.Net.WebSockets.WebSocket) VIEApps.net @@ -25,8 +25,8 @@ ..\ websocket;websockets;websocket-client;websocket-server;websocket-wrapper;vieapps;vieapps.components Improvement: add async close methods + https://vieapps.net/ https://github.com/vieapps/Components.Utility/raw/master/logo.png - https://github.com/vieapps/Components.WebSockets https://github.com/vieapps/Components.WebSockets @@ -39,7 +39,7 @@ - + \ No newline at end of file diff --git a/WebSocket.cs b/WebSocket.cs index 05d7f5c..0fab515 100644 --- a/WebSocket.cs +++ b/WebSocket.cs @@ -222,12 +222,12 @@ public void StartListen(int port = 46429, X509Certificate2 certificate = null, A if (this._logger.IsEnabled(LogLevel.Debug)) { - var platform = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? "Windows" - : RuntimeInformation.IsOSPlatform(OSPlatform.Linux) - ? "Linux" - : "macOS"; - platform += $" ({RuntimeInformation.FrameworkDescription.Trim()}) - SSL: {this.Certificate != null}"; + var platform = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + ? "Linux" + : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) + ? "macOS" + : "Windows"; + platform += $" {RuntimeInformation.OSArchitecture.ToString().ToLower()} ({RuntimeInformation.FrameworkDescription.Trim()}) - SSL: {this.Certificate != null}"; if (this.Certificate != null) platform += $" ({this.Certificate.GetNameInfo(X509NameType.DnsName, false)} :: Issued by {this.Certificate.GetNameInfo(X509NameType.DnsName, true)})"; this._logger.LogInformation($"The listener is started => {this._tcpListener.Server.LocalEndPoint}\r\nPlatform: {platform}\r\nPowered by {WebSocketHelper.AgentName} v{this.GetType().Assembly.GetVersion()}"); @@ -1316,7 +1316,6 @@ public void Dispose() /// public abstract class ManagedWebSocket : System.Net.WebSockets.WebSocket { - protected bool _disposing = false, _disposed = false; #region Properties /// @@ -1363,6 +1362,10 @@ public abstract class ManagedWebSocket : System.Net.WebSockets.WebSocket /// Gets the state to include the full exception (with stack trace) in the close response when an exception is encountered and the WebSocket connection is closed /// protected abstract bool IncludeExceptionInCloseResponse { get; } + + protected bool IsDisposing { get; set; } = false; + + protected bool IsDisposed { get; set; } = false; #endregion #region Methods @@ -1441,9 +1444,9 @@ public override void Dispose() internal virtual async Task DisposeAsync(WebSocketCloseStatus closeStatus = WebSocketCloseStatus.EndpointUnavailable, string closeStatusDescription = "Service is unavailable", CancellationToken cancellationToken = default(CancellationToken), Action onDisposed = null) { - if (!this._disposing && !this._disposed) + if (!this.IsDisposing && !this.IsDisposed) { - this._disposing = true; + this.IsDisposing = true; Events.Log.WebSocketDispose(this.ID, this.State); await Task.WhenAll(this.State == WebSocketState.Open ? this.CloseOutputTimeoutAsync(closeStatus, closeStatusDescription, null, () => Events.Log.WebSocketDisposeCloseTimeout(this.ID, this.State), ex => Events.Log.WebSocketDisposeError(this.ID, this.State, ex.ToString())) : Task.CompletedTask).ConfigureAwait(false); try @@ -1451,8 +1454,8 @@ public override void Dispose() onDisposed?.Invoke(); } catch { } - this._disposed = true; - this._disposing = false; + this.IsDisposed = true; + this.IsDisposing = false; } } diff --git a/WebSocketImplementation.cs b/WebSocketImplementation.cs index 29e4f7a..85dd79a 100644 --- a/WebSocketImplementation.cs +++ b/WebSocketImplementation.cs @@ -90,7 +90,7 @@ public WebSocketImplementation(Guid id, bool isClient, Func recycl async Task PutOnTheWireAsync(MemoryStream stream, CancellationToken cancellationToken) { // check disposed - if (this._disposed) + if (this.IsDisposed) { if (this._logger.IsEnabled(LogLevel.Debug)) this._logger.LogWarning($"Object disposed => {this.ID}"); @@ -450,7 +450,7 @@ public override void Abort() internal override void Close() { - if (!this._disposing && !this._disposed) + if (!this.IsDisposing && !this.IsDisposed) { this._processingCTS.Cancel(); this._processingCTS.Dispose(); diff --git a/WebSocketWrapper.cs b/WebSocketWrapper.cs index 3ac143f..17e09bb 100644 --- a/WebSocketWrapper.cs +++ b/WebSocketWrapper.cs @@ -79,7 +79,7 @@ public override Task ReceiveAsync(ArraySegment buf public override async Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) { // check disposed - if (this._disposed) + if (this.IsDisposed) { if (this._logger.IsEnabled(LogLevel.Debug)) this._logger.LogWarning($"Object disposed => {this.ID}"); @@ -160,7 +160,7 @@ public override void Abort() internal override void Close() { - if (!this._disposing && !this._disposed && "System.Net.WebSockets.ManagedWebSocket".Equals($"{this._websocket.GetType()}")) + if (!this.IsDisposing && !this.IsDisposed && "System.Net.WebSockets.ManagedWebSocket".Equals($"{this._websocket.GetType()}")) this._websocket.Dispose(); } From 0ff128425613c0f36e7eca04553da32e561b4961 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Wed, 12 Jun 2019 09:39:08 +0700 Subject: [PATCH 15/34] Improvement: add async close methods --- VIEApps.Components.WebSockets.csproj | 10 +++++----- WebSocket.cs | 2 +- WebSocketHelper.cs | 3 +-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/VIEApps.Components.WebSockets.csproj b/VIEApps.Components.WebSockets.csproj index b24c2de..5c457ee 100644 --- a/VIEApps.Components.WebSockets.csproj +++ b/VIEApps.Components.WebSockets.csproj @@ -9,14 +9,14 @@ VIEApps NGX WebSockets false ../VIEApps.Components.snk - 10.2.1906.1 - 10.2.1906.1 - v10.2.netstandard-2+rev:2019.05.23-extensions + 10.2.1906.2 + 10.2.1906.2 + v10.2.netstandard-2+rev:2019.06.12-async.close VIEApps NGX VIEApps.net false VIEApps.Components.WebSockets - 10.2.1906.1 + 10.2.1906.2 VIEApps NGX WebSockets High performance WebSocket on .NET Standard 2.0 (both server and client - standalone or wrapper of System.Net.WebSockets.WebSocket) VIEApps.net @@ -39,7 +39,7 @@ - + \ No newline at end of file diff --git a/WebSocket.cs b/WebSocket.cs index 0fab515..7dcbd34 100644 --- a/WebSocket.cs +++ b/WebSocket.cs @@ -47,7 +47,7 @@ public class WebSocket : IDisposable /// /// Gets or Sets the SSL protocol for securing connections with SSL Certificate (server) /// - public SslProtocols SslProtocol { get; set; } = SslProtocols.Tls; + public SslProtocols SslProtocol { get; set; } = SslProtocols.Tls12; /// /// Gets or Sets the collection of supported sub-protocol (server) diff --git a/WebSocketHelper.cs b/WebSocketHelper.cs index 32e2088..b4d81f9 100644 --- a/WebSocketHelper.cs +++ b/WebSocketHelper.cs @@ -44,8 +44,7 @@ public static Func GetRecyclableMemoryStreamFactory() { var buffer = new byte[WebSocketHelper.ReceiveBufferSize]; var offset = 0; - var read = 0; - + int read; do { if (offset >= WebSocketHelper.ReceiveBufferSize) From 9d8b09a30c00efc9f4ddd093696177ff0b36c450 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Sun, 3 Mar 2019 00:42:01 +0700 Subject: [PATCH 16/34] Improvement: add try/catch on all .SendAsync methods --- WebSocket.cs | 107 +++++++++++++++++++++++++++++++------ WebSocketImplementation.cs | 29 +++++----- WebSocketWrapper.cs | 15 +++--- 3 files changed, 116 insertions(+), 35 deletions(-) diff --git a/WebSocket.cs b/WebSocket.cs index 9d6b0da..f46cc1c 100644 --- a/WebSocket.cs +++ b/WebSocket.cs @@ -985,10 +985,28 @@ async Task ReceiveAsync(ManagedWebSocket websocket) /// true if this message is a standalone message (this is the norm), false if it is a multi-part message (and true for the last message) /// The cancellation token /// - public Task SendAsync(Guid id, ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken = default(CancellationToken)) - => this._websockets.TryGetValue(id, out ManagedWebSocket websocket) - ? websocket.SendAsync(buffer, messageType, endOfMessage, cancellationToken) - : Task.FromException(new InformationNotFoundException($"WebSocket connection with identity \"{id}\" is not found")); + public async Task SendAsync(Guid id, ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken = default(CancellationToken)) + { + ManagedWebSocket websocket = null; + try + { + if (this._websockets.TryGetValue(id, out websocket)) + await websocket.SendAsync(buffer, messageType, endOfMessage, cancellationToken).ConfigureAwait(false); + else + throw new InformationNotFoundException($"WebSocket connection with identity \"{id}\" is not found"); + } + catch (Exception ex) + { + try + { + this.ErrorHandler?.Invoke(websocket, ex); + } + catch (Exception e) + { + this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); + } + } + } /// /// Sends the message to a WebSocket connection @@ -998,10 +1016,28 @@ async Task ReceiveAsync(ManagedWebSocket websocket) /// true if this message is a standalone message (this is the norm), false if it is a multi-part message (and true for the last message) /// The cancellation token /// - public Task SendAsync(Guid id, string message, bool endOfMessage, CancellationToken cancellationToken = default(CancellationToken)) - => this._websockets.TryGetValue(id, out ManagedWebSocket websocket) - ? websocket.SendAsync(message, endOfMessage, cancellationToken) - : Task.FromException(new InformationNotFoundException($"WebSocket connection with identity \"{id}\" is not found")); + public async Task SendAsync(Guid id, string message, bool endOfMessage, CancellationToken cancellationToken = default(CancellationToken)) + { + ManagedWebSocket websocket = null; + try + { + if (this._websockets.TryGetValue(id, out websocket)) + await websocket.SendAsync(message, endOfMessage, cancellationToken).ConfigureAwait(false); + else + throw new InformationNotFoundException($"WebSocket connection with identity \"{id}\" is not found"); + } + catch (Exception ex) + { + try + { + this.ErrorHandler?.Invoke(websocket, ex); + } + catch (Exception e) + { + this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); + } + } + } /// /// Sends the message to a WebSocket connection @@ -1011,10 +1047,28 @@ async Task ReceiveAsync(ManagedWebSocket websocket) /// true if this message is a standalone message (this is the norm), false if it is a multi-part message (and true for the last message) /// The cancellation token /// - public Task SendAsync(Guid id, byte[] message, bool endOfMessage, CancellationToken cancellationToken = default(CancellationToken)) - => this._websockets.TryGetValue(id, out ManagedWebSocket websocket) - ? websocket.SendAsync(message, endOfMessage, cancellationToken) - : Task.FromException(new InformationNotFoundException($"WebSocket connection with identity \"{id}\" is not found")); + public async Task SendAsync(Guid id, byte[] message, bool endOfMessage, CancellationToken cancellationToken = default(CancellationToken)) + { + ManagedWebSocket websocket = null; + try + { + if (this._websockets.TryGetValue(id, out websocket)) + await websocket.SendAsync(message, endOfMessage, cancellationToken).ConfigureAwait(false); + else + throw new InformationNotFoundException($"WebSocket connection with identity \"{id}\" is not found"); + } + catch (Exception ex) + { + try + { + this.ErrorHandler?.Invoke(websocket, ex); + } + catch (Exception e) + { + this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); + } + } + } /// /// Sends the message to the WebSocket connections that matched with the predicate @@ -1025,8 +1079,25 @@ async Task ReceiveAsync(ManagedWebSocket websocket) /// true if this message is a standalone message (this is the norm), false if it is a multi-part message (and true for the last message) /// The cancellation token /// - public Task SendAsync(Func predicate, ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken = default(CancellationToken)) => this.GetWebSockets(predicate).ToList().ForEachAsync((connection, token) - => connection.SendAsync(buffer.Clone(), messageType, endOfMessage, token), cancellationToken); + public Task SendAsync(Func predicate, ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken = default(CancellationToken)) + => this.GetWebSockets(predicate).ForEachAsync(async (websocket, token) => + { + try + { + await websocket.SendAsync(buffer.Clone(), messageType, endOfMessage, token).ConfigureAwait(false); + } + catch (Exception ex) + { + try + { + this.ErrorHandler?.Invoke(websocket, ex); + } + catch (Exception e) + { + this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); + } + } + }, cancellationToken); /// /// Sends the message to the WebSocket connections that matched with the predicate @@ -1298,7 +1369,7 @@ await Task.WhenAll( protected bool _disposing = false, _disposed = false; - internal virtual async Task DisposeAsync(WebSocketCloseStatus closeStatus = WebSocketCloseStatus.EndpointUnavailable, string closeStatusDescription = "Service is unavailable", CancellationToken cancellationToken = default(CancellationToken), Action onCompleted = null) + internal virtual async Task DisposeAsync(WebSocketCloseStatus closeStatus = WebSocketCloseStatus.EndpointUnavailable, string closeStatusDescription = "Service is unavailable", CancellationToken cancellationToken = default(CancellationToken), Action onDisposed = null) { if (!this._disposing && !this._disposed) { @@ -1306,7 +1377,11 @@ await Task.WhenAll( Events.Log.WebSocketDispose(this.ID, this.State); if (this.State == WebSocketState.Open) await this.CloseOutputTimeoutAsync(closeStatus, closeStatusDescription, null, () => Events.Log.WebSocketDisposeCloseTimeout(this.ID, this.State), ex => Events.Log.WebSocketDisposeError(this.ID, this.State, ex.ToString())).ConfigureAwait(false); - onCompleted?.Invoke(); + try + { + onDisposed?.Invoke(); + } + catch { } this._disposed = true; this._disposing = false; } diff --git a/WebSocketImplementation.cs b/WebSocketImplementation.cs index 77071f5..3d73c12 100644 --- a/WebSocketImplementation.cs +++ b/WebSocketImplementation.cs @@ -86,13 +86,13 @@ public WebSocketImplementation(Guid id, bool isClient, Func recycl /// /// Puts data on the wire /// - /// + /// /// /// - async Task PutOnTheWireAsync(MemoryStream data, CancellationToken cancellationToken) + async Task PutOnTheWireAsync(MemoryStream stream, CancellationToken cancellationToken) { // add into queue - this._buffers.Enqueue(data.ToArraySegment()); + this._buffers.Enqueue(stream.ToArraySegment()); // check pending write operations if (this._writting) @@ -341,13 +341,14 @@ public override async Task SendAsync(ArraySegment buffer, WebSocketMessage } // send - using (var stream = this._recycledStreamFactory()) - { - stream.Write(opCode, buffer, endOfMessage, this.IsClient); - Events.Log.SendingFrame(this.ID, opCode, endOfMessage, buffer.Count, false); - await this.PutOnTheWireAsync(stream, cancellationToken).ConfigureAwait(false); - this._isContinuationFrame = !endOfMessage; - } + if (this._state == WebSocketState.Open) + using (var stream = this._recycledStreamFactory()) + { + stream.Write(opCode, buffer, endOfMessage, this.IsClient); + Events.Log.SendingFrame(this.ID, opCode, endOfMessage, buffer.Count, false); + await this.PutOnTheWireAsync(stream, cancellationToken).ConfigureAwait(false); + this._isContinuationFrame = !endOfMessage; + } } /// @@ -428,11 +429,15 @@ public override void Abort() this._processingCTS.Cancel(); } - internal override Task DisposeAsync(WebSocketCloseStatus closeStatus = WebSocketCloseStatus.EndpointUnavailable, string closeStatusDescription = "Service is unavailable", CancellationToken cancellationToken = default(CancellationToken), Action onCompleted = null) + internal override Task DisposeAsync(WebSocketCloseStatus closeStatus = WebSocketCloseStatus.EndpointUnavailable, string closeStatusDescription = "Service is unavailable", CancellationToken cancellationToken = default(CancellationToken), Action onDisposed = null) => base.DisposeAsync(closeStatus, closeStatusDescription, cancellationToken, () => { this.Close(); - onCompleted?.Invoke(); + try + { + onDisposed?.Invoke(); + } + catch { } }); internal override void Close() diff --git a/WebSocketWrapper.cs b/WebSocketWrapper.cs index 5369258..d51404e 100644 --- a/WebSocketWrapper.cs +++ b/WebSocketWrapper.cs @@ -126,20 +126,21 @@ public override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string c public override void Abort() => this._websocket.Abort(); - internal override Task DisposeAsync(WebSocketCloseStatus closeStatus = WebSocketCloseStatus.EndpointUnavailable, string closeStatusDescription = "Service is unavailable", CancellationToken cancellationToken = default(CancellationToken), Action onCompleted = null) + internal override Task DisposeAsync(WebSocketCloseStatus closeStatus = WebSocketCloseStatus.EndpointUnavailable, string closeStatusDescription = "Service is unavailable", CancellationToken cancellationToken = default(CancellationToken), Action onDisposed = null) => base.DisposeAsync(closeStatus, closeStatusDescription, cancellationToken, () => { this.Close(); - onCompleted?.Invoke(); + try + { + onDisposed?.Invoke(); + } + catch { } }); internal override void Close() { - if (!this._disposing && !this._disposed) - { - if ("System.Net.WebSockets.ManagedWebSocket".Equals($"{this._websocket.GetType()}")) - this._websocket.Dispose(); - } + if (!this._disposing && !this._disposed && "System.Net.WebSockets.ManagedWebSocket".Equals($"{this._websocket.GetType()}")) + this._websocket.Dispose(); } ~WebSocketWrapper() From 0f746581583747f8b7b0ea933ba752935495b0c5 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Wed, 6 Mar 2019 17:11:36 +0700 Subject: [PATCH 17/34] Upgrade to latest components --- VIEApps.Components.WebSockets.csproj | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/VIEApps.Components.WebSockets.csproj b/VIEApps.Components.WebSockets.csproj index 2e44917..5aaf8d1 100644 --- a/VIEApps.Components.WebSockets.csproj +++ b/VIEApps.Components.WebSockets.csproj @@ -9,14 +9,14 @@ VIEApps NGX WebSockets false ../VIEApps.Components.snk - 10.2.1903.1 - 10.2.1903.1 - v10.2.netstandard-2+rev:2019.03.01-latest.components-ignore.cert.errors + 10.2.1903.3 + 10.2.1903.3 + v10.2.netstandard-2+rev:2019.03.06-latest.components VIEApps NGX VIEApps.net false VIEApps.Components.WebSockets - 10.2.1903.1 + 10.2.1903.3 VIEApps NGX WebSockets High performance WebSocket on .NET Standard 2.0 (both server and client - standalone or wrapper of System.Net.WebSockets.WebSocket) VIEApps.net @@ -24,7 +24,7 @@ LICENSE.md ..\ websocket;websockets;websocket-client;websocket-server;websocket-wrapper;vieapps;vieapps.components - Upgrade to latest components, add options to ignore remote certificate errors (support for client certificates) + Upgrade to latest components https://github.com/vieapps/Components.Utility/raw/master/logo.png https://github.com/vieapps/Components.WebSockets https://github.com/vieapps/Components.WebSockets @@ -39,7 +39,7 @@ - + \ No newline at end of file From de5a0d640c2605dca0effe47a1df83808a99e233 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Tue, 12 Mar 2019 23:08:59 +0700 Subject: [PATCH 18/34] Improvements: add locking mechanims, check disposed objects when sending, ... --- PingPong.cs | 4 ++-- README.md | 4 ++-- VIEApps.Components.WebSockets.csproj | 2 +- WebSocket.cs | 6 +++--- WebSocketImplementation.cs | 27 +++++++++++++++++++-------- WebSocketWrapper.cs | 24 ++++++++++++++++++------ 6 files changed, 45 insertions(+), 22 deletions(-) diff --git a/PingPong.cs b/PingPong.cs index c57ca0d..b707ccb 100644 --- a/PingPong.cs +++ b/PingPong.cs @@ -17,7 +17,7 @@ internal class PongEventArgs : EventArgs /// /// The data extracted from a Pong WebSocket frame /// - public ArraySegment Payload { get; private set; } + public ArraySegment Payload { get; } /// /// Initialises a new instance of the PongEventArgs class @@ -41,7 +41,7 @@ internal interface IPingPongManager /// /// Sends a ping frame /// - /// The payload (must be 125 bytes of less) + /// The payload (must be 125 bytes or less) /// The cancellation token Task SendPingAsync(ArraySegment payload, CancellationToken cancellation = default(CancellationToken)); } diff --git a/README.md b/README.md index 21933dd..572fef3 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ Use the **StopListen** method to stop the listener. ### WebSocket server with Secure WebSockets (wss://) Enabling secure connections requires two things: -- Pointing certificate to an x509 certificate that containing a public and private key. +- Pointing certificate to an X.509 certificate that containing a public and private key. - Using the scheme **wss** instead of **ws** (or **https** instead of **http**) on all clients ```csharp @@ -149,7 +149,7 @@ websocket.StartListen(); Want to have a free SSL certificate? Take a look at [Let's Encrypt](https://letsencrypt.org/). -Special: A simple tool named [lets-encrypt-win-simple](https://github.com/PKISharp/win-acme) will help your IIS works with Let's Encrypt very well. +Special: A simple tool named [win-acme](https://github.com/PKISharp/win-acme) will help your IIS works with Let's Encrypt very well. ### SubProtocol Negotiation diff --git a/VIEApps.Components.WebSockets.csproj b/VIEApps.Components.WebSockets.csproj index 5aaf8d1..02681c2 100644 --- a/VIEApps.Components.WebSockets.csproj +++ b/VIEApps.Components.WebSockets.csproj @@ -11,7 +11,7 @@ ../VIEApps.Components.snk 10.2.1903.3 10.2.1903.3 - v10.2.netstandard-2+rev:2019.03.06-latest.components + v10.2.netstandard-2+rev:2019.03.12-disposed.objects VIEApps NGX VIEApps.net false diff --git a/WebSocket.cs b/WebSocket.cs index f46cc1c..6df00e2 100644 --- a/WebSocket.cs +++ b/WebSocket.cs @@ -1246,6 +1246,7 @@ public void Dispose() /// public abstract class ManagedWebSocket : System.Net.WebSockets.WebSocket { + protected bool _disposing = false, _disposed = false; #region Properties /// @@ -1365,9 +1366,8 @@ await Task.WhenAll( /// /// Cleans up unmanaged resources (will send a close frame if the connection is still open) /// - public override void Dispose() => this.DisposeAsync().Wait(4321); - - protected bool _disposing = false, _disposed = false; + public override void Dispose() + => this.DisposeAsync().Wait(4321); internal virtual async Task DisposeAsync(WebSocketCloseStatus closeStatus = WebSocketCloseStatus.EndpointUnavailable, string closeStatusDescription = "Service is unavailable", CancellationToken cancellationToken = default(CancellationToken), Action onDisposed = null) { diff --git a/WebSocketImplementation.cs b/WebSocketImplementation.cs index 3d73c12..8913cb8 100644 --- a/WebSocketImplementation.cs +++ b/WebSocketImplementation.cs @@ -25,7 +25,9 @@ internal class WebSocketImplementation : ManagedWebSocket WebSocketMessageType _continuationFrameMessageType = WebSocketMessageType.Binary; WebSocketCloseStatus? _closeStatus; string _closeStatusDescription; - bool _isContinuationFrame, _writting = false; + bool _isContinuationFrame = false; + readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); + bool _pending = false; readonly string _subProtocol; readonly CancellationTokenSource _processingCTS; readonly ConcurrentQueue> _buffers = new ConcurrentQueue>(); @@ -91,19 +93,22 @@ public WebSocketImplementation(Guid id, bool isClient, Func recycl /// async Task PutOnTheWireAsync(MemoryStream stream, CancellationToken cancellationToken) { - // add into queue + // add into queue and check pending operations this._buffers.Enqueue(stream.ToArraySegment()); - - // check pending write operations - if (this._writting) + if (this._pending) { Events.Log.PendingOperations(this.ID); - Logger.Log(LogLevel.Debug, LogLevel.Warning, $"Pending operations => {this._buffers.Count:#,##0} ({this.ID} @ {this.RemoteEndPoint})"); + Logger.Log(LogLevel.Debug, LogLevel.Warning, $"#{Thread.CurrentThread.ManagedThreadId} Pendings => {this._buffers.Count:#,##0} ({this.ID} @ {this.RemoteEndPoint})"); return; } + // check disposed + if (this._disposing || this._disposed) + throw new ObjectDisposedException("WebSocketImplementation"); + // put data to wire - this._writting = true; + this._pending = true; + await this._lock.WaitAsync(cancellationToken).ConfigureAwait(false); try { while (this._buffers.Count > 0) @@ -116,7 +121,8 @@ async Task PutOnTheWireAsync(MemoryStream stream, CancellationToken cancellation } finally { - this._writting = false; + this._pending = false; + this._lock.Release(); } } @@ -438,6 +444,11 @@ public override void Abort() onDisposed?.Invoke(); } catch { } + try + { + this._lock.Dispose(); + } + catch { } }); internal override void Close() diff --git a/WebSocketWrapper.cs b/WebSocketWrapper.cs index d51404e..1df1327 100644 --- a/WebSocketWrapper.cs +++ b/WebSocketWrapper.cs @@ -17,7 +17,8 @@ internal class WebSocketWrapper : ManagedWebSocket #region Properties readonly System.Net.WebSockets.WebSocket _websocket = null; readonly ConcurrentQueue, WebSocketMessageType, bool>> _buffers = new ConcurrentQueue, WebSocketMessageType, bool>>(); - bool _sending = false; + readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); + bool _pending = false; /// /// Gets the state that indicates the reason why the remote endpoint initiated the close handshake @@ -73,17 +74,22 @@ public override Task ReceiveAsync(ArraySegment buf /// public override async Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) { - // add into queue and check pending write operations + // add into queue and check pending operations this._buffers.Enqueue(new Tuple, WebSocketMessageType, bool>(buffer, messageType, endOfMessage)); - if (this._sending) + if (this._pending) { Events.Log.PendingOperations(this.ID); - Logger.Log(LogLevel.Debug, LogLevel.Warning, $"Pending operations => {this._buffers.Count:#,##0} ({this.ID} @ {this.RemoteEndPoint})"); + Logger.Log(LogLevel.Debug, LogLevel.Warning, $"#{Thread.CurrentThread.ManagedThreadId} Pendings => {this._buffers.Count:#,##0} ({this.ID} @ {this.RemoteEndPoint})"); return; } + // check disposed + if (this._disposing || this._disposed) + throw new ObjectDisposedException("WebSocketImplementation"); + // put data to wire - this._sending = true; + this._pending = true; + await this._lock.WaitAsync(cancellationToken).ConfigureAwait(false); try { while (this.State == WebSocketState.Open && this._buffers.Count > 0) @@ -96,7 +102,8 @@ public override async Task SendAsync(ArraySegment buffer, WebSocketMessage } finally { - this._sending = false; + this._pending = false; + this._lock.Release(); } } @@ -135,6 +142,11 @@ public override void Abort() onDisposed?.Invoke(); } catch { } + try + { + this._lock.Dispose(); + } + catch { } }); internal override void Close() From 6bb72c9f6c936db528c7a99b973eb19f73de44c9 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Sat, 16 Mar 2019 17:30:00 +0700 Subject: [PATCH 19/34] Code refactoring --- VIEApps.Components.WebSockets.csproj | 2 +- WebSocket.cs | 271 ++++++++++++++++----------- WebSocketHelper.cs | 30 +-- WebSocketImplementation.cs | 12 +- WebSocketWrapper.cs | 12 +- 5 files changed, 184 insertions(+), 143 deletions(-) diff --git a/VIEApps.Components.WebSockets.csproj b/VIEApps.Components.WebSockets.csproj index 02681c2..7f39d76 100644 --- a/VIEApps.Components.WebSockets.csproj +++ b/VIEApps.Components.WebSockets.csproj @@ -11,7 +11,7 @@ ../VIEApps.Components.snk 10.2.1903.3 10.2.1903.3 - v10.2.netstandard-2+rev:2019.03.12-disposed.objects + v10.2.netstandard-2+rev:2019.03.15-disposed.objects.headers VIEApps NGX VIEApps.net false diff --git a/WebSocket.cs b/WebSocket.cs index 6df00e2..139ab19 100644 --- a/WebSocket.cs +++ b/WebSocket.cs @@ -197,7 +197,8 @@ public void StartListen(int port = 46429, X509Certificate2 certificate = null, A } catch (Exception ex) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); } return; } @@ -214,23 +215,27 @@ public void StartListen(int port = 46429, X509Certificate2 certificate = null, A this._tcpListener.Server.SetKeepAliveInterval(); this._tcpListener.Start(1024); - var platform = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? "Windows" - : RuntimeInformation.IsOSPlatform(OSPlatform.Linux) - ? "Linux" - : "macOS"; - platform += $" ({RuntimeInformation.FrameworkDescription.Trim()}) - SSL: {this.Certificate != null}"; - if (this.Certificate != null) - platform += $" ({this.Certificate.GetNameInfo(X509NameType.DnsName, false)} :: Issued by {this.Certificate.GetNameInfo(X509NameType.DnsName, true)})"; + if (this._logger.IsEnabled(LogLevel.Debug)) + { + var platform = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? "Windows" + : RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + ? "Linux" + : "macOS"; + platform += $" ({RuntimeInformation.FrameworkDescription.Trim()}) - SSL: {this.Certificate != null}"; + if (this.Certificate != null) + platform += $" ({this.Certificate.GetNameInfo(X509NameType.DnsName, false)} :: Issued by {this.Certificate.GetNameInfo(X509NameType.DnsName, true)})"; + this._logger.LogInformation($"The listener is started (listening port: {this.Port})\r\nPlatform: {platform}\r\nPowered by {WebSocketHelper.AgentName} v{this.GetType().Assembly.GetVersion()}"); + } - this._logger.LogInformation($"The listener is started (listening port: {this.Port})\r\nPlatform: {platform}\r\nPowered by {WebSocketHelper.AgentName} {this.GetType().Assembly.GetVersion()}"); try { onSuccess?.Invoke(); } catch (Exception ex) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); } // listen for incoming connection requests @@ -239,26 +244,30 @@ public void StartListen(int port = 46429, X509Certificate2 certificate = null, A catch (SocketException ex) { var message = $"Error occurred while listening on port \"{this.Port}\". Make sure another application is not running and consuming this port."; - this._logger.Log(LogLevel.Debug, LogLevel.Error, message, ex); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, message, ex); try { onFailure?.Invoke(new ListenerSocketException(message, ex)); } catch (Exception e) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); } } catch (Exception ex) { - this._logger.Log(LogLevel.Debug, LogLevel.Error, $"Got an unexpected error while listening: {ex.Message}", ex); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Got an unexpected error while listening: {ex.Message}", ex); try { onFailure?.Invoke(ex); } catch (Exception e) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); } } } @@ -326,7 +335,7 @@ async Task ListenAsync() { this.StopListen(false); if (ex is OperationCanceledException || ex is TaskCanceledException || ex is ObjectDisposedException || ex is SocketException || ex is IOException) - this._logger.LogInformation($"The listener is stopped {(this._logger.IsEnabled(LogLevel.Debug) ? $"({ex.GetType()})" : "")}"); + this._logger.LogDebug($"The listener is stopped {(this._logger.IsEnabled(LogLevel.Debug) ? $"({ex.GetType()})" : "")}"); else this._logger.LogError($"The listener is stopped ({ex.Message})", ex); } @@ -349,7 +358,8 @@ async Task AcceptClientAsync(TcpClient tcpClient) try { Events.Log.AttemptingToSecureConnection(id); - this._logger.Log(LogLevel.Trace, LogLevel.Debug, $"Attempting to secure the connection ({id} @ {endpoint})"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"Attempting to secure the connection ({id} @ {endpoint})"); stream = new SslStream(tcpClient.GetStream(), false); await (stream as SslStream).AuthenticateAsServerAsync( @@ -360,7 +370,8 @@ async Task AcceptClientAsync(TcpClient tcpClient) ).WithCancellationToken(this._listeningCTS.Token).ConfigureAwait(false); Events.Log.ConnectionSecured(id); - this._logger.Log(LogLevel.Trace, LogLevel.Debug, $"The connection successfully secured ({id} @ {endpoint})"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"The connection successfully secured ({id} @ {endpoint})"); } catch (OperationCanceledException) { @@ -377,15 +388,18 @@ async Task AcceptClientAsync(TcpClient tcpClient) else { Events.Log.ConnectionNotSecured(id); - this._logger.Log(LogLevel.Trace, LogLevel.Debug, $"Use insecured connection ({id} @ {endpoint})"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"Use insecured connection ({id} @ {endpoint})"); stream = tcpClient.GetStream(); } // parse request - this._logger.Log(LogLevel.Trace, LogLevel.Debug, $"The connection is opened, then parse the request ({id} @ {endpoint})"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"The connection is opened, then parse the request ({id} @ {endpoint})"); var header = await stream.ReadHeaderAsync(this._listeningCTS.Token).ConfigureAwait(false); - this._logger.Log(LogLevel.Trace, LogLevel.Debug, $"Handshake request ({id} @ {endpoint}) => \r\n{header.Trim()}"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"Handshake request ({id} @ {endpoint}) => \r\n{header.Trim()}"); var isWebSocketRequest = false; var path = string.Empty; @@ -400,7 +414,8 @@ async Task AcceptClientAsync(TcpClient tcpClient) // verify request if (!isWebSocketRequest) { - this._logger.Log(LogLevel.Trace, LogLevel.Debug, $"The request contains no WebSocket upgrade request, then ignore ({id} @ {endpoint})"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"The request contains no WebSocket upgrade request, then ignore ({id} @ {endpoint})"); stream.Close(); tcpClient.Close(); return; @@ -412,20 +427,17 @@ async Task AcceptClientAsync(TcpClient tcpClient) KeepAliveInterval = this.KeepAliveInterval }; Events.Log.AcceptWebSocketStarted(id); - this._logger.Log(LogLevel.Trace, LogLevel.Debug, $"The request has requested an upgrade to WebSocket protocol, negotiating WebSocket handshake ({id} @ {endpoint})"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"The request has requested an upgrade to WebSocket protocol, negotiating WebSocket handshake ({id} @ {endpoint})"); try { // check the version (support version 13 and above) match = new Regex("Sec-WebSocket-Version: (.*)").Match(header); - if (match.Success) - { - var secWebSocketVersion = match.Groups[1].Value.Trim().CastAs(); - if (secWebSocketVersion < 13) - throw new VersionNotSupportedException($"WebSocket Version {secWebSocketVersion} is not supported, must be 13 or above"); - } - else + if (!match.Success || !Int32.TryParse(match.Groups[1].Value, out int version)) throw new VersionNotSupportedException("Unable to find \"Sec-WebSocket-Version\" in the upgrade request"); + else if (version < 13) + throw new VersionNotSupportedException($"WebSocket Version {version} is not supported, must be 13 or above"); // get the request key match = new Regex("Sec-WebSocket-Key: (.*)").Match(header); @@ -454,7 +466,8 @@ async Task AcceptClientAsync(TcpClient tcpClient) Events.Log.SendingHandshake(id, handshake); await stream.WriteHeaderAsync(handshake, this._listeningCTS.Token).ConfigureAwait(false); Events.Log.HandshakeSent(id, handshake); - this._logger.Log(LogLevel.Trace, LogLevel.Debug, $"Handshake response ({id} @ {endpoint}) => \r\n{handshake.Trim()}"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"Handshake response ({id} @ {endpoint}) => \r\n{handshake.Trim()}"); } catch (VersionNotSupportedException ex) { @@ -470,7 +483,8 @@ async Task AcceptClientAsync(TcpClient tcpClient) } Events.Log.ServerHandshakeSuccess(id); - this._logger.Log(LogLevel.Trace, LogLevel.Debug, $"WebSocket handshake response has been sent, the stream is ready ({id} @ {endpoint})"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"WebSocket handshake response has been sent, the stream is ready ({id} @ {endpoint})"); // update the connected WebSocket connection match = new Regex("Sec-WebSocket-Extensions: (.*)").Match(header); @@ -483,26 +497,11 @@ async Task AcceptClientAsync(TcpClient tcpClient) ? match.Groups[1].Value.Trim() : string.Empty; - websocket = new WebSocketImplementation(id, false, this._recycledStreamFactory, stream, options, new Uri($"ws{(this.Certificate != null ? "s" : "")}://{host}{path}"), endpoint, tcpClient.Client.LocalEndPoint); - - match = new Regex("User-Agent: (.*)").Match(header); - websocket.Extra["User-Agent"] = match.Success - ? match.Groups[1].Value.Trim() - : string.Empty; - - match = new Regex("Referer: (.*)").Match(header); - if (match.Success) - websocket.Extra["Referer"] = match.Groups[1].Value.Trim(); - else - { - match = new Regex("Origin: (.*)").Match(header); - websocket.Extra["Referer"] = match.Success - ? match.Groups[1].Value.Trim() - : string.Empty; - } - // add into the collection + websocket = new WebSocketImplementation(id, false, this._recycledStreamFactory, stream, options, new Uri($"ws{(this.Certificate != null ? "s" : "")}://{host}{path}"), endpoint, tcpClient.Client.LocalEndPoint, header.ToDictionary()); await this.AddWebSocketAsync(websocket).ConfigureAwait(false); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"The server WebSocket connection was successfully established ({websocket.ID} @ {websocket.RemoteEndPoint})\r\n- URI: {websocket.RequestUri}\r\n- Headers:\r\n\t{websocket.Headers.ToString("\r\n\t", kvp => $"{kvp.Key}: {kvp.Value}")}"); // callback try @@ -511,7 +510,8 @@ async Task AcceptClientAsync(TcpClient tcpClient) } catch (Exception e) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); } // receive messages @@ -525,14 +525,16 @@ async Task AcceptClientAsync(TcpClient tcpClient) } else { - this._logger.Log(LogLevel.Debug, LogLevel.Error, $"Error occurred while accepting an incoming connection request: {ex.Message}", ex); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while accepting an incoming connection request: {ex.Message}", ex); try { this.ErrorHandler?.Invoke(websocket, ex); } catch (Exception e) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); } } } @@ -542,7 +544,9 @@ async Task AcceptClientAsync(TcpClient tcpClient) #region Connect to remote endpoints as client async Task ConnectAsync(Uri uri, WebSocketOptions options, Action onSuccess = null, Action onFailure = null) { - this._logger.Log(LogLevel.Trace, LogLevel.Debug, $"Attempting to connect ({uri})"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"Attempting to connect ({uri})"); + try { // connect the TCP client @@ -565,7 +569,8 @@ async Task ConnectAsync(Uri uri, WebSocketOptions options, Action options.IgnoreCertificateErrors || RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || sslPolicyErrors == SslPolicyErrors.None, + userCertificateValidationCallback: (sender, certificate, chain, sslPolicyErrors) => sslPolicyErrors == SslPolicyErrors.None || options.IgnoreCertificateErrors || RuntimeInformation.IsOSPlatform(OSPlatform.Linux), userCertificateSelectionCallback: (sender, host, certificates, certificate, issuers) => this.Certificate ); await (stream as SslStream).AuthenticateAsClientAsync(targetHost: sniHost ?? uri.Host).WithCancellationToken(this._processingCTS.Token).ConfigureAwait(false); Events.Log.ConnectionSecured(id); - this._logger.Log(LogLevel.Trace, LogLevel.Debug, $"The connection successfully secured ({id} @ {endpoint})"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"The connection successfully secured ({id} @ {endpoint})"); } catch (OperationCanceledException) { @@ -604,12 +611,15 @@ async Task ConnectAsync(Uri uri, WebSocketOptions options, Action \r\n{handshake.Trim()}"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"Handshake request ({id} @ {endpoint}) => \r\n{handshake.Trim()}"); // read response Events.Log.ReadingResponse(id); @@ -638,7 +649,8 @@ async Task ConnectAsync(Uri uri, WebSocketOptions options, Action \r\n{response.Trim()}"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"Handshake response ({id} @ {endpoint}) => \r\n{response.Trim()}"); } catch (Exception ex) { @@ -674,13 +686,14 @@ async Task ConnectAsync(Uri uri, WebSocketOptions options, Action $"{kvp.Key}: {kvp.Value}")}"); // callback try @@ -700,7 +715,8 @@ async Task ConnectAsync(Uri uri, WebSocketOptions options, Action {ex.Message}", ex); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); } // receive messages @@ -712,14 +728,16 @@ async Task ConnectAsync(Uri uri, WebSocketOptions options, Action {e.Message}", e); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); } } } @@ -772,33 +790,18 @@ public void Connect(string location, Action onSuccess, Action< /// The original request URI of the WebSocket connection /// The remote endpoint of the WebSocket connection /// The local endpoint of the WebSocket connection - /// The string that presents the user agent of the client that made this request to the WebSocket connection - /// The string that presents the url referer of the client that made this request to the WebSocket connection - /// The string that presents the headers of the client that made this request to the WebSocket connection - /// The string that presents the cookies of the client that made this request to the WebSocket connection + /// The collection that presents the headers of the client that made this request to the WebSocket connection /// The action to fire when the WebSocket connection is wrap success /// A task that run the receiving process when wrap successful or an exception when failed - public Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, EndPoint remoteEndPoint = null, EndPoint localEndPoint = null, string userAgent = null, string urlReferer = null, string headers = null, string cookies = null, Action onSuccess = null) + public Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, EndPoint remoteEndPoint = null, EndPoint localEndPoint = null, Dictionary headers = null, Action onSuccess = null) { try { // create - var websocket = new WebSocketWrapper(webSocket, requestUri, remoteEndPoint, localEndPoint); - - if (!string.IsNullOrWhiteSpace(userAgent)) - websocket.Extra["User-Agent"] = userAgent; - - if (!string.IsNullOrWhiteSpace(urlReferer)) - websocket.Extra["Referer"] = urlReferer; - - if (!string.IsNullOrWhiteSpace(headers)) - websocket.Extra["Headers"] = headers; - - if (!string.IsNullOrWhiteSpace(cookies)) - websocket.Extra["Cookies"] = cookies; - + var websocket = new WebSocketWrapper(webSocket, requestUri, remoteEndPoint, localEndPoint, headers); this.AddWebSocket(websocket); - this._logger.Log(LogLevel.Trace, LogLevel.Debug, $"Wrap a WebSocket connection [{webSocket.GetType()}] successful ({websocket.ID} @ {websocket.RemoteEndPoint}"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"Wrap a WebSocket connection [{webSocket.GetType()}] successful ({websocket.ID} @ {websocket.RemoteEndPoint})\r\n- URI: {websocket.RequestUri}\r\n- Headers:\r\n\t{websocket.Headers.ToString("\r\n\t", kvp => $"{kvp.Key}: {kvp.Value}")}"); // callback try @@ -808,7 +811,8 @@ public Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, } catch (Exception ex) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); } // receive messages @@ -816,11 +820,22 @@ public Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, } catch (Exception ex) { - this._logger.Log(LogLevel.Debug, LogLevel.Error, $"Unable to wrap a WebSocket connection [{webSocket.GetType()}]: {ex.Message}", ex); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Unable to wrap a WebSocket connection [{webSocket.GetType()}]: {ex.Message}", ex); return Task.FromException(new WrapWebSocketFailedException($"Unable to wrap a WebSocket connection [{webSocket.GetType()}]", ex)); } } + /// + /// Wraps a WebSocket connection of ASP.NET / ASP.NET Core and acts like a WebSocket server + /// + /// The WebSocket connection of ASP.NET / ASP.NET Core + /// The original request URI of the WebSocket connection + /// The remote endpoint of the WebSocket connection + /// A task that run the receiving process when wrap successful or an exception when failed + public Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, EndPoint remoteEndPoint) + => this.WrapAsync(webSocket, requestUri, remoteEndPoint, null, new Dictionary(), null); + /// /// Wraps a WebSocket connection of ASP.NET / ASP.NET Core and acts like a WebSocket server /// @@ -830,10 +845,13 @@ public Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, /// The local endpoint of the WebSocket connection /// The string that presents the user agent of the client that made this request to the WebSocket connection /// The string that presents the url referer of the client that made this request to the WebSocket connection + /// The string that presents the headers of the client that made this request to the WebSocket connection + /// The string that presents the cookies of the client that made this request to the WebSocket connection /// The action to fire when the WebSocket connection is wrap success /// A task that run the receiving process when wrap successful or an exception when failed - public Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, string userAgent, string urlReferer, Action onSuccess) - => this.WrapAsync(webSocket, requestUri, remoteEndPoint, localEndPoint, userAgent, urlReferer, null, null, onSuccess); + [Obsolete] + public Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, string userAgent, string urlReferer, string headers, string cookies, Action onSuccess = null) + => this.WrapAsync(webSocket, requestUri, remoteEndPoint, localEndPoint, (headers ?? "").ToDictionary(), onSuccess); /// /// Wraps a WebSocket connection of ASP.NET / ASP.NET Core and acts like a WebSocket server @@ -841,9 +859,14 @@ public Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, /// The WebSocket connection of ASP.NET / ASP.NET Core /// The original request URI of the WebSocket connection /// The remote endpoint of the WebSocket connection + /// The local endpoint of the WebSocket connection + /// The string that presents the user agent of the client that made this request to the WebSocket connection + /// The string that presents the url referer of the client that made this request to the WebSocket connection + /// The action to fire when the WebSocket connection is wrap success /// A task that run the receiving process when wrap successful or an exception when failed - public Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, EndPoint remoteEndPoint) - => this.WrapAsync(webSocket, requestUri, remoteEndPoint, null, null, null, null, null, null); + [Obsolete] + public Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, string userAgent, string urlReferer, Action onSuccess) + => this.WrapAsync(webSocket, requestUri, remoteEndPoint, localEndPoint, new Dictionary(), onSuccess); #endregion #region Receive messages @@ -878,21 +901,27 @@ async Task ReceiveAsync(ManagedWebSocket websocket) } catch (Exception e) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); } if (ex is OperationCanceledException || ex is TaskCanceledException || ex is ObjectDisposedException || ex is WebSocketException || ex is SocketException || ex is IOException) - this._logger.Log(LogLevel.Trace, LogLevel.Debug, $"Stop receiving process when got an error: {ex.Message} ({ex.GetType().GetTypeName(true)})"); + { + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"Stop receiving process when got an error: {ex.Message} ({ex.GetType().GetTypeName(true)})"); + } else { - this._logger.Log(LogLevel.Debug, LogLevel.Error, closeStatusDescription, ex); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, closeStatusDescription, ex); try { this.ErrorHandler?.Invoke(websocket, ex); } catch (Exception e) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); } } return; @@ -901,7 +930,8 @@ async Task ReceiveAsync(ManagedWebSocket websocket) // message to close if (result.MessageType == WebSocketMessageType.Close) { - this._logger.Log(LogLevel.Trace, LogLevel.Debug, $"The remote endpoint is initiated to close - Status: {result.CloseStatus} - Description: {result.CloseStatusDescription ?? "N/A"} ({websocket.ID} @ {websocket.RemoteEndPoint})"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"The remote endpoint is initiated to close - Status: {result.CloseStatus} - Description: {result.CloseStatusDescription ?? "N/A"} ({websocket.ID} @ {websocket.RemoteEndPoint})"); this.CloseWebSocket(websocket); try { @@ -909,7 +939,8 @@ async Task ReceiveAsync(ManagedWebSocket websocket) } catch (Exception ex) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); } return; } @@ -918,7 +949,8 @@ async Task ReceiveAsync(ManagedWebSocket websocket) if (result.Count > WebSocketHelper.ReceiveBufferSize) { var message = $"WebSocket frame cannot exceed buffer size of {WebSocketHelper.ReceiveBufferSize:#,##0} bytes"; - this._logger.Log(LogLevel.Debug, LogLevel.Debug, $"Close the connection because {message} ({websocket.ID} @ {websocket.RemoteEndPoint})"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"Close the connection because {message} ({websocket.ID} @ {websocket.RemoteEndPoint})"); await websocket.CloseAsync(WebSocketCloseStatus.MessageTooBig, $"{message}, send multiple frames instead.", CancellationToken.None).ConfigureAwait(false); this.CloseWebSocket(websocket); @@ -929,7 +961,8 @@ async Task ReceiveAsync(ManagedWebSocket websocket) } catch (Exception ex) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); } return; } @@ -937,14 +970,16 @@ async Task ReceiveAsync(ManagedWebSocket websocket) // got a message if (result.Count > 0) { - this._logger.Log(LogLevel.Trace, LogLevel.Debug, $"A message was received - Type: {result.MessageType} - End of message: {result.EndOfMessage} - Length: {result.Count:#,##0} ({websocket.ID} @ {websocket.RemoteEndPoint})"); + if (this._logger.IsEnabled(LogLevel.Trace)) + this._logger.Log(LogLevel.Debug, $"A message was received - Type: {result.MessageType} - End of message: {result.EndOfMessage} - Length: {result.Count:#,##0} ({websocket.ID} @ {websocket.RemoteEndPoint})"); try { this.MessageReceivedHandler?.Invoke(websocket, result, buffer.Take(result.Count).ToArray()); } catch (Exception ex) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); } } @@ -963,7 +998,8 @@ async Task ReceiveAsync(ManagedWebSocket websocket) } catch (Exception ex) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); } return; } @@ -1003,7 +1039,8 @@ async Task ReceiveAsync(ManagedWebSocket websocket) } catch (Exception e) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); } } } @@ -1034,7 +1071,8 @@ async Task ReceiveAsync(ManagedWebSocket websocket) } catch (Exception e) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); } } } @@ -1065,7 +1103,8 @@ async Task ReceiveAsync(ManagedWebSocket websocket) } catch (Exception e) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); } } } @@ -1094,7 +1133,8 @@ async Task ReceiveAsync(ManagedWebSocket websocket) } catch (Exception e) { - this._logger.Log(LogLevel.Information, LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); } } }, cancellationToken); @@ -1289,6 +1329,14 @@ public abstract class ManagedWebSocket : System.Net.WebSockets.WebSocket /// public Dictionary Extra { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + /// + /// Gets the header information of the WebSocket connection + /// + public Dictionary Headers + { + get => this.Extra.TryGetValue("Headers", out object headers) && headers is Dictionary ? headers as Dictionary : new Dictionary(); + } + /// /// Gets the state to include the full exception (with stack trace) in the close response when an exception is encountered and the WebSocket connection is closed /// @@ -1367,7 +1415,7 @@ await Task.WhenAll( /// Cleans up unmanaged resources (will send a close frame if the connection is still open) /// public override void Dispose() - => this.DisposeAsync().Wait(4321); + => this.DisposeAsync().GetAwaiter().GetResult(); internal virtual async Task DisposeAsync(WebSocketCloseStatus closeStatus = WebSocketCloseStatus.EndpointUnavailable, string closeStatusDescription = "Service is unavailable", CancellationToken cancellationToken = default(CancellationToken), Action onDisposed = null) { @@ -1375,8 +1423,7 @@ public override void Dispose() { this._disposing = true; Events.Log.WebSocketDispose(this.ID, this.State); - if (this.State == WebSocketState.Open) - await this.CloseOutputTimeoutAsync(closeStatus, closeStatusDescription, null, () => Events.Log.WebSocketDisposeCloseTimeout(this.ID, this.State), ex => Events.Log.WebSocketDisposeError(this.ID, this.State, ex.ToString())).ConfigureAwait(false); + await Task.WhenAll(this.State == WebSocketState.Open ? this.CloseOutputTimeoutAsync(closeStatus, closeStatusDescription, null, () => Events.Log.WebSocketDisposeCloseTimeout(this.ID, this.State), ex => Events.Log.WebSocketDisposeError(this.ID, this.State, ex.ToString())) : Task.CompletedTask).ConfigureAwait(false); try { onDisposed?.Invoke(); diff --git a/WebSocketHelper.cs b/WebSocketHelper.cs index f663cce..b9126f4 100644 --- a/WebSocketHelper.cs +++ b/WebSocketHelper.cs @@ -74,35 +74,25 @@ public static Func GetRecyclableMemoryStreamFactory() public static Task WriteHeaderAsync(this Stream stream, string header, CancellationToken cancellationToken = default(CancellationToken)) => stream.WriteAsync((header.Trim() + "\r\n\r\n").ToArraySegment(), cancellationToken); - /// - /// Computes a WebSocket accept key from a given key - /// - /// The WebSocket request key - /// A WebSocket accept key - public static string ComputeAcceptKey(this string key) + internal static string ComputeAcceptKey(this string key) => (key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").GetHash("SHA1").ToBase64(); - /// - /// Negotiates sub-protocol - /// - /// - /// - /// - public static string NegotiateSubProtocol(this IEnumerable requestedSubProtocols, IEnumerable supportedSubProtocols) + internal static string NegotiateSubProtocol(this IEnumerable requestedSubProtocols, IEnumerable supportedSubProtocols) => requestedSubProtocols == null || supportedSubProtocols == null || !requestedSubProtocols.Any() || !supportedSubProtocols.Any() ? null : requestedSubProtocols.Intersect(supportedSubProtocols).FirstOrDefault() ?? throw new SubProtocolNegotiationFailedException("Unable to negotiate a sub-protocol"); - /// - /// Set keep-alive interval to something more reasonable (because the TCP keep-alive default values of Windows are huge ~7200s) - /// - /// - /// - /// - public static void SetKeepAliveInterval(this Socket socket, uint keepaliveInterval = 60000, uint retryInterval = 10000) + internal static void SetKeepAliveInterval(this Socket socket, uint keepaliveInterval = 60000, uint retryInterval = 10000) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) socket.IOControl(IOControlCode.KeepAliveValues, ((uint)1).ToBytes().Concat(keepaliveInterval.ToBytes(), retryInterval.ToBytes()), null); } + + internal static Dictionary ToDictionary(this string @string) + => string.IsNullOrWhiteSpace(@string) + ? new Dictionary(StringComparer.OrdinalIgnoreCase) + : @string.Replace("\r", "").ToList("\n") + .Where(header => header.IndexOf(":") > 0) + .ToDictionary(header => header.Left(header.IndexOf(":")).Trim(), header => header.Right(header.Length - header.IndexOf(":") - 1).Trim(), StringComparer.OrdinalIgnoreCase); } } \ No newline at end of file diff --git a/WebSocketImplementation.cs b/WebSocketImplementation.cs index 8913cb8..cda357e 100644 --- a/WebSocketImplementation.cs +++ b/WebSocketImplementation.cs @@ -6,6 +6,7 @@ using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; +using System.Collections.Generic; using System.Collections.Concurrent; using Microsoft.Extensions.Logging; using net.vieapps.Components.WebSockets.Exceptions; @@ -60,7 +61,7 @@ internal class WebSocketImplementation : ManagedWebSocket protected override bool IncludeExceptionInCloseResponse { get; } #endregion - public WebSocketImplementation(Guid id, bool isClient, Func recycledStreamFactory, Stream stream, WebSocketOptions options, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint) + public WebSocketImplementation(Guid id, bool isClient, Func recycledStreamFactory, Stream stream, WebSocketOptions options, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, Dictionary headers) { if (options.KeepAliveInterval.Ticks < 0) throw new ArgumentException("KeepAliveInterval must be Zero or positive", nameof(options)); @@ -72,6 +73,7 @@ public WebSocketImplementation(Guid id, bool isClient, Func recycl this.RequestUri = requestUri; this.RemoteEndPoint = remoteEndPoint; this.LocalEndPoint = localEndPoint; + this.Extra["Headers"] = headers ?? new Dictionary(StringComparer.OrdinalIgnoreCase); this._recycledStreamFactory = recycledStreamFactory ?? WebSocketHelper.GetRecyclableMemoryStreamFactory(); this._stream = stream; @@ -93,6 +95,10 @@ public WebSocketImplementation(Guid id, bool isClient, Func recycl /// async Task PutOnTheWireAsync(MemoryStream stream, CancellationToken cancellationToken) { + // check disposed + if (this._disposed) + throw new ObjectDisposedException("WebSocketImplementation"); + // add into queue and check pending operations this._buffers.Enqueue(stream.ToArraySegment()); if (this._pending) @@ -102,10 +108,6 @@ async Task PutOnTheWireAsync(MemoryStream stream, CancellationToken cancellation return; } - // check disposed - if (this._disposing || this._disposed) - throw new ObjectDisposedException("WebSocketImplementation"); - // put data to wire this._pending = true; await this._lock.WaitAsync(cancellationToken).ConfigureAwait(false); diff --git a/WebSocketWrapper.cs b/WebSocketWrapper.cs index 1df1327..0d1ab58 100644 --- a/WebSocketWrapper.cs +++ b/WebSocketWrapper.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using System.Collections.Concurrent; +using System.Collections.Generic; using Microsoft.Extensions.Logging; using net.vieapps.Components.Utility; #endregion @@ -46,13 +47,14 @@ internal class WebSocketWrapper : ManagedWebSocket protected override bool IncludeExceptionInCloseResponse { get; } = false; #endregion - public WebSocketWrapper(System.Net.WebSockets.WebSocket websocket, Uri requestUri, EndPoint remoteEndPoint = null, EndPoint localEndPoint = null) + public WebSocketWrapper(System.Net.WebSockets.WebSocket websocket, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, Dictionary headers) { this._websocket = websocket; this.ID = Guid.NewGuid(); this.RequestUri = requestUri; this.RemoteEndPoint = remoteEndPoint; this.LocalEndPoint = localEndPoint; + this.Extra["Headers"] = headers ?? new Dictionary(StringComparer.OrdinalIgnoreCase); } /// @@ -74,6 +76,10 @@ public override Task ReceiveAsync(ArraySegment buf /// public override async Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) { + // check disposed + if (this._disposed) + throw new ObjectDisposedException("WebSocketWrapper"); + // add into queue and check pending operations this._buffers.Enqueue(new Tuple, WebSocketMessageType, bool>(buffer, messageType, endOfMessage)); if (this._pending) @@ -83,10 +89,6 @@ public override async Task SendAsync(ArraySegment buffer, WebSocketMessage return; } - // check disposed - if (this._disposing || this._disposed) - throw new ObjectDisposedException("WebSocketImplementation"); - // put data to wire this._pending = true; await this._lock.WaitAsync(cancellationToken).ConfigureAwait(false); From a7d934eb222158c10a5af2e0887fd5511cd0c8bb Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Tue, 26 Mar 2019 09:27:15 +0700 Subject: [PATCH 20/34] Improvement: add support of dual IP modes (v4 & v6) --- VIEApps.Components.WebSockets.csproj | 2 +- WebSocket.cs | 45 +++++++++++----------------- WebSocketHelper.cs | 12 +++++++- 3 files changed, 30 insertions(+), 29 deletions(-) diff --git a/VIEApps.Components.WebSockets.csproj b/VIEApps.Components.WebSockets.csproj index 7f39d76..54f18d7 100644 --- a/VIEApps.Components.WebSockets.csproj +++ b/VIEApps.Components.WebSockets.csproj @@ -11,7 +11,7 @@ ../VIEApps.Components.snk 10.2.1903.3 10.2.1903.3 - v10.2.netstandard-2+rev:2019.03.15-disposed.objects.headers + v10.2.netstandard-2+rev:2019.03.25-dual.IP.modes VIEApps NGX VIEApps.net false diff --git a/WebSocket.cs b/WebSocket.cs index 139ab19..71d569c 100644 --- a/WebSocket.cs +++ b/WebSocket.cs @@ -39,11 +39,6 @@ public class WebSocket : IDisposable TcpListener _tcpListener = null; bool _disposing = false, _disposed = false; - /// - /// Gets the listening port of the listener - /// - public int Port { get; private set; } = 46429; - /// /// Gets or sets the SSL certificate for securing connections /// @@ -203,17 +198,16 @@ public void StartListen(int port = 46429, X509Certificate2 certificate = null, A return; } - // listen + // set X.509 certificate + this.Certificate = certificate ?? this.Certificate; + + // open the listener and listen for incoming requests try { // open the listener - this.Port = port > IPEndPoint.MinPort && port < IPEndPoint.MaxPort ? port : 46429; - this.Certificate = certificate ?? this.Certificate; - - this._tcpListener = new TcpListener(IPAddress.Any, this.Port); - this._tcpListener.Server.NoDelay = this.NoDelay; - this._tcpListener.Server.SetKeepAliveInterval(); - this._tcpListener.Start(1024); + this._tcpListener = new TcpListener(IPAddress.IPv6Any, port > IPEndPoint.MinPort && port < IPEndPoint.MaxPort ? port : 46429); + this._tcpListener.Server.SetOptions(this.NoDelay, true); + this._tcpListener.Start(512); if (this._logger.IsEnabled(LogLevel.Debug)) { @@ -225,9 +219,10 @@ public void StartListen(int port = 46429, X509Certificate2 certificate = null, A platform += $" ({RuntimeInformation.FrameworkDescription.Trim()}) - SSL: {this.Certificate != null}"; if (this.Certificate != null) platform += $" ({this.Certificate.GetNameInfo(X509NameType.DnsName, false)} :: Issued by {this.Certificate.GetNameInfo(X509NameType.DnsName, true)})"; - this._logger.LogInformation($"The listener is started (listening port: {this.Port})\r\nPlatform: {platform}\r\nPowered by {WebSocketHelper.AgentName} v{this.GetType().Assembly.GetVersion()}"); + this._logger.LogInformation($"The listener is started => {this._tcpListener.Server.LocalEndPoint}\r\nPlatform: {platform}\r\nPowered by {WebSocketHelper.AgentName} v{this.GetType().Assembly.GetVersion()}"); } + // callback when success try { onSuccess?.Invoke(); @@ -243,7 +238,7 @@ public void StartListen(int port = 46429, X509Certificate2 certificate = null, A } catch (SocketException ex) { - var message = $"Error occurred while listening on port \"{this.Port}\". Make sure another application is not running and consuming this port."; + var message = $"Error occurred while listening on port \"{(port > IPEndPoint.MinPort && port < IPEndPoint.MaxPort ? port : 46429)}\". Make sure another application is not running and consuming this port."; if (this._logger.IsEnabled(LogLevel.Debug)) this._logger.Log(LogLevel.Error, message, ex); try @@ -325,11 +320,7 @@ async Task ListenAsync() try { while (!this._listeningCTS.IsCancellationRequested) - { - var tcpClient = await this._tcpListener.AcceptTcpClientAsync().WithCancellationToken(this._listeningCTS.Token).ConfigureAwait(false); - tcpClient.Client.SetKeepAliveInterval(); - this.AcceptClient(tcpClient); - } + this.AcceptClient(await this._tcpListener.AcceptTcpClientAsync().WithCancellationToken(this._listeningCTS.Token).ConfigureAwait(false)); } catch (Exception ex) { @@ -349,10 +340,12 @@ async Task AcceptClientAsync(TcpClient tcpClient) ManagedWebSocket websocket = null; try { - var id = Guid.NewGuid(); - var endpoint = tcpClient.Client.RemoteEndPoint; + // set optins + tcpClient.Client.SetOptions(this.NoDelay); // get stream + var id = Guid.NewGuid(); + var endpoint = tcpClient.Client.RemoteEndPoint; Stream stream = null; if (this.Certificate != null) try @@ -551,11 +544,9 @@ async Task ConnectAsync(Uri uri, WebSocketOptions options, Action requestedSu ? null : requestedSubProtocols.Intersect(supportedSubProtocols).FirstOrDefault() ?? throw new SubProtocolNegotiationFailedException("Unable to negotiate a sub-protocol"); - internal static void SetKeepAliveInterval(this Socket socket, uint keepaliveInterval = 60000, uint retryInterval = 10000) + internal static void SetOptions(this Socket socket, bool noDelay = true, bool dualMode = false, uint keepaliveInterval = 60000, uint retryInterval = 10000) { + // general options + socket.NoDelay = noDelay; + if (dualMode) + { + socket.DualMode = true; + socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, false); + socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1); + } + + // specifict options (only avalable when running on Windows) if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) socket.IOControl(IOControlCode.KeepAliveValues, ((uint)1).ToBytes().Concat(keepaliveInterval.ToBytes(), retryInterval.ToBytes()), null); } From cce26735e2a5929bbeadafa037f328cf850ce300 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Mon, 1 Apr 2019 22:02:21 +0700 Subject: [PATCH 21/34] Upgrade to latest components to release new nuget version --- VIEApps.Components.WebSockets.csproj | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/VIEApps.Components.WebSockets.csproj b/VIEApps.Components.WebSockets.csproj index 54f18d7..757bc9a 100644 --- a/VIEApps.Components.WebSockets.csproj +++ b/VIEApps.Components.WebSockets.csproj @@ -9,14 +9,14 @@ VIEApps NGX WebSockets false ../VIEApps.Components.snk - 10.2.1903.3 - 10.2.1903.3 - v10.2.netstandard-2+rev:2019.03.25-dual.IP.modes + 10.2.1904.1 + 10.2.1904.1 + v10.2.netstandard-2+rev:2019.04.01-dual.IP.modes VIEApps NGX VIEApps.net false VIEApps.Components.WebSockets - 10.2.1903.3 + 10.2.1904.1 VIEApps NGX WebSockets High performance WebSocket on .NET Standard 2.0 (both server and client - standalone or wrapper of System.Net.WebSockets.WebSocket) VIEApps.net @@ -24,7 +24,7 @@ LICENSE.md ..\ websocket;websockets;websocket-client;websocket-server;websocket-wrapper;vieapps;vieapps.components - Upgrade to latest components + Do some improvement: dual IP modes, locking mechanims, extra headers, ... https://github.com/vieapps/Components.Utility/raw/master/logo.png https://github.com/vieapps/Components.WebSockets https://github.com/vieapps/Components.WebSockets @@ -39,7 +39,7 @@ - + \ No newline at end of file From 71b2281470f70bf131c52a355901a626a0bbb516 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Tue, 9 Apr 2019 03:17:57 +0700 Subject: [PATCH 22/34] Add options to build custom ping/pong payload --- PingPong.cs | 112 ++++++++--------------------- WebSocket.cs | 140 +++++++++++++++++++++++++++---------- WebSocketImplementation.cs | 31 +++----- WebSocketOptions.cs | 33 ++++++--- WebSocketWrapper.cs | 2 +- 5 files changed, 168 insertions(+), 150 deletions(-) diff --git a/PingPong.cs b/PingPong.cs index b707ccb..fc6ac6a 100644 --- a/PingPong.cs +++ b/PingPong.cs @@ -9,86 +9,40 @@ namespace net.vieapps.Components.WebSockets { - /// - /// Pong EventArgs - /// - internal class PongEventArgs : EventArgs - { - /// - /// The data extracted from a Pong WebSocket frame - /// - public ArraySegment Payload { get; } - - /// - /// Initialises a new instance of the PongEventArgs class - /// - /// The pong payload must be 125 bytes or less (can be zero bytes) - public PongEventArgs(ArraySegment payload) => this.Payload = payload; - } - - // -------------------------------------------------- - - /// - /// Ping Pong Manager used to facilitate ping pong WebSocket messages - /// - internal interface IPingPongManager - { - /// - /// Raised when a Pong frame is received - /// - event EventHandler Pong; - - /// - /// Sends a ping frame - /// - /// The payload (must be 125 bytes or less) - /// The cancellation token - Task SendPingAsync(ArraySegment payload, CancellationToken cancellation = default(CancellationToken)); - } - - // -------------------------------------------------- - - /// - /// Ping Pong Manager used to facilitate ping pong WebSocket messages - /// - internal class PingPongManager : IPingPongManager + internal class PingPongManager { readonly WebSocketImplementation _websocket; - readonly Task _pingTask; readonly CancellationToken _cancellationToken; - readonly Stopwatch _stopwatch; - long _pingSentTicks; + readonly Action _onPong; + readonly Func _getPongPayload; + readonly Func _getPingPayload; + long _pingTimestamp = 0; - /// - /// Raised when a Pong frame is received - /// - public event EventHandler Pong; - - /// - /// Initialises a new instance of the PingPongManager to facilitate ping pong WebSocket messages. - /// - /// The WebSocket instance used to listen to ping messages and send pong messages - /// The token used to cancel a pending ping send AND the automatic sending of ping messages if KeepAliveInterval is positive - public PingPongManager(WebSocketImplementation websocket, CancellationToken cancellationToken) + public PingPongManager(WebSocketImplementation websocket, WebSocketOptions options, CancellationToken cancellationToken) { this._websocket = websocket; - this._websocket.Pong += this.DoPong; this._cancellationToken = cancellationToken; - this._stopwatch = Stopwatch.StartNew(); - this._pingTask = Task.Run(this.DoPingAsync); + this._getPongPayload = options.GetPongPayload; + this._onPong = options.OnPong; + if (this._websocket.KeepAliveInterval != TimeSpan.Zero) + { + this._getPingPayload = options.GetPingPayload; + Task.Run(this.SendPingAsync).ConfigureAwait(false); + } + } + + public void OnPong(byte[] pong) + { + this._pingTimestamp = 0; + this._onPong?.Invoke(this._websocket, pong); } - /// - /// Sends a ping frame - /// - /// The payload (must be 125 bytes of less) - /// The cancellation token - public Task SendPingAsync(ArraySegment payload, CancellationToken cancellationToken = default(CancellationToken)) - => this._websocket.SendPingAsync(payload, cancellationToken); + public Task SendPongAsync(byte[] ping) + => this._websocket.SendPongAsync((this._getPongPayload?.Invoke(this._websocket, ping) ?? ping).ToArraySegment(), this._cancellationToken); - async Task DoPingAsync() + public async Task SendPingAsync() { - Events.Log.PingPongManagerStarted(this._websocket.ID, (int)this._websocket.KeepAliveInterval.TotalSeconds); + Events.Log.PingPongManagerStarted(this._websocket.ID, this._websocket.KeepAliveInterval.TotalSeconds.CastAs()); try { while (!this._cancellationToken.IsCancellationRequested) @@ -97,30 +51,22 @@ async Task DoPingAsync() if (this._websocket.State != WebSocketState.Open) break; - if (this._pingSentTicks != 0) + if (this._pingTimestamp != 0) { Events.Log.KeepAliveIntervalExpired(this._websocket.ID, (int)this._websocket.KeepAliveInterval.TotalSeconds); - await this._websocket.CloseAsync(WebSocketCloseStatus.NormalClosure, $"No Pong message received in response to a Ping after KeepAliveInterval ({this._websocket.KeepAliveInterval})", this._cancellationToken).ConfigureAwait(false); + await this._websocket.CloseAsync(WebSocketCloseStatus.NormalClosure, $"No PONG message received in response to a PING message after keep-alive-interval ({this._websocket.KeepAliveInterval})", this._cancellationToken).ConfigureAwait(false); break; } - this._pingSentTicks = this._stopwatch.Elapsed.Ticks; - await this.SendPingAsync(this._pingSentTicks.ToArraySegment(), this._cancellationToken).ConfigureAwait(false); + this._pingTimestamp = DateTime.Now.ToUnixTimestamp(); + await this._websocket.SendPingAsync((this._getPingPayload?.Invoke(this._websocket) ?? this._pingTimestamp.ToBytes()).ToArraySegment(), this._cancellationToken).ConfigureAwait(false); } } - catch (OperationCanceledException) + catch (Exception) { - // normal, do nothing + // do nothing } Events.Log.PingPongManagerEnded(this._websocket.ID); } - - protected virtual void OnPong(PongEventArgs args) => this.Pong?.Invoke(this, args); - - void DoPong(object sender, PongEventArgs arg) - { - this._pingSentTicks = 0; - this.OnPong(arg); - } } } \ No newline at end of file diff --git a/WebSocket.cs b/WebSocket.cs index 71d569c..4ec1364 100644 --- a/WebSocket.cs +++ b/WebSocket.cs @@ -40,27 +40,27 @@ public class WebSocket : IDisposable bool _disposing = false, _disposed = false; /// - /// Gets or sets the SSL certificate for securing connections + /// Gets or Sets the SSL certificate for securing connections /// public X509Certificate2 Certificate { get; set; } = null; /// - /// Gets or sets the SSL protocol for securing connections with SSL Certificate + /// Gets or Sets the SSL protocol for securing connections with SSL Certificate /// public SslProtocols SslProtocol { get; set; } = SslProtocols.Tls; /// - /// Gets or sets the collection of supported sub-protocol + /// Gets or Sets the collection of supported sub-protocol /// public IEnumerable SupportedSubProtocols { get; set; } = new string[0]; /// - /// Gets or sets keep-alive interval (seconds) for sending ping messages from server + /// Gets or Sets the keep-alive interval for sending ping messages from server /// public TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(60); /// - /// Gets or sets a value that specifies whether the listener is disable the Nagle algorithm or not (default is true - means disable for better performance) + /// Gets or Sets a value that specifies whether the listener is disable the Nagle algorithm or not (default is true - means disable for better performance) /// /// /// Set to true to send a message immediately with the least amount of latency (typical usage for chat) @@ -71,9 +71,9 @@ public class WebSocket : IDisposable public bool NoDelay { get; set; } = true; /// - /// Gets or sets await interval (miliseconds) while receiving messages + /// Gets or Sets await interval between two rounds of receiving messages /// - public int AwaitInterval { get; set; } = 0; + public TimeSpan ReceivingAwaitInterval { get; set; } = TimeSpan.Zero; #endregion #region Event Handlers @@ -181,7 +181,10 @@ public static string AgentName /// The SSL Certificate to secure connections /// Action to fire when start successful /// Action to fire when failed to start - public void StartListen(int port = 46429, X509Certificate2 certificate = null, Action onSuccess = null, Action onFailure = null) + /// The function to get the custom 'PING' playload to send a 'PING' message + /// The function to get the custom 'PONG' playload to response to a 'PING' message + /// The action to fire when a 'PONG' message has been sent + public void StartListen(int port = 46429, X509Certificate2 certificate = null, Action onSuccess = null, Action onFailure = null, Func getPingPayload = null, Func getPongPayload = null, Action onPong = null) { // check if (this._tcpListener != null) @@ -234,7 +237,7 @@ public void StartListen(int port = 46429, X509Certificate2 certificate = null, A } // listen for incoming connection requests - this.Listen(); + this.Listen(getPingPayload, getPongPayload, onPong); } catch (SocketException ex) { @@ -267,6 +270,18 @@ public void StartListen(int port = 46429, X509Certificate2 certificate = null, A } } + /// + /// Starts to listen for client requests as a WebSocket server + /// + /// The port for listening + /// Action to fire when start successful + /// Action to fire when failed to start + /// The function to get the custom 'PING' playload to send a 'PING' message + /// The function to get the custom 'PONG' playload to response to a 'PING' message + /// The action to fire when a 'PONG' message has been sent + public void StartListen(int port, Action onSuccess, Action onFailure, Func getPingPayload, Func getPongPayload, Action onPong) + => this.StartListen(port, null, onSuccess, onFailure, getPingPayload, getPongPayload, onPong); + /// /// Starts to listen for client requests as a WebSocket server /// @@ -274,14 +289,24 @@ public void StartListen(int port = 46429, X509Certificate2 certificate = null, A /// Action to fire when start successful /// Action to fire when failed to start public void StartListen(int port, Action onSuccess, Action onFailure) - => this.StartListen(port, null, onSuccess, onFailure); + => this.StartListen(port, onSuccess, onFailure, null, null, null); + + /// + /// Starts to listen for client requests as a WebSocket server + /// + /// The port for listening + /// The function to get the custom 'PING' playload to send a 'PING' message + /// The function to get the custom 'PONG' playload to response to a 'PING' message + /// The action to fire when a 'PONG' message has been sent + public void StartListen(int port, Func getPingPayload, Func getPongPayload, Action onPong) + => this.StartListen(port, null, null, getPingPayload, getPongPayload, onPong); /// /// Starts to listen for client requests as a WebSocket server /// /// The port for listening public void StartListen(int port) - => this.StartListen(port, null, null); + => this.StartListen(port, null, null, null); /// /// Stops listen @@ -309,18 +334,18 @@ public void StopListen(bool cancelPendings = true) } } - Task Listen() + Task Listen(Func getPingPayload, Func getPongPayload, Action onPong) { this._listeningCTS = CancellationTokenSource.CreateLinkedTokenSource(this._processingCTS.Token); - return this.ListenAsync(); + return this.ListenAsync(getPingPayload, getPongPayload, onPong); } - async Task ListenAsync() + async Task ListenAsync(Func getPingPayload, Func getPongPayload, Action onPong) { try { while (!this._listeningCTS.IsCancellationRequested) - this.AcceptClient(await this._tcpListener.AcceptTcpClientAsync().WithCancellationToken(this._listeningCTS.Token).ConfigureAwait(false)); + this.AcceptClient(await this._tcpListener.AcceptTcpClientAsync().WithCancellationToken(this._listeningCTS.Token).ConfigureAwait(false), getPingPayload, getPongPayload, onPong); } catch (Exception ex) { @@ -332,10 +357,10 @@ async Task ListenAsync() } } - void AcceptClient(TcpClient tcpClient) - => Task.Run(() => this.AcceptClientAsync(tcpClient)).ConfigureAwait(false); + void AcceptClient(TcpClient tcpClient, Func getPingPayload, Func getPongPayload, Action onPong) + => Task.Run(() => this.AcceptClientAsync(tcpClient, getPingPayload, getPongPayload, onPong)).ConfigureAwait(false); - async Task AcceptClientAsync(TcpClient tcpClient) + async Task AcceptClientAsync(TcpClient tcpClient, Func getPingPayload, Func getPongPayload, Action onPong) { ManagedWebSocket websocket = null; try @@ -415,14 +440,18 @@ async Task AcceptClientAsync(TcpClient tcpClient) } // accept the request - var options = new WebSocketOptions - { - KeepAliveInterval = this.KeepAliveInterval - }; Events.Log.AcceptWebSocketStarted(id); if (this._logger.IsEnabled(LogLevel.Trace)) this._logger.Log(LogLevel.Debug, $"The request has requested an upgrade to WebSocket protocol, negotiating WebSocket handshake ({id} @ {endpoint})"); + var options = new WebSocketOptions + { + KeepAliveInterval = this.KeepAliveInterval.Ticks < 0 ? TimeSpan.FromSeconds(60) : this.KeepAliveInterval, + GetPingPayload = getPingPayload, + GetPongPayload = getPongPayload, + OnPong = onPong + }; + try { // check the version (support version 13 and above) @@ -441,7 +470,7 @@ async Task AcceptClientAsync(TcpClient tcpClient) // negotiate subprotocol match = new Regex("Sec-WebSocket-Protocol: (.*)").Match(header); options.SubProtocol = match.Success - ? match.Groups[1].Value.Trim().Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries).NegotiateSubProtocol(this.SupportedSubProtocols) + ? match.Groups[1].Value?.Trim().Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries).NegotiateSubProtocol(this.SupportedSubProtocols) : null; // handshake @@ -656,7 +685,7 @@ async Task ConnectAsync(Uri uri, WebSocketOptions options, Action 0) + if (this.ReceivingAwaitInterval.Ticks > 0) try { - await Task.Delay(this.AwaitInterval, this._processingCTS.Token).ConfigureAwait(false); + await Task.Delay(this.ReceivingAwaitInterval, this._processingCTS.Token).ConfigureAwait(false); } catch { @@ -1320,14 +1349,6 @@ public abstract class ManagedWebSocket : System.Net.WebSockets.WebSocket /// public Dictionary Extra { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); - /// - /// Gets the header information of the WebSocket connection - /// - public Dictionary Headers - { - get => this.Extra.TryGetValue("Headers", out object headers) && headers is Dictionary ? headers as Dictionary : new Dictionary(); - } - /// /// Gets the state to include the full exception (with stack trace) in the close response when an exception is encountered and the WebSocket connection is closed /// @@ -1434,5 +1455,54 @@ internal virtual void Close() { } } #endregion + #region Extra information + /// + /// Sets the value of a specified key of the extra information + /// + /// + /// + /// + public void Set(string key, T value) + => this.Extra[key] = value; + + /// + /// Gets the value of a specified key from the extra information + /// + /// + /// + /// + /// + public T Get(string key, T @default = default(T)) + => this.Extra.TryGetValue(key, out object value) && value != null && value is T + ? (T)value + : @default; + + /// + /// Removes the value of a specified key from the extra information + /// + /// + /// + public bool Remove(string key) + => this.Extra.Remove(key); + + /// + /// Removes the value of a specified key from the extra information + /// + /// + /// + /// + /// + public bool Remove(string key, out T value) + { + value = this.Get(key); + return this.Remove(key); + } + + /// + /// Gets the header information of the WebSocket connection + /// + public Dictionary Headers => this.Get("Headers", new Dictionary()); + #endregion + } } \ No newline at end of file diff --git a/WebSocketImplementation.cs b/WebSocketImplementation.cs index cda357e..3db331f 100644 --- a/WebSocketImplementation.cs +++ b/WebSocketImplementation.cs @@ -1,6 +1,7 @@ #region Related components using System; using System.Net; +using System.Linq; using System.IO; using System.IO.Compression; using System.Net.WebSockets; @@ -21,7 +22,7 @@ internal class WebSocketImplementation : ManagedWebSocket #region Properties readonly Func _recycledStreamFactory; readonly Stream _stream; - readonly IPingPongManager _pingpongManager; + readonly PingPongManager _pingpongManager; WebSocketState _state; WebSocketMessageType _continuationFrameMessageType = WebSocketMessageType.Binary; WebSocketCloseStatus? _closeStatus; @@ -33,8 +34,6 @@ internal class WebSocketImplementation : ManagedWebSocket readonly CancellationTokenSource _processingCTS; readonly ConcurrentQueue> _buffers = new ConcurrentQueue>(); - public event EventHandler Pong; - /// /// Gets the state that indicates the reason why the remote endpoint initiated the close handshake /// @@ -73,18 +72,14 @@ public WebSocketImplementation(Guid id, bool isClient, Func recycl this.RequestUri = requestUri; this.RemoteEndPoint = remoteEndPoint; this.LocalEndPoint = localEndPoint; - this.Extra["Headers"] = headers ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + this.Set("Headers", headers); this._recycledStreamFactory = recycledStreamFactory ?? WebSocketHelper.GetRecyclableMemoryStreamFactory(); this._stream = stream; this._state = WebSocketState.Open; this._subProtocol = options.SubProtocol; this._processingCTS = new CancellationTokenSource(); - - if (this.KeepAliveInterval == TimeSpan.Zero) - Events.Log.KeepAliveIntervalZero(this.ID); - else - this._pingpongManager = new PingPongManager(this, this._processingCTS.Token); + this._pingpongManager = new PingPongManager(this, options, this._processingCTS.Token); } /// @@ -183,11 +178,11 @@ public override async Task ReceiveAsync(ArraySegment(buffer.Array, buffer.Offset, frame.Count), cts.Token).ConfigureAwait(false); + await this._pingpongManager.SendPongAsync(buffer.Take(frame.Count).ToArray()).ConfigureAwait(false); break; case WebSocketOpCode.Pong: - this.Pong?.Invoke(this, new PongEventArgs(new ArraySegment(buffer.Array, frame.Count, buffer.Offset))); + this._pingpongManager.OnPong(buffer.Take(frame.Count).ToArray()); break; case WebSocketOpCode.Text: @@ -260,24 +255,18 @@ async Task RespondToCloseFrameAsync(WebSocketFrame frame return new WebSocketReceiveResult(frame.Count, WebSocketMessageType.Close, frame.IsFinBitSet, frame.CloseStatus, frame.CloseStatusDescription); } - /// - /// Called when a Pong frame is received - /// - /// - protected virtual void OnPong(PongEventArgs args) => this.Pong?.Invoke(this, args); - /// /// Calls this when got ping messages (pong payload must be 125 bytes or less, pong should contain the same payload as the ping) /// /// /// /// - async Task SendPongAsync(ArraySegment payload, CancellationToken cancellationToken) + public async Task SendPongAsync(ArraySegment payload, CancellationToken cancellationToken) { // exceeded max length if (payload.Count > 125) { - var ex = new BufferOverflowException($"Max pong message size is 125 bytes, exceeded: {payload.Count}"); + var ex = new BufferOverflowException($"Max PONG message size is 125 bytes, exceeded: {payload.Count}"); await this.CloseOutputTimeoutAsync(WebSocketCloseStatus.ProtocolError, ex.Message, ex).ConfigureAwait(false); throw ex; } @@ -294,7 +283,7 @@ async Task SendPongAsync(ArraySegment payload, CancellationToken cancellat } catch (Exception ex) { - await this.CloseOutputTimeoutAsync(WebSocketCloseStatus.EndpointUnavailable, "Unable to send Pong response", ex).ConfigureAwait(false); + await this.CloseOutputTimeoutAsync(WebSocketCloseStatus.EndpointUnavailable, "Unable to send PONG response", ex).ConfigureAwait(false); throw; } } @@ -308,7 +297,7 @@ async Task SendPongAsync(ArraySegment payload, CancellationToken cancellat public async Task SendPingAsync(ArraySegment payload, CancellationToken cancellationToken) { if (payload.Count > 125) - throw new BufferOverflowException($"Max ping message size is 125 bytes, exceeded: {payload.Count}"); + throw new BufferOverflowException($"Max PING message size is 125 bytes, exceeded: {payload.Count}"); if (this._state == WebSocketState.Open) using (var stream = this._recycledStreamFactory()) diff --git a/WebSocketOptions.cs b/WebSocketOptions.cs index 9633d70..8c72eff 100644 --- a/WebSocketOptions.cs +++ b/WebSocketOptions.cs @@ -12,26 +12,24 @@ public class WebSocketOptions /// Gets or sets how often to send ping requests to the remote endpoint /// /// - /// This is done to prevent proxy servers from closing your connection - /// The default is TimeSpan.Zero meaning that it is disabled. + /// This is done to prevent proxy servers from closing your connection, the default is TimeSpan.Zero meaning that it is disabled. /// WebSocket servers usually send ping messages so it is not normally necessary for the client to send them (hence the TimeSpan.Zero default) - /// You can manually control ping pong messages using the PingPongManager class. - /// If you do that it is advisible to set this KeepAliveInterval to zero + /// You can manually control ping pong messages using the PingPongManager class. If you do that it is advisible to set this KeepAliveInterval to zero. /// public TimeSpan KeepAliveInterval { get; set; } = TimeSpan.Zero; /// - /// Gets or sets the sub-protocol (Sec-WebSocket-Protocol) + /// Gets or Sets the sub-protocol (Sec-WebSocket-Protocol) /// public string SubProtocol { get; set; } /// - /// Gets or sets the extensions (Sec-WebSocket-Extensions) + /// Gets or Sets the extensions (Sec-WebSocket-Extensions) /// public string Extensions { get; set; } /// - /// Gets or sets state to send a message immediately or not + /// Gets or Sets state to send a message immediately or not /// /// /// Set to true to send a message immediately with the least amount of latency (typical usage for chat) @@ -42,12 +40,12 @@ public class WebSocketOptions public bool NoDelay { get; set; } = true; /// - /// Gets or sets the additional headers + /// Gets or Sets the additional headers /// public Dictionary AdditionalHeaders { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); /// - /// Gets or sets the state to include the full exception (with stack trace) in the close response when an exception is encountered and the WebSocket connection is closed + /// Gets or Sets the state to include the full exception (with stack trace) in the close response when an exception is encountered and the WebSocket connection is closed /// /// /// The default is false @@ -55,11 +53,26 @@ public class WebSocketOptions public bool IncludeExceptionInCloseResponse { get; set; } = false; /// - /// Gets or sets whether remote certificate errors should be ignored + /// Gets or Sets whether remote certificate errors should be ignored /// /// /// The default is false /// public bool IgnoreCertificateErrors { get; set; } = false; + + /// + /// Gets or Sets the function to prepare the custom 'PING' playload to send a 'PING' message + /// + public Func GetPingPayload { get; set; } + + /// + /// Gets or Sets the function to prepare the custom 'PONG' playload to response to a 'PING' message + /// + public Func GetPongPayload { get; set; } + + /// + /// Gets or Sets the action to fire when a 'PONG' message has been sent + /// + public Action OnPong { get; set; } } } \ No newline at end of file diff --git a/WebSocketWrapper.cs b/WebSocketWrapper.cs index 0d1ab58..7c9792c 100644 --- a/WebSocketWrapper.cs +++ b/WebSocketWrapper.cs @@ -54,7 +54,7 @@ public WebSocketWrapper(System.Net.WebSockets.WebSocket websocket, Uri requestUr this.RequestUri = requestUri; this.RemoteEndPoint = remoteEndPoint; this.LocalEndPoint = localEndPoint; - this.Extra["Headers"] = headers ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + this.Set("Headers", headers); } /// From d3c4d04008941e3f285abe313970c74972400dc5 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Tue, 9 Apr 2019 09:55:05 +0700 Subject: [PATCH 23/34] Add options to build custom ping/pong payload --- PingPong.cs | 2 +- WebSocket.cs | 24 ++++++++++++++++-------- WebSocketImplementation.cs | 27 ++++++++++++--------------- 3 files changed, 29 insertions(+), 24 deletions(-) diff --git a/PingPong.cs b/PingPong.cs index fc6ac6a..2ba1bbb 100644 --- a/PingPong.cs +++ b/PingPong.cs @@ -54,7 +54,7 @@ public async Task SendPingAsync() if (this._pingTimestamp != 0) { Events.Log.KeepAliveIntervalExpired(this._websocket.ID, (int)this._websocket.KeepAliveInterval.TotalSeconds); - await this._websocket.CloseAsync(WebSocketCloseStatus.NormalClosure, $"No PONG message received in response to a PING message after keep-alive-interval ({this._websocket.KeepAliveInterval})", this._cancellationToken).ConfigureAwait(false); + await this._websocket.CloseAsync(WebSocketCloseStatus.NormalClosure, $"No PONG message received in response to a PING message after keep-alive interval ({this._websocket.KeepAliveInterval})", this._cancellationToken).ConfigureAwait(false); break; } diff --git a/WebSocket.cs b/WebSocket.cs index 4ec1364..2891097 100644 --- a/WebSocket.cs +++ b/WebSocket.cs @@ -40,22 +40,22 @@ public class WebSocket : IDisposable bool _disposing = false, _disposed = false; /// - /// Gets or Sets the SSL certificate for securing connections + /// Gets or Sets the SSL certificate for securing connections (server) /// - public X509Certificate2 Certificate { get; set; } = null; + public X509Certificate2 Certificate { get; set; } /// - /// Gets or Sets the SSL protocol for securing connections with SSL Certificate + /// Gets or Sets the SSL protocol for securing connections with SSL Certificate (server) /// public SslProtocols SslProtocol { get; set; } = SslProtocols.Tls; /// - /// Gets or Sets the collection of supported sub-protocol + /// Gets or Sets the collection of supported sub-protocol (server) /// public IEnumerable SupportedSubProtocols { get; set; } = new string[0]; /// - /// Gets or Sets the keep-alive interval for sending ping messages from server + /// Gets or Sets the keep-alive interval for sending ping messages (server) /// public TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(60); @@ -134,12 +134,20 @@ public Action OnMessageReceive } #endregion + /// + /// Creates new an instance of the centralized WebSocket + /// + /// The cancellation token + public WebSocket(CancellationToken cancellationToken) + : this(null, cancellationToken) { } + /// /// Creates new an instance of the centralized WebSocket /// /// The logger factory /// The cancellation token - public WebSocket(ILoggerFactory loggerFactory, CancellationToken cancellationToken) : this(loggerFactory, null, cancellationToken) { } + public WebSocket(ILoggerFactory loggerFactory, CancellationToken cancellationToken) + : this(loggerFactory, null, cancellationToken) { } /// /// Creates new an instance of the centralized WebSocket @@ -802,7 +810,7 @@ public void Connect(string location, Action onSuccess, Action< => this.Connect(location, null, onSuccess, onFailure); #endregion - #region Wrap a WebSocket connection of ASP.NET / ASP.NET Core + #region Wrap a WebSocket connection /// /// Wraps a WebSocket connection of ASP.NET / ASP.NET Core and acts like a WebSocket server /// @@ -1193,7 +1201,7 @@ async Task AddWebSocketAsync(ManagedWebSocket websocket) if (!this.AddWebSocket(websocket)) { if (websocket != null) - await Task.Delay(UtilityService.GetRandomNumber(123, 456)).ConfigureAwait(false); + await Task.Delay(UtilityService.GetRandomNumber(123, 456), this._processingCTS.Token).ConfigureAwait(false); return this.AddWebSocket(websocket); } return true; diff --git a/WebSocketImplementation.cs b/WebSocketImplementation.cs index 3db331f..db190ae 100644 --- a/WebSocketImplementation.cs +++ b/WebSocketImplementation.cs @@ -16,7 +16,7 @@ namespace net.vieapps.Components.WebSockets { - internal class WebSocketImplementation : ManagedWebSocket + public class WebSocketImplementation : ManagedWebSocket { #region Properties @@ -24,7 +24,7 @@ internal class WebSocketImplementation : ManagedWebSocket readonly Stream _stream; readonly PingPongManager _pingpongManager; WebSocketState _state; - WebSocketMessageType _continuationFrameMessageType = WebSocketMessageType.Binary; + WebSocketMessageType _continuationMessageType = WebSocketMessageType.Binary; WebSocketCloseStatus? _closeStatus; string _closeStatusDescription; bool _isContinuationFrame = false; @@ -60,15 +60,12 @@ internal class WebSocketImplementation : ManagedWebSocket protected override bool IncludeExceptionInCloseResponse { get; } #endregion - public WebSocketImplementation(Guid id, bool isClient, Func recycledStreamFactory, Stream stream, WebSocketOptions options, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, Dictionary headers) + internal WebSocketImplementation(Guid id, bool isClient, Func recycledStreamFactory, Stream stream, WebSocketOptions options, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, Dictionary headers) { - if (options.KeepAliveInterval.Ticks < 0) - throw new ArgumentException("KeepAliveInterval must be Zero or positive", nameof(options)); - this.ID = id; this.IsClient = isClient; this.IncludeExceptionInCloseResponse = options.IncludeExceptionInCloseResponse; - this.KeepAliveInterval = options.KeepAliveInterval; + this.KeepAliveInterval = options.KeepAliveInterval.Ticks < 0 ? TimeSpan.FromSeconds(60) : options.KeepAliveInterval; this.RequestUri = requestUri; this.RemoteEndPoint = remoteEndPoint; this.LocalEndPoint = localEndPoint; @@ -148,27 +145,27 @@ public override async Task ReceiveAsync(ArraySegment ReceiveAsync(ArraySegment Date: Thu, 11 Apr 2019 08:49:53 +0700 Subject: [PATCH 24/34] Improvement: custom ping/pong payload --- README.md | 4 ++-- VIEApps.Components.WebSockets.csproj | 12 ++++++------ WebSocketImplementation.cs | 25 +++++++++++++++++-------- WebSocketWrapper.cs | 11 +++++++++-- 4 files changed, 34 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 572fef3..c64f366 100644 --- a/README.md +++ b/README.md @@ -117,8 +117,8 @@ var websocket = new WebSocket And this class has some methods for working on both side of client and server role: ```csharp -void Connect(Uri uri, WebSocketOptions options, Action onSuccess, Action onFailed); -void StartListen(int port, X509Certificate2 certificate, Action onSuccess, Action onFailed); +void Connect(Uri uri, WebSocketOptions options, Action onSuccess, Action onFailure); +void StartListen(int port, X509Certificate2 certificate, Action onSuccess, Action onFailure, Func getPingPayload, Func getPongPayload, Action onPong); void StopListen(); ``` diff --git a/VIEApps.Components.WebSockets.csproj b/VIEApps.Components.WebSockets.csproj index 757bc9a..6d2c844 100644 --- a/VIEApps.Components.WebSockets.csproj +++ b/VIEApps.Components.WebSockets.csproj @@ -9,14 +9,14 @@ VIEApps NGX WebSockets false ../VIEApps.Components.snk - 10.2.1904.1 - 10.2.1904.1 - v10.2.netstandard-2+rev:2019.04.01-dual.IP.modes + 10.2.1904.3 + 10.2.1904.3 + v10.2.netstandard-2+rev:2019.04.11-custom.ping.pong VIEApps NGX VIEApps.net false VIEApps.Components.WebSockets - 10.2.1904.1 + 10.2.1904.3 VIEApps NGX WebSockets High performance WebSocket on .NET Standard 2.0 (both server and client - standalone or wrapper of System.Net.WebSockets.WebSocket) VIEApps.net @@ -24,7 +24,7 @@ LICENSE.md ..\ websocket;websockets;websocket-client;websocket-server;websocket-wrapper;vieapps;vieapps.components - Do some improvement: dual IP modes, locking mechanims, extra headers, ... + Improvement: custom ping/pong payload, dual IP modes, locking mechanims, extra headers, ... https://github.com/vieapps/Components.Utility/raw/master/logo.png https://github.com/vieapps/Components.WebSockets https://github.com/vieapps/Components.WebSockets @@ -39,7 +39,7 @@ - + \ No newline at end of file diff --git a/WebSocketImplementation.cs b/WebSocketImplementation.cs index db190ae..29e4f7a 100644 --- a/WebSocketImplementation.cs +++ b/WebSocketImplementation.cs @@ -16,23 +16,24 @@ namespace net.vieapps.Components.WebSockets { - public class WebSocketImplementation : ManagedWebSocket + internal class WebSocketImplementation : ManagedWebSocket { #region Properties readonly Func _recycledStreamFactory; readonly Stream _stream; readonly PingPongManager _pingpongManager; + readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); + readonly string _subProtocol; + readonly CancellationTokenSource _processingCTS; + readonly ConcurrentQueue> _buffers = new ConcurrentQueue>(); + readonly ILogger _logger; WebSocketState _state; WebSocketMessageType _continuationMessageType = WebSocketMessageType.Binary; WebSocketCloseStatus? _closeStatus; string _closeStatusDescription; bool _isContinuationFrame = false; - readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); bool _pending = false; - readonly string _subProtocol; - readonly CancellationTokenSource _processingCTS; - readonly ConcurrentQueue> _buffers = new ConcurrentQueue>(); /// /// Gets the state that indicates the reason why the remote endpoint initiated the close handshake @@ -60,7 +61,7 @@ public class WebSocketImplementation : ManagedWebSocket protected override bool IncludeExceptionInCloseResponse { get; } #endregion - internal WebSocketImplementation(Guid id, bool isClient, Func recycledStreamFactory, Stream stream, WebSocketOptions options, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, Dictionary headers) + public WebSocketImplementation(Guid id, bool isClient, Func recycledStreamFactory, Stream stream, WebSocketOptions options, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, Dictionary headers) { this.ID = id; this.IsClient = isClient; @@ -77,6 +78,7 @@ internal WebSocketImplementation(Guid id, bool isClient, Func recy this._subProtocol = options.SubProtocol; this._processingCTS = new CancellationTokenSource(); this._pingpongManager = new PingPongManager(this, options, this._processingCTS.Token); + this._logger = Logger.CreateLogger(); } /// @@ -89,14 +91,19 @@ async Task PutOnTheWireAsync(MemoryStream stream, CancellationToken cancellation { // check disposed if (this._disposed) - throw new ObjectDisposedException("WebSocketImplementation"); + { + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.LogWarning($"Object disposed => {this.ID}"); + throw new ObjectDisposedException($"WebSocketImplementation => {this.ID}"); + } // add into queue and check pending operations this._buffers.Enqueue(stream.ToArraySegment()); if (this._pending) { Events.Log.PendingOperations(this.ID); - Logger.Log(LogLevel.Debug, LogLevel.Warning, $"#{Thread.CurrentThread.ManagedThreadId} Pendings => {this._buffers.Count:#,##0} ({this.ID} @ {this.RemoteEndPoint})"); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.LogWarning($"#{Thread.CurrentThread.ManagedThreadId} Pendings => {this._buffers.Count:#,##0} ({this.ID} @ {this.RemoteEndPoint})"); return; } @@ -211,6 +218,8 @@ public override async Task ReceiveAsync(ArraySegment {ex.Message}"); throw ex; } } diff --git a/WebSocketWrapper.cs b/WebSocketWrapper.cs index 7c9792c..3ac143f 100644 --- a/WebSocketWrapper.cs +++ b/WebSocketWrapper.cs @@ -19,6 +19,7 @@ internal class WebSocketWrapper : ManagedWebSocket readonly System.Net.WebSockets.WebSocket _websocket = null; readonly ConcurrentQueue, WebSocketMessageType, bool>> _buffers = new ConcurrentQueue, WebSocketMessageType, bool>>(); readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); + readonly ILogger _logger; bool _pending = false; /// @@ -50,6 +51,7 @@ internal class WebSocketWrapper : ManagedWebSocket public WebSocketWrapper(System.Net.WebSockets.WebSocket websocket, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, Dictionary headers) { this._websocket = websocket; + this._logger = Logger.CreateLogger(); this.ID = Guid.NewGuid(); this.RequestUri = requestUri; this.RemoteEndPoint = remoteEndPoint; @@ -78,14 +80,19 @@ public override async Task SendAsync(ArraySegment buffer, WebSocketMessage { // check disposed if (this._disposed) - throw new ObjectDisposedException("WebSocketWrapper"); + { + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.LogWarning($"Object disposed => {this.ID}"); + throw new ObjectDisposedException($"WebSocketWrapper => {this.ID}"); + } // add into queue and check pending operations this._buffers.Enqueue(new Tuple, WebSocketMessageType, bool>(buffer, messageType, endOfMessage)); if (this._pending) { Events.Log.PendingOperations(this.ID); - Logger.Log(LogLevel.Debug, LogLevel.Warning, $"#{Thread.CurrentThread.ManagedThreadId} Pendings => {this._buffers.Count:#,##0} ({this.ID} @ {this.RemoteEndPoint})"); + if (this._logger.IsEnabled(LogLevel.Debug)) + this._logger.LogWarning($"#{Thread.CurrentThread.ManagedThreadId} Pendings => {this._buffers.Count:#,##0} ({this.ID} @ {this.RemoteEndPoint})"); return; } From 8339f1a7c7942ac85e8f046a4b6db088f3171dec Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Wed, 1 May 2019 07:21:13 +0700 Subject: [PATCH 25/34] Upgrade to latest components --- PingPong.cs | 5 +---- README.md | 9 +++++--- VIEApps.Components.WebSockets.csproj | 12 +++++------ WebSocket.cs | 32 ---------------------------- 4 files changed, 13 insertions(+), 45 deletions(-) diff --git a/PingPong.cs b/PingPong.cs index 2ba1bbb..1364dd4 100644 --- a/PingPong.cs +++ b/PingPong.cs @@ -62,10 +62,7 @@ public async Task SendPingAsync() await this._websocket.SendPingAsync((this._getPingPayload?.Invoke(this._websocket) ?? this._pingTimestamp.ToBytes()).ToArraySegment(), this._cancellationToken).ConfigureAwait(false); } } - catch (Exception) - { - // do nothing - } + catch { } Events.Log.PingPongManagerEnded(this._websocket.ID); } } diff --git a/README.md b/README.md index c64f366..3bb5741 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,9 @@ public EndPoint LocalEndPoint { get; } // Extra information public Dictionary Extra { get; } + +// Headers information +public Dictionary Headers { get; } ``` ## Fly on the sky with Event-liked driven @@ -135,7 +138,7 @@ Use the **StopListen** method to stop the listener. ### WebSocket server with Secure WebSockets (wss://) Enabling secure connections requires two things: -- Pointing certificate to an X.509 certificate that containing a public and private key. +- Pointing certificate to an x509 certificate that containing a public and private key. - Using the scheme **wss** instead of **ws** (or **https** instead of **http**) on all clients ```csharp @@ -185,7 +188,7 @@ When integrate this component with your app that hosted by ASP.NET / ASP.NET Cor then the method **WrapAsync** is here to help. This method will return a task that run a process for receiving messages from this WebSocket connection. ```csharp -Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, string userAgent, string urlReferrer, string headers, string cookies, Action onSuccess); +Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, Dictionary headers, Action onSuccess); ``` And might be you need an extension method to wrap an existing WebSocket connection, then take a look at some lines of code below: @@ -316,7 +319,7 @@ bool CloseWebSocket(ManagedWebSocket websocket, WebSocketCloseStatus closeStatus Our prefers: - [Microsoft.Extensions.Logging.Console](https://www.nuget.org/packages/Microsoft.Extensions.Logging.Console): live logs -- [Serilog.Extensions.Logging.File](https://www.nuget.org/packages/Serilog.Extensions.Logging.File): rolling log files (by date) - high performance, and very simple to use +- [Serilog.Extensions.Logging.File](https://www.nuget.org/packages/Serilog.Extensions.Logging.File): rolling log files (by hour or date) - high performance, and very simple to use ### Namespaces diff --git a/VIEApps.Components.WebSockets.csproj b/VIEApps.Components.WebSockets.csproj index 6d2c844..9d18376 100644 --- a/VIEApps.Components.WebSockets.csproj +++ b/VIEApps.Components.WebSockets.csproj @@ -9,14 +9,14 @@ VIEApps NGX WebSockets false ../VIEApps.Components.snk - 10.2.1904.3 - 10.2.1904.3 - v10.2.netstandard-2+rev:2019.04.11-custom.ping.pong + 10.2.1905.1 + 10.2.1905.1 + v10.2.netstandard-2+rev:2019.05.01-latest.components VIEApps NGX VIEApps.net false VIEApps.Components.WebSockets - 10.2.1904.3 + 10.2.1905.1 VIEApps NGX WebSockets High performance WebSocket on .NET Standard 2.0 (both server and client - standalone or wrapper of System.Net.WebSockets.WebSocket) VIEApps.net @@ -24,7 +24,7 @@ LICENSE.md ..\ websocket;websockets;websocket-client;websocket-server;websocket-wrapper;vieapps;vieapps.components - Improvement: custom ping/pong payload, dual IP modes, locking mechanims, extra headers, ... + Upgrade to latest components https://github.com/vieapps/Components.Utility/raw/master/logo.png https://github.com/vieapps/Components.WebSockets https://github.com/vieapps/Components.WebSockets @@ -39,7 +39,7 @@ - + \ No newline at end of file diff --git a/WebSocket.cs b/WebSocket.cs index 2891097..eef821b 100644 --- a/WebSocket.cs +++ b/WebSocket.cs @@ -863,38 +863,6 @@ public Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, /// A task that run the receiving process when wrap successful or an exception when failed public Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, EndPoint remoteEndPoint) => this.WrapAsync(webSocket, requestUri, remoteEndPoint, null, new Dictionary(), null); - - /// - /// Wraps a WebSocket connection of ASP.NET / ASP.NET Core and acts like a WebSocket server - /// - /// The WebSocket connection of ASP.NET / ASP.NET Core - /// The original request URI of the WebSocket connection - /// The remote endpoint of the WebSocket connection - /// The local endpoint of the WebSocket connection - /// The string that presents the user agent of the client that made this request to the WebSocket connection - /// The string that presents the url referer of the client that made this request to the WebSocket connection - /// The string that presents the headers of the client that made this request to the WebSocket connection - /// The string that presents the cookies of the client that made this request to the WebSocket connection - /// The action to fire when the WebSocket connection is wrap success - /// A task that run the receiving process when wrap successful or an exception when failed - [Obsolete] - public Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, string userAgent, string urlReferer, string headers, string cookies, Action onSuccess = null) - => this.WrapAsync(webSocket, requestUri, remoteEndPoint, localEndPoint, (headers ?? "").ToDictionary(), onSuccess); - - /// - /// Wraps a WebSocket connection of ASP.NET / ASP.NET Core and acts like a WebSocket server - /// - /// The WebSocket connection of ASP.NET / ASP.NET Core - /// The original request URI of the WebSocket connection - /// The remote endpoint of the WebSocket connection - /// The local endpoint of the WebSocket connection - /// The string that presents the user agent of the client that made this request to the WebSocket connection - /// The string that presents the url referer of the client that made this request to the WebSocket connection - /// The action to fire when the WebSocket connection is wrap success - /// A task that run the receiving process when wrap successful or an exception when failed - [Obsolete] - public Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, string userAgent, string urlReferer, Action onSuccess) - => this.WrapAsync(webSocket, requestUri, remoteEndPoint, localEndPoint, new Dictionary(), onSuccess); #endregion #region Receive messages From 8b1ed45d847bf92230af2986c502c4c89a3d0db5 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Tue, 7 May 2019 03:01:36 +0700 Subject: [PATCH 26/34] Close websocket --- VIEApps.Components.WebSockets.csproj | 2 +- WebSocket.cs | 53 +++++++++++++++++++++++----- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/VIEApps.Components.WebSockets.csproj b/VIEApps.Components.WebSockets.csproj index 9d18376..c524d8d 100644 --- a/VIEApps.Components.WebSockets.csproj +++ b/VIEApps.Components.WebSockets.csproj @@ -11,7 +11,7 @@ ../VIEApps.Components.snk 10.2.1905.1 10.2.1905.1 - v10.2.netstandard-2+rev:2019.05.01-latest.components + v10.2.netstandard-2+rev:2019.05.07-closing VIEApps NGX VIEApps.net false diff --git a/WebSocket.cs b/WebSocket.cs index eef821b..06b21e0 100644 --- a/WebSocket.cs +++ b/WebSocket.cs @@ -890,7 +890,7 @@ async Task ReceiveAsync(ManagedWebSocket websocket) closeStatusDescription = websocket.IsClient ? "Disconnected" : "Service is unavailable"; } - this.CloseWebSocket(websocket, closeStatus, closeStatusDescription); + await this.CloseWebSocketAsync(websocket, closeStatus, closeStatusDescription).ConfigureAwait(false); try { this.ConnectionBrokenHandler?.Invoke(websocket); @@ -928,7 +928,7 @@ async Task ReceiveAsync(ManagedWebSocket websocket) { if (this._logger.IsEnabled(LogLevel.Trace)) this._logger.Log(LogLevel.Debug, $"The remote endpoint is initiated to close - Status: {result.CloseStatus} - Description: {result.CloseStatusDescription ?? "N/A"} ({websocket.ID} @ {websocket.RemoteEndPoint})"); - this.CloseWebSocket(websocket); + await this.CloseWebSocketAsync(websocket).ConfigureAwait(false); try { this.ConnectionBrokenHandler?.Invoke(websocket); @@ -949,7 +949,7 @@ async Task ReceiveAsync(ManagedWebSocket websocket) this._logger.Log(LogLevel.Debug, $"Close the connection because {message} ({websocket.ID} @ {websocket.RemoteEndPoint})"); await websocket.CloseAsync(WebSocketCloseStatus.MessageTooBig, $"{message}, send multiple frames instead.", CancellationToken.None).ConfigureAwait(false); - this.CloseWebSocket(websocket); + await this.CloseWebSocketAsync(websocket).ConfigureAwait(false); try { this.ConnectionBrokenHandler?.Invoke(websocket); @@ -987,7 +987,7 @@ async Task ReceiveAsync(ManagedWebSocket websocket) } catch { - this.CloseWebSocket(websocket, websocket.IsClient ? WebSocketCloseStatus.NormalClosure : WebSocketCloseStatus.EndpointUnavailable, websocket.IsClient ? "Disconnected" : "Service is unavailable"); + await this.CloseWebSocketAsync(websocket, websocket.IsClient ? WebSocketCloseStatus.NormalClosure : WebSocketCloseStatus.EndpointUnavailable, websocket.IsClient ? "Disconnected" : "Service is unavailable").ConfigureAwait(false); try { this.ConnectionBrokenHandler?.Invoke(websocket); @@ -1202,15 +1202,28 @@ public IEnumerable GetWebSockets(Func /// The close status to use /// A description of why we are closing /// - bool CloseWebsocket(ManagedWebSocket websocket, WebSocketCloseStatus closeStatus, string closeStatusDescription) + async Task CloseWebsocketAsync(ManagedWebSocket websocket, WebSocketCloseStatus closeStatus, string closeStatusDescription) { if (websocket.State == WebSocketState.Open) - Task.Run(() => websocket.DisposeAsync(closeStatus, closeStatusDescription)).ConfigureAwait(false); + await websocket.DisposeAsync(closeStatus, closeStatusDescription).ConfigureAwait(false); else websocket.Close(); return true; } + /// + /// Closes the WebSocket connection and remove from the centralized collections + /// + /// The WebSocket connection to close + /// The close status to use + /// A description of why we are closing + /// + bool CloseWebsocket(ManagedWebSocket websocket, WebSocketCloseStatus closeStatus, string closeStatusDescription) + { + Task.Run(() => this.CloseWebsocketAsync(websocket, closeStatus, closeStatusDescription)).ConfigureAwait(false); + return true; + } + /// /// Closes the WebSocket connection and remove from the centralized collections /// @@ -1219,10 +1232,22 @@ bool CloseWebsocket(ManagedWebSocket websocket, WebSocketCloseStatus closeStatus /// A description of why we are closing /// true if closed and destroyed public bool CloseWebSocket(Guid id, WebSocketCloseStatus closeStatus = WebSocketCloseStatus.EndpointUnavailable, string closeStatusDescription = "Service is unavailable") - => this._websockets.TryRemove(id, out ManagedWebSocket websocket) + => this._websockets.TryRemove(id, out var websocket) ? this.CloseWebsocket(websocket, closeStatus, closeStatusDescription) : false; + /// + /// Closes the WebSocket connection and remove from the centralized collections + /// + /// The identity of a WebSocket connection to close + /// The close status to use + /// A description of why we are closing + /// true if closed and destroyed + public Task CloseWebSocketAsync(Guid id, WebSocketCloseStatus closeStatus = WebSocketCloseStatus.EndpointUnavailable, string closeStatusDescription = "Service is unavailable") + => this._websockets.TryRemove(id, out var websocket) + ? this.CloseWebsocketAsync(websocket, closeStatus, closeStatusDescription) + : Task.FromResult(false); + /// /// Closes the WebSocket connection and remove from the centralized collections /// @@ -1233,7 +1258,19 @@ public bool CloseWebSocket(Guid id, WebSocketCloseStatus closeStatus = WebSocket public bool CloseWebSocket(ManagedWebSocket websocket, WebSocketCloseStatus closeStatus = WebSocketCloseStatus.EndpointUnavailable, string closeStatusDescription = "Service is unavailable") => websocket == null ? false - : this.CloseWebsocket(this._websockets.TryRemove(websocket.ID, out ManagedWebSocket webSocket) ? webSocket : websocket, closeStatus, closeStatusDescription); + : this.CloseWebsocket(this._websockets.TryRemove(websocket.ID, out var webSocket) ? webSocket : websocket, closeStatus, closeStatusDescription); + + /// + /// Closes the WebSocket connection and remove from the centralized collections + /// + /// The WebSocket connection to close + /// The close status to use + /// A description of why we are closing + /// true if closed and destroyed + public Task CloseWebSocketAsync(ManagedWebSocket websocket, WebSocketCloseStatus closeStatus = WebSocketCloseStatus.EndpointUnavailable, string closeStatusDescription = "Service is unavailable") + => websocket == null + ? Task.FromResult(false) + : this.CloseWebsocketAsync(this._websockets.TryRemove(websocket.ID, out var webSocket) ? webSocket : websocket, closeStatus, closeStatusDescription); #endregion #region Dispose From 78947cd07512c16ae12e248f7180307cfb2295c9 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Tue, 7 May 2019 11:13:23 +0700 Subject: [PATCH 27/34] Improvement: add async close methods --- VIEApps.Components.WebSockets.csproj | 10 +++++----- WebSocketHelper.cs | 8 ++++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/VIEApps.Components.WebSockets.csproj b/VIEApps.Components.WebSockets.csproj index c524d8d..29e5281 100644 --- a/VIEApps.Components.WebSockets.csproj +++ b/VIEApps.Components.WebSockets.csproj @@ -9,14 +9,14 @@ VIEApps NGX WebSockets false ../VIEApps.Components.snk - 10.2.1905.1 - 10.2.1905.1 + 10.2.1905.2 + 10.2.1905.2 v10.2.netstandard-2+rev:2019.05.07-closing VIEApps NGX VIEApps.net false VIEApps.Components.WebSockets - 10.2.1905.1 + 10.2.1905.2 VIEApps NGX WebSockets High performance WebSocket on .NET Standard 2.0 (both server and client - standalone or wrapper of System.Net.WebSockets.WebSocket) VIEApps.net @@ -24,7 +24,7 @@ LICENSE.md ..\ websocket;websockets;websocket-client;websocket-server;websocket-wrapper;vieapps;vieapps.components - Upgrade to latest components + Improvement: add async close methods https://github.com/vieapps/Components.Utility/raw/master/logo.png https://github.com/vieapps/Components.WebSockets https://github.com/vieapps/Components.WebSockets @@ -39,7 +39,7 @@ - + \ No newline at end of file diff --git a/WebSocketHelper.cs b/WebSocketHelper.cs index 9e2d450..32e2088 100644 --- a/WebSocketHelper.cs +++ b/WebSocketHelper.cs @@ -98,11 +98,15 @@ internal static void SetOptions(this Socket socket, bool noDelay = true, bool du socket.IOControl(IOControlCode.KeepAliveValues, ((uint)1).ToBytes().Concat(keepaliveInterval.ToBytes(), retryInterval.ToBytes()), null); } - internal static Dictionary ToDictionary(this string @string) - => string.IsNullOrWhiteSpace(@string) + internal static Dictionary ToDictionary(this string @string, Action> onPreCompleted = null) + { + var dictionary = string.IsNullOrWhiteSpace(@string) ? new Dictionary(StringComparer.OrdinalIgnoreCase) : @string.Replace("\r", "").ToList("\n") .Where(header => header.IndexOf(":") > 0) .ToDictionary(header => header.Left(header.IndexOf(":")).Trim(), header => header.Right(header.Length - header.IndexOf(":") - 1).Trim(), StringComparer.OrdinalIgnoreCase); + onPreCompleted?.Invoke(dictionary); + return dictionary; + } } } \ No newline at end of file From de99061d25d113fd84a273c49243b8993c7f94e1 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Wed, 22 May 2019 14:53:32 +0700 Subject: [PATCH 28/34] Improvement: OS name --- VIEApps.Components.WebSockets.csproj | 12 ++++++------ WebSocket.cs | 25 ++++++++++++++----------- WebSocketImplementation.cs | 4 ++-- WebSocketWrapper.cs | 4 ++-- 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/VIEApps.Components.WebSockets.csproj b/VIEApps.Components.WebSockets.csproj index 29e5281..b24c2de 100644 --- a/VIEApps.Components.WebSockets.csproj +++ b/VIEApps.Components.WebSockets.csproj @@ -9,14 +9,14 @@ VIEApps NGX WebSockets false ../VIEApps.Components.snk - 10.2.1905.2 - 10.2.1905.2 - v10.2.netstandard-2+rev:2019.05.07-closing + 10.2.1906.1 + 10.2.1906.1 + v10.2.netstandard-2+rev:2019.05.23-extensions VIEApps NGX VIEApps.net false VIEApps.Components.WebSockets - 10.2.1905.2 + 10.2.1906.1 VIEApps NGX WebSockets High performance WebSocket on .NET Standard 2.0 (both server and client - standalone or wrapper of System.Net.WebSockets.WebSocket) VIEApps.net @@ -25,8 +25,8 @@ ..\ websocket;websockets;websocket-client;websocket-server;websocket-wrapper;vieapps;vieapps.components Improvement: add async close methods + https://vieapps.net/ https://github.com/vieapps/Components.Utility/raw/master/logo.png - https://github.com/vieapps/Components.WebSockets https://github.com/vieapps/Components.WebSockets @@ -39,7 +39,7 @@ - + \ No newline at end of file diff --git a/WebSocket.cs b/WebSocket.cs index 06b21e0..b3ff8d2 100644 --- a/WebSocket.cs +++ b/WebSocket.cs @@ -222,12 +222,12 @@ public void StartListen(int port = 46429, X509Certificate2 certificate = null, A if (this._logger.IsEnabled(LogLevel.Debug)) { - var platform = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? "Windows" - : RuntimeInformation.IsOSPlatform(OSPlatform.Linux) - ? "Linux" - : "macOS"; - platform += $" ({RuntimeInformation.FrameworkDescription.Trim()}) - SSL: {this.Certificate != null}"; + var platform = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + ? "Linux" + : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) + ? "macOS" + : "Windows"; + platform += $" {RuntimeInformation.OSArchitecture.ToString().ToLower()} ({RuntimeInformation.FrameworkDescription.Trim()}) - SSL: {this.Certificate != null}"; if (this.Certificate != null) platform += $" ({this.Certificate.GetNameInfo(X509NameType.DnsName, false)} :: Issued by {this.Certificate.GetNameInfo(X509NameType.DnsName, true)})"; this._logger.LogInformation($"The listener is started => {this._tcpListener.Server.LocalEndPoint}\r\nPlatform: {platform}\r\nPowered by {WebSocketHelper.AgentName} v{this.GetType().Assembly.GetVersion()}"); @@ -1319,7 +1319,6 @@ public void Dispose() /// public abstract class ManagedWebSocket : System.Net.WebSockets.WebSocket { - protected bool _disposing = false, _disposed = false; #region Properties /// @@ -1366,6 +1365,10 @@ public abstract class ManagedWebSocket : System.Net.WebSockets.WebSocket /// Gets the state to include the full exception (with stack trace) in the close response when an exception is encountered and the WebSocket connection is closed /// protected abstract bool IncludeExceptionInCloseResponse { get; } + + protected bool IsDisposing { get; set; } = false; + + protected bool IsDisposed { get; set; } = false; #endregion #region Methods @@ -1444,9 +1447,9 @@ public override void Dispose() internal virtual async Task DisposeAsync(WebSocketCloseStatus closeStatus = WebSocketCloseStatus.EndpointUnavailable, string closeStatusDescription = "Service is unavailable", CancellationToken cancellationToken = default(CancellationToken), Action onDisposed = null) { - if (!this._disposing && !this._disposed) + if (!this.IsDisposing && !this.IsDisposed) { - this._disposing = true; + this.IsDisposing = true; Events.Log.WebSocketDispose(this.ID, this.State); await Task.WhenAll(this.State == WebSocketState.Open ? this.CloseOutputTimeoutAsync(closeStatus, closeStatusDescription, null, () => Events.Log.WebSocketDisposeCloseTimeout(this.ID, this.State), ex => Events.Log.WebSocketDisposeError(this.ID, this.State, ex.ToString())) : Task.CompletedTask).ConfigureAwait(false); try @@ -1454,8 +1457,8 @@ public override void Dispose() onDisposed?.Invoke(); } catch { } - this._disposed = true; - this._disposing = false; + this.IsDisposed = true; + this.IsDisposing = false; } } diff --git a/WebSocketImplementation.cs b/WebSocketImplementation.cs index 29e4f7a..85dd79a 100644 --- a/WebSocketImplementation.cs +++ b/WebSocketImplementation.cs @@ -90,7 +90,7 @@ public WebSocketImplementation(Guid id, bool isClient, Func recycl async Task PutOnTheWireAsync(MemoryStream stream, CancellationToken cancellationToken) { // check disposed - if (this._disposed) + if (this.IsDisposed) { if (this._logger.IsEnabled(LogLevel.Debug)) this._logger.LogWarning($"Object disposed => {this.ID}"); @@ -450,7 +450,7 @@ public override void Abort() internal override void Close() { - if (!this._disposing && !this._disposed) + if (!this.IsDisposing && !this.IsDisposed) { this._processingCTS.Cancel(); this._processingCTS.Dispose(); diff --git a/WebSocketWrapper.cs b/WebSocketWrapper.cs index 3ac143f..17e09bb 100644 --- a/WebSocketWrapper.cs +++ b/WebSocketWrapper.cs @@ -79,7 +79,7 @@ public override Task ReceiveAsync(ArraySegment buf public override async Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) { // check disposed - if (this._disposed) + if (this.IsDisposed) { if (this._logger.IsEnabled(LogLevel.Debug)) this._logger.LogWarning($"Object disposed => {this.ID}"); @@ -160,7 +160,7 @@ public override void Abort() internal override void Close() { - if (!this._disposing && !this._disposed && "System.Net.WebSockets.ManagedWebSocket".Equals($"{this._websocket.GetType()}")) + if (!this.IsDisposing && !this.IsDisposed && "System.Net.WebSockets.ManagedWebSocket".Equals($"{this._websocket.GetType()}")) this._websocket.Dispose(); } From 475edf551775245f5ad1b1e944400e8410105701 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Wed, 12 Jun 2019 09:39:08 +0700 Subject: [PATCH 29/34] Improvement: add async close methods --- VIEApps.Components.WebSockets.csproj | 10 +++++----- WebSocket.cs | 2 +- WebSocketHelper.cs | 3 +-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/VIEApps.Components.WebSockets.csproj b/VIEApps.Components.WebSockets.csproj index b24c2de..5c457ee 100644 --- a/VIEApps.Components.WebSockets.csproj +++ b/VIEApps.Components.WebSockets.csproj @@ -9,14 +9,14 @@ VIEApps NGX WebSockets false ../VIEApps.Components.snk - 10.2.1906.1 - 10.2.1906.1 - v10.2.netstandard-2+rev:2019.05.23-extensions + 10.2.1906.2 + 10.2.1906.2 + v10.2.netstandard-2+rev:2019.06.12-async.close VIEApps NGX VIEApps.net false VIEApps.Components.WebSockets - 10.2.1906.1 + 10.2.1906.2 VIEApps NGX WebSockets High performance WebSocket on .NET Standard 2.0 (both server and client - standalone or wrapper of System.Net.WebSockets.WebSocket) VIEApps.net @@ -39,7 +39,7 @@ - + \ No newline at end of file diff --git a/WebSocket.cs b/WebSocket.cs index b3ff8d2..6f30384 100644 --- a/WebSocket.cs +++ b/WebSocket.cs @@ -47,7 +47,7 @@ public class WebSocket : IDisposable /// /// Gets or Sets the SSL protocol for securing connections with SSL Certificate (server) /// - public SslProtocols SslProtocol { get; set; } = SslProtocols.Tls; + public SslProtocols SslProtocol { get; set; } = SslProtocols.Tls12; /// /// Gets or Sets the collection of supported sub-protocol (server) diff --git a/WebSocketHelper.cs b/WebSocketHelper.cs index 32e2088..b4d81f9 100644 --- a/WebSocketHelper.cs +++ b/WebSocketHelper.cs @@ -44,8 +44,7 @@ public static Func GetRecyclableMemoryStreamFactory() { var buffer = new byte[WebSocketHelper.ReceiveBufferSize]; var offset = 0; - var read = 0; - + int read; do { if (offset >= WebSocketHelper.ReceiveBufferSize) From 127cce2932018dd9d0e4f224a88adc728142c11e Mon Sep 17 00:00:00 2001 From: Viktor Nikolaev Date: Fri, 1 Mar 2019 09:50:22 +0300 Subject: [PATCH 30/34] Support for Server Name Indication --- WebSocket.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/WebSocket.cs b/WebSocket.cs index 7dcbd34..6f30384 100644 --- a/WebSocket.cs +++ b/WebSocket.cs @@ -599,6 +599,9 @@ async Task ConnectAsync(Uri uri, WebSocketOptions options, Action sslPolicyErrors == SslPolicyErrors.None || options.IgnoreCertificateErrors || RuntimeInformation.IsOSPlatform(OSPlatform.Linux), userCertificateSelectionCallback: (sender, host, certificates, certificate, issuers) => this.Certificate ); - await (stream as SslStream).AuthenticateAsClientAsync(targetHost: uri.Host).WithCancellationToken(this._processingCTS.Token).ConfigureAwait(false); + await (stream as SslStream).AuthenticateAsClientAsync(targetHost: sniHost ?? uri.Host).WithCancellationToken(this._processingCTS.Token).ConfigureAwait(false); Events.Log.ConnectionSecured(id); if (this._logger.IsEnabled(LogLevel.Trace)) @@ -648,7 +651,7 @@ async Task ConnectAsync(Uri uri, WebSocketOptions options, Action Date: Tue, 12 Mar 2019 23:08:59 +0700 Subject: [PATCH 31/34] Improvements: add locking mechanims, check disposed objects when sending, ... --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3bb5741..688197f 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ Use the **StopListen** method to stop the listener. ### WebSocket server with Secure WebSockets (wss://) Enabling secure connections requires two things: -- Pointing certificate to an x509 certificate that containing a public and private key. +- Pointing certificate to an X.509 certificate that containing a public and private key. - Using the scheme **wss** instead of **ws** (or **https** instead of **http**) on all clients ```csharp From 91141e08ebfc2ab9c63cfffc2575f348f9dfc8cb Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Tue, 9 Apr 2019 09:55:05 +0700 Subject: [PATCH 32/34] Add options to build custom ping/pong payload --- WebSocketImplementation.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WebSocketImplementation.cs b/WebSocketImplementation.cs index 85dd79a..6327f4e 100644 --- a/WebSocketImplementation.cs +++ b/WebSocketImplementation.cs @@ -16,7 +16,7 @@ namespace net.vieapps.Components.WebSockets { - internal class WebSocketImplementation : ManagedWebSocket + public class WebSocketImplementation : ManagedWebSocket { #region Properties @@ -61,7 +61,7 @@ internal class WebSocketImplementation : ManagedWebSocket protected override bool IncludeExceptionInCloseResponse { get; } #endregion - public WebSocketImplementation(Guid id, bool isClient, Func recycledStreamFactory, Stream stream, WebSocketOptions options, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, Dictionary headers) + internal WebSocketImplementation(Guid id, bool isClient, Func recycledStreamFactory, Stream stream, WebSocketOptions options, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, Dictionary headers) { this.ID = id; this.IsClient = isClient; From b92e651032f199bdc6029b3062fb578fefa5413f Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Wed, 1 May 2019 07:21:13 +0700 Subject: [PATCH 33/34] Upgrade to latest components --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 688197f..3bb5741 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ Use the **StopListen** method to stop the listener. ### WebSocket server with Secure WebSockets (wss://) Enabling secure connections requires two things: -- Pointing certificate to an X.509 certificate that containing a public and private key. +- Pointing certificate to an x509 certificate that containing a public and private key. - Using the scheme **wss** instead of **ws** (or **https** instead of **http**) on all clients ```csharp From 0da2b07fc8c3a3cab74433899cf5ff658e7e1d93 Mon Sep 17 00:00:00 2001 From: Viktor Nikolaev Date: Thu, 20 Jun 2019 15:00:34 +0300 Subject: [PATCH 34/34] fixed headers filtering --- WebSocket.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebSocket.cs b/WebSocket.cs index 6f30384..0cc5935 100644 --- a/WebSocket.cs +++ b/WebSocket.cs @@ -663,7 +663,7 @@ async Task ConnectAsync(Uri uri, WebSocketOptions options, Action handshake += $"{kvp.Key}: {kvp.Value}\r\n"); + options.AdditionalHeaders?.Where(x => !x.Key.Equals("Host", StringComparison.OrdinalIgnoreCase)).ForEach(kvp => handshake += $"{kvp.Key}: {kvp.Value}\r\n"); Events.Log.SendingHandshake(id, handshake); await stream.WriteHeaderAsync(handshake, this._processingCTS.Token).ConfigureAwait(false);