├── 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 | [](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 |
--------------------------------------------------------------------------------