├── Icon.png ├── Exceptions ├── KeyMissingException.cs ├── BufferOverflowException.cs ├── ListenerSocketException.cs ├── HandshakeFailedException.cs ├── VersionNotSupportedException.cs ├── WrapWebSocketFailedException.cs ├── SubProtocolNegotiationFailedException.cs ├── EntityTooLargeException.cs └── InvalidResponseCodeException.cs ├── WebSocketOpCode.cs ├── .gitattributes ├── PingPong.cs ├── VIEApps.Components.WebSockets.csproj ├── WebSocketOptions.cs ├── .gitignore ├── WebSocketHelper.cs ├── WebSocketWrapper.cs ├── Events.cs ├── LICENSE.md ├── WebSocketFrame.cs ├── README.md ├── WebSocketImplementation.cs └── WebSocket.cs /Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vieapps/Components.WebSockets/HEAD/Icon.png -------------------------------------------------------------------------------- /Exceptions/KeyMissingException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace net.vieapps.Components.WebSockets.Exceptions 4 | { 5 | [Serializable] 6 | public class KeyMissingException : Exception 7 | { 8 | public KeyMissingException() : base() { } 9 | 10 | public KeyMissingException(string message) : base(message) { } 11 | 12 | public KeyMissingException(string message, Exception inner) : base(message, inner) { } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Exceptions/BufferOverflowException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace net.vieapps.Components.WebSockets.Exceptions 4 | { 5 | [Serializable] 6 | public class BufferOverflowException : Exception 7 | { 8 | public BufferOverflowException() : base() { } 9 | 10 | public BufferOverflowException(string message) : base(message) { } 11 | 12 | public BufferOverflowException(string message, Exception inner) : base(message, inner) { } 13 | } 14 | } -------------------------------------------------------------------------------- /Exceptions/ListenerSocketException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace net.vieapps.Components.WebSockets.Exceptions 4 | { 5 | [Serializable] 6 | public class ListenerSocketException : Exception 7 | { 8 | public ListenerSocketException() : base() { } 9 | 10 | public ListenerSocketException(string message) : base(message) { } 11 | 12 | public ListenerSocketException(string message, Exception inner) : base(message, inner) { } 13 | } 14 | } -------------------------------------------------------------------------------- /Exceptions/HandshakeFailedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace net.vieapps.Components.WebSockets.Exceptions 4 | { 5 | [Serializable] 6 | public class HandshakeFailedException : Exception 7 | { 8 | public HandshakeFailedException() : base() { } 9 | 10 | public HandshakeFailedException(string message) : base(message) { } 11 | 12 | public HandshakeFailedException(string message, Exception inner) : base(message, inner) { } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Exceptions/VersionNotSupportedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace net.vieapps.Components.WebSockets.Exceptions 4 | { 5 | [Serializable] 6 | public class VersionNotSupportedException : Exception 7 | { 8 | public VersionNotSupportedException() : base() { } 9 | 10 | public VersionNotSupportedException(string message) : base(message) { } 11 | 12 | public VersionNotSupportedException(string message, Exception inner) : base(message, inner) { } 13 | } 14 | } -------------------------------------------------------------------------------- /Exceptions/WrapWebSocketFailedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace net.vieapps.Components.WebSockets.Exceptions 4 | { 5 | [Serializable] 6 | public class WrapWebSocketFailedException : Exception 7 | { 8 | public WrapWebSocketFailedException() : base() { } 9 | 10 | public WrapWebSocketFailedException(string message) : base(message) { } 11 | 12 | public WrapWebSocketFailedException(string message, Exception innerException) : base(message, innerException) { } 13 | } 14 | } -------------------------------------------------------------------------------- /Exceptions/SubProtocolNegotiationFailedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace net.vieapps.Components.WebSockets.Exceptions 4 | { 5 | [Serializable] 6 | public class SubProtocolNegotiationFailedException : Exception 7 | { 8 | public SubProtocolNegotiationFailedException() : base() { } 9 | 10 | public SubProtocolNegotiationFailedException(string message) : base(message) { } 11 | 12 | public SubProtocolNegotiationFailedException(string message, Exception innerException) : base(message, innerException) { } 13 | } 14 | } -------------------------------------------------------------------------------- /Exceptions/EntityTooLargeException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace net.vieapps.Components.WebSockets.Exceptions 4 | { 5 | [Serializable] 6 | public class EntityTooLargeException : Exception 7 | { 8 | public EntityTooLargeException() : base() { } 9 | 10 | /// 11 | /// HTTP header too large to fit in buffer 12 | /// 13 | public EntityTooLargeException(string message) : base(message) { } 14 | 15 | public EntityTooLargeException(string message, Exception inner) : base(message, inner) { } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /WebSocketOpCode.cs: -------------------------------------------------------------------------------- 1 | namespace net.vieapps.Components.WebSockets 2 | { 3 | public enum WebSocketOpCode 4 | { 5 | /// 6 | /// Continuous message 7 | /// 8 | Continuation = 0, 9 | 10 | /// 11 | /// Text message 12 | /// 13 | Text = 1, 14 | 15 | /// 16 | /// Binary message 17 | /// 18 | Binary = 2, 19 | 20 | /// 21 | /// Closing message 22 | /// 23 | ConnectionClose = 8, 24 | 25 | /// 26 | /// Ping message 27 | /// 28 | Ping = 9, 29 | 30 | /// 31 | /// Pong message 32 | /// 33 | Pong = 10 34 | } 35 | } -------------------------------------------------------------------------------- /Exceptions/InvalidResponseCodeException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace net.vieapps.Components.WebSockets.Exceptions 4 | { 5 | [Serializable] 6 | public class InvalidResponseCodeException : Exception 7 | { 8 | public string ResponseCode { get; private set; } 9 | 10 | public string ResponseHeader { get; private set; } 11 | 12 | public string ResponseDetails { get; private set; } 13 | 14 | public InvalidResponseCodeException() : base() { } 15 | 16 | public InvalidResponseCodeException(string message) : base(message) { } 17 | 18 | public InvalidResponseCodeException(string responseCode, string responseDetails, string responseHeader) : base(responseCode) 19 | { 20 | this.ResponseCode = responseCode; 21 | this.ResponseDetails = responseDetails; 22 | this.ResponseHeader = responseHeader; 23 | } 24 | 25 | public InvalidResponseCodeException(string message, Exception inner) : base(message, inner) { } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | *.cs eol=crlf 6 | 7 | ############################################################################### 8 | # Set default behavior for command prompt diff. 9 | # 10 | # This is need for earlier builds of msysgit that does not have it on by 11 | # default for csharp files. 12 | # Note: This is only used by command line 13 | ############################################################################### 14 | #*.cs diff=csharp 15 | 16 | ############################################################################### 17 | # Set the merge driver for project and solution files 18 | # 19 | # Merging from the command prompt will add diff markers to the files if there 20 | # are conflicts (Merging from VS is not affected by the settings below, in VS 21 | # the diff markers are never inserted). Diff markers may cause the following 22 | # file extensions to fail to load in VS. An alternative would be to treat 23 | # these files as binary and thus will always conflict and require user 24 | # intervention with every merge. To do so, just uncomment the entries below 25 | ############################################################################### 26 | #*.sln merge=binary 27 | #*.csproj merge=binary 28 | #*.vbproj merge=binary 29 | #*.vcxproj merge=binary 30 | #*.vcproj merge=binary 31 | #*.dbproj merge=binary 32 | #*.fsproj merge=binary 33 | #*.lsproj merge=binary 34 | #*.wixproj merge=binary 35 | #*.modelproj merge=binary 36 | #*.sqlproj merge=binary 37 | #*.wwaproj merge=binary 38 | 39 | ############################################################################### 40 | # behavior for image files 41 | # 42 | # image files are treated as binary by default. 43 | ############################################################################### 44 | #*.jpg binary 45 | #*.png binary 46 | #*.gif binary 47 | 48 | ############################################################################### 49 | # diff behavior for common document formats 50 | # 51 | # Convert binary document formats to text before diffing them. This feature 52 | # is only available from the command line. Turn it on by uncommenting the 53 | # entries below. 54 | ############################################################################### 55 | #*.doc diff=astextplain 56 | #*.DOC diff=astextplain 57 | #*.docx diff=astextplain 58 | #*.DOCX diff=astextplain 59 | #*.dot diff=astextplain 60 | #*.DOT diff=astextplain 61 | #*.pdf diff=astextplain 62 | #*.PDF diff=astextplain 63 | #*.rtf diff=astextplain 64 | #*.RTF diff=astextplain 65 | -------------------------------------------------------------------------------- /PingPong.cs: -------------------------------------------------------------------------------- 1 | #region Related components 2 | using System; 3 | using System.Net.WebSockets; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using net.vieapps.Components.Utility; 7 | #endregion 8 | 9 | namespace net.vieapps.Components.WebSockets 10 | { 11 | internal class PingPongManager 12 | { 13 | readonly WebSocketImplementation _websocket; 14 | readonly CancellationToken _cancellationToken; 15 | readonly Action _onPong; 16 | readonly Func _getPongPayload; 17 | readonly Func _getPingPayload; 18 | long _pingTimestamp = 0; 19 | 20 | public PingPongManager(WebSocketImplementation websocket, WebSocketOptions options, CancellationToken cancellationToken) 21 | { 22 | this._websocket = websocket; 23 | this._cancellationToken = cancellationToken; 24 | this._getPongPayload = options.GetPongPayload; 25 | this._onPong = options.OnPong; 26 | if (this._websocket.KeepAliveInterval != TimeSpan.Zero) 27 | { 28 | this._getPingPayload = options.GetPingPayload; 29 | this.SendPingAsync().Execute(); 30 | } 31 | } 32 | 33 | public void OnPong(byte[] pong) 34 | { 35 | this._pingTimestamp = 0; 36 | this._onPong?.Invoke(this._websocket, pong); 37 | } 38 | 39 | public ValueTask SendPongAsync(byte[] ping) 40 | => this._websocket.SendPongAsync((this._getPongPayload?.Invoke(this._websocket, ping) ?? ping).ToArraySegment(), this._cancellationToken); 41 | 42 | public async Task SendPingAsync() 43 | { 44 | Events.Log.PingPongManagerStarted(this._websocket.ID, this._websocket.KeepAliveInterval.TotalSeconds.CastAs()); 45 | try 46 | { 47 | while (!this._cancellationToken.IsCancellationRequested) 48 | { 49 | await Task.Delay(this._websocket.KeepAliveInterval, this._cancellationToken).ConfigureAwait(false); 50 | if (this._websocket.State != WebSocketState.Open) 51 | break; 52 | 53 | if (this._pingTimestamp != 0) 54 | { 55 | Events.Log.KeepAliveIntervalExpired(this._websocket.ID, (int)this._websocket.KeepAliveInterval.TotalSeconds); 56 | 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); 57 | break; 58 | } 59 | 60 | this._pingTimestamp = DateTime.Now.ToUnixTimestamp(); 61 | await this._websocket.SendPingAsync((this._getPingPayload?.Invoke(this._websocket) ?? this._pingTimestamp.ToBytes()).ToArraySegment(), this._cancellationToken).ConfigureAwait(false); 62 | } 63 | } 64 | catch { } 65 | Events.Log.PingPongManagerEnded(this._websocket.ID); 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /VIEApps.Components.WebSockets.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0;netstandard2.0 5 | 3 6 | 1573,1591,IDE0054,IDE0063,IDE0066,IDE0090,CA1822 7 | bin/$(Configuration)/$(TargetFramework)/VIEApps.Components.WebSockets.xml 8 | net.vieapps.Components.WebSockets 9 | VIEApps.Components.WebSockets 10 | VIEApps NGX WebSockets 11 | 10.10$(ReleaseVersion) 12 | 10.10$(ReleaseVersion) 13 | 10.10.$([System.DateTime]::Now.Year).$([System.DateTime]::Now.Month).$([System.DateTime]::Now.Day)@$(TargetFramework)#sn:$(Sign)#$(ReleaseRevision) 14 | 10.10$(ReleaseVersion)$(ReleaseSuffix) 15 | VIEApps NGX 16 | VIEApps NGX WebSockets 17 | High performance WebSocket on .NET (both server and client - standalone or wrapper of System.Net.WebSockets.WebSocket) 18 | © $([System.DateTime]::Now.Year) VIEApps.net 19 | VIEApps.net 20 | VIEApps.net 21 | 22 | 23 | 24 | $(Sign) 25 | ../VIEApps.Components.snk 26 | $(SignConstant) 27 | $(GeneratePackage) 28 | true 29 | snupkg 30 | VIEApps.Components.WebSockets$(PackageSuffix) 31 | LICENSE.md 32 | README.md 33 | Icon.png 34 | ../ 35 | websocket;websockets;websocket-client;websocket-server;websocket-wrapper;vieapps;vieapps.components 36 | Latest components of .NET 10 37 | https://vieapps.net/ 38 | https://github.com/vieapps/Components.WebSockets 39 | git 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /WebSocketOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace net.vieapps.Components.WebSockets 5 | { 6 | /// 7 | /// Options for initializing a WebSocket connection 8 | /// 9 | public class WebSocketOptions 10 | { 11 | /// 12 | /// Gets or sets how often to send ping requests to the remote endpoint 13 | /// 14 | /// 15 | /// This is done to prevent proxy servers from closing your connection, the default is TimeSpan.Zero meaning that it is disabled. 16 | /// WebSocket servers usually send ping messages so it is not normally necessary for the client to send them (hence the TimeSpan.Zero default) 17 | /// You can manually control ping pong messages using the PingPongManager class. If you do that it is advisible to set this KeepAliveInterval to zero. 18 | /// 19 | public TimeSpan KeepAliveInterval { get; set; } = TimeSpan.Zero; 20 | 21 | /// 22 | /// Gets or Sets the sub-protocol (Sec-WebSocket-Protocol) 23 | /// 24 | public string SubProtocol { get; set; } 25 | 26 | /// 27 | /// Gets or Sets the extensions (Sec-WebSocket-Extensions) 28 | /// 29 | public string Extensions { get; set; } 30 | 31 | /// 32 | /// Gets or Sets state to send a message immediately or not 33 | /// 34 | /// 35 | /// Set to true to send a message immediately with the least amount of latency (typical usage for chat) 36 | /// This will disable Nagle's algorithm which can cause high tcp latency for small packets sent infrequently 37 | /// However, if you are streaming large packets or sending large numbers of small packets frequently it is advisable to set NoDelay to false 38 | /// This way data will be bundled into larger packets for better throughput 39 | /// 40 | public bool NoDelay { get; set; } = true; 41 | 42 | /// 43 | /// Gets or Sets the additional headers 44 | /// 45 | public Dictionary AdditionalHeaders { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); 46 | 47 | /// 48 | /// 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 49 | /// 50 | /// 51 | /// The default is false 52 | /// 53 | public bool IncludeExceptionInCloseResponse { get; set; } = false; 54 | 55 | /// 56 | /// Gets or Sets whether remote certificate errors should be ignored 57 | /// 58 | /// 59 | /// The default is false 60 | /// 61 | public bool IgnoreCertificateErrors { get; set; } = false; 62 | 63 | /// 64 | /// Gets or Sets the function to prepare the custom 'PING' playload to send a 'PING' message 65 | /// 66 | public Func GetPingPayload { get; set; } 67 | 68 | /// 69 | /// Gets or Sets the function to prepare the custom 'PONG' playload to response to a 'PING' message 70 | /// 71 | public Func GetPongPayload { get; set; } 72 | 73 | /// 74 | /// Gets or Sets the action to fire when a 'PONG' message has been sent 75 | /// 76 | public Action OnPong { get; set; } 77 | } 78 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc 262 | 263 | # Other kinds 264 | *~ 265 | .DS_Store 266 | .vs/ 267 | .vscode/ 268 | -------------------------------------------------------------------------------- /WebSocketHelper.cs: -------------------------------------------------------------------------------- 1 | #region Related components 2 | using System; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net.Sockets; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using System.Collections.Generic; 9 | using System.Runtime.InteropServices; 10 | using Microsoft.IO; 11 | using net.vieapps.Components.Utility; 12 | using net.vieapps.Components.WebSockets.Exceptions; 13 | #endregion 14 | 15 | namespace net.vieapps.Components.WebSockets 16 | { 17 | public static class WebSocketHelper 18 | { 19 | static RecyclableMemoryStreamManager RecyclableMemoryStreamManager { get; set; } 20 | 21 | /// 22 | /// Gets or sets the size (length) of the protocol buffer used to receive and parse frames 23 | /// 24 | public static int ReceiveBufferSize { get; internal set; } = 16 * 1024; 25 | 26 | /// 27 | /// Gets or sets the agent name of the protocol for working with related headers 28 | /// 29 | public static string AgentName { get; internal set; } = "VIEApps NGX WebSockets"; 30 | 31 | /// 32 | /// Gets a factory to get recyclable memory stream with RecyclableMemoryStreamManager class to limit LOH fragmentation and improve performance 33 | /// 34 | /// 35 | public static Func GetRecyclableMemoryStreamFactory() 36 | => () => 37 | { 38 | try 39 | { 40 | WebSocketHelper.RecyclableMemoryStreamManager = WebSocketHelper.RecyclableMemoryStreamManager ?? new RecyclableMemoryStreamManager(new RecyclableMemoryStreamManager.Options(16 * 1024, 4, 128 * 1024, 16 * 1024, 128 * 1024)); 41 | return WebSocketHelper.RecyclableMemoryStreamManager.GetStream(); 42 | } 43 | catch 44 | { 45 | return new MemoryStream(); 46 | } 47 | }; 48 | 49 | /// 50 | /// Reads the header 51 | /// 52 | /// The stream to read from 53 | /// The cancellation token 54 | /// The HTTP header 55 | public static async ValueTask ReadHeaderAsync(this Stream stream, CancellationToken cancellationToken = default) 56 | { 57 | var buffer = new byte[WebSocketHelper.ReceiveBufferSize]; 58 | var offset = 0; 59 | int read; 60 | do 61 | { 62 | if (offset >= WebSocketHelper.ReceiveBufferSize) 63 | throw new EntityTooLargeException("Header is too large to fit into the buffer"); 64 | 65 | #if NETSTANDARD2_0 66 | read = await stream.ReadAsync(buffer, offset, WebSocketHelper.ReceiveBufferSize - offset, cancellationToken).ConfigureAwait(false); 67 | #else 68 | read = await stream.ReadAsync(buffer.AsMemory(offset, WebSocketHelper.ReceiveBufferSize - offset), cancellationToken).ConfigureAwait(false); 69 | #endif 70 | offset += read; 71 | var header = buffer.GetString(offset); 72 | 73 | // as per specs, all headers should end like this 74 | if (header.Contains("\r\n\r\n")) 75 | return header; 76 | } 77 | while (read > 0); 78 | 79 | return string.Empty; 80 | } 81 | 82 | /// 83 | /// Writes the header 84 | /// 85 | /// The stream to write to 86 | /// The header (without the new line characters) 87 | /// The cancellation token 88 | /// 89 | public static async ValueTask WriteHeaderAsync(this Stream stream, string header, CancellationToken cancellationToken = default) 90 | => await stream.WriteAsync((header.Trim() + "\r\n\r\n").ToArraySegment(), cancellationToken).ConfigureAwait(false); 91 | 92 | internal static string ComputeAcceptKey(this string key) 93 | => (key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").GetHash("SHA1").ToBase64(); 94 | 95 | internal static string NegotiateSubProtocol(this IEnumerable requestedSubProtocols, IEnumerable supportedSubProtocols) 96 | => requestedSubProtocols == null || supportedSubProtocols == null || !requestedSubProtocols.Any() || !supportedSubProtocols.Any() 97 | ? null 98 | : requestedSubProtocols.Intersect(supportedSubProtocols).FirstOrDefault() ?? throw new SubProtocolNegotiationFailedException("Unable to negotiate a sub-protocol"); 99 | 100 | internal static void SetOptions(this Socket socket, bool noDelay = true, bool dualMode = false, uint keepaliveInterval = 60000, uint retryInterval = 10000) 101 | { 102 | // general options 103 | socket.NoDelay = noDelay; 104 | if (dualMode) 105 | { 106 | socket.DualMode = true; 107 | socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, false); 108 | socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1); 109 | } 110 | 111 | // specifict options (only avalable when running on Windows) 112 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 113 | socket.IOControl(IOControlCode.KeepAliveValues, ((uint)1).ToBytes().Concat(keepaliveInterval.ToBytes(), retryInterval.ToBytes()), null); 114 | } 115 | 116 | internal static Dictionary ToDictionary(this string @string, Action> onPreCompleted = null) 117 | { 118 | var dictionary = string.IsNullOrWhiteSpace(@string) 119 | ? new Dictionary(StringComparer.OrdinalIgnoreCase) 120 | : @string.Replace("\r", "").ToList("\n") 121 | .Where(header => header.IndexOf(":") > 0) 122 | .ToDictionary(header => header.Left(header.IndexOf(":")).Trim(), header => header.Right(header.Length - header.IndexOf(":") - 1).Trim(), StringComparer.OrdinalIgnoreCase); 123 | onPreCompleted?.Invoke(dictionary); 124 | return dictionary; 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /WebSocketWrapper.cs: -------------------------------------------------------------------------------- 1 | #region Related components 2 | using System; 3 | using System.Net; 4 | using System.Net.WebSockets; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using System.Collections.Concurrent; 8 | using System.Collections.Generic; 9 | using Microsoft.Extensions.Logging; 10 | using net.vieapps.Components.Utility; 11 | #endregion 12 | 13 | namespace net.vieapps.Components.WebSockets 14 | { 15 | internal class WebSocketWrapper : ManagedWebSocket 16 | { 17 | 18 | #region Properties 19 | readonly System.Net.WebSockets.WebSocket _websocket = null; 20 | readonly ConcurrentQueue, WebSocketMessageType, bool>> _buffers = new ConcurrentQueue, WebSocketMessageType, bool>>(); 21 | readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); 22 | readonly ILogger _logger; 23 | bool _pending = false; 24 | 25 | /// 26 | /// Gets the state that indicates the reason why the remote endpoint initiated the close handshake 27 | /// 28 | public override WebSocketCloseStatus? CloseStatus => this._websocket.CloseStatus; 29 | 30 | /// 31 | /// Gets the description to describe the reason why the connection was closed 32 | /// 33 | public override string CloseStatusDescription => this._websocket.CloseStatusDescription; 34 | 35 | /// 36 | /// Gets the current state of the WebSocket connection 37 | /// 38 | public override WebSocketState State => this._websocket.State; 39 | 40 | /// 41 | /// Gets the subprotocol that was negotiated during the opening handshake 42 | /// 43 | public override string SubProtocol => this._websocket.SubProtocol; 44 | 45 | /// 46 | /// 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 47 | /// 48 | protected override bool IncludeExceptionInCloseResponse { get; } = false; 49 | #endregion 50 | 51 | public WebSocketWrapper(System.Net.WebSockets.WebSocket websocket, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, Dictionary headers) 52 | { 53 | this._websocket = websocket; 54 | this._logger = Logger.CreateLogger(); 55 | this.ID = Guid.NewGuid(); 56 | this.RequestUri = requestUri; 57 | this.RemoteEndPoint = remoteEndPoint; 58 | this.LocalEndPoint = localEndPoint; 59 | this.Set("Headers", headers); 60 | } 61 | 62 | /// 63 | /// Receives data from the WebSocket connection asynchronously 64 | /// 65 | /// The buffer to copy data into 66 | /// The cancellation token 67 | /// 68 | public override Task ReceiveAsync(ArraySegment buffer, CancellationToken cancellationToken) 69 | => this._websocket.ReceiveAsync(buffer, cancellationToken); 70 | 71 | /// 72 | /// Sends data over the WebSocket connection asynchronously 73 | /// 74 | /// The buffer containing data to send 75 | /// The message type, can be Text or Binary 76 | /// true if this message is a standalone message (this is the norm), if its a multi-part message then false (and true for the last) 77 | /// the cancellation token 78 | /// 79 | public override async Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) 80 | { 81 | // check disposed 82 | if (this.IsDisposed) 83 | { 84 | if (this._logger.IsEnabled(LogLevel.Debug)) 85 | this._logger.LogWarning($"Object disposed => {this.ID}"); 86 | throw new ObjectDisposedException($"WebSocketWrapper => {this.ID}"); 87 | } 88 | 89 | // add into queue and check pending operations 90 | this._buffers.Enqueue(new Tuple, WebSocketMessageType, bool>(buffer, messageType, endOfMessage)); 91 | if (this._pending) 92 | { 93 | Events.Log.PendingOperations(this.ID); 94 | if (this._logger.IsEnabled(LogLevel.Debug)) 95 | this._logger.LogWarning($"WebSocketWrapper #{Environment.CurrentManagedThreadId} Pendings => {this._buffers.Count:#,##0} ({this.ID} @ {this.RemoteEndPoint})"); 96 | return; 97 | } 98 | 99 | // put data to wire 100 | this._pending = true; 101 | await this._lock.WaitAsync(cancellationToken).ConfigureAwait(false); 102 | try 103 | { 104 | while (this.State == WebSocketState.Open && !this._buffers.IsEmpty) 105 | if (this._buffers.TryDequeue(out var data)) 106 | await this._websocket.SendAsync(buffer: data.Item1, messageType: data.Item2, endOfMessage: data.Item3, cancellationToken: cancellationToken).ConfigureAwait(false); 107 | } 108 | catch (Exception) 109 | { 110 | throw; 111 | } 112 | finally 113 | { 114 | this._pending = false; 115 | this._lock.Release(); 116 | } 117 | } 118 | 119 | /// 120 | /// Polite close (use the close handshake) 121 | /// 122 | /// The close status to use 123 | /// A description of why we are closing 124 | /// The timeout cancellation token 125 | /// 126 | public override Task CloseAsync(WebSocketCloseStatus closeStatus, string closeStatusDescription, CancellationToken cancellationToken) 127 | => this._websocket.CloseAsync(closeStatus, closeStatusDescription, cancellationToken); 128 | 129 | /// 130 | /// Fire and forget close 131 | /// 132 | /// The close status to use 133 | /// A description of why we are closing 134 | /// The timeout cancellation token 135 | /// 136 | public override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string closeStatusDescription, CancellationToken cancellationToken) 137 | => this._websocket.CloseOutputAsync(closeStatus, closeStatusDescription, cancellationToken); 138 | 139 | /// 140 | /// Aborts the WebSocket without sending a Close frame 141 | /// 142 | public override void Abort() 143 | => this._websocket.Abort(); 144 | 145 | internal override ValueTask DisposeAsync(WebSocketCloseStatus closeStatus, string closeStatusDescription = "Service is unavailable", Action next = null) 146 | => base.DisposeAsync(closeStatus, closeStatusDescription, _ => 147 | { 148 | if ("System.Net.WebSockets.ManagedWebSocket".Equals($"{this._websocket.GetType()}")) 149 | this._websocket.Dispose(); 150 | this._lock.Dispose(); 151 | next?.Invoke(this); 152 | }); 153 | 154 | public override ValueTask DisposeAsync() 155 | { 156 | GC.SuppressFinalize(this); 157 | return this.IsDisposed ? new ValueTask(Task.CompletedTask) : this.DisposeAsync(WebSocketCloseStatus.EndpointUnavailable); 158 | } 159 | 160 | public override void Dispose() 161 | => this.DisposeAsync().Execute(true); 162 | 163 | ~WebSocketWrapper() 164 | => this.Dispose(); 165 | } 166 | } -------------------------------------------------------------------------------- /Events.cs: -------------------------------------------------------------------------------- 1 | #region Related components 2 | using System; 3 | using System.Net.Security; 4 | using System.Net.WebSockets; 5 | using System.Diagnostics.Tracing; 6 | #endregion 7 | 8 | namespace net.vieapps.Components.WebSockets 9 | { 10 | /// 11 | /// Use the Guid to locate this EventSource in PerfView using the Additional Providers box (without wildcard characters) 12 | /// 13 | [EventSource(Name = "WebSockets", Guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")] 14 | internal sealed class Events : EventSource 15 | { 16 | public static Events Log = new Events(); 17 | 18 | [Event(1, Level = EventLevel.Informational)] 19 | public void ClientConnectingToIPAddress(Guid guid, string ipAddress, int port) 20 | { 21 | if (this.IsEnabled()) 22 | this.WriteEvent(1, guid, ipAddress, port); 23 | } 24 | 25 | [Event(2, Level = EventLevel.Informational)] 26 | public void ClientConnectingToHost(Guid guid, string host, int port) 27 | { 28 | if (this.IsEnabled()) 29 | this.WriteEvent(2, guid, host, port); 30 | } 31 | 32 | [Event(3, Level = EventLevel.Informational)] 33 | public void AttemptingToSecureConnection(Guid guid) 34 | { 35 | if (this.IsEnabled()) 36 | this.WriteEvent(3, guid); 37 | } 38 | 39 | [Event(4, Level = EventLevel.Informational)] 40 | public void ConnectionSecured(Guid guid) 41 | { 42 | if (this.IsEnabled()) 43 | this.WriteEvent(4, guid); 44 | } 45 | 46 | [Event(5, Level = EventLevel.Informational)] 47 | public void ConnectionNotSecured(Guid guid) 48 | { 49 | if (this.IsEnabled()) 50 | this.WriteEvent(5, guid); 51 | } 52 | 53 | [Event(6, Level = EventLevel.Error)] 54 | public void ClientSslCertificateError(Guid guid, string exception) 55 | { 56 | if (this.IsEnabled()) 57 | this.WriteEvent(6, guid, exception); 58 | } 59 | 60 | [Event(7, Level = EventLevel.Informational)] 61 | public void HandshakeSent(Guid guid, string httpHeader) 62 | { 63 | if (this.IsEnabled()) 64 | this.WriteEvent(7, guid, httpHeader ?? string.Empty); 65 | } 66 | 67 | [Event(8, Level = EventLevel.Informational)] 68 | public void ReadingResponse(Guid guid) 69 | { 70 | if (this.IsEnabled()) 71 | this.WriteEvent(8, guid); 72 | } 73 | 74 | [Event(9, Level = EventLevel.Error)] 75 | public void ReadResponseError(Guid guid, string exception) 76 | { 77 | if (this.IsEnabled()) 78 | this.WriteEvent(9, guid, exception ?? string.Empty); 79 | } 80 | 81 | [Event(10, Level = EventLevel.Warning)] 82 | public void InvalidResponseCode(Guid guid, string response) 83 | { 84 | if (this.IsEnabled()) 85 | this.WriteEvent(10, guid, response ?? string.Empty); 86 | } 87 | 88 | [Event(11, Level = EventLevel.Error)] 89 | public void HandshakeFailure(Guid guid, string message) 90 | { 91 | if (this.IsEnabled()) 92 | this.WriteEvent(11, guid, message ?? string.Empty); 93 | } 94 | 95 | [Event(12, Level = EventLevel.Informational)] 96 | public void ClientHandshakeSuccess(Guid guid) 97 | { 98 | if (this.IsEnabled()) 99 | this.WriteEvent(12, guid); 100 | } 101 | 102 | [Event(13, Level = EventLevel.Informational)] 103 | public void ServerHandshakeSuccess(Guid guid) 104 | { 105 | if (this.IsEnabled()) 106 | this.WriteEvent(13, guid); 107 | } 108 | 109 | [Event(14, Level = EventLevel.Informational)] 110 | public void AcceptWebSocketStarted(Guid guid) 111 | { 112 | if (this.IsEnabled()) 113 | this.WriteEvent(14, guid); 114 | } 115 | 116 | [Event(15, Level = EventLevel.Informational)] 117 | public void SendingHandshake(Guid guid, string response) 118 | { 119 | if (this.IsEnabled()) 120 | this.WriteEvent(15, guid, response ?? string.Empty); 121 | } 122 | 123 | [Event(16, Level = EventLevel.Error)] 124 | public void WebSocketVersionNotSupported(Guid guid, string exception) 125 | { 126 | if (this.IsEnabled()) 127 | this.WriteEvent(16, guid, exception ?? string.Empty); 128 | } 129 | 130 | [Event(17, Level = EventLevel.Error)] 131 | public void BadRequest(Guid guid, string exception) 132 | { 133 | if (this.IsEnabled()) 134 | this.WriteEvent(17, guid, exception ?? string.Empty); 135 | } 136 | 137 | [Event(18, Level = EventLevel.Informational)] 138 | public void KeepAliveIntervalZero(Guid guid) 139 | { 140 | if (this.IsEnabled()) 141 | this.WriteEvent(18, guid); 142 | } 143 | 144 | [Event(19, Level = EventLevel.Informational)] 145 | public void PingPongManagerStarted(Guid guid, int keepAliveIntervalSeconds) 146 | { 147 | if (this.IsEnabled()) 148 | this.WriteEvent(19, guid, keepAliveIntervalSeconds); 149 | } 150 | 151 | [Event(20, Level = EventLevel.Informational)] 152 | public void PingPongManagerEnded(Guid guid) 153 | { 154 | if (this.IsEnabled()) 155 | this.WriteEvent(20, guid); 156 | } 157 | 158 | [Event(21, Level = EventLevel.Warning)] 159 | public void KeepAliveIntervalExpired(Guid guid, int keepAliveIntervalSeconds) 160 | { 161 | if (this.IsEnabled()) 162 | this.WriteEvent(21, guid, keepAliveIntervalSeconds); 163 | } 164 | 165 | [Event(22, Level = EventLevel.Warning)] 166 | public void CloseOutputAutoTimeout(Guid guid, WebSocketCloseStatus closeStatus, string statusDescription, string exception) 167 | { 168 | if (this.IsEnabled()) 169 | this.WriteEvent(22, guid, closeStatus, statusDescription ?? string.Empty, exception ?? string.Empty); 170 | } 171 | 172 | [Event(23, Level = EventLevel.Error)] 173 | public void CloseOutputAutoTimeoutCancelled(Guid guid, int timeoutSeconds, WebSocketCloseStatus closeStatus, string statusDescription, string exception) 174 | { 175 | if (this.IsEnabled()) 176 | this.WriteEvent(23, guid, timeoutSeconds, closeStatus, statusDescription ?? string.Empty, exception ?? string.Empty); 177 | } 178 | 179 | [Event(24, Level = EventLevel.Error)] 180 | public void CloseOutputAutoTimeoutError(Guid guid, string closeException, WebSocketCloseStatus closeStatus, string statusDescription, string exception) 181 | { 182 | if (this.IsEnabled()) 183 | this.WriteEvent(24, guid, closeException ?? string.Empty, closeStatus, statusDescription ?? string.Empty, exception ?? string.Empty); 184 | } 185 | 186 | [Event(25, Level = EventLevel.Error)] 187 | public void ServerSslCertificateError(Guid guid, string exception) 188 | { 189 | if (this.IsEnabled()) 190 | this.WriteEvent(25, guid, exception); 191 | } 192 | 193 | [Event(26, Level = EventLevel.Verbose)] 194 | public void SendingFrame(Guid guid, WebSocketOpCode webSocketOpCode, bool isFinBitSet, int numBytes, bool isPayloadCompressed) 195 | { 196 | if (this.IsEnabled(EventLevel.Verbose, EventKeywords.None)) 197 | this.WriteEvent(26, guid, webSocketOpCode, isFinBitSet, numBytes, isPayloadCompressed); 198 | } 199 | 200 | [Event(27, Level = EventLevel.Verbose)] 201 | public void ReceivedFrame(Guid guid, WebSocketOpCode webSocketOpCode, bool isFinBitSet, int numBytes) 202 | { 203 | if (this.IsEnabled(EventLevel.Verbose, EventKeywords.None)) 204 | this.WriteEvent(27, guid, webSocketOpCode, isFinBitSet, numBytes); 205 | } 206 | 207 | [Event(28, Level = EventLevel.Informational)] 208 | public void CloseOutputNoHandshake(Guid guid, WebSocketCloseStatus? closeStatus, string statusDescription) 209 | { 210 | if (this.IsEnabled()) 211 | this.WriteEvent(28, guid, $"{closeStatus}", statusDescription ?? string.Empty); 212 | } 213 | 214 | [Event(29, Level = EventLevel.Informational)] 215 | public void CloseHandshakeStarted(Guid guid, WebSocketCloseStatus? closeStatus, string statusDescription) 216 | { 217 | if (this.IsEnabled()) 218 | this.WriteEvent(29, guid, $"{closeStatus}", statusDescription ?? string.Empty); 219 | } 220 | 221 | [Event(30, Level = EventLevel.Informational)] 222 | public void CloseHandshakeRespond(Guid guid, WebSocketCloseStatus? closeStatus, string statusDescription) 223 | { 224 | if (this.IsEnabled()) 225 | this.WriteEvent(30, guid, $"{closeStatus}", statusDescription ?? string.Empty); 226 | } 227 | 228 | [Event(31, Level = EventLevel.Informational)] 229 | public void CloseHandshakeComplete(Guid guid) 230 | { 231 | if (this.IsEnabled()) 232 | this.WriteEvent(31, guid); 233 | } 234 | 235 | [Event(32, Level = EventLevel.Warning)] 236 | public void CloseFrameReceivedInUnexpectedState(Guid guid, WebSocketState webSocketState, WebSocketCloseStatus? closeStatus, string statusDescription) 237 | { 238 | if (this.IsEnabled()) 239 | this.WriteEvent(32, guid, webSocketState, $"{closeStatus}", statusDescription ?? string.Empty); 240 | } 241 | 242 | [Event(33, Level = EventLevel.Informational)] 243 | public void WebSocketDispose(Guid guid, WebSocketState webSocketState) 244 | { 245 | if (this.IsEnabled()) 246 | this.WriteEvent(33, guid, webSocketState); 247 | } 248 | 249 | [Event(34, Level = EventLevel.Warning)] 250 | public void WebSocketDisposeCloseTimeout(Guid guid, WebSocketState webSocketState) 251 | { 252 | if (this.IsEnabled()) 253 | this.WriteEvent(34, guid, webSocketState); 254 | } 255 | 256 | [Event(35, Level = EventLevel.Error)] 257 | public void WebSocketDisposeError(Guid guid, WebSocketState webSocketState, string exception) 258 | { 259 | if (this.IsEnabled()) 260 | this.WriteEvent(35, guid, webSocketState, exception ?? string.Empty); 261 | } 262 | 263 | [Event(36, Level = EventLevel.Warning)] 264 | public void InvalidStateBeforeClose(Guid guid, WebSocketState webSocketState) 265 | { 266 | if (this.IsEnabled()) 267 | this.WriteEvent(36, guid, webSocketState); 268 | } 269 | 270 | [Event(37, Level = EventLevel.Warning)] 271 | public void InvalidStateBeforeCloseOutput(Guid guid, WebSocketState webSocketState) 272 | { 273 | if (this.IsEnabled()) 274 | this.WriteEvent(37, guid, webSocketState); 275 | } 276 | 277 | [Event(38, Level = EventLevel.Warning)] 278 | public void PendingOperations(Guid guid) 279 | { 280 | if (this.IsEnabled()) 281 | this.WriteEvent(38, guid); 282 | } 283 | } 284 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /WebSocketFrame.cs: -------------------------------------------------------------------------------- 1 | #region Related components 2 | using System; 3 | using System.IO; 4 | using System.Net.WebSockets; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using net.vieapps.Components.Utility; 9 | #endregion 10 | 11 | namespace net.vieapps.Components.WebSockets 12 | { 13 | internal class WebSocketFrame 14 | { 15 | public bool IsFinBitSet { get; private set; } 16 | 17 | public WebSocketOpCode OpCode { get; private set; } 18 | 19 | public int Count { get; private set; } 20 | 21 | public WebSocketCloseStatus? CloseStatus { get; private set; } 22 | 23 | public string CloseStatusDescription { get; private set; } 24 | 25 | public WebSocketFrame(bool isFinBitSet, WebSocketOpCode webSocketOpCode, int count) 26 | { 27 | this.IsFinBitSet = isFinBitSet; 28 | this.OpCode = webSocketOpCode; 29 | this.Count = count; 30 | } 31 | 32 | public WebSocketFrame(bool isFinBitSet, WebSocketOpCode webSocketOpCode, int count, WebSocketCloseStatus closeStatus, string closeStatusDescription) : this(isFinBitSet, webSocketOpCode, count) 33 | { 34 | this.CloseStatus = closeStatus; 35 | this.CloseStatusDescription = closeStatusDescription; 36 | } 37 | } 38 | 39 | internal static class WebSocketFrameExtensions 40 | { 41 | public const int MaskKeyLength = 4; 42 | 43 | /// 44 | /// Mutate payload with the mask key. This is a reversible process, if you apply this to masked data it will be unmasked and visa versa. 45 | /// 46 | /// The 4 byte mask key 47 | /// The payload to mutate 48 | static void ToggleMask(this ArraySegment maskKey, ArraySegment payload) 49 | { 50 | if (maskKey.Count != WebSocketFrameExtensions.MaskKeyLength) 51 | throw new Exception($"MaskKey key must be {WebSocketFrameExtensions.MaskKeyLength} bytes"); 52 | 53 | var buffer = payload.Array; 54 | var maskKeyArray = maskKey.Array; 55 | 56 | // apply the mask key (this is a reversible process so no need to copy the payload) 57 | for (var index = payload.Offset; index < payload.Count; index++) 58 | { 59 | int payloadIndex = index - payload.Offset; // index should start at zero 60 | int maskKeyIndex = maskKey.Offset + (payloadIndex % WebSocketFrameExtensions.MaskKeyLength); 61 | buffer[index] = (Byte)(buffer[index] ^ maskKeyArray[maskKeyIndex]); 62 | } 63 | } 64 | 65 | /// 66 | /// Extracts close status and close description information from the web socket frame 67 | /// 68 | /// 69 | /// 70 | /// 71 | /// 72 | /// 73 | static WebSocketFrame DecodeCloseFrame(bool isFinBitSet, WebSocketOpCode opCode, int count, ArraySegment buffer) 74 | { 75 | WebSocketCloseStatus closeStatus; 76 | string closeStatusDescription; 77 | 78 | if (count >= 2) 79 | { 80 | Array.Reverse(buffer.Array, buffer.Offset, 2); // network byte order 81 | var closeStatusCode = (int)BitConverter.ToUInt16(buffer.Array, buffer.Offset); 82 | closeStatus = Enum.IsDefined(typeof(WebSocketCloseStatus), closeStatusCode) 83 | ? (WebSocketCloseStatus)closeStatusCode 84 | : WebSocketCloseStatus.Empty; 85 | 86 | int offset = buffer.Offset + 2; 87 | int descCount = count - 2; 88 | 89 | closeStatusDescription = descCount > 0 90 | ? Encoding.UTF8.GetString(buffer.Array, offset, descCount) 91 | : null; 92 | } 93 | else 94 | { 95 | closeStatus = WebSocketCloseStatus.Empty; 96 | closeStatusDescription = null; 97 | } 98 | 99 | return new WebSocketFrame(isFinBitSet, opCode, count, closeStatus, closeStatusDescription); 100 | } 101 | 102 | /// 103 | /// Reads the length of the payload according to the contents of byte2 104 | /// 105 | /// 106 | /// 107 | /// 108 | /// 109 | /// 110 | static async ValueTask ReadLengthAsync(this Stream stream, byte byte2, ArraySegment buffer, CancellationToken cancellationToken = default) 111 | { 112 | byte payloadLengthFlag = 0x7F; 113 | var length = (uint)(byte2 & payloadLengthFlag); 114 | 115 | // read a short length or a long length depending on the value of len 116 | if (length == 126) 117 | length = await stream.ReadShortExactlyAsync(false, buffer, cancellationToken).ConfigureAwait(false); 118 | 119 | else if (length == 127) 120 | { 121 | length = (uint)await stream.ReadLongExactlyAsync(false, buffer, cancellationToken).ConfigureAwait(false); 122 | const uint maxLength = 2147483648; // 2GB - not part of the spec but just a precaution. Send large volumes of data in smaller frames. 123 | 124 | // protect ourselves against bad data 125 | if (length > maxLength || length < 0) 126 | throw new ArgumentOutOfRangeException($"Payload length out of range. Min 0 max 2GB. Actual {length:#,##0} bytes."); 127 | } 128 | 129 | return length; 130 | } 131 | 132 | static async ValueTask ReadExactlyAsync(this Stream stream, int length, ArraySegment buffer, CancellationToken cancellationToken) 133 | { 134 | if (length == 0) 135 | return; 136 | 137 | if (buffer.Count < length) 138 | { 139 | // TODO: it is not impossible to get rid of this, just a little tricky 140 | // if the supplied buffer is too small for the payload then we should only return the number of bytes in the buffer 141 | // this will have to propogate all the way up the chain 142 | throw new InternalBufferOverflowException($"Unable to read {length} bytes into buffer (offset: {buffer.Offset} size: {buffer.Count}). Use a larger read buffer"); 143 | } 144 | 145 | var offset = 0; 146 | do 147 | { 148 | #if NETSTANDARD2_0 149 | var read = await stream.ReadAsync(buffer.Array, buffer.Offset + offset, length - offset, cancellationToken).ConfigureAwait(false); 150 | #else 151 | var read = await stream.ReadAsync(buffer.Array.AsMemory(buffer.Offset + offset, length - offset), cancellationToken).ConfigureAwait(false); 152 | #endif 153 | if (read == 0) 154 | throw new EndOfStreamException($"Unexpected end of stream encountered whilst attempting to read {length:#,##0} bytes"); 155 | offset += read; 156 | } 157 | while (offset < length); 158 | } 159 | 160 | static async ValueTask ReadShortExactlyAsync(this Stream stream, bool isLittleEndian, ArraySegment buffer, CancellationToken cancellationToken) 161 | { 162 | await stream.ReadExactlyAsync(2, buffer, cancellationToken).ConfigureAwait(false); 163 | if (!isLittleEndian) 164 | Array.Reverse(buffer.Array, buffer.Offset, 2); 165 | return BitConverter.ToUInt16(buffer.Array, buffer.Offset); 166 | } 167 | 168 | static async ValueTask ReadLongExactlyAsync(this Stream stream, bool isLittleEndian, ArraySegment buffer, CancellationToken cancellationToken) 169 | { 170 | await stream.ReadExactlyAsync(8, buffer, cancellationToken).ConfigureAwait(false); 171 | if (!isLittleEndian) 172 | Array.Reverse(buffer.Array, buffer.Offset, 8); 173 | return BitConverter.ToUInt64(buffer.Array, buffer.Offset); 174 | } 175 | 176 | static void WriteLong(this Stream stream, ulong value, bool isLittleEndian) 177 | { 178 | var buffer = value.ToBytes(); 179 | if (BitConverter.IsLittleEndian && !isLittleEndian) 180 | Array.Reverse(buffer); 181 | stream.Write(buffer, 0, buffer.Length); 182 | } 183 | 184 | static void WriteShort(this Stream stream, ushort value, bool isLittleEndian) 185 | { 186 | var buffer = value.ToBytes(); 187 | if (BitConverter.IsLittleEndian && !isLittleEndian) 188 | Array.Reverse(buffer); 189 | stream.Write(buffer, 0, buffer.Length); 190 | } 191 | 192 | /// 193 | /// Read a WebSocket frame from the stream 194 | /// 195 | /// The stream to read from 196 | /// The buffer to read into 197 | /// the cancellation token 198 | /// A websocket frame 199 | public static async ValueTask ReadFrameAsync(this Stream stream, ArraySegment buffer, CancellationToken cancellationToken) 200 | { 201 | // allocate a small buffer to read small chunks of data from the stream 202 | var smallBuffer = new ArraySegment(new byte[8]); 203 | 204 | await stream.ReadExactlyAsync(2, smallBuffer, cancellationToken).ConfigureAwait(false); 205 | byte byte1 = smallBuffer.Array[0]; 206 | byte byte2 = smallBuffer.Array[1]; 207 | 208 | // process first byte 209 | byte finBitFlag = 0x80; 210 | byte opCodeFlag = 0x0F; 211 | bool isFinBitSet = (byte1 & finBitFlag) == finBitFlag; 212 | var opCode = (WebSocketOpCode)(byte1 & opCodeFlag); 213 | 214 | // read and process second byte 215 | byte maskFlag = 0x80; 216 | bool isMaskBitSet = (byte2 & maskFlag) == maskFlag; 217 | uint length = await stream.ReadLengthAsync(byte2, smallBuffer, cancellationToken).ConfigureAwait(false); 218 | int count = (int)length; 219 | 220 | // use the masking key to decode the data if needed 221 | if (isMaskBitSet) 222 | { 223 | var maskKey = new ArraySegment(smallBuffer.Array, 0, WebSocketFrameExtensions.MaskKeyLength); 224 | await stream.ReadExactlyAsync(maskKey.Count, maskKey, cancellationToken).ConfigureAwait(false); 225 | await stream.ReadExactlyAsync(count, buffer, cancellationToken).ConfigureAwait(false); 226 | maskKey.ToggleMask(new ArraySegment(buffer.Array, buffer.Offset, count)); 227 | } 228 | else 229 | await stream.ReadExactlyAsync(count, buffer, cancellationToken).ConfigureAwait(false); 230 | 231 | return opCode == WebSocketOpCode.ConnectionClose 232 | ? WebSocketFrameExtensions.DecodeCloseFrame(isFinBitSet, opCode, count, buffer) 233 | : new WebSocketFrame(isFinBitSet, opCode, count); // note that by this point the payload will be populated 234 | } 235 | 236 | /// 237 | /// Writes a WebSocket frame into this stream 238 | /// 239 | /// Stream to write to 240 | /// The web socket opcode 241 | /// Array segment to get payload data from 242 | /// True is this is the last frame in this message (usually true) 243 | public static void WriteFrame(this MemoryStream stream, WebSocketOpCode opCode, ArraySegment payload, bool isLastFrame, bool isClient) 244 | { 245 | var finBitSetAsByte = isLastFrame ? (byte)0x80 : (byte)0x00; 246 | var byte1 = (byte)(finBitSetAsByte | (byte)opCode); 247 | stream.WriteByte(byte1); 248 | 249 | // NB, set the mask flag if we are constructing a client frame 250 | var maskBitSetAsByte = isClient ? (byte)0x80 : (byte)0x00; 251 | 252 | // depending on the size of the length we want to write it as a byte, ushort or ulong 253 | if (payload.Count < 126) 254 | { 255 | var byte2 = (byte)(maskBitSetAsByte | (byte)payload.Count); 256 | stream.WriteByte(byte2); 257 | } 258 | else if (payload.Count <= ushort.MaxValue) 259 | { 260 | var byte2 = (byte)(maskBitSetAsByte | 126); 261 | stream.WriteByte(byte2); 262 | stream.WriteShort((ushort)payload.Count, false); 263 | } 264 | else 265 | { 266 | var byte2 = (byte)(maskBitSetAsByte | 127); 267 | stream.WriteByte(byte2); 268 | stream.WriteLong((ulong)payload.Count, false); 269 | } 270 | 271 | // if we are creating a client frame then we MUST mack the payload as per the spec 272 | if (isClient) 273 | { 274 | var maskKey = CryptoService.GenerateRandomKey(WebSocketFrameExtensions.MaskKeyLength); 275 | stream.Write(maskKey, 0, maskKey.Length); 276 | 277 | // mask the payload 278 | var maskKeyArraySegment = new ArraySegment(maskKey, 0, maskKey.Length); 279 | maskKeyArraySegment.ToggleMask(payload); 280 | } 281 | 282 | stream.Write(payload.Array, payload.Offset, payload.Count); 283 | } 284 | } 285 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VIEApps.Components.WebSockets 2 | 3 | A concrete implementation of the **System.Net.WebSockets.WebSocket** abstract, 4 | that allows you to make WebSocket connections as a client or to respond to WebSocket requests as a server 5 | (or wrap existing WebSocket connections of ASP.NET / ASP.NET Core). 6 | 7 | ## NuGet 8 | 9 | [![NuGet](https://img.shields.io/nuget/v/VIEApps.Components.WebSockets.svg)](https://www.nuget.org/packages/VIEApps.Components.WebSockets) 10 | 11 | ## Walking on the ground 12 | 13 | The class **ManagedWebSocket** is an implementation or a wrapper of the *System.Net.WebSockets.WebSocket* abstract class, 14 | that allows you send and receive messages in the same way for both side of client and server role. 15 | 16 | ### Receiving messages: 17 | ```csharp 18 | async Task ReceiveAsync(ManagedWebSocket websocket) 19 | { 20 | var buffer = new ArraySegment(new byte[1024]); 21 | while (true) 22 | { 23 | WebSocketReceiveResult result = await websocket.ReceiveAsync(buffer, CancellationToken.None).ConfigureAwait(false); 24 | switch (result.MessageType) 25 | { 26 | case WebSocketMessageType.Close: 27 | return; 28 | case WebSocketMessageType.Text: 29 | case WebSocketMessageType.Binary: 30 | var value = Encoding.UTF8.GetString(buffer, result.Count); 31 | Console.WriteLine(value); 32 | break; 33 | } 34 | } 35 | } 36 | ``` 37 | 38 | ### Sending messages: 39 | ```csharp 40 | async Task SendAsync(ManagedWebSocket websocket) 41 | { 42 | var buffer = new ArraySegment(Encoding.UTF8.GetBytes("Hello World")); 43 | await websocket.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None).ConfigureAwait(false); 44 | } 45 | ``` 46 | 47 | ### Useful properties: 48 | 49 | ```csharp 50 | // the identity of the connection 51 | public Guid ID { get; } 52 | 53 | // true if the connection was made when connect to a remote endpoint (mean client role) 54 | public bool IsClient { get; } 55 | 56 | // original requesting URI of the connection 57 | public Uri RequestUri { get; } 58 | 59 | // the time when the connection is established 60 | public DateTime Timestamp { get; } 61 | 62 | // the remote endpoint 63 | public EndPoint RemoteEndPoint { get; } 64 | 65 | // the local endpoint 66 | public EndPoint LocalEndPoint { get; } 67 | 68 | // Extra information 69 | public Dictionary Extra { get; } 70 | 71 | // Headers information 72 | public Dictionary Headers { get; } 73 | ``` 74 | 75 | ## Fly on the sky with Event-liked driven 76 | 77 | ### Using the WebSocket class 78 | 79 | This is a centralized element for working with both side of client and server role. 80 | This class has 04 action properties (event handlers) to take care of all working cases, you just need to assign your code to cover its. 81 | 82 | ```csharp 83 | // fire when got any error 84 | Action OnError; 85 | 86 | // fire when a connection is established 87 | Action OnConnectionEstablished; 88 | 89 | // fire when a connection is broken 90 | Action OnConnectionBroken; 91 | 92 | // fire when a message is received 93 | Action OnMessageReceived; 94 | ``` 95 | 96 | Example: 97 | 98 | ```csharp 99 | var websocket = new WebSocket 100 | { 101 | OnError = (webSocket, exception) => 102 | { 103 | // your code to handle error 104 | }, 105 | OnConnectionEstablished = (webSocket) => 106 | { 107 | // your code to handle established connection 108 | }, 109 | OnConnectionBroken = (webSocket) => 110 | { 111 | // your code to handle broken connection 112 | }, 113 | OnMessageReceived = (webSocket, result, data) => 114 | { 115 | // your code to handle received message 116 | } 117 | }; 118 | ``` 119 | 120 | And this class has some methods for working on both side of client and server role: 121 | 122 | ```csharp 123 | void Connect(Uri uri, WebSocketOptions options, Action onSuccess, Action onFailure); 124 | void StartListen(int port, X509Certificate2 certificate, Action onSuccess, Action onFailure, Func getPingPayload, Func getPongPayload, Action onPong); 125 | void StopListen(); 126 | ``` 127 | 128 | ### WebSocket client 129 | 130 | Use the **Connect** method to connect to a remote endpoint 131 | 132 | ### WebSocket server 133 | 134 | Use the **StartListen** method to start the listener to listen incoming connection requests. 135 | 136 | Use the **StopListen** method to stop the listener. 137 | 138 | ### WebSocket server with Secure WebSockets (wss://) 139 | 140 | Enabling secure connections requires two things: 141 | - Pointing certificate to an x509 certificate that containing a public and private key. 142 | - Using the scheme **wss** instead of **ws** (or **https** instead of **http**) on all clients 143 | 144 | ```csharp 145 | var websocket = new WebSocket 146 | { 147 | Certificate = new X509Certificate2("my-certificate.pfx") 148 | // Certificate = new X509Certificate2("my-certificate.pfx", "cert-password", X509KeyStorageFlags.UserKeySet) 149 | }; 150 | websocket.StartListen(); 151 | ``` 152 | 153 | Want to have a free SSL certificate? Take a look at [Let's Encrypt](https://letsencrypt.org/). 154 | 155 | Special: A simple tool named [win-acme](https://github.com/PKISharp/win-acme) will help your IIS works with Let's Encrypt very well. 156 | 157 | ### SubProtocol Negotiation 158 | 159 | To enable negotiation of subprotocols, specify the supported protocols on *SupportedSubProtocols* property. 160 | The negotiated subprotocol will be available on the socket's *SubProtocol*. 161 | 162 | If no supported subprotocols are found on the client request (Sec-WebSocket-Protocol), the listener will raises the *SubProtocolNegotiationFailedException* exception. 163 | 164 | ```csharp 165 | var websocket = new WebSocket 166 | { 167 | SupportedSubProtocols = new[] { "messenger", "chat" } 168 | }; 169 | websocket.StartListen(); 170 | ``` 171 | 172 | ### Nagle's Algorithm 173 | 174 | The Nagle's Algorithm is disabled by default (to send a message immediately). 175 | If you want to enable the Nagle's Algorithm, set *NoDelay* to *false* 176 | 177 | ```csharp 178 | var websocket = new WebSocket 179 | { 180 | NoDelay = false 181 | }; 182 | websocket.StartListen(); 183 | ``` 184 | 185 | ### Wrap an existing WebSocket connection of ASP.NET / ASP.NET Core 186 | 187 | When integrate this component with your app that hosted by ASP.NET / ASP.NET Core, you might want to use the WebSocket connections of ASP.NET / ASP.NET Core directly, 188 | 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. 189 | 190 | ```csharp 191 | Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, Dictionary headers, Action onSuccess); 192 | ``` 193 | 194 | And might be you need an extension method to wrap an existing WebSocket connection, then take a look at some lines of code below: 195 | 196 | **ASP.NET** 197 | 198 | ```csharp 199 | public static Task WrapAsync(this net.vieapps.Components.WebSockets.WebSocket websocket, AspNetWebSocketContext context) 200 | { 201 | var serviceProvider = (IServiceProvider)HttpContext.Current; 202 | var httpWorker = serviceProvider?.GetService(); 203 | var remoteAddress = httpWorker == null ? context.UserHostAddress : httpWorker.GetRemoteAddress(); 204 | var remotePort = httpWorker == null ? 0 : httpWorker.GetRemotePort(); 205 | var remoteEndpoint = IPAddress.TryParse(remoteAddress, out IPAddress ipAddress) 206 | ? new IPEndPoint(ipAddress, remotePort > 0 ? remotePort : context.RequestUri.Port) as EndPoint 207 | : new DnsEndPoint(context.UserHostName, remotePort > 0 ? remotePort : context.RequestUri.Port) as EndPoint; 208 | var localAddress = httpWorker == null ? context.RequestUri.Host : httpWorker.GetLocalAddress(); 209 | var localPort = httpWorker == null ? 0 : httpWorker.GetLocalPort(); 210 | var localEndpoint = IPAddress.TryParse(localAddress, out ipAddress) 211 | ? new IPEndPoint(ipAddress, localPort > 0 ? localPort : context.RequestUri.Port) as EndPoint 212 | : new DnsEndPoint(context.RequestUri.Host, localPort > 0 ? localPort : context.RequestUri.Port) as EndPoint; 213 | return websocket.WrapAsync(context.WebSocket, context.RequestUri, remoteEndpoint, localEndpoint); 214 | } 215 | ``` 216 | 217 | **ASP.NET Core** 218 | 219 | ```csharp 220 | public static async Task WrapAsync(this net.vieapps.Components.WebSockets.WebSocket websocket, HttpContext context) 221 | { 222 | if (context.WebSockets.IsWebSocketRequest) 223 | { 224 | var webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false); 225 | var requestUri = new Uri($"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}{context.Request.PathBase}{context.Request.QueryString}"); 226 | var remoteEndPoint = new IPEndPoint(context.Connection.RemoteIpAddress, context.Connection.RemotePort); 227 | var localEndPoint = new IPEndPoint(context.Connection.LocalIpAddress, context.Connection.LocalPort); 228 | await websocket.WrapAsync(webSocket, requestUri, remoteEndPoint, localEndPoint).ConfigureAwait(false); 229 | } 230 | } 231 | ``` 232 | 233 | While working with ASP.NET Core, we think that you need a middle-ware to handle all request of WebSocket connections, just look like this: 234 | 235 | ```csharp 236 | public class WebSocketMiddleware 237 | { 238 | readonly RequestDelegate _next; 239 | net.vieapps.Components.WebSockets.WebSocket _websocket; 240 | 241 | public WebSocketMiddleware(RequestDelegate next, ILoggerFactory loggerFactory) 242 | { 243 | var logger = loggerFactory.CreateLogger(); 244 | this._websocket = new net.vieapps.Components.WebSockets.WebSocket(loggerFactory) 245 | { 246 | OnError = (websocket, exception) => 247 | { 248 | logger.LogError(exception, $"Got an error: {websocket?.ID} @ {websocket?.RemoteEndPoint} => {exception.Message}"); 249 | }, 250 | OnConnectionEstablished = (websocket) => 251 | { 252 | logger.LogDebug($"Connection is established: {websocket.ID} @ {websocket.RemoteEndPoint}"); 253 | }, 254 | OnConnectionBroken = (websocket) => 255 | { 256 | logger.LogDebug($"Connection is broken: {websocket.ID} @ {websocket.RemoteEndPoint}"); 257 | }, 258 | OnMessageReceived = (websocket, result, data) => 259 | { 260 | var message = result.MessageType == System.Net.WebSockets.WebSocketMessageType.Text ? data.GetString() : "(binary message)"; 261 | logger.LogDebug($"Got a message: {websocket.ID} @ {websocket.RemoteEndPoint} => {message}"); 262 | } 263 | }; 264 | this._next = next; 265 | } 266 | 267 | public async Task Invoke(HttpContext context) 268 | { 269 | await this._websocket.WrapAsync(context).ConfigureAwait(false); 270 | await this._next.Invoke(context).ConfigureAwait(false); 271 | } 272 | } 273 | ``` 274 | 275 | And remember to tell APS.NET Core uses your middleware (at **Configure** method of *Startup.cs*) 276 | 277 | ```csharp 278 | app.UseWebSockets(); 279 | app.UseMiddleware(); 280 | ``` 281 | 282 | ### Receiving and Sending messages: 283 | 284 | Messages are received automatically via parallel tasks, and you only need to assign **OnMessageReceived** event for handling its. 285 | 286 | Sending messages are the same as **ManagedWebSocket**, with a little different: the first argument - you need to specify a WebSocket connection (by an identity) for sending your messages. 287 | 288 | ```csharp 289 | Task SendAsync(Guid id, ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken); 290 | Task SendAsync(Guid id, string message, bool endOfMessage, CancellationToken cancellationToken); 291 | Task SendAsync(Guid id, byte[] message, bool endOfMessage, CancellationToken cancellationToken); 292 | Task SendAsync(Func predicate, ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken); 293 | Task SendAsync(Func predicate, string message, bool endOfMessage, CancellationToken cancellationToken); 294 | Task SendAsync(Func predicate, byte[] message, bool endOfMessage, CancellationToken cancellationToken); 295 | ``` 296 | 297 | ### Connection management 298 | 299 | Take a look at some methods *GetWebSocket...* to work with all connections. 300 | 301 | ```csharp 302 | ManagedWebSocket GetWebSocket(Guid id); 303 | IEnumerable GetWebSockets(Func predicate); 304 | bool CloseWebSocket(Guid id, WebSocketCloseStatus closeStatus, string closeStatusDescription); 305 | bool CloseWebSocket(ManagedWebSocket websocket, WebSocketCloseStatus closeStatus, string closeStatusDescription); 306 | ``` 307 | 308 | ## Others 309 | 310 | ### The important things 311 | 312 | - 16K is default size of the protocol buffer for receiving messages (its large enough for most case because we are usually use WebSocket to send/receive small data). If you want to change (to receive large messages), then set a new value for the static property named **ReceiveBufferSize** of the *WebSocket* class. 313 | - Some portion of codes are reference from [NinjaSource WebSocket](https://github.com/ninjasource/Ninja.WebSockets). 314 | 315 | ### Logging 316 | 317 | - Can be any provider that supports extension of Microsoft.Extensions.Logging (via dependency injection). 318 | - Set the log's level to *Trace* to see all processing logs 319 | 320 | Our prefers: 321 | - [Microsoft.Extensions.Logging.Console](https://www.nuget.org/packages/Microsoft.Extensions.Logging.Console): live logs 322 | - [Serilog.Extensions.Logging](https://www.nuget.org/packages/Serilog.Extensions.Logging): rolling log files (by hour or date - Serilog.Sink.File) - high performance, and very simple to use 323 | 324 | ### Namespaces 325 | 326 | ```csharp 327 | using net.vieapps.Components.Utility; 328 | using net.vieapps.Components.WebSockets; 329 | ``` 330 | 331 | ## A very simple stress test 332 | 333 | ### Environment 334 | 335 | - 01 server with Windows 2012 R2 x64 on Intel Xeon E3-1220 v3 3.1GHz - 8GB RAM 336 | - 05 clients with Windows 10 x64 and Ubuntu Linux 16.04 x64 337 | 338 | ### The scenario 339 | - Clients (05 stations) made 20,000 concurrent connections to the server, all connections are secured (use Let's Encrypt SSL certificate) 340 | - Clients send 02 messages per second to server (means server receives 40,000 messages/second) - size of 01 message: 1024 bytes (1K) 341 | - Server sends 01 message to all connections (20,000 messages) each 10 minutes - size of 01 message: 1024 bytes (1K) 342 | 343 | ### The results 344 | - Server is still alive after 01 week (60 * 24 * 7 = 10,080 minutes) 345 | - No dropped connection 346 | - No hang 347 | - Used memory: 1.3 GB - 1.7 GB 348 | - CPU usages: 3% - 15% while receiving messages, 18% - 35% while sending messages 349 | 350 | ## Performance Tuning 351 | 352 | While working directly with this component, performance is not your problem, but when you wrap WebSocket connections of ASP.NET 353 | or ASP.NET Core (with IIS Integration), may be you reach max 5,000 concurrent connections (because IIS allows 5,000 CCU by default). 354 | 355 | ASP.NET and IIS scale very well, but you'll need to change a few settings to set up your server for lots of concurrent connections, 356 | as opposed to lots of requests per second. 357 | 358 | ### IIS Configuration 359 | 360 | #### Max concurrent requests per application 361 | 362 | Increase the number of concurrent requests IIS will serve at once: 363 | 364 | - Open an administrator command prompt at *%windir%\System32\inetsrv* 365 | - Run the command below to update the **appConcurrentRequestLimit** attribute to a suitable number (5000 is the default in IIS7+) 366 | 367 | Example: 368 | 369 | *appcmd.exe set config /section:system.webserver/serverRuntime /appConcurrentRequestLimit:100000* 370 | 371 | ### ASP.NET Configuration 372 | 373 | #### Maximum Concurrent Requests Per CPU 374 | 375 | By default ASP.NET 4.0 sets the maximum concurrent connections to 5000 per CPU. 376 | If you need more concurrent connections then you need to increase the *maxConcurrentRequestsPerCPU* setting. 377 | 378 | Open *%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet.config* (*Framework64* for 64 bit processes) 379 | 380 | Copy from the sample below (ensure case is correct!) 381 | 382 | Example: 383 | 384 | ```xml 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | ``` 400 | 401 | #### Request Queue Limit 402 | 403 | When the total amount of connections exceed the *maxConcurrentRequestsPerCPU* setting (i.e. maxConcurrentRequestsPerCPU * number of logical processors), 404 | ASP.NET will start throttling requests using a queue. To control the size of the queue, you can tweak the *requestQueueLimit*. 405 | 406 | - Open *%windir%\Microsoft.NET\Framework\v4.0.30319\Config\machine.config* (*Framework64* for 64 bit processes) 407 | - Locate the processModel element 408 | - Set the *autoConfig* attribute to *false* and the *requestQueueLimit* attribute to a suitable number 409 | 410 | Example: 411 | 412 | ```xml 413 | 414 | ``` 415 | 416 | #### Performance Counters 417 | 418 | The following performance counters may be useful to watch while conducting concurrency testing and adjusting the settings detailed above: 419 | 420 | Memory 421 | - .NET CLR Memory# bytes in all Heaps (for w3wp) 422 | 423 | ASP.NET 424 | - ASP.NET\Requests Current 425 | - ASP.NET\Queued 426 | - ASP.NET\Rejected 427 | 428 | CPU 429 | - Processor Information\Processor Time 430 | 431 | TCP/IP 432 | - TCPv6\Connections Established 433 | - TCPv4\Connections Established 434 | 435 | Web Service 436 | - Web Service\Current Connections 437 | - Web Service\Maximum Connections 438 | 439 | Threading 440 | - .NET CLR LocksAndThreads\ # of current logical Threads 441 | - .NET CLR LocksAndThreads\ # of current physical Threads 442 | -------------------------------------------------------------------------------- /WebSocketImplementation.cs: -------------------------------------------------------------------------------- 1 | #region Related components 2 | using System; 3 | using System.Net; 4 | using System.Linq; 5 | using System.IO; 6 | using System.Net.WebSockets; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using System.Collections.Generic; 10 | using System.Collections.Concurrent; 11 | using Microsoft.Extensions.Logging; 12 | using net.vieapps.Components.Utility; 13 | using net.vieapps.Components.WebSockets.Exceptions; 14 | #endregion 15 | 16 | namespace net.vieapps.Components.WebSockets 17 | { 18 | internal class WebSocketImplementation : ManagedWebSocket 19 | { 20 | 21 | #region Properties 22 | readonly Func _recycledStreamFactory; 23 | readonly Stream _stream; 24 | readonly PingPongManager _pingpongManager; 25 | readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); 26 | readonly string _subProtocol; 27 | readonly CancellationTokenSource _processingCTS; 28 | readonly ConcurrentQueue> _buffers = new ConcurrentQueue>(); 29 | readonly ILogger _logger; 30 | WebSocketState _state; 31 | WebSocketMessageType _continuationMessageType = WebSocketMessageType.Binary; 32 | WebSocketCloseStatus? _closeStatus; 33 | string _closeStatusDescription; 34 | bool _isContinuationFrame = false; 35 | bool _pending = false; 36 | 37 | /// 38 | /// Gets the state that indicates the reason why the remote endpoint initiated the close handshake 39 | /// 40 | public override WebSocketCloseStatus? CloseStatus => this._closeStatus; 41 | 42 | /// 43 | /// Gets the description to describe the reason why the connection was closed 44 | /// 45 | public override string CloseStatusDescription => this._closeStatusDescription; 46 | 47 | /// 48 | /// Gets the current state of the WebSocket connection 49 | /// 50 | public override WebSocketState State => this._state; 51 | 52 | /// 53 | /// Gets the subprotocol that was negotiated during the opening handshake 54 | /// 55 | public override string SubProtocol => this._subProtocol; 56 | 57 | /// 58 | /// 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 59 | /// 60 | protected override bool IncludeExceptionInCloseResponse { get; } 61 | #endregion 62 | 63 | public WebSocketImplementation(Guid id, bool isClient, Func recycledStreamFactory, Stream stream, WebSocketOptions options, Uri requestUri, EndPoint remoteEndPoint, EndPoint localEndPoint, Dictionary headers) 64 | { 65 | this.ID = id; 66 | this.IsClient = isClient; 67 | this.IncludeExceptionInCloseResponse = options.IncludeExceptionInCloseResponse; 68 | this.KeepAliveInterval = options.KeepAliveInterval.Ticks < 0 ? TimeSpan.FromSeconds(60) : options.KeepAliveInterval; 69 | this.RequestUri = requestUri; 70 | this.RemoteEndPoint = remoteEndPoint; 71 | this.LocalEndPoint = localEndPoint; 72 | this.Set("Headers", headers); 73 | 74 | this._recycledStreamFactory = recycledStreamFactory ?? WebSocketHelper.GetRecyclableMemoryStreamFactory(); 75 | this._stream = stream; 76 | this._state = WebSocketState.Open; 77 | this._subProtocol = options.SubProtocol; 78 | this._processingCTS = new CancellationTokenSource(); 79 | this._pingpongManager = new PingPongManager(this, options, this._processingCTS.Token); 80 | this._logger = Logger.CreateLogger(); 81 | } 82 | 83 | /// 84 | /// Puts data on the wire 85 | /// 86 | /// 87 | /// 88 | /// 89 | async ValueTask PutOnTheWireAsync(MemoryStream stream, CancellationToken cancellationToken) 90 | { 91 | // check disposed 92 | if (this.IsDisposed) 93 | { 94 | if (this._logger.IsEnabled(LogLevel.Debug)) 95 | this._logger.LogWarning($"Object disposed => {this.ID}"); 96 | throw new ObjectDisposedException($"WebSocketImplementation => {this.ID}"); 97 | } 98 | 99 | // add into queue and check pending operations 100 | this._buffers.Enqueue(stream.ToArraySegment()); 101 | if (this._pending) 102 | { 103 | Events.Log.PendingOperations(this.ID); 104 | if (this._logger.IsEnabled(LogLevel.Debug)) 105 | this._logger.LogWarning($"WebSocketImplementation #{Environment.CurrentManagedThreadId} Pendings => {this._buffers.Count:#,##0} ({this.ID} @ {this.RemoteEndPoint})"); 106 | return; 107 | } 108 | 109 | // put data to wire 110 | this._pending = true; 111 | await this._lock.WaitAsync(cancellationToken).ConfigureAwait(false); 112 | try 113 | { 114 | while (!this._buffers.IsEmpty) 115 | if (this._buffers.TryDequeue(out var buffer)) 116 | await this._stream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); 117 | } 118 | catch (Exception) 119 | { 120 | throw; 121 | } 122 | finally 123 | { 124 | this._pending = false; 125 | this._lock.Release(); 126 | } 127 | } 128 | 129 | /// 130 | /// Receives data from the WebSocket connection asynchronously 131 | /// 132 | /// The buffer to copy data into 133 | /// The cancellation token 134 | /// 135 | public override async Task ReceiveAsync(ArraySegment buffer, CancellationToken cancellationToken) 136 | { 137 | try 138 | { 139 | // we may receive control frames so reading needs to happen in an infinite loop 140 | while (true) 141 | { 142 | // allow this operation to be cancelled from iniside OR outside this instance 143 | using (var cts = CancellationTokenSource.CreateLinkedTokenSource(this._processingCTS.Token, cancellationToken)) 144 | { 145 | WebSocketFrame frame = null; 146 | try 147 | { 148 | frame = await this._stream.ReadFrameAsync(buffer, cts.Token).ConfigureAwait(false); 149 | Events.Log.ReceivedFrame(this.ID, frame.OpCode, frame.IsFinBitSet, frame.Count); 150 | } 151 | catch (InternalBufferOverflowException ex) 152 | { 153 | await this.CloseOutputTimeoutAsync(WebSocketCloseStatus.MessageTooBig, "Frame is too large to fit in buffer. Use message fragmentation.", ex).ConfigureAwait(false); 154 | throw; 155 | } 156 | catch (ArgumentOutOfRangeException ex) 157 | { 158 | await this.CloseOutputTimeoutAsync(WebSocketCloseStatus.ProtocolError, "Payload length is out of range", ex).ConfigureAwait(false); 159 | throw; 160 | } 161 | catch (EndOfStreamException ex) 162 | { 163 | await this.CloseOutputTimeoutAsync(WebSocketCloseStatus.InvalidPayloadData, "Unexpected end of stream encountered", ex).ConfigureAwait(false); 164 | throw; 165 | } 166 | catch (OperationCanceledException ex) 167 | { 168 | await this.CloseOutputTimeoutAsync(WebSocketCloseStatus.EndpointUnavailable, "Operation cancelled", ex).ConfigureAwait(false); 169 | throw; 170 | } 171 | catch (Exception ex) 172 | { 173 | await this.CloseOutputTimeoutAsync(WebSocketCloseStatus.InternalServerError, "Error reading WebSocket frame", ex).ConfigureAwait(false); 174 | throw; 175 | } 176 | 177 | // process op-code 178 | switch (frame.OpCode) 179 | { 180 | case WebSocketOpCode.ConnectionClose: 181 | return await this.RespondToCloseFrameAsync(frame, buffer, cts.Token).ConfigureAwait(false); 182 | 183 | case WebSocketOpCode.Ping: 184 | await this._pingpongManager.SendPongAsync(buffer.Take(frame.Count).ToArray()).ConfigureAwait(false); 185 | break; 186 | 187 | case WebSocketOpCode.Pong: 188 | this._pingpongManager.OnPong(buffer.Take(frame.Count).ToArray()); 189 | break; 190 | 191 | case WebSocketOpCode.Text: 192 | // continuation frames will follow, record the message type as text 193 | if (!frame.IsFinBitSet) 194 | this._continuationMessageType = WebSocketMessageType.Text; 195 | return new WebSocketReceiveResult(frame.Count, WebSocketMessageType.Text, frame.IsFinBitSet); 196 | 197 | case WebSocketOpCode.Binary: 198 | // continuation frames will follow, record the message type as binary 199 | if (!frame.IsFinBitSet) 200 | this._continuationMessageType = WebSocketMessageType.Binary; 201 | return new WebSocketReceiveResult(frame.Count, WebSocketMessageType.Binary, frame.IsFinBitSet); 202 | 203 | case WebSocketOpCode.Continuation: 204 | return new WebSocketReceiveResult(frame.Count, this._continuationMessageType, frame.IsFinBitSet); 205 | 206 | default: 207 | var ex = new NotSupportedException($"Unknown WebSocket op-code: {frame.OpCode}"); 208 | await this.CloseOutputTimeoutAsync(WebSocketCloseStatus.ProtocolError, ex.Message, ex).ConfigureAwait(false); 209 | throw ex; 210 | } 211 | } 212 | } 213 | } 214 | catch (Exception ex) 215 | { 216 | // most exceptions will be caught closer to their source to send an appropriate close message (and set the WebSocketState) 217 | // however, if an unhandled exception is encountered and a close message not sent then send one here 218 | if (this._state == WebSocketState.Open) 219 | await this.CloseOutputTimeoutAsync(WebSocketCloseStatus.InternalServerError, "Got an unexpected error while reading from WebSocket", ex).ConfigureAwait(false); 220 | if (this._logger.IsEnabled(LogLevel.Trace)) 221 | this._logger.LogError(ex, $"Error occurred while receiving ({this.ID} @ {this.RemoteEndPoint}) => {ex.Message}"); 222 | throw; 223 | } 224 | } 225 | 226 | /// 227 | /// Called when a Close frame is received 228 | /// Send a response close frame if applicable 229 | /// 230 | async ValueTask RespondToCloseFrameAsync(WebSocketFrame frame, ArraySegment buffer, CancellationToken cancellationToken) 231 | { 232 | this._closeStatus = frame.CloseStatus; 233 | this._closeStatusDescription = frame.CloseStatusDescription; 234 | 235 | // this is a response to close handshake initiated by this instance 236 | if (this._state == WebSocketState.CloseSent) 237 | { 238 | this._state = WebSocketState.Closed; 239 | Events.Log.CloseHandshakeComplete(this.ID); 240 | } 241 | 242 | // this is in response to a close handshake initiated by the remote instance 243 | else if (this._state == WebSocketState.Open) 244 | { 245 | this._state = WebSocketState.CloseReceived; 246 | Events.Log.CloseHandshakeRespond(this.ID, frame.CloseStatus, frame.CloseStatusDescription); 247 | using (var stream = this._recycledStreamFactory()) 248 | { 249 | var closePayload = new ArraySegment(buffer.Array, buffer.Offset, frame.Count); 250 | stream.WriteFrame(WebSocketOpCode.ConnectionClose, closePayload, true, this.IsClient); 251 | Events.Log.SendingFrame(this.ID, WebSocketOpCode.ConnectionClose, true, closePayload.Count, false); 252 | await this.PutOnTheWireAsync(stream, cancellationToken).ConfigureAwait(false); 253 | } 254 | } 255 | 256 | // unexpected state 257 | else 258 | Events.Log.CloseFrameReceivedInUnexpectedState(this.ID, this._state, frame.CloseStatus, frame.CloseStatusDescription); 259 | 260 | return new WebSocketReceiveResult(frame.Count, WebSocketMessageType.Close, frame.IsFinBitSet, frame.CloseStatus, frame.CloseStatusDescription); 261 | } 262 | 263 | /// 264 | /// Calls this when got ping messages (pong payload must be 125 bytes or less, pong should contain the same payload as the ping) 265 | /// 266 | /// 267 | /// 268 | /// 269 | public async ValueTask SendPongAsync(ArraySegment payload, CancellationToken cancellationToken) 270 | { 271 | // exceeded max length 272 | if (payload.Count > 125) 273 | { 274 | var ex = new BufferOverflowException($"Max PONG message size is 125 bytes, exceeded: {payload.Count}"); 275 | await this.CloseOutputTimeoutAsync(WebSocketCloseStatus.ProtocolError, ex.Message, ex).ConfigureAwait(false); 276 | throw ex; 277 | } 278 | 279 | try 280 | { 281 | if (this._state == WebSocketState.Open) 282 | using (var stream = this._recycledStreamFactory()) 283 | { 284 | stream.WriteFrame(WebSocketOpCode.Pong, payload, true, this.IsClient); 285 | Events.Log.SendingFrame(this.ID, WebSocketOpCode.Pong, true, payload.Count, false); 286 | await this.PutOnTheWireAsync(stream, cancellationToken).ConfigureAwait(false); 287 | } 288 | } 289 | catch (Exception ex) 290 | { 291 | await this.CloseOutputTimeoutAsync(WebSocketCloseStatus.EndpointUnavailable, "Unable to send PONG response", ex).ConfigureAwait(false); 292 | throw; 293 | } 294 | } 295 | 296 | /// 297 | /// Calls this automatically from server side each KeepAliveInterval period (ping payload must be 125 bytes or less) 298 | /// 299 | /// 300 | /// 301 | /// 302 | public async ValueTask SendPingAsync(ArraySegment payload, CancellationToken cancellationToken) 303 | { 304 | if (payload.Count > 125) 305 | throw new BufferOverflowException($"Max PING message size is 125 bytes, exceeded: {payload.Count}"); 306 | 307 | if (this._state == WebSocketState.Open) 308 | using (var stream = this._recycledStreamFactory()) 309 | { 310 | stream.WriteFrame(WebSocketOpCode.Ping, payload, true, this.IsClient); 311 | Events.Log.SendingFrame(this.ID, WebSocketOpCode.Ping, true, payload.Count, false); 312 | await this.PutOnTheWireAsync(stream, cancellationToken).ConfigureAwait(false); 313 | } 314 | } 315 | 316 | /// 317 | /// Sends data over the WebSocket connection asynchronously 318 | /// 319 | /// The buffer containing data to send 320 | /// The message type, can be Text or Binary 321 | /// true if this message is a standalone message (this is the norm), if its a multi-part message then false (and true for the last) 322 | /// the cancellation token 323 | /// 324 | public override async Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) 325 | { 326 | // prepare op-code 327 | WebSocketOpCode opCode; 328 | if (this._isContinuationFrame) 329 | opCode = WebSocketOpCode.Continuation; 330 | else 331 | switch (messageType) 332 | { 333 | case WebSocketMessageType.Binary: 334 | opCode = WebSocketOpCode.Binary; 335 | break; 336 | case WebSocketMessageType.Text: 337 | opCode = WebSocketOpCode.Text; 338 | break; 339 | case WebSocketMessageType.Close: 340 | throw new NotSupportedException("Cannot use Send function to send a close frame, change to use Close function"); 341 | default: 342 | throw new NotSupportedException($"MessageType \"{messageType}\" is not supported"); 343 | } 344 | 345 | // send 346 | if (this._state == WebSocketState.Open) 347 | using (var stream = this._recycledStreamFactory()) 348 | { 349 | stream.WriteFrame(opCode, buffer, endOfMessage, this.IsClient); 350 | Events.Log.SendingFrame(this.ID, opCode, endOfMessage, buffer.Count, false); 351 | await this.PutOnTheWireAsync(stream, cancellationToken).ConfigureAwait(false); 352 | this._isContinuationFrame = !endOfMessage; 353 | } 354 | } 355 | 356 | /// 357 | /// As per the spec, write the close status followed by the close reason 358 | /// 359 | /// The close status 360 | /// Optional extra close details 361 | /// The payload to sent in the close frame 362 | ArraySegment BuildClosePayload(WebSocketCloseStatus closeStatus, string closeStatusDescription) 363 | { 364 | var buffer = ((ushort)closeStatus).ToBytes(); 365 | Array.Reverse(buffer); // network byte order (big endian) 366 | return string.IsNullOrWhiteSpace(closeStatusDescription) 367 | ? buffer.ToArraySegment() 368 | : buffer.Concat(closeStatusDescription.ToBytes()).ToArraySegment(); 369 | } 370 | 371 | /// 372 | /// Polite close (use the close handshake) 373 | /// 374 | /// The close status to use 375 | /// A description of why we are closing 376 | /// The timeout cancellation token 377 | /// 378 | public override async Task CloseAsync(WebSocketCloseStatus closeStatus, string closeStatusDescription, CancellationToken cancellationToken) 379 | { 380 | if (this._state == WebSocketState.Open) 381 | using (var stream = this._recycledStreamFactory()) 382 | { 383 | var buffer = this.BuildClosePayload(closeStatus, closeStatusDescription); 384 | stream.WriteFrame(WebSocketOpCode.ConnectionClose, buffer, true, this.IsClient); 385 | Events.Log.CloseHandshakeStarted(this.ID, closeStatus, closeStatusDescription); 386 | Events.Log.SendingFrame(this.ID, WebSocketOpCode.ConnectionClose, true, buffer.Count, false); 387 | await this.PutOnTheWireAsync(stream, cancellationToken).ConfigureAwait(false); 388 | this._state = WebSocketState.CloseSent; 389 | } 390 | else 391 | Events.Log.InvalidStateBeforeClose(this.ID, this._state); 392 | } 393 | 394 | /// 395 | /// Fire and forget close 396 | /// 397 | /// The close status to use 398 | /// A description of why we are closing 399 | /// The timeout cancellation token 400 | /// 401 | public override async Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string closeStatusDescription, CancellationToken cancellationToken) 402 | { 403 | if (this._state == WebSocketState.Open) 404 | { 405 | // set the state before we write to the network because the write may fail 406 | this._state = WebSocketState.Closed; 407 | 408 | // send close frame 409 | using (var stream = this._recycledStreamFactory()) 410 | { 411 | var buffer = this.BuildClosePayload(closeStatus, closeStatusDescription); 412 | stream.WriteFrame(WebSocketOpCode.ConnectionClose, buffer, true, this.IsClient); 413 | Events.Log.CloseOutputNoHandshake(this.ID, closeStatus, closeStatusDescription); 414 | Events.Log.SendingFrame(this.ID, WebSocketOpCode.ConnectionClose, true, buffer.Count, false); 415 | await this.PutOnTheWireAsync(stream, cancellationToken).ConfigureAwait(false); 416 | } 417 | } 418 | else 419 | Events.Log.InvalidStateBeforeCloseOutput(this.ID, this._state); 420 | } 421 | 422 | /// 423 | /// Aborts the WebSocket without sending a close frame 424 | /// 425 | public override void Abort() 426 | { 427 | this._state = WebSocketState.Aborted; 428 | this._processingCTS.Cancel(); 429 | } 430 | 431 | internal override ValueTask DisposeAsync(WebSocketCloseStatus closeStatus, string closeStatusDescription = "Service is unavailable", Action next = null) 432 | => base.DisposeAsync(closeStatus, closeStatusDescription, _ => 433 | { 434 | this._processingCTS.Cancel(); 435 | this._processingCTS.Dispose(); 436 | this._stream.Close(); 437 | this._stream.Dispose(); 438 | this._lock.Dispose(); 439 | next?.Invoke(this); 440 | }); 441 | 442 | public override ValueTask DisposeAsync() 443 | => this.IsDisposed ? new ValueTask(Task.CompletedTask) : this.DisposeAsync(WebSocketCloseStatus.EndpointUnavailable); 444 | 445 | public override void Dispose() 446 | { 447 | GC.SuppressFinalize(this); 448 | this.DisposeAsync().Execute(true); 449 | } 450 | 451 | ~WebSocketImplementation() 452 | => this.Dispose(); 453 | } 454 | } -------------------------------------------------------------------------------- /WebSocket.cs: -------------------------------------------------------------------------------- 1 | #region Related components 2 | using System; 3 | using System.Linq; 4 | using System.IO; 5 | using System.Net; 6 | using System.Net.Sockets; 7 | using System.Net.WebSockets; 8 | using System.Net.Security; 9 | using System.Text; 10 | using System.Text.RegularExpressions; 11 | using System.Threading; 12 | using System.Threading.Tasks; 13 | using System.Collections.Generic; 14 | using System.Collections.Concurrent; 15 | using System.Security.Authentication; 16 | using System.Security.Cryptography.X509Certificates; 17 | using System.Runtime.InteropServices; 18 | using Microsoft.Extensions.Logging; 19 | using net.vieapps.Components.WebSockets.Exceptions; 20 | using net.vieapps.Components.Utility; 21 | #endregion 22 | 23 | #if !SIGN 24 | [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("VIEApps.Components.XUnitTests")] 25 | #endif 26 | 27 | namespace net.vieapps.Components.WebSockets 28 | { 29 | /// 30 | /// The centralized point for working with WebSocket 31 | /// 32 | public class WebSocket : IDisposable, IAsyncDisposable 33 | { 34 | 35 | #region Properties 36 | readonly ConcurrentDictionary _websockets = new ConcurrentDictionary(); 37 | readonly ILogger _logger = null; 38 | readonly Func _recycledStreamFactory = null; 39 | readonly CancellationTokenSource _processingCTS = null; 40 | CancellationTokenSource _listeningCTS = null; 41 | TcpListener _tcpListener = null; 42 | 43 | /// 44 | /// Gets or Sets the SSL certificate for securing connections (server) 45 | /// 46 | public X509Certificate2 Certificate { get; set; } 47 | 48 | /// 49 | /// Gets or Sets the SSL protocol for securing connections with SSL Certificate (server) 50 | /// 51 | public SslProtocols SslProtocol { get; set; } = SslProtocols.Tls12; 52 | 53 | /// 54 | /// Gets or Sets the collection of supported sub-protocol (server) 55 | /// 56 | public IEnumerable SupportedSubProtocols { get; set; } = new string[0]; 57 | 58 | /// 59 | /// Gets or Sets the keep-alive interval for sending ping messages (server) 60 | /// 61 | public TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(60); 62 | 63 | /// 64 | /// 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) 65 | /// 66 | /// 67 | /// Set to true to send a message immediately with the least amount of latency (typical usage for chat) 68 | /// This will disable Nagle's algorithm which can cause high tcp latency for small packets sent infrequently 69 | /// However, if you are streaming large packets or sending large numbers of small packets frequently it is advisable to set NoDelay to false 70 | /// This way data will be bundled into larger packets for better throughput 71 | /// 72 | public bool NoDelay { get; set; } = true; 73 | 74 | /// 75 | /// Gets or Sets await interval between two rounds of receiving messages 76 | /// 77 | public TimeSpan ReceivingAwaitInterval { get; set; } = TimeSpan.Zero; 78 | 79 | /// 80 | /// Gets the state that determines the WebSocket object was disposed or not 81 | /// 82 | public bool IsDisposed { get; private set; } = false; 83 | #endregion 84 | 85 | #region Event Handlers 86 | /// 87 | /// Event to fire when got an error while processing 88 | /// 89 | public event Action ErrorHandler; 90 | 91 | /// 92 | /// Gets or Sets the action to run when got an error while processing 93 | /// 94 | public Action OnError 95 | { 96 | set => this.ErrorHandler += value; 97 | get => this.ErrorHandler; 98 | } 99 | 100 | /// 101 | /// Event to fire when a connection is established 102 | /// 103 | public event Action ConnectionEstablishedHandler; 104 | 105 | /// 106 | /// Gets or Sets the action to run when a connection is established 107 | /// 108 | public Action OnConnectionEstablished 109 | { 110 | set => this.ConnectionEstablishedHandler += value; 111 | get => this.ConnectionEstablishedHandler; 112 | } 113 | 114 | /// 115 | /// Event to fire when a connection is broken 116 | /// 117 | public event Action ConnectionBrokenHandler; 118 | 119 | /// 120 | /// Gets or Sets the action to run when a connection is broken 121 | /// 122 | public Action OnConnectionBroken 123 | { 124 | set => this.ConnectionBrokenHandler += value; 125 | get => this.ConnectionBrokenHandler; 126 | } 127 | 128 | /// 129 | /// Event to fire when a message is received 130 | /// 131 | public event Action MessageReceivedHandler; 132 | 133 | /// 134 | /// Gets or Sets the action to run when a message is received 135 | /// 136 | public Action OnMessageReceived 137 | { 138 | set => this.MessageReceivedHandler += value; 139 | get => this.MessageReceivedHandler; 140 | } 141 | #endregion 142 | 143 | /// 144 | /// Creates new an instance of the centralized WebSocket 145 | /// 146 | /// The cancellation token 147 | public WebSocket(CancellationToken cancellationToken) 148 | : this(null, cancellationToken) { } 149 | 150 | /// 151 | /// Creates new an instance of the centralized WebSocket 152 | /// 153 | /// The logger factory 154 | /// The cancellation token 155 | public WebSocket(ILoggerFactory loggerFactory, CancellationToken cancellationToken) 156 | : this(loggerFactory, null, cancellationToken) { } 157 | 158 | /// 159 | /// Creates new an instance of the centralized WebSocket 160 | /// 161 | /// The logger factory 162 | /// Used to get a recyclable memory stream (this can be used with the Microsoft.IO.RecyclableMemoryStreamManager class) 163 | /// The cancellation token 164 | public WebSocket(ILoggerFactory loggerFactory = null, Func recycledStreamFactory = null, CancellationToken cancellationToken = default) 165 | { 166 | Logger.AssignLoggerFactory(loggerFactory); 167 | this._logger = Logger.CreateLogger(); 168 | this._recycledStreamFactory = recycledStreamFactory ?? WebSocketHelper.GetRecyclableMemoryStreamFactory(); 169 | this._processingCTS = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); 170 | } 171 | 172 | /// 173 | /// Gets or sets the size (length) of the protocol buffer used to receive and parse frames, the default is 16kb, the minimum is 1kb (1024 bytes) 174 | /// 175 | public static int ReceiveBufferSize 176 | { 177 | get => WebSocketHelper.ReceiveBufferSize; 178 | set => WebSocketHelper.ReceiveBufferSize = value >= 1024 ? value : WebSocketHelper.ReceiveBufferSize; 179 | } 180 | 181 | /// 182 | /// Gets or sets the agent name of the protocol for working with related headers 183 | /// 184 | public static string AgentName 185 | { 186 | get => WebSocketHelper.AgentName; 187 | set => WebSocketHelper.AgentName = !string.IsNullOrWhiteSpace(value) ? value : WebSocketHelper.AgentName; 188 | } 189 | 190 | #region Listen for client requests as server 191 | /// 192 | /// Starts to listen for client requests as a WebSocket server 193 | /// 194 | /// The port for listening 195 | /// The SSL Certificate to secure connections 196 | /// Action to fire when start successful 197 | /// Action to fire when failed to start 198 | /// The function to get the custom 'PING' playload to send a 'PING' message 199 | /// The function to get the custom 'PONG' playload to response to a 'PING' message 200 | /// The action to run when a 'PONG' message has been sent 201 | public void StartListen(int port = 46429, X509Certificate2 certificate = null, Action onSuccess = null, Action onFailure = null, Func getPingPayload = null, Func getPongPayload = null, Action onPong = null) 202 | { 203 | // check 204 | if (this._tcpListener != null) 205 | { 206 | try 207 | { 208 | onSuccess?.Invoke(); 209 | } 210 | catch (Exception ex) 211 | { 212 | if (this._logger.IsEnabled(LogLevel.Debug)) 213 | this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); 214 | } 215 | return; 216 | } 217 | 218 | // set X.509 certificate 219 | this.Certificate = certificate ?? this.Certificate; 220 | 221 | // open the listener and listen for incoming requests 222 | try 223 | { 224 | // open the listener 225 | this._tcpListener = new TcpListener(IPAddress.IPv6Any, port > IPEndPoint.MinPort && port < IPEndPoint.MaxPort ? port : 46429); 226 | this._tcpListener.Server.SetOptions(this.NoDelay, true); 227 | this._tcpListener.Start(512); 228 | 229 | if (this._logger.IsEnabled(LogLevel.Debug)) 230 | { 231 | var platform = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) 232 | ? "Windows" 233 | : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) 234 | ? "macOS" 235 | : RuntimeInformation.IsOSPlatform(OSPlatform.Linux) 236 | ? "Linux" 237 | #if NETSTANDARD2_0 238 | : "Generic OS"; 239 | #else 240 | : RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD) ? "FreeBSD" : "Generic OS"; 241 | #endif 242 | 243 | platform += $" {RuntimeInformation.OSArchitecture.ToString().ToLower()} ({RuntimeInformation.FrameworkDescription.Trim()}) - SSL: {this.Certificate != null}"; 244 | if (this.Certificate != null) 245 | platform += $" ({this.Certificate.GetNameInfo(X509NameType.DnsName, false)} :: Issued by {this.Certificate.GetNameInfo(X509NameType.DnsName, true)})"; 246 | this._logger.LogInformation($"The listener is started => {this._tcpListener.Server.LocalEndPoint}\r\nPlatform: {platform}\r\nPowered by {WebSocketHelper.AgentName} v{this.GetType().Assembly.GetVersion()}"); 247 | } 248 | 249 | // callback when success 250 | try 251 | { 252 | onSuccess?.Invoke(); 253 | } 254 | catch (Exception ex) 255 | { 256 | if (this._logger.IsEnabled(LogLevel.Debug)) 257 | this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); 258 | } 259 | 260 | // listen for incoming connection requests 261 | this._listeningCTS = CancellationTokenSource.CreateLinkedTokenSource(this._processingCTS.Token); 262 | this.ListenAsync(getPingPayload, getPongPayload, onPong).Execute(); 263 | } 264 | catch (SocketException ex) 265 | { 266 | 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."; 267 | if (this._logger.IsEnabled(LogLevel.Debug)) 268 | this._logger.Log(LogLevel.Error, message, ex); 269 | try 270 | { 271 | onFailure?.Invoke(new ListenerSocketException(message, ex)); 272 | } 273 | catch (Exception e) 274 | { 275 | if (this._logger.IsEnabled(LogLevel.Debug)) 276 | this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); 277 | } 278 | } 279 | catch (Exception ex) 280 | { 281 | if (this._logger.IsEnabled(LogLevel.Debug)) 282 | this._logger.Log(LogLevel.Error, $"Got an unexpected error while listening: {ex.Message}", ex); 283 | try 284 | { 285 | onFailure?.Invoke(ex); 286 | } 287 | catch (Exception e) 288 | { 289 | if (this._logger.IsEnabled(LogLevel.Debug)) 290 | this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); 291 | } 292 | } 293 | } 294 | 295 | /// 296 | /// Starts to listen for client requests as a WebSocket server 297 | /// 298 | /// The port for listening 299 | /// Action to fire when start successful 300 | /// Action to fire when failed to start 301 | /// The function to get the custom 'PING' playload to send a 'PING' message 302 | /// The function to get the custom 'PONG' playload to response to a 'PING' message 303 | /// The action to run when a 'PONG' message has been sent 304 | public void StartListen(int port, Action onSuccess, Action onFailure, Func getPingPayload, Func getPongPayload, Action onPong) 305 | => this.StartListen(port, null, onSuccess, onFailure, getPingPayload, getPongPayload, onPong); 306 | 307 | /// 308 | /// Starts to listen for client requests as a WebSocket server 309 | /// 310 | /// The port for listening 311 | /// Action to fire when start successful 312 | /// Action to fire when failed to start 313 | public void StartListen(int port, Action onSuccess, Action onFailure) 314 | => this.StartListen(port, onSuccess, onFailure, null, null, null); 315 | 316 | /// 317 | /// Starts to listen for client requests as a WebSocket server 318 | /// 319 | /// The port for listening 320 | /// The function to get the custom 'PING' playload to send a 'PING' message 321 | /// The function to get the custom 'PONG' playload to response to a 'PING' message 322 | /// The action to run when a 'PONG' message has been sent 323 | public void StartListen(int port, Func getPingPayload, Func getPongPayload, Action onPong) 324 | => this.StartListen(port, null, null, getPingPayload, getPongPayload, onPong); 325 | 326 | /// 327 | /// Starts to listen for client requests as a WebSocket server 328 | /// 329 | /// The port for listening 330 | public void StartListen(int port) 331 | => this.StartListen(port, null, null, null); 332 | 333 | /// 334 | /// Stops listen 335 | /// 336 | /// true to cancel the pending connections 337 | public void StopListen(bool cancelPendings = true) 338 | { 339 | // cancel all pending connections 340 | if (cancelPendings) 341 | this._listeningCTS?.Cancel(); 342 | 343 | // dispose 344 | try 345 | { 346 | this._tcpListener?.Server?.Close(); 347 | this._tcpListener?.Stop(); 348 | } 349 | catch (Exception ex) 350 | { 351 | this._logger.Log(LogLevel.Debug, LogLevel.Error, $"Got an unexpected error when stop the listener: {ex.Message}", ex); 352 | } 353 | finally 354 | { 355 | this._tcpListener = null; 356 | } 357 | } 358 | 359 | async Task ListenAsync(Func getPingPayload, Func getPongPayload, Action onPong) 360 | { 361 | try 362 | { 363 | while (!this._listeningCTS.IsCancellationRequested) 364 | this.AcceptClientAsync(await this._tcpListener.AcceptTcpClientAsync().WithCancellationToken(this._listeningCTS.Token).ConfigureAwait(false), getPingPayload, getPongPayload, onPong).Execute(); 365 | } 366 | catch (Exception ex) 367 | { 368 | this.StopListen(false); 369 | if (ex is OperationCanceledException || ex is TaskCanceledException || ex is ObjectDisposedException || ex is SocketException || ex is IOException) 370 | this._logger.LogDebug($"The listener is stopped {(this._logger.IsEnabled(LogLevel.Debug) ? $"({ex.GetType()})" : "")}"); 371 | else 372 | this._logger.LogError($"The listener is stopped ({ex.Message})", ex); 373 | } 374 | } 375 | 376 | async Task AcceptClientAsync(TcpClient tcpClient, Func getPingPayload, Func getPongPayload, Action onPong) 377 | { 378 | ManagedWebSocket websocket = null; 379 | try 380 | { 381 | // set optins 382 | tcpClient.Client.SetOptions(this.NoDelay); 383 | 384 | // get stream 385 | var id = Guid.NewGuid(); 386 | var endpoint = tcpClient.Client.RemoteEndPoint; 387 | Stream stream = null; 388 | if (this.Certificate != null) 389 | try 390 | { 391 | Events.Log.AttemptingToSecureConnection(id); 392 | if (this._logger.IsEnabled(LogLevel.Trace)) 393 | this._logger.Log(LogLevel.Debug, $"Attempting to secure the connection ({id} @ {endpoint})"); 394 | 395 | stream = new SslStream(tcpClient.GetStream(), false); 396 | await (stream as SslStream).AuthenticateAsServerAsync( 397 | serverCertificate: this.Certificate, 398 | clientCertificateRequired: false, 399 | enabledSslProtocols: this.SslProtocol, 400 | checkCertificateRevocation: false 401 | ).WithCancellationToken(this._listeningCTS.Token).ConfigureAwait(false); 402 | 403 | Events.Log.ConnectionSecured(id); 404 | if (this._logger.IsEnabled(LogLevel.Trace)) 405 | this._logger.Log(LogLevel.Debug, $"The connection successfully secured ({id} @ {endpoint})"); 406 | } 407 | catch (OperationCanceledException) 408 | { 409 | return; 410 | } 411 | catch (Exception ex) 412 | { 413 | Events.Log.ServerSslCertificateError(id, ex.ToString()); 414 | if (ex is AuthenticationException) 415 | throw; 416 | throw new AuthenticationException($"Cannot secure the connection: {ex.Message}", ex); 417 | } 418 | else 419 | { 420 | Events.Log.ConnectionNotSecured(id); 421 | if (this._logger.IsEnabled(LogLevel.Trace)) 422 | this._logger.Log(LogLevel.Debug, $"Use insecured connection ({id} @ {endpoint})"); 423 | stream = tcpClient.GetStream(); 424 | } 425 | 426 | // parse request 427 | if (this._logger.IsEnabled(LogLevel.Trace)) 428 | this._logger.Log(LogLevel.Debug, $"The connection is opened, then parse the request ({id} @ {endpoint})"); 429 | 430 | var header = await stream.ReadHeaderAsync(this._listeningCTS.Token).ConfigureAwait(false); 431 | if (this._logger.IsEnabled(LogLevel.Trace)) 432 | this._logger.Log(LogLevel.Debug, $"Handshake request ({id} @ {endpoint}) => \r\n{header.Trim()}"); 433 | 434 | var isWebSocketRequest = false; 435 | var path = string.Empty; 436 | var match = new Regex(@"^GET(.*)HTTP\/1\.1", RegexOptions.IgnoreCase).Match(header); 437 | if (match.Success) 438 | { 439 | isWebSocketRequest = new Regex("Upgrade: WebSocket", RegexOptions.IgnoreCase).Match(header).Success; 440 | if (isWebSocketRequest) 441 | path = match.Groups[1].Value.Trim(); 442 | } 443 | 444 | // verify request 445 | if (!isWebSocketRequest) 446 | { 447 | if (this._logger.IsEnabled(LogLevel.Trace)) 448 | this._logger.Log(LogLevel.Debug, $"The request contains no WebSocket upgrade request, then ignore ({id} @ {endpoint})"); 449 | stream.Close(); 450 | tcpClient.Close(); 451 | return; 452 | } 453 | 454 | // accept the request 455 | Events.Log.AcceptWebSocketStarted(id); 456 | if (this._logger.IsEnabled(LogLevel.Trace)) 457 | this._logger.Log(LogLevel.Debug, $"The request has requested an upgrade to WebSocket protocol, negotiating WebSocket handshake ({id} @ {endpoint})"); 458 | 459 | var options = new WebSocketOptions 460 | { 461 | KeepAliveInterval = this.KeepAliveInterval.Ticks < 0 ? TimeSpan.FromSeconds(60) : this.KeepAliveInterval, 462 | GetPingPayload = getPingPayload, 463 | GetPongPayload = getPongPayload, 464 | OnPong = onPong 465 | }; 466 | 467 | try 468 | { 469 | // check the version (support version 13 and above) 470 | match = new Regex("Sec-WebSocket-Version: (.*)").Match(header); 471 | if (!match.Success || !Int32.TryParse(match.Groups[1].Value, out var version)) 472 | throw new VersionNotSupportedException("Unable to find \"Sec-WebSocket-Version\" in the upgrade request"); 473 | else if (version < 13) 474 | throw new VersionNotSupportedException($"WebSocket Version {version} is not supported, must be 13 or above"); 475 | 476 | // get the request key 477 | match = new Regex("Sec-WebSocket-Key: (.*)").Match(header); 478 | var requestKey = match.Success 479 | ? match.Groups[1].Value.Trim() 480 | : throw new KeyMissingException("Unable to find \"Sec-WebSocket-Key\" in the upgrade request"); 481 | 482 | // negotiate subprotocol 483 | match = new Regex("Sec-WebSocket-Protocol: (.*)").Match(header); 484 | options.SubProtocol = match.Success 485 | ? match.Groups[1].Value?.Trim().Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries).NegotiateSubProtocol(this.SupportedSubProtocols) 486 | : null; 487 | 488 | // handshake 489 | var handshake = 490 | $"HTTP/1.1 101 Switching Protocols\r\n" + 491 | $"Connection: Upgrade\r\n" + 492 | $"Upgrade: websocket\r\n" + 493 | $"Server: {WebSocketHelper.AgentName}\r\n" + 494 | $"Date: {DateTime.Now.ToHttpString()}\r\n" + 495 | $"Sec-WebSocket-Accept: {requestKey.ComputeAcceptKey()}\r\n"; 496 | if (!string.IsNullOrWhiteSpace(options.SubProtocol)) 497 | handshake += $"Sec-WebSocket-Protocol: {options.SubProtocol}\r\n"; 498 | options.AdditionalHeaders?.ForEach(kvp => handshake += $"{kvp.Key}: {kvp.Value}\r\n"); 499 | 500 | Events.Log.SendingHandshake(id, handshake); 501 | await stream.WriteHeaderAsync(handshake, this._listeningCTS.Token).ConfigureAwait(false); 502 | Events.Log.HandshakeSent(id, handshake); 503 | if (this._logger.IsEnabled(LogLevel.Trace)) 504 | this._logger.Log(LogLevel.Debug, $"Handshake response ({id} @ {endpoint}) => \r\n{handshake.Trim()}"); 505 | } 506 | catch (VersionNotSupportedException ex) 507 | { 508 | Events.Log.WebSocketVersionNotSupported(id, ex.ToString()); 509 | await stream.WriteHeaderAsync($"HTTP/1.1 426 Upgrade Required\r\nSec-WebSocket-Version: 13\r\nException: {ex.Message}", this._listeningCTS.Token).ConfigureAwait(false); 510 | throw; 511 | } 512 | catch (Exception ex) 513 | { 514 | Events.Log.BadRequest(id, ex.ToString()); 515 | await stream.WriteHeaderAsync($"HTTP/1.1 400 Bad Request\r\nException: {ex.Message}", this._listeningCTS.Token).ConfigureAwait(false); 516 | throw; 517 | } 518 | 519 | Events.Log.ServerHandshakeSuccess(id); 520 | if (this._logger.IsEnabled(LogLevel.Trace)) 521 | this._logger.Log(LogLevel.Debug, $"WebSocket handshake response has been sent, the stream is ready ({id} @ {endpoint})"); 522 | 523 | // update the connected WebSocket connection 524 | match = new Regex("Sec-WebSocket-Extensions: (.*)").Match(header); 525 | options.Extensions = match.Success 526 | ? match.Groups[1].Value.Trim() 527 | : null; 528 | 529 | match = new Regex("Host: (.*)").Match(header); 530 | var host = match.Success 531 | ? match.Groups[1].Value.Trim() 532 | : string.Empty; 533 | 534 | // add into the collection 535 | websocket = new WebSocketImplementation(id, false, this._recycledStreamFactory, stream, options, new Uri($"ws{(this.Certificate != null ? "s" : "")}://{host}{path}"), endpoint, tcpClient.Client.LocalEndPoint, header.ToDictionary()); 536 | await this.AddWebSocketAsync(websocket).ConfigureAwait(false); 537 | if (this._logger.IsEnabled(LogLevel.Trace)) 538 | 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}")}"); 539 | 540 | // callback 541 | try 542 | { 543 | this.ConnectionEstablishedHandler?.Invoke(websocket); 544 | } 545 | catch (Exception e) 546 | { 547 | if (this._logger.IsEnabled(LogLevel.Debug)) 548 | this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); 549 | } 550 | 551 | // receive messages 552 | this.ReceiveAsync(websocket).Execute(); 553 | } 554 | catch (Exception ex) 555 | { 556 | if (ex is OperationCanceledException || ex is TaskCanceledException || ex is ObjectDisposedException || ex is SocketException || ex is IOException) 557 | { 558 | // normal, do nothing 559 | } 560 | else 561 | { 562 | if (this._logger.IsEnabled(LogLevel.Debug)) 563 | this._logger.Log(LogLevel.Error, $"Error occurred while accepting an incoming connection request: {ex.Message}", ex); 564 | try 565 | { 566 | this.ErrorHandler?.Invoke(websocket, ex); 567 | } 568 | catch (Exception e) 569 | { 570 | if (this._logger.IsEnabled(LogLevel.Debug)) 571 | this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); 572 | } 573 | } 574 | } 575 | } 576 | #endregion 577 | 578 | #region Connect to remote endpoints as client 579 | async Task ConnectAsync(Uri uri, WebSocketOptions options, Action onSuccess = null, Action onFailure = null) 580 | { 581 | if (this._logger.IsEnabled(LogLevel.Trace)) 582 | this._logger.Log(LogLevel.Debug, $"Attempting to connect ({uri})"); 583 | 584 | try 585 | { 586 | // connect the TCP client 587 | var id = Guid.NewGuid(); 588 | 589 | var tcpClient = new TcpClient(); 590 | tcpClient.Client.SetOptions(options.NoDelay); 591 | 592 | if (IPAddress.TryParse(uri.Host, out var ipAddress)) 593 | { 594 | Events.Log.ClientConnectingToIPAddress(id, ipAddress.ToString(), uri.Port); 595 | await tcpClient.ConnectAsync(address: ipAddress, port: uri.Port).WithCancellationToken(this._processingCTS.Token).ConfigureAwait(false); 596 | } 597 | else 598 | { 599 | Events.Log.ClientConnectingToHost(id, uri.Host, uri.Port); 600 | await tcpClient.ConnectAsync(host: uri.Host, port: uri.Port).WithCancellationToken(this._processingCTS.Token).ConfigureAwait(false); 601 | } 602 | 603 | var endpoint = tcpClient.Client.RemoteEndPoint; 604 | if (this._logger.IsEnabled(LogLevel.Trace)) 605 | this._logger.Log(LogLevel.Debug, $"The endpoint ({uri}) is connected ({id} @ {endpoint})"); 606 | 607 | // get the connected stream 608 | Stream stream = null; 609 | if (uri.Scheme.IsEquals("wss") || uri.Scheme.IsEquals("https")) 610 | try 611 | { 612 | Events.Log.AttemptingToSecureConnection(id); 613 | if (this._logger.IsEnabled(LogLevel.Trace)) 614 | this._logger.Log(LogLevel.Debug, $"Attempting to secure the connection ({id} @ {endpoint})"); 615 | 616 | stream = new SslStream( 617 | innerStream: tcpClient.GetStream(), 618 | leaveInnerStreamOpen: false, 619 | userCertificateValidationCallback: (sender, certificate, chain, sslPolicyErrors) => sslPolicyErrors == SslPolicyErrors.None || options.IgnoreCertificateErrors || RuntimeInformation.IsOSPlatform(OSPlatform.Linux), 620 | userCertificateSelectionCallback: (sender, host, certificates, certificate, issuers) => this.Certificate 621 | ); 622 | await (stream as SslStream).AuthenticateAsClientAsync(targetHost: uri.Host).WithCancellationToken(this._processingCTS.Token).ConfigureAwait(false); 623 | 624 | Events.Log.ConnectionSecured(id); 625 | if (this._logger.IsEnabled(LogLevel.Trace)) 626 | this._logger.Log(LogLevel.Debug, $"The connection successfully secured ({id} @ {endpoint})"); 627 | } 628 | catch (OperationCanceledException) 629 | { 630 | throw; 631 | } 632 | catch (Exception ex) 633 | { 634 | Events.Log.ClientSslCertificateError(id, ex.ToString()); 635 | if (ex is AuthenticationException) 636 | throw; 637 | throw new AuthenticationException($"Cannot secure the connection: {ex.Message}", ex); 638 | } 639 | else 640 | { 641 | Events.Log.ConnectionNotSecured(id); 642 | if (this._logger.IsEnabled(LogLevel.Trace)) 643 | this._logger.Log(LogLevel.Debug, $"Use insecured connection ({id} @ {endpoint})"); 644 | stream = tcpClient.GetStream(); 645 | } 646 | 647 | // send handshake 648 | if (this._logger.IsEnabled(LogLevel.Trace)) 649 | this._logger.Log(LogLevel.Debug, $"Negotiating WebSocket handshake ({id} @ {endpoint})"); 650 | 651 | var requestAcceptKey = CryptoService.GenerateRandomKey(16).ToBase64(); 652 | var handshake = 653 | $"GET {uri.PathAndQuery} HTTP/1.1\r\n" + 654 | $"Host: {uri.Host}:{uri.Port}\r\n" + 655 | $"Origin: {uri.Scheme.Replace("ws", "http")}://{uri.Host}{(uri.Port != 80 && uri.Port != 443 ? $":{uri.Port}" : "")}\r\n" + 656 | $"Connection: Upgrade\r\n" + 657 | $"Upgrade: websocket\r\n" + 658 | $"User-Agent: Mozilla/5.0 ({WebSocketHelper.AgentName}/{RuntimeInformation.FrameworkDescription.Trim()}/{(RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "Macintosh; Mac OS X; " : "")}{RuntimeInformation.OSDescription.Trim()})\r\n" + 659 | $"Date: {DateTime.Now.ToHttpString()}\r\n" + 660 | $"Sec-WebSocket-Version: 13\r\n" + 661 | $"Sec-WebSocket-Key: {requestAcceptKey}\r\n"; 662 | if (!string.IsNullOrWhiteSpace(options.SubProtocol)) 663 | handshake += $"Sec-WebSocket-Protocol: {options.SubProtocol}\r\n"; 664 | if (!string.IsNullOrWhiteSpace(options.Extensions)) 665 | handshake += $"Sec-WebSocket-Extensions: {options.Extensions}\r\n"; 666 | options.AdditionalHeaders?.ForEach(kvp => handshake += $"{kvp.Key}: {kvp.Value}\r\n"); 667 | 668 | Events.Log.SendingHandshake(id, handshake); 669 | await stream.WriteHeaderAsync(handshake, this._processingCTS.Token).ConfigureAwait(false); 670 | Events.Log.HandshakeSent(id, handshake); 671 | if (this._logger.IsEnabled(LogLevel.Trace)) 672 | this._logger.Log(LogLevel.Debug, $"Handshake request ({id} @ {endpoint}) => \r\n{handshake.Trim()}"); 673 | 674 | // read response 675 | Events.Log.ReadingResponse(id); 676 | var response = string.Empty; 677 | try 678 | { 679 | response = await stream.ReadHeaderAsync(this._processingCTS.Token).ConfigureAwait(false); 680 | if (this._logger.IsEnabled(LogLevel.Trace)) 681 | this._logger.Log(LogLevel.Debug, $"Handshake response ({id} @ {endpoint}) => \r\n{response.Trim()}"); 682 | } 683 | catch (Exception ex) 684 | { 685 | Events.Log.ReadResponseError(id, ex.ToString()); 686 | throw new HandshakeFailedException("Handshake unexpected failure", ex); 687 | } 688 | 689 | // get the response code 690 | var match = new Regex(@"HTTP\/1\.1 (.*)", RegexOptions.IgnoreCase).Match(response); 691 | var responseCode = match.Success 692 | ? match.Groups[1].Value.Trim() 693 | : null; 694 | 695 | // throw if got invalid response code 696 | if (!"101 Switching Protocols".IsEquals(responseCode) && !"101 Web Socket Protocol Handshake".IsEquals(responseCode)) 697 | { 698 | var lines = response.Split(new[] { "\r\n" }, StringSplitOptions.None); 699 | for (var index = 0; index < lines.Length; index++) 700 | if (string.IsNullOrWhiteSpace(lines[index])) // if there is more to the message than just the header 701 | { 702 | var builder = new StringBuilder(); 703 | for (var idx = index + 1; idx < lines.Length - 1; idx++) 704 | builder.AppendLine(lines[idx]); 705 | throw new InvalidResponseCodeException(responseCode, builder.ToString(), response); 706 | } 707 | } 708 | 709 | // check the accepted key 710 | match = new Regex("Sec-WebSocket-Accept: (.*)").Match(response); 711 | var actualAcceptKey = match.Success 712 | ? match.Groups[1].Value.Trim() 713 | : null; 714 | var expectedAcceptKey = requestAcceptKey.ComputeAcceptKey(); 715 | if (!expectedAcceptKey.IsEquals(actualAcceptKey)) 716 | { 717 | var warning = $"Handshake failed because the accept key {(actualAcceptKey == null ? "was not found" : $"from the server \"{actualAcceptKey}\" was not the expected \"{expectedAcceptKey}\"")}"; 718 | Events.Log.HandshakeFailure(id, warning); 719 | throw new HandshakeFailedException(warning); 720 | } 721 | 722 | Events.Log.ClientHandshakeSuccess(id); 723 | if (this._logger.IsEnabled(LogLevel.Trace)) 724 | this._logger.Log(LogLevel.Debug, $"Handshake success ({id} @ {endpoint})"); 725 | 726 | // get the accepted sub-protocol 727 | match = new Regex("Sec-WebSocket-Protocol: (.*)").Match(response); 728 | options.SubProtocol = match.Success 729 | ? match.Groups[1].Value?.Trim() 730 | : null; 731 | 732 | // update the connected WebSocket connection 733 | var websocket = new WebSocketImplementation(id, true, this._recycledStreamFactory, stream, options, uri, endpoint, tcpClient.Client.LocalEndPoint, handshake.ToDictionary()); 734 | await this.AddWebSocketAsync(websocket).ConfigureAwait(false); 735 | if (this._logger.IsEnabled(LogLevel.Trace)) 736 | this._logger.Log(LogLevel.Debug, $"The client 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}")}"); 737 | 738 | // callback 739 | try 740 | { 741 | this.ConnectionEstablishedHandler?.Invoke(websocket); 742 | onSuccess?.Invoke(websocket); 743 | } 744 | catch (Exception ex) 745 | { 746 | if (this._logger.IsEnabled(LogLevel.Debug)) 747 | this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); 748 | } 749 | 750 | // receive messages 751 | this.ReceiveAsync(websocket).Execute(); 752 | } 753 | catch (OperationCanceledException) 754 | { 755 | return; 756 | } 757 | catch (Exception ex) 758 | { 759 | if (this._logger.IsEnabled(LogLevel.Debug)) 760 | this._logger.Log(LogLevel.Error, $"Could not connect ({uri}): {ex.Message}", ex); 761 | try 762 | { 763 | onFailure?.Invoke(ex); 764 | } 765 | catch (Exception e) 766 | { 767 | if (this._logger.IsEnabled(LogLevel.Debug)) 768 | this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); 769 | } 770 | } 771 | } 772 | 773 | /// 774 | /// Connects to a remote endpoint as a WebSocket client 775 | /// 776 | /// The address of the remote endpoint to connect to 777 | /// The options 778 | /// Action to fire when connect successful 779 | /// Action to fire when failed to connect 780 | public void Connect(Uri uri, WebSocketOptions options, Action onSuccess = null, Action onFailure = null) 781 | => this.ConnectAsync(uri, options ?? new WebSocketOptions(), onSuccess, onFailure).Execute(); 782 | 783 | /// 784 | /// Connects to a remote endpoint as a WebSocket client 785 | /// 786 | /// The address of the remote endpoint to connect to 787 | /// The sub-protocol 788 | /// Action to fire when connect successful 789 | /// Action to fire when failed to connect 790 | public void Connect(Uri uri, string subProtocol = null, Action onSuccess = null, Action onFailure = null) 791 | => this.Connect(uri, new WebSocketOptions { SubProtocol = subProtocol }, onSuccess, onFailure); 792 | 793 | /// 794 | /// Connects to a remote endpoint as a WebSocket client 795 | /// 796 | /// The address of the remote endpoint to connect to 797 | /// The sub-protocol 798 | /// Action to fire when connect successful 799 | /// Action to fire when failed to connect 800 | public void Connect(string location, string subProtocol = null, Action onSuccess = null, Action onFailure = null) 801 | => this.Connect(new Uri(location), subProtocol, onSuccess, onFailure); 802 | 803 | /// 804 | /// Connects to a remote endpoint as a WebSocket client 805 | /// 806 | /// The address of the remote endpoint to connect to 807 | /// Action to fire when connect successful 808 | /// Action to fire when failed to connect 809 | public void Connect(string location, Action onSuccess, Action onFailure) 810 | => this.Connect(location, null, onSuccess, onFailure); 811 | #endregion 812 | 813 | #region Wrap a WebSocket connection 814 | /// 815 | /// Wraps a WebSocket connection of ASP.NET / ASP.NET Core and acts like a WebSocket server 816 | /// 817 | /// The WebSocket connection of ASP.NET / ASP.NET Core 818 | /// The original request URI of the WebSocket connection 819 | /// The remote endpoint of the WebSocket connection 820 | /// The local endpoint of the WebSocket connection 821 | /// The collection that presents the headers of the client that made this request to the WebSocket connection 822 | /// The action to run when the WebSocket connection is wrap success 823 | /// A task that run the receiving process when wrap successful or an exception when failed 824 | public Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, EndPoint remoteEndPoint = null, EndPoint localEndPoint = null, Dictionary headers = null, Action onSuccess = null) 825 | { 826 | try 827 | { 828 | // create 829 | var websocket = new WebSocketWrapper(webSocket, requestUri, remoteEndPoint, localEndPoint, headers); 830 | this.AddWebSocket(websocket); 831 | if (this._logger.IsEnabled(LogLevel.Trace)) 832 | 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}")}"); 833 | 834 | // callback 835 | try 836 | { 837 | this.ConnectionEstablishedHandler?.Invoke(websocket); 838 | onSuccess?.Invoke(websocket); 839 | } 840 | catch (Exception ex) 841 | { 842 | if (this._logger.IsEnabled(LogLevel.Debug)) 843 | this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); 844 | } 845 | 846 | // receive messages 847 | return this.ReceiveAsync(websocket); 848 | } 849 | catch (Exception ex) 850 | { 851 | if (this._logger.IsEnabled(LogLevel.Debug)) 852 | this._logger.Log(LogLevel.Error, $"Unable to wrap a WebSocket connection [{webSocket.GetType()}]: {ex.Message}", ex); 853 | return Task.FromException(new WrapWebSocketFailedException($"Unable to wrap a WebSocket connection [{webSocket.GetType()}]", ex)); 854 | } 855 | } 856 | 857 | /// 858 | /// Wraps a WebSocket connection of ASP.NET / ASP.NET Core and acts like a WebSocket server 859 | /// 860 | /// The WebSocket connection of ASP.NET / ASP.NET Core 861 | /// The original request URI of the WebSocket connection 862 | /// The remote endpoint of the WebSocket connection 863 | /// A task that run the receiving process when wrap successful or an exception when failed 864 | public Task WrapAsync(System.Net.WebSockets.WebSocket webSocket, Uri requestUri, EndPoint remoteEndPoint) 865 | => this.WrapAsync(webSocket, requestUri, remoteEndPoint, null, new Dictionary(), null); 866 | #endregion 867 | 868 | #region Receive messages 869 | async Task ReceiveAsync(ManagedWebSocket websocket) 870 | { 871 | var buffer = new ArraySegment(new byte[WebSocketHelper.ReceiveBufferSize]); 872 | while (!this._processingCTS.IsCancellationRequested) 873 | { 874 | // receive message from the WebSocket connection 875 | WebSocketReceiveResult result = null; 876 | try 877 | { 878 | result = await websocket.ReceiveAsync(buffer, this._processingCTS.Token).ConfigureAwait(false); 879 | } 880 | catch (Exception ex) 881 | { 882 | var closeStatus = WebSocketCloseStatus.InternalServerError; 883 | var closeStatusDescription = $"Got an unexpected error: {ex.Message}"; 884 | if (ex is OperationCanceledException || ex is TaskCanceledException || ex is ObjectDisposedException || ex is WebSocketException || ex is SocketException || ex is IOException) 885 | { 886 | closeStatus = websocket.IsClient ? WebSocketCloseStatus.NormalClosure : WebSocketCloseStatus.EndpointUnavailable; 887 | closeStatusDescription = websocket.IsClient ? "Disconnected" : "Service is unavailable"; 888 | } 889 | 890 | await this.CloseWebSocketAsync(websocket, closeStatus, closeStatusDescription).ConfigureAwait(false); 891 | try 892 | { 893 | this.ConnectionBrokenHandler?.Invoke(websocket); 894 | } 895 | catch (Exception e) 896 | { 897 | if (this._logger.IsEnabled(LogLevel.Debug)) 898 | this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); 899 | } 900 | 901 | if (ex is OperationCanceledException || ex is TaskCanceledException || ex is ObjectDisposedException || ex is WebSocketException || ex is SocketException || ex is IOException) 902 | { 903 | if (this._logger.IsEnabled(LogLevel.Trace)) 904 | this._logger.Log(LogLevel.Debug, $"Stop receiving process when got an error: {ex.Message} ({ex.GetType().GetTypeName(true)})"); 905 | } 906 | else 907 | { 908 | if (this._logger.IsEnabled(LogLevel.Debug)) 909 | this._logger.Log(LogLevel.Error, closeStatusDescription, ex); 910 | try 911 | { 912 | this.ErrorHandler?.Invoke(websocket, ex); 913 | } 914 | catch (Exception e) 915 | { 916 | if (this._logger.IsEnabled(LogLevel.Debug)) 917 | this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); 918 | } 919 | } 920 | return; 921 | } 922 | 923 | // message to close 924 | if (result.MessageType == WebSocketMessageType.Close) 925 | { 926 | if (this._logger.IsEnabled(LogLevel.Trace)) 927 | this._logger.Log(LogLevel.Debug, $"The remote endpoint is initiated to close - Status: {result.CloseStatus} - Description: {result.CloseStatusDescription ?? "N/A"} ({websocket.ID} @ {websocket.RemoteEndPoint})"); 928 | await this.CloseWebSocketAsync(websocket).ConfigureAwait(false); 929 | try 930 | { 931 | this.ConnectionBrokenHandler?.Invoke(websocket); 932 | } 933 | catch (Exception ex) 934 | { 935 | if (this._logger.IsEnabled(LogLevel.Debug)) 936 | this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); 937 | } 938 | return; 939 | } 940 | 941 | // exceed buffer size 942 | if (result.Count > WebSocketHelper.ReceiveBufferSize) 943 | { 944 | var message = $"WebSocket frame cannot exceed buffer size of {WebSocketHelper.ReceiveBufferSize:#,##0} bytes"; 945 | if (this._logger.IsEnabled(LogLevel.Trace)) 946 | this._logger.Log(LogLevel.Debug, $"Close the connection because {message} ({websocket.ID} @ {websocket.RemoteEndPoint})"); 947 | await websocket.CloseAsync(WebSocketCloseStatus.MessageTooBig, $"{message}, send multiple frames instead.", CancellationToken.None).ConfigureAwait(false); 948 | 949 | await this.CloseWebSocketAsync(websocket).ConfigureAwait(false); 950 | try 951 | { 952 | this.ConnectionBrokenHandler?.Invoke(websocket); 953 | this.ErrorHandler?.Invoke(websocket, new BufferOverflowException(message)); 954 | } 955 | catch (Exception ex) 956 | { 957 | if (this._logger.IsEnabled(LogLevel.Debug)) 958 | this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); 959 | } 960 | return; 961 | } 962 | 963 | // got a message 964 | if (result.Count > 0) 965 | { 966 | if (this._logger.IsEnabled(LogLevel.Trace)) 967 | this._logger.Log(LogLevel.Debug, $"A message was received - Type: {result.MessageType} - EoM: {result.EndOfMessage} - Length: {result.Count:#,##0} ({websocket.ID} @ {websocket.RemoteEndPoint})"); 968 | try 969 | { 970 | this.MessageReceivedHandler?.Invoke(websocket, result, buffer.Take(result.Count)); 971 | } 972 | catch (Exception ex) 973 | { 974 | if (this._logger.IsEnabled(LogLevel.Debug)) 975 | this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); 976 | } 977 | } 978 | 979 | // wait for next round 980 | if (this.ReceivingAwaitInterval.Ticks > 0) 981 | try 982 | { 983 | await Task.Delay(this.ReceivingAwaitInterval, this._processingCTS.Token).ConfigureAwait(false); 984 | } 985 | catch 986 | { 987 | await this.CloseWebSocketAsync(websocket, websocket.IsClient ? WebSocketCloseStatus.NormalClosure : WebSocketCloseStatus.EndpointUnavailable, websocket.IsClient ? "Disconnected" : "Service is unavailable").ConfigureAwait(false); 988 | try 989 | { 990 | this.ConnectionBrokenHandler?.Invoke(websocket); 991 | } 992 | catch (Exception ex) 993 | { 994 | if (this._logger.IsEnabled(LogLevel.Debug)) 995 | this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {ex.Message}", ex); 996 | } 997 | return; 998 | } 999 | 1000 | // prepare buffer for next round 1001 | if (!buffer.Array.Length.Equals(WebSocketHelper.ReceiveBufferSize)) 1002 | buffer = new ArraySegment(new byte[WebSocketHelper.ReceiveBufferSize]); 1003 | } 1004 | } 1005 | #endregion 1006 | 1007 | #region Send messages 1008 | /// 1009 | /// Sends the message to a WebSocket connection 1010 | /// 1011 | /// The identity of a WebSocket connection 1012 | /// The buffer containing message to send 1013 | /// The message type, can be Text or Binary 1014 | /// 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) 1015 | /// The cancellation token 1016 | /// 1017 | public async Task SendAsync(Guid id, ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken = default) 1018 | { 1019 | ManagedWebSocket websocket = null; 1020 | try 1021 | { 1022 | if (this._websockets.TryGetValue(id, out websocket)) 1023 | await websocket.SendAsync(buffer, messageType, endOfMessage, cancellationToken).ConfigureAwait(false); 1024 | else 1025 | throw new InformationNotFoundException($"WebSocket connection with identity \"{id}\" is not found"); 1026 | } 1027 | catch (Exception ex) 1028 | { 1029 | try 1030 | { 1031 | this.ErrorHandler?.Invoke(websocket, ex); 1032 | } 1033 | catch (Exception e) 1034 | { 1035 | if (this._logger.IsEnabled(LogLevel.Debug)) 1036 | this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); 1037 | } 1038 | } 1039 | } 1040 | 1041 | /// 1042 | /// Sends the message to a WebSocket connection 1043 | /// 1044 | /// The identity of a WebSocket connection 1045 | /// The text message to send 1046 | /// 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) 1047 | /// The cancellation token 1048 | /// 1049 | public async Task SendAsync(Guid id, string message, bool endOfMessage, CancellationToken cancellationToken = default) 1050 | { 1051 | ManagedWebSocket websocket = null; 1052 | try 1053 | { 1054 | if (this._websockets.TryGetValue(id, out websocket)) 1055 | await websocket.SendAsync(message, endOfMessage, cancellationToken).ConfigureAwait(false); 1056 | else 1057 | throw new InformationNotFoundException($"WebSocket connection with identity \"{id}\" is not found"); 1058 | } 1059 | catch (Exception ex) 1060 | { 1061 | try 1062 | { 1063 | this.ErrorHandler?.Invoke(websocket, ex); 1064 | } 1065 | catch (Exception e) 1066 | { 1067 | if (this._logger.IsEnabled(LogLevel.Debug)) 1068 | this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); 1069 | } 1070 | } 1071 | } 1072 | 1073 | /// 1074 | /// Sends the message to a WebSocket connection 1075 | /// 1076 | /// The identity of a WebSocket connection 1077 | /// The binary message to send 1078 | /// 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) 1079 | /// The cancellation token 1080 | /// 1081 | public async Task SendAsync(Guid id, byte[] message, bool endOfMessage, CancellationToken cancellationToken = default) 1082 | { 1083 | ManagedWebSocket websocket = null; 1084 | try 1085 | { 1086 | if (this._websockets.TryGetValue(id, out websocket)) 1087 | await websocket.SendAsync(message, endOfMessage, cancellationToken).ConfigureAwait(false); 1088 | else 1089 | throw new InformationNotFoundException($"WebSocket connection with identity \"{id}\" is not found"); 1090 | } 1091 | catch (Exception ex) 1092 | { 1093 | try 1094 | { 1095 | this.ErrorHandler?.Invoke(websocket, ex); 1096 | } 1097 | catch (Exception e) 1098 | { 1099 | if (this._logger.IsEnabled(LogLevel.Debug)) 1100 | this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); 1101 | } 1102 | } 1103 | } 1104 | 1105 | /// 1106 | /// Sends the message to the WebSocket connections that matched with the predicate 1107 | /// 1108 | /// The predicate for selecting WebSocket connections 1109 | /// The buffer containing message to send 1110 | /// The message type. Can be Text or Binary 1111 | /// 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) 1112 | /// The cancellation token 1113 | /// 1114 | public Task SendAsync(Func predicate, ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken = default) 1115 | => this.GetWebSockets(predicate).ForEachAsync(async websocket => 1116 | { 1117 | try 1118 | { 1119 | await websocket.SendAsync(buffer.Clone(), messageType, endOfMessage, cancellationToken).ConfigureAwait(false); 1120 | } 1121 | catch (Exception ex) 1122 | { 1123 | try 1124 | { 1125 | this.ErrorHandler?.Invoke(websocket, ex); 1126 | } 1127 | catch (Exception e) 1128 | { 1129 | if (this._logger.IsEnabled(LogLevel.Debug)) 1130 | this._logger.Log(LogLevel.Error, $"Error occurred while calling the handler => {e.Message}", e); 1131 | } 1132 | } 1133 | }); 1134 | 1135 | /// 1136 | /// Sends the message to the WebSocket connections that matched with the predicate 1137 | /// 1138 | /// The predicate for selecting WebSocket connections 1139 | /// The text message to send 1140 | /// 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) 1141 | /// The cancellation token 1142 | /// 1143 | public Task SendAsync(Func predicate, string message, bool endOfMessage, CancellationToken cancellationToken = default) 1144 | => this.SendAsync(predicate, message.ToArraySegment(), WebSocketMessageType.Text, endOfMessage, cancellationToken); 1145 | 1146 | /// 1147 | /// Sends the message to the WebSocket connections that matched with the predicate 1148 | /// 1149 | /// The predicate for selecting WebSocket connections 1150 | /// The binary message to send 1151 | /// 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) 1152 | /// The cancellation token 1153 | /// 1154 | public Task SendAsync(Func predicate, byte[] message, bool endOfMessage, CancellationToken cancellationToken = default) 1155 | => this.SendAsync(predicate, message.ToArraySegment(), WebSocketMessageType.Binary, endOfMessage, cancellationToken); 1156 | #endregion 1157 | 1158 | #region Connection management 1159 | bool AddWebSocket(ManagedWebSocket websocket) 1160 | => websocket != null && this._websockets.TryAdd(websocket.ID, websocket); 1161 | 1162 | async Task AddWebSocketAsync(ManagedWebSocket websocket) 1163 | { 1164 | if (!this.AddWebSocket(websocket)) 1165 | { 1166 | if (websocket != null) 1167 | await Task.Delay(UtilityService.GetRandomNumber(123, 456), this._processingCTS.Token).ConfigureAwait(false); 1168 | return this.AddWebSocket(websocket); 1169 | } 1170 | return true; 1171 | } 1172 | 1173 | /// 1174 | /// Gets a WebSocket connection that specifed by identity 1175 | /// 1176 | /// 1177 | /// 1178 | public ManagedWebSocket GetWebSocket(Guid id) 1179 | => this._websockets.TryGetValue(id, out ManagedWebSocket websocket) 1180 | ? websocket 1181 | : null; 1182 | 1183 | /// 1184 | /// Gets the collection of WebSocket connections that matched with the predicate 1185 | /// 1186 | /// Predicate for selecting WebSocket connections, if no predicate is provied then return all 1187 | /// 1188 | public IEnumerable GetWebSockets(Func predicate = null) 1189 | => predicate != null 1190 | ? this._websockets.Values.Where(websocket => predicate(websocket)) 1191 | : this._websockets.Values; 1192 | 1193 | /// 1194 | /// Closes the WebSocket connection and remove from the centralized collections 1195 | /// 1196 | /// The WebSocket connection to close 1197 | /// The close status to use 1198 | /// A description of why we are closing 1199 | /// 1200 | async Task CloseWebsocketAsync(ManagedWebSocket websocket, WebSocketCloseStatus closeStatus, string closeStatusDescription) 1201 | { 1202 | if (websocket.State == WebSocketState.Open) 1203 | await websocket.DisposeAsync(closeStatus, closeStatusDescription).ConfigureAwait(false); 1204 | return true; 1205 | } 1206 | 1207 | /// 1208 | /// Closes the WebSocket connection and remove from the centralized collections 1209 | /// 1210 | /// The WebSocket connection to close 1211 | /// The close status to use 1212 | /// A description of why we are closing 1213 | /// 1214 | bool CloseWebsocket(ManagedWebSocket websocket, WebSocketCloseStatus closeStatus, string closeStatusDescription) 1215 | { 1216 | this.CloseWebsocketAsync(websocket, closeStatus, closeStatusDescription).Execute(); 1217 | return true; 1218 | } 1219 | 1220 | /// 1221 | /// Closes the WebSocket connection and remove from the centralized collections 1222 | /// 1223 | /// The identity of a WebSocket connection to close 1224 | /// The close status to use 1225 | /// A description of why we are closing 1226 | /// true if closed and destroyed 1227 | public bool CloseWebSocket(Guid id, WebSocketCloseStatus closeStatus = WebSocketCloseStatus.EndpointUnavailable, string closeStatusDescription = "Service is unavailable") 1228 | => this._websockets.TryRemove(id, out var websocket) && this.CloseWebsocket(websocket, closeStatus, closeStatusDescription); 1229 | 1230 | /// 1231 | /// Closes the WebSocket connection and remove from the centralized collections 1232 | /// 1233 | /// The identity of a WebSocket connection to close 1234 | /// The close status to use 1235 | /// A description of why we are closing 1236 | /// true if closed and destroyed 1237 | public Task CloseWebSocketAsync(Guid id, WebSocketCloseStatus closeStatus = WebSocketCloseStatus.EndpointUnavailable, string closeStatusDescription = "Service is unavailable") 1238 | => this._websockets.TryRemove(id, out var websocket) 1239 | ? this.CloseWebsocketAsync(websocket, closeStatus, closeStatusDescription) 1240 | : Task.FromResult(false); 1241 | 1242 | /// 1243 | /// Closes the WebSocket connection and remove from the centralized collections 1244 | /// 1245 | /// The WebSocket connection to close 1246 | /// The close status to use 1247 | /// A description of why we are closing 1248 | /// true if closed and destroyed 1249 | public bool CloseWebSocket(ManagedWebSocket websocket, WebSocketCloseStatus closeStatus = WebSocketCloseStatus.EndpointUnavailable, string closeStatusDescription = "Service is unavailable") 1250 | => websocket != null && this.CloseWebsocket(this._websockets.TryRemove(websocket.ID, out var webSocket) ? webSocket : websocket, closeStatus, closeStatusDescription); 1251 | 1252 | /// 1253 | /// Closes the WebSocket connection and remove from the centralized collections 1254 | /// 1255 | /// The WebSocket connection to close 1256 | /// The close status to use 1257 | /// A description of why we are closing 1258 | /// true if closed and destroyed 1259 | public Task CloseWebSocketAsync(ManagedWebSocket websocket, WebSocketCloseStatus closeStatus = WebSocketCloseStatus.EndpointUnavailable, string closeStatusDescription = "Service is unavailable") 1260 | => websocket == null 1261 | ? Task.FromResult(false) 1262 | : this.CloseWebsocketAsync(this._websockets.TryRemove(websocket.ID, out var webSocket) ? webSocket : websocket, closeStatus, closeStatusDescription); 1263 | #endregion 1264 | 1265 | #region Dispose 1266 | /// 1267 | /// Disposes this WebSocket 1268 | /// 1269 | /// The action to run when the WebSocket object was disposed 1270 | /// 1271 | async ValueTask DisposeAsync(Action next) 1272 | { 1273 | // update state 1274 | this.IsDisposed = true; 1275 | 1276 | // stop listener 1277 | this.StopListen(); 1278 | 1279 | // close all WebSocket connections 1280 | await this._websockets.Values.ForEachAsync(async websocket => await websocket.DisposeAsync(WebSocketCloseStatus.NormalClosure, "Disconnected").ConfigureAwait(false)).ConfigureAwait(false); 1281 | this._websockets.Clear(); 1282 | 1283 | // cancel all pending operations 1284 | this._listeningCTS?.Dispose(); 1285 | this._processingCTS.Cancel(); 1286 | this._processingCTS.Dispose(); 1287 | 1288 | // run the next action 1289 | try 1290 | { 1291 | next?.Invoke(this); 1292 | } 1293 | catch { } 1294 | } 1295 | 1296 | /// 1297 | /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources asynchronously 1298 | /// 1299 | /// 1300 | public ValueTask DisposeAsync() 1301 | => this.IsDisposed ? new ValueTask(Task.CompletedTask) : this.DisposeAsync(null); 1302 | 1303 | public void Dispose() 1304 | { 1305 | GC.SuppressFinalize(this); 1306 | this.DisposeAsync().Execute(true); 1307 | } 1308 | 1309 | ~WebSocket() 1310 | => this.Dispose(); 1311 | #endregion 1312 | 1313 | } 1314 | 1315 | // ------------------------------------------------------------------------ 1316 | 1317 | /// 1318 | /// An implementation or a wrapper of the WebSocket abstract class with more useful information 1319 | /// 1320 | public abstract class ManagedWebSocket : System.Net.WebSockets.WebSocket, IAsyncDisposable 1321 | { 1322 | 1323 | #region Properties 1324 | /// 1325 | /// Gets the identity of the WebSocket connection 1326 | /// 1327 | public Guid ID { get; internal set; } 1328 | 1329 | /// 1330 | /// Gets the state that indicates the WebSocket connection is client mode or not (client mode means the WebSocket connection is connected to a remote endpoint) 1331 | /// 1332 | public bool IsClient { get; protected set; } = false; 1333 | 1334 | /// 1335 | /// Gets the keep-alive interval (seconds) the WebSocket connection (for send ping message from server) 1336 | /// 1337 | public TimeSpan KeepAliveInterval { get; internal set; } = TimeSpan.Zero; 1338 | 1339 | /// 1340 | /// Gets the time-stamp when the WebSocket connection is established 1341 | /// 1342 | public DateTime Timestamp { get; } = DateTime.Now; 1343 | 1344 | /// 1345 | /// Gets the original Uniform Resource Identifier (URI) of the WebSocket connection 1346 | /// 1347 | public Uri RequestUri { get; internal set; } 1348 | 1349 | /// 1350 | /// Gets the remote endpoint of the WebSocket connection 1351 | /// 1352 | public EndPoint RemoteEndPoint { get; internal set; } 1353 | 1354 | /// 1355 | /// Gets the local endpoint of the WebSocket connection 1356 | /// 1357 | public EndPoint LocalEndPoint { get; internal set; } 1358 | 1359 | /// 1360 | /// Gets the extra information of the WebSocket connection 1361 | /// 1362 | public ConcurrentDictionary Extra { get; } = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); 1363 | 1364 | /// 1365 | /// 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 1366 | /// 1367 | protected abstract bool IncludeExceptionInCloseResponse { get; } 1368 | 1369 | /// 1370 | /// Gets the state that determines the WebSocket object was disposed or not 1371 | /// 1372 | protected bool IsDisposed { get; private set; } = false; 1373 | #endregion 1374 | 1375 | #region Send methods 1376 | /// 1377 | /// Sends data over the WebSocket connection asynchronously 1378 | /// 1379 | /// The message to send 1380 | /// true if this message is a standalone message (this is the norm), if its a multi-part message then false (and true for the last) 1381 | /// The cancellation token 1382 | /// 1383 | public Task SendAsync(ArraySegment message, bool endOfMessage, CancellationToken cancellationToken = default) 1384 | => this.SendAsync(message, WebSocketMessageType.Binary, endOfMessage, cancellationToken); 1385 | 1386 | /// 1387 | /// Sends data over the WebSocket connection asynchronously 1388 | /// 1389 | /// The message to send 1390 | /// The cancellation token 1391 | /// 1392 | public async Task SendAsync(ArraySegment message, CancellationToken cancellationToken = default) 1393 | { 1394 | var messages = message.Split(WebSocketHelper.ReceiveBufferSize); 1395 | await messages.ForEachAsync((msg, index) => this.SendAsync(msg, index == messages.Count - 1, cancellationToken), true, false).ConfigureAwait(false); 1396 | } 1397 | 1398 | /// 1399 | /// Sends data over the WebSocket connection asynchronously 1400 | /// 1401 | /// The message to send 1402 | /// true if this message is a standalone message (this is the norm), if its a multi-part message then false (and true for the last) 1403 | /// The cancellation token 1404 | /// 1405 | public Task SendAsync(byte[] message, bool endOfMessage, CancellationToken cancellationToken = default) 1406 | => this.SendAsync((message ?? Array.Empty()).ToArraySegment(), endOfMessage, cancellationToken); 1407 | 1408 | /// 1409 | /// Sends data over the WebSocket connection asynchronously 1410 | /// 1411 | /// The message to send 1412 | /// The cancellation token 1413 | /// 1414 | public Task SendAsync(byte[] message, CancellationToken cancellationToken = default) 1415 | => this.SendAsync((message ?? Array.Empty()).ToArraySegment(), cancellationToken); 1416 | 1417 | /// 1418 | /// Sends data over the WebSocket connection asynchronously 1419 | /// 1420 | /// The message to send 1421 | /// true if this message is a standalone message (this is the norm), if its a multi-part message then false (and true for the last) 1422 | /// The cancellation token 1423 | /// 1424 | public Task SendAsync(string message, bool endOfMessage, CancellationToken cancellationToken = default) 1425 | => this.SendAsync((message ?? "").ToArraySegment(), WebSocketMessageType.Text, endOfMessage, cancellationToken); 1426 | 1427 | /// 1428 | /// Sends data over the WebSocket connection asynchronously 1429 | /// 1430 | /// The message to send 1431 | /// The cancellation token 1432 | /// 1433 | public async Task SendAsync(string message, CancellationToken cancellationToken = default) 1434 | { 1435 | var messages = (message ?? "").ToArraySegment().Split(WebSocketHelper.ReceiveBufferSize); 1436 | await messages.ForEachAsync((msg, index) => this.SendAsync(msg, WebSocketMessageType.Text, index == messages.Count - 1, cancellationToken), true, false).ConfigureAwait(false); 1437 | } 1438 | #endregion 1439 | 1440 | #region Close & Dispose methods 1441 | /// 1442 | /// Closes the WebSocket connection automatically in period of time (to response to some invalid data from the remote host) 1443 | /// 1444 | /// The close status to use 1445 | /// A description of why we are closing 1446 | /// The exception (for logging) 1447 | /// The action to run when the connection was closed successful 1448 | /// The action to run when got any error 1449 | /// The seconds for awaiting 1450 | /// 1451 | internal async ValueTask CloseOutputTimeoutAsync(WebSocketCloseStatus closeStatus, string closeStatusDescription, Exception exception, Action onSuccess = null, Action onError = null, int awaitingTimes = 3) 1452 | { 1453 | Events.Log.CloseOutputAutoTimeout(this.ID, closeStatus, closeStatusDescription, exception != null ? exception.ToString() : "N/A"); 1454 | using (var timeoutToken = new CancellationTokenSource(TimeSpan.FromSeconds(awaitingTimes > 0 ? awaitingTimes : 3))) 1455 | { 1456 | try 1457 | { 1458 | await this.CloseOutputAsync(closeStatus, (closeStatusDescription ?? "") + (this.IncludeExceptionInCloseResponse && exception != null ? "\r\n\r\n" + exception.ToString() : ""), timeoutToken.Token).ConfigureAwait(false); 1459 | onSuccess?.Invoke(this); 1460 | } 1461 | catch (OperationCanceledException ex) 1462 | { 1463 | Events.Log.CloseOutputAutoTimeoutCancelled(this.ID, awaitingTimes > 0 ? awaitingTimes : 3, closeStatus, closeStatusDescription, exception != null ? exception.ToString() : "N/A"); 1464 | onError?.Invoke(ex); 1465 | } 1466 | catch (Exception ex) 1467 | { 1468 | Events.Log.CloseOutputAutoTimeoutError(this.ID, ex.ToString(), closeStatus, closeStatusDescription, exception != null ? exception.ToString() : "N/A"); 1469 | Logger.Log(LogLevel.Debug, LogLevel.Warning, $"Error occurred while closing a WebSocket connection by time-out => {ex.Message} ({this.ID} @ {this.RemoteEndPoint})", ex); 1470 | onError?.Invoke(ex); 1471 | } 1472 | } 1473 | } 1474 | 1475 | /// 1476 | /// Cleans up unmanaged resources (will send a close frame if the connection is still open) 1477 | /// 1478 | /// The closing status 1479 | /// The closing status description 1480 | /// The action to run when the WebSocket object was disposed 1481 | /// 1482 | internal virtual async ValueTask DisposeAsync(WebSocketCloseStatus closeStatus, string closeStatusDescription = "Service is unavailable", Action next = null) 1483 | { 1484 | if (!this.IsDisposed) 1485 | { 1486 | // close output 1487 | this.IsDisposed = true; 1488 | Events.Log.WebSocketDispose(this.ID, this.State); 1489 | if (this.State == WebSocketState.Open) 1490 | await this.CloseOutputTimeoutAsync( 1491 | closeStatus, 1492 | closeStatusDescription, 1493 | null, 1494 | _ => Events.Log.WebSocketDisposeCloseTimeout(this.ID, this.State), 1495 | ex => Events.Log.WebSocketDisposeError(this.ID, this.State, ex.ToString()) 1496 | ).ConfigureAwait(false); 1497 | 1498 | // run the next action 1499 | try 1500 | { 1501 | next?.Invoke(this); 1502 | } 1503 | catch (Exception ex) 1504 | { 1505 | Events.Log.WebSocketDisposeError(this.ID, this.State, ex.ToString()); 1506 | } 1507 | } 1508 | } 1509 | 1510 | /// 1511 | /// Cleans up unmanaged resources (will send a close frame if the connection is still open) 1512 | /// 1513 | /// 1514 | public virtual ValueTask DisposeAsync() 1515 | { 1516 | GC.SuppressFinalize(this); 1517 | return this.IsDisposed ? new ValueTask(Task.CompletedTask) : this.DisposeAsync(WebSocketCloseStatus.EndpointUnavailable); 1518 | } 1519 | 1520 | /// 1521 | /// Cleans up unmanaged resources (will send a close frame if the connection is still open) 1522 | /// 1523 | public override void Dispose() 1524 | => this.DisposeAsync().Execute(true); 1525 | 1526 | ~ManagedWebSocket() 1527 | => this.Dispose(); 1528 | #endregion 1529 | 1530 | #region Working with Extra information 1531 | /// 1532 | /// Sets the value of a specified key of the extra information 1533 | /// 1534 | /// 1535 | /// 1536 | /// 1537 | /// 1538 | public bool Set(string key, T value) 1539 | { 1540 | this.Extra[key] = value; 1541 | return true; 1542 | } 1543 | 1544 | /// 1545 | /// Gets the value of a specified key from the extra information 1546 | /// 1547 | /// 1548 | /// 1549 | /// 1550 | /// 1551 | public T Get(string key, T @default = default) 1552 | => this.Extra.TryGetValue(key, out object value) && value != null && value is T valueIsT ? valueIsT : @default; 1553 | 1554 | /// 1555 | /// Removes the value of a specified key from the extra information 1556 | /// 1557 | /// 1558 | /// 1559 | public bool Remove(string key) 1560 | => this.Extra.Remove(key); 1561 | 1562 | /// 1563 | /// Removes the value of a specified key from the extra information 1564 | /// 1565 | /// 1566 | /// 1567 | /// 1568 | /// 1569 | public bool Remove(string key, out T value) 1570 | { 1571 | if (this.Extra.Remove(key, out var val) && val is T valueIsT) 1572 | { 1573 | value = valueIsT; 1574 | return true; 1575 | } 1576 | value = default; 1577 | return false; 1578 | } 1579 | 1580 | /// 1581 | /// Gets the header information of the WebSocket connection 1582 | /// 1583 | public Dictionary Headers => this.Get("Headers", new Dictionary()); 1584 | #endregion 1585 | 1586 | } 1587 | } 1588 | --------------------------------------------------------------------------------