├── .gitattributes ├── .gitignore ├── AssemblyVersioning.ps1 ├── LICENSE.txt ├── NetCoreStack.WebSockets.sln ├── NuGet.config ├── README.md ├── appveyor.yml ├── src ├── NetCoreStack.WebSockets.ProxyClient │ ├── ClientWebSocketConnector.cs │ ├── ClientWebSocketConnectorOfT.cs │ ├── ClientWebSocketReceiver.cs │ ├── ConnectorHostPair.cs │ ├── DefaultClientInvocatorContextFactory.cs │ ├── Extensions │ │ ├── ApplicationBuilderExtensions.cs │ │ ├── ClientWebSocketReceiverExtensions.cs │ │ ├── ConsoleApplicationBuilderExtensions.cs │ │ └── ServiceCollectionExtensions.cs │ ├── IClientInvocatorContextFactory.cs │ ├── IClientWebSocketCommandInvocator.cs │ ├── IWebSocketConnector.cs │ ├── InvocatorFactory.cs │ ├── InvocatorsHelper.cs │ ├── NetCoreStack.WebSockets.ProxyClient.csproj │ ├── ProxyClientMarkerService.cs │ ├── ProxyLogHelper.cs │ ├── ProxyOptions.cs │ ├── ProxyWebSocketsBuilder.cs │ └── Types │ │ ├── ClientInvocatorContext.cs │ │ └── ClientWebSocketReceiverContext.cs └── NetCoreStack.WebSockets │ ├── ConnectionManager.cs │ ├── ConnectionManagerOfT.cs │ ├── DefaultHandshakeStateTransport.cs │ ├── DefaultHeaderProvider.cs │ ├── EnumExtensions.cs │ ├── Extensions │ ├── ByteExtensions.cs │ ├── SocketApplicationBuilderExtensions.cs │ ├── SocketServiceCollectionExtensions.cs │ ├── WebSocketExtensions.cs │ └── WebSocketReceiverExtensions.cs │ ├── Interfaces │ ├── IConnectionManager.cs │ ├── IHandshakeStateTransport.cs │ ├── IHeaderProvider.cs │ ├── IInvocatorContext.cs │ ├── IServerInvocatorContextFactory.cs │ ├── IServerWebSocketCommandInvocator.cs │ ├── IStreamCompressor.cs │ └── IWebSocketCommandInvocator.cs │ ├── Internal │ ├── DefaultClientInvocatorContextFactory.cs │ ├── FQNHelper.cs │ ├── GZipHelper.cs │ ├── GZipStreamCompressor.cs │ ├── LogHelper.cs │ ├── NCSConstants.cs │ ├── WebSocketMiddleware.cs │ ├── WebSocketReceiver.cs │ └── WebSocketTransport.cs │ ├── NetCoreStack.WebSockets.csproj │ ├── Types │ ├── InvocatorContext.cs │ ├── JsonObject.cs │ ├── ServerSocketOptions.cs │ ├── SocketsOptions.cs │ ├── WebSocketMessageContext.cs │ ├── WebSocketReceiverContext.cs │ ├── WebSocketReceiverContextBase.cs │ └── WebSocketSupportedSchemes.cs │ └── WebSocketCommands.cs └── test ├── Common.Libs ├── ApplicationVariables.cs ├── CacheHelper.cs ├── CacheItem1.cs ├── CacheItem2.cs ├── CacheItem3.cs ├── CacheItemDescriptor.cs ├── CacheItemWeights.cs ├── Common.Libs.csproj ├── InMemoryCacheExtensions.cs ├── InMemoryCacheProvider.cs ├── TypeCoreExtensions.cs └── WebSocketHeaderNames.cs ├── ConsoleAppProxyClient ├── ConsoleAppProxyClient.csproj ├── DataStreamingInvocator.cs └── Program.cs ├── NetCoreStack.WebSockets.Tests ├── AgentsWebSocketCommandInvocator.cs ├── AnotherEndpointWebSocketCommandInvocator.cs ├── CustomInvocatorContextFactory.cs ├── CustomWebSocketCommandInvocator.cs ├── NetCoreStack.WebSockets.Tests.csproj └── ProxyBuilderTests.cs ├── ServerTestApp ├── Controllers │ └── DiscoveryController.cs ├── DistributedCacheExtensions.cs ├── Models │ └── Models.cs ├── MyHandshakeStateTransport.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── ServerTestApp.csproj ├── ServerWebSocketCommandInvocator.cs ├── Startup.cs └── appsettings.json ├── WebClientTestApp ├── AgentsWebSocketCommandInvocator.cs ├── AnotherEndpointWebSocketCommandInvocator.cs ├── BroadcastMessageContext.cs ├── ClientExceptionFilterAttribute.cs ├── Constants.cs ├── Controllers │ ├── ChatController.cs │ ├── DiscoveryController.cs │ └── HomeController.cs ├── CustomInvocatorContextFactory.cs ├── CustomWebSocketCommandInvocator.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Startup.cs ├── Views │ ├── Home │ │ ├── Index.cshtml │ │ └── ReConnectTest.cshtml │ ├── Process │ │ └── Index.cshtml │ ├── Shared │ │ ├── Error.cshtml │ │ └── _Layout.cshtml │ ├── _ViewImports.cshtml │ └── _ViewStart.cshtml ├── WebClientTestApp.csproj ├── appsettings.json └── wwwroot │ ├── _references.js │ ├── css │ └── bootstrap.min.css │ ├── favicon.ico │ └── js │ ├── bootstrap.min.js │ ├── cookie.js │ ├── jquery-2.2.0.min.js │ ├── knockout-min.js │ └── reconnecting-websocket.min.js └── WebClientTestApp2 ├── AgentsWebSocketCommandInvocator.cs ├── Constants.cs ├── Controllers └── HomeController.cs ├── Program.cs ├── Properties └── launchSettings.json ├── Startup.cs ├── Views ├── Home │ ├── Index.cshtml │ └── ReConnectTest.cshtml ├── Shared │ ├── Error.cshtml │ └── _Layout.cshtml ├── _ViewImports.cshtml └── _ViewStart.cshtml ├── WebClientTestApp2.csproj ├── WebSocketCommandInvocator.cs ├── appsettings.json └── wwwroot ├── _references.js ├── css └── bootstrap.min.css ├── favicon.ico └── js ├── bootstrap.min.js ├── cookie.js ├── jquery-2.2.0.min.js ├── knockout-min.js └── reconnecting-websocket.min.js /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.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 | [Xx]64/ 19 | [Xx]86/ 20 | [Bb]uild/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 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 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | 85 | # Visual Studio profiler 86 | *.psess 87 | *.vsp 88 | *.vspx 89 | *.sap 90 | 91 | # TFS 2012 Local Workspace 92 | $tf/ 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | *.DotSettings.user 101 | 102 | # JustCode is a .NET coding add-in 103 | .JustCode 104 | 105 | # TeamCity is a build add-in 106 | _TeamCity* 107 | 108 | # DotCover is a Code Coverage Tool 109 | *.dotCover 110 | 111 | # NCrunch 112 | _NCrunch_* 113 | .*crunch*.local.xml 114 | nCrunchTemp_* 115 | 116 | # MightyMoose 117 | *.mm.* 118 | AutoTest.Net/ 119 | 120 | # Web workbench (sass) 121 | .sass-cache/ 122 | 123 | # Installshield output folder 124 | [Ee]xpress/ 125 | 126 | # DocProject is a documentation generator add-in 127 | DocProject/buildhelp/ 128 | DocProject/Help/*.HxT 129 | DocProject/Help/*.HxC 130 | DocProject/Help/*.hhc 131 | DocProject/Help/*.hhk 132 | DocProject/Help/*.hhp 133 | DocProject/Help/Html2 134 | DocProject/Help/html 135 | 136 | # Click-Once directory 137 | publish/ 138 | 139 | # Publish Web Output 140 | *.[Pp]ublish.xml 141 | *.azurePubxml 142 | 143 | # TODO: Un-comment the next line if you do not want to checkin 144 | # your web deploy settings because they may include unencrypted 145 | # passwords 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # NuGet Packages 150 | *.nupkg 151 | # The packages folder can be ignored because of Package Restore 152 | **/packages/* 153 | # except build/, which is used as an MSBuild target. 154 | !**/packages/build/ 155 | # Uncomment if necessary however generally it will be regenerated when needed 156 | #!**/packages/repositories.config 157 | # NuGet v3's project.json files produces more ignoreable files 158 | *.nuget.props 159 | *.nuget.targets 160 | 161 | # Microsoft Azure Build Output 162 | csx/ 163 | *.build.csdef 164 | 165 | # Microsoft Azure Emulator 166 | ecf/ 167 | rcf/ 168 | 169 | # Windows Store app package directory 170 | AppPackages/ 171 | BundleArtifacts/ 172 | 173 | # Visual Studio cache files 174 | # files ending in .cache can be ignored 175 | *.[Cc]ache 176 | # but keep track of directories ending in .cache 177 | !*.[Cc]ache/ 178 | 179 | # Others 180 | ClientBin/ 181 | [Ss]tyle[Cc]op.* 182 | ~$* 183 | *~ 184 | *.dbmdl 185 | *.dbproj.schemaview 186 | *.pfx 187 | *.publishsettings 188 | node_modules/ 189 | orleans.codegen.cs 190 | 191 | # RIA/Silverlight projects 192 | Generated_Code/ 193 | 194 | # Backup & report files from converting an old project file 195 | # to a newer Visual Studio version. Backup files are not needed, 196 | # because we have git ;-) 197 | _UpgradeReport_Files/ 198 | Backup*/ 199 | UpgradeLog*.XML 200 | UpgradeLog*.htm 201 | 202 | # SQL Server files 203 | *.mdf 204 | *.ldf 205 | 206 | # Business Intelligence projects 207 | *.rdl.data 208 | *.bim.layout 209 | *.bim_*.settings 210 | 211 | # Microsoft Fakes 212 | FakesAssemblies/ 213 | 214 | # GhostDoc plugin setting file 215 | *.GhostDoc.xml 216 | 217 | # Node.js Tools for Visual Studio 218 | .ntvs_analysis.dat 219 | 220 | # Visual Studio 6 build log 221 | *.plg 222 | 223 | # Visual Studio 6 workspace options file 224 | *.opt 225 | 226 | # Visual Studio LightSwitch build output 227 | **/*.HTMLClient/GeneratedArtifacts 228 | **/*.DesktopClient/GeneratedArtifacts 229 | **/*.DesktopClient/ModelManifest.xml 230 | **/*.Server/GeneratedArtifacts 231 | **/*.Server/ModelManifest.xml 232 | _Pvt_Extensions 233 | 234 | # LightSwitch generated files 235 | GeneratedArtifacts/ 236 | ModelManifest.xml 237 | 238 | # Paket dependency manager 239 | .paket/paket.exe 240 | 241 | # FAKE - F# Make 242 | .fake/ 243 | 244 | # Build 245 | .build 246 | -------------------------------------------------------------------------------- /AssemblyVersioning.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory=$True)] 3 | [string]$rootDir, 4 | [Parameter(Mandatory=$True)] 5 | [string]$fileNamePattern, 6 | [Parameter(Mandatory=$True)] 7 | [string]$major, 8 | [Parameter(Mandatory=$True)] 9 | [string]$minor 10 | ) 11 | 12 | Write-Verbose 'Entering assemblyversioning.ps1' 13 | 14 | ### First, mount the assembly version ### 15 | 16 | #Calculate today's julian date 17 | 18 | function Get-JulianDate { 19 | #Calculate today's julian date 20 | $Year = get-date -format yy 21 | $JulianYear = $Year.Substring(1) 22 | $DayOfYear = (Get-Date).DayofYear 23 | $JulianDate = $JulianYear + "{0:D3}" -f $DayOfYear 24 | $JulianDate 25 | return 26 | } 27 | 28 | $build = Get-JulianDate 29 | 30 | $buildNumberFromVso = $($env:BUILD_BUILDNUMBER) 31 | 32 | $revision = $buildNumberFromVso.Split(".")[1] 33 | 34 | # The Assembly Version 35 | 36 | $assemblyVersion = "$major.$minor.$build.$revision" 37 | 38 | Write-Host "The version this build will generate is $assemblyVersion" 39 | 40 | $assemblyVersionString = "AssemblyVersion(""$assemblyVersion"")" 41 | $assemblyFileVersionString = "AssemblyFileVersion(""$assemblyVersion"")" 42 | 43 | $assemblyInfoFiles = Get-ChildItem -Path $rootDir -Filter $fileNamePattern -Recurse 44 | $fileCount = $assemblyInfoFiles.Count 45 | 46 | Write-Host "" 47 | Write-Host "Started writing the $fileNamePattern files..." 48 | Write-Host "" 49 | 50 | foreach($file in $assemblyInfoFiles){ 51 | $fullFilePath = Join-Path $file.Directory $file.Name 52 | 53 | Write-Host "Editing $fullFilePath" 54 | 55 | Add-Content -Path $fullFilePath -Value "`n[assembly: $assemblyVersionString]" 56 | Add-Content -Path $fullFilePath -Value "[assembly: $assemblyFileVersionString]" 57 | } 58 | 59 | Write-Host "" 60 | Write-Host "Finished editing $fileCount AssemblyInfo files." -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 2 | these files except in compliance with the License. You may obtain a copy of the 3 | License at 4 | 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed 8 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 9 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 10 | specific language governing permissions and limitations under the License. 11 | -------------------------------------------------------------------------------- /NetCoreStack.WebSockets.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27130.2027 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{52C54080-F48B-4DBA-A418-8057611CAF6D}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{A1704016-03C8-4917-9C50-AE9DDF55ECB7}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A5D3D4D6-7DAB-4ACD-97EB-5435C9AE74A9}" 11 | ProjectSection(SolutionItems) = preProject 12 | NuGet.config = NuGet.config 13 | README.md = README.md 14 | EndProjectSection 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServerTestApp", "test\ServerTestApp\ServerTestApp.csproj", "{B8DE19DA-F0D1-48BA-B808-631A27D74C2B}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebClientTestApp", "test\WebClientTestApp\WebClientTestApp.csproj", "{079F8A19-BC40-46AC-A31F-B133F7D7F1B8}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetCoreStack.WebSockets", "src\NetCoreStack.WebSockets\NetCoreStack.WebSockets.csproj", "{C6BD4266-256A-4FF6-B991-D44EFC825FC6}" 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebClientTestApp2", "test\WebClientTestApp2\WebClientTestApp2.csproj", "{7B3B956E-DF5A-4AD8-BCB9-14235D53471C}" 23 | EndProject 24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common.Libs", "test\Common.Libs\Common.Libs.csproj", "{7ACFEED0-C1D7-43DE-95D7-3D2CEFDF6F9F}" 25 | EndProject 26 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleAppProxyClient", "test\ConsoleAppProxyClient\ConsoleAppProxyClient.csproj", "{1AAECD28-5C1D-4ED6-8CBC-6FCAA3804ABF}" 27 | EndProject 28 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetCoreStack.WebSockets.ProxyClient", "src\NetCoreStack.WebSockets.ProxyClient\NetCoreStack.WebSockets.ProxyClient.csproj", "{1738B969-D294-4417-999C-A3AC3B04EFE4}" 29 | EndProject 30 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetCoreStack.WebSockets.Tests", "test\NetCoreStack.WebSockets.Tests\NetCoreStack.WebSockets.Tests.csproj", "{8A756067-8C6F-475C-B2CF-97B582A4DB47}" 31 | EndProject 32 | Global 33 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 34 | Debug|Any CPU = Debug|Any CPU 35 | Release|Any CPU = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 38 | {B8DE19DA-F0D1-48BA-B808-631A27D74C2B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {B8DE19DA-F0D1-48BA-B808-631A27D74C2B}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {B8DE19DA-F0D1-48BA-B808-631A27D74C2B}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {B8DE19DA-F0D1-48BA-B808-631A27D74C2B}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {079F8A19-BC40-46AC-A31F-B133F7D7F1B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {079F8A19-BC40-46AC-A31F-B133F7D7F1B8}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {079F8A19-BC40-46AC-A31F-B133F7D7F1B8}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {079F8A19-BC40-46AC-A31F-B133F7D7F1B8}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {C6BD4266-256A-4FF6-B991-D44EFC825FC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {C6BD4266-256A-4FF6-B991-D44EFC825FC6}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {C6BD4266-256A-4FF6-B991-D44EFC825FC6}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {C6BD4266-256A-4FF6-B991-D44EFC825FC6}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {7B3B956E-DF5A-4AD8-BCB9-14235D53471C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {7B3B956E-DF5A-4AD8-BCB9-14235D53471C}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {7B3B956E-DF5A-4AD8-BCB9-14235D53471C}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {7B3B956E-DF5A-4AD8-BCB9-14235D53471C}.Release|Any CPU.Build.0 = Release|Any CPU 54 | {7ACFEED0-C1D7-43DE-95D7-3D2CEFDF6F9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 55 | {7ACFEED0-C1D7-43DE-95D7-3D2CEFDF6F9F}.Debug|Any CPU.Build.0 = Debug|Any CPU 56 | {7ACFEED0-C1D7-43DE-95D7-3D2CEFDF6F9F}.Release|Any CPU.ActiveCfg = Release|Any CPU 57 | {7ACFEED0-C1D7-43DE-95D7-3D2CEFDF6F9F}.Release|Any CPU.Build.0 = Release|Any CPU 58 | {1AAECD28-5C1D-4ED6-8CBC-6FCAA3804ABF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 59 | {1AAECD28-5C1D-4ED6-8CBC-6FCAA3804ABF}.Debug|Any CPU.Build.0 = Debug|Any CPU 60 | {1AAECD28-5C1D-4ED6-8CBC-6FCAA3804ABF}.Release|Any CPU.ActiveCfg = Release|Any CPU 61 | {1AAECD28-5C1D-4ED6-8CBC-6FCAA3804ABF}.Release|Any CPU.Build.0 = Release|Any CPU 62 | {1738B969-D294-4417-999C-A3AC3B04EFE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 63 | {1738B969-D294-4417-999C-A3AC3B04EFE4}.Debug|Any CPU.Build.0 = Debug|Any CPU 64 | {1738B969-D294-4417-999C-A3AC3B04EFE4}.Release|Any CPU.ActiveCfg = Release|Any CPU 65 | {1738B969-D294-4417-999C-A3AC3B04EFE4}.Release|Any CPU.Build.0 = Release|Any CPU 66 | {8A756067-8C6F-475C-B2CF-97B582A4DB47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 67 | {8A756067-8C6F-475C-B2CF-97B582A4DB47}.Debug|Any CPU.Build.0 = Debug|Any CPU 68 | {8A756067-8C6F-475C-B2CF-97B582A4DB47}.Release|Any CPU.ActiveCfg = Release|Any CPU 69 | {8A756067-8C6F-475C-B2CF-97B582A4DB47}.Release|Any CPU.Build.0 = Release|Any CPU 70 | EndGlobalSection 71 | GlobalSection(SolutionProperties) = preSolution 72 | HideSolutionNode = FALSE 73 | EndGlobalSection 74 | GlobalSection(NestedProjects) = preSolution 75 | {B8DE19DA-F0D1-48BA-B808-631A27D74C2B} = {A1704016-03C8-4917-9C50-AE9DDF55ECB7} 76 | {079F8A19-BC40-46AC-A31F-B133F7D7F1B8} = {A1704016-03C8-4917-9C50-AE9DDF55ECB7} 77 | {C6BD4266-256A-4FF6-B991-D44EFC825FC6} = {52C54080-F48B-4DBA-A418-8057611CAF6D} 78 | {7B3B956E-DF5A-4AD8-BCB9-14235D53471C} = {A1704016-03C8-4917-9C50-AE9DDF55ECB7} 79 | {7ACFEED0-C1D7-43DE-95D7-3D2CEFDF6F9F} = {A1704016-03C8-4917-9C50-AE9DDF55ECB7} 80 | {1AAECD28-5C1D-4ED6-8CBC-6FCAA3804ABF} = {A1704016-03C8-4917-9C50-AE9DDF55ECB7} 81 | {1738B969-D294-4417-999C-A3AC3B04EFE4} = {52C54080-F48B-4DBA-A418-8057611CAF6D} 82 | {8A756067-8C6F-475C-B2CF-97B582A4DB47} = {A1704016-03C8-4917-9C50-AE9DDF55ECB7} 83 | EndGlobalSection 84 | GlobalSection(ExtensibilityGlobals) = postSolution 85 | SolutionGuid = {F299AD98-9AB4-4C7B-B125-5E82EB9B8700} 86 | EndGlobalSection 87 | EndGlobal 88 | -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Cross-Platform WebSockets Proxy 2 | 3 | Minimalist websocket framework for .NET Core. You can use it on your APIs to communicate among your various client types. 4 | 5 | [![NuGet](https://img.shields.io/nuget/v/NetCoreStack.WebSockets.svg?longCache=true&style=flat-square)](https://www.nuget.org/packages/NetCoreStack.WebSockets) 6 | [![NuGet](https://img.shields.io/nuget/dt/NetCoreStack.WebSockets.svg?longCache=true&style=flat-square)](https://www.nuget.org/packages/NetCoreStack.WebSockets) 7 | 8 | [Latest release on Nuget](https://www.nuget.org/packages/NetCoreStack.WebSockets/) 9 | 10 | 11 | ### Usage for API - Service Layer 12 | 13 | #### Startup ConfigureServices 14 | ```csharp 15 | // Add NetCoreStack Native Socket Services. 16 | services.AddNativeWebSockets(); 17 | ``` 18 | 19 | #### Startup Configure 20 | ```csharp 21 | app.UseNativeWebSockets(); 22 | ``` 23 | 24 | #### Controller with dependency injection 25 | ```csharp 26 | public MyController(IConnectionManager connectionManager) 27 | { 28 | _connectionManager = connectionManager; 29 | } 30 | 31 | [HttpPost(nameof(SendAsync))] 32 | public async Task SendAsync([FromBody]SimpleModel model) 33 | { 34 | var echo = $"Echo from server '{model.Message}' - {DateTime.Now}"; 35 | var obj = new { message = echo }; 36 | var webSocketContext = new WebSocketMessageContext { Command = WebSocketCommands.DataSend, Value = obj }; 37 | await _connectionManager.BroadcastAsync(webSocketContext); 38 | return Ok(); 39 | } 40 | ``` 41 | 42 | ### Clients 43 | #### Startup ConfigureServices 44 | ```csharp 45 | // WebSockets for Browsers 46 | services.AddNativeWebSockets(); 47 | 48 | // Client WebSocket - Proxy connections 49 | var builder = services.AddProxyWebSockets(); 50 | 51 | var connectorname = $"TestWebApp-{Environment.MachineName}"; 52 | builder.Register(connectorname, "localhost:7803"); 53 | 54 | // Runtime context factory 55 | builder.Register(); 56 | ``` 57 | #### Startup Configure 58 | ```csharp 59 | // Client WebSocket - Proxy connections 60 | app.UseProxyWebSockets(); 61 | 62 | // WebSockets for Browsers 63 | app.UseNativeWebSockets(); 64 | 65 | // Use MVC 66 | app.UseMvc(); 67 | ``` 68 | 69 | #### Invocator With Dependency Injection on Clients 70 | 71 | ```csharp 72 | public class CustomWebSocketCommandInvocator : IClientWebSocketCommandInvocator 73 | { 74 | private readonly IConnectionManager _connectionManager; 75 | public CustomWebSocketCommandInvocator(IConnectionManager connectionManager) 76 | { 77 | _connectionManager = connectionManager; 78 | } 79 | 80 | public Task InvokeAsync(WebSocketMessageContext context) 81 | { 82 | // Sending incoming data from backend to the clients (Browsers) 83 | _connectionManager.BroadcastAsync(context); 84 | return Task.CompletedTask; 85 | } 86 | } 87 | ``` 88 | 89 | ```csharp 90 | public class ClientDiscoveryController : Controller 91 | { 92 | private readonly IWebSocketConnector _connector; 93 | public ClientDiscoveryController(IWebSocketConnector connector) 94 | { 95 | _connector = connector; 96 | } 97 | 98 | [HttpGet] 99 | public async Task KeepAlive() 100 | { 101 | await _connector.SendAsync(new WebSocketMessageContext 102 | { 103 | Command = WebSocketCommands.DataSend, 104 | Value = new { Id = 1, Name = "Hello World!", DateTime = DateTime.Now } 105 | }); 106 | 107 | return Ok(); 108 | } 109 | } 110 | ``` 111 | 112 | ### Prerequisites 113 | > [ASP.NET Core](https://github.com/aspnet/Home) 114 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | init: 2 | - git config --global core.autocrlf true 3 | branches: 4 | only: 5 | - master 6 | artifacts: 7 | - path: artifacts\build\*.nupkg 8 | build_script: 9 | - build.cmd --quiet verify 10 | clone_depth: 1 11 | test: off 12 | deploy: off -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets.ProxyClient/ClientWebSocketConnector.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using NetCoreStack.WebSockets.Interfaces; 3 | using NetCoreStack.WebSockets.Internal; 4 | using System; 5 | using System.Linq; 6 | using System.Net.WebSockets; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace NetCoreStack.WebSockets.ProxyClient 11 | { 12 | public abstract class ClientWebSocketConnector : IWebSocketConnector 13 | { 14 | private string _connectionId; 15 | private ClientWebSocket _webSocket; 16 | private readonly IServiceProvider _serviceProvider; 17 | private readonly IStreamCompressor _compressor; 18 | private readonly ILoggerFactory _loggerFactory; 19 | private readonly ILogger _logger; 20 | 21 | public string ConnectionId 22 | { 23 | get 24 | { 25 | return _connectionId; 26 | } 27 | } 28 | 29 | public WebSocketState WebSocketState 30 | { 31 | get 32 | { 33 | if (_webSocket == null) 34 | throw new InvalidOperationException("Make sure async instantiation completed and try again Connect!"); 35 | 36 | return _webSocket.State; 37 | } 38 | } 39 | 40 | public ClientWebSocketConnector(IServiceProvider serviceProvider, 41 | IStreamCompressor compressor, 42 | ILoggerFactory loggerFactory) 43 | { 44 | _serviceProvider = serviceProvider; 45 | _compressor = compressor; 46 | _loggerFactory = loggerFactory; 47 | _logger = _loggerFactory.CreateLogger(); 48 | } 49 | 50 | public abstract ClientInvocatorContext InvocatorContext { get; } 51 | 52 | private async Task TryConnectAsync(CancellationToken cancellationToken) 53 | { 54 | _webSocket = new ClientWebSocket(); 55 | _webSocket.Options.SetRequestHeader(NCSConstants.ConnectorName, InvocatorContext.ConnectorName); 56 | try 57 | { 58 | await _webSocket.ConnectAsync(InvocatorContext.Uri, cancellationToken); 59 | } 60 | catch (Exception ex) 61 | { 62 | ProxyLogHelper.Log(_loggerFactory, InvocatorContext, "Error", ex); 63 | return null; 64 | } 65 | 66 | var receiverContext = new ClientWebSocketReceiverContext 67 | { 68 | Compressor = _compressor, 69 | InvocatorContext = InvocatorContext, 70 | LoggerFactory = _loggerFactory, 71 | WebSocket = _webSocket 72 | }; 73 | 74 | var receiver = new ClientWebSocketReceiver(_serviceProvider, receiverContext, Close, (connectionId) => { 75 | _connectionId = connectionId; 76 | }); 77 | 78 | return receiver; 79 | } 80 | 81 | public async Task ConnectAsync(CancellationToken cancellationToken) 82 | { 83 | ClientWebSocketReceiver receiver = null; 84 | while (!cancellationToken.IsCancellationRequested) 85 | { 86 | _logger.LogInformation("===TryConnectAsync to: {0}", InvocatorContext.Uri.ToString()); 87 | receiver = await TryConnectAsync(cancellationToken); 88 | if (receiver != null && WebSocketState == WebSocketState.Open) 89 | { 90 | break; 91 | } 92 | 93 | _logger.LogInformation("===Retry..."); 94 | await Task.Delay(1000); 95 | } 96 | 97 | _logger.LogInformation("===WebSocketConnected to: {0}", InvocatorContext.Uri.ToString()); 98 | 99 | if (InvocatorContext.OnConnectedAsync != null) 100 | { 101 | await InvocatorContext.OnConnectedAsync(_webSocket); 102 | } 103 | 104 | await Task.WhenAll(receiver.ReceiveAsync(cancellationToken)); 105 | 106 | // Disconnected 107 | if (_webSocket.CloseStatus.HasValue || _webSocket.State == WebSocketState.Aborted) 108 | { 109 | if (InvocatorContext.OnDisconnectedAsync != null) 110 | { 111 | await InvocatorContext.OnDisconnectedAsync(_webSocket); 112 | } 113 | else 114 | { 115 | await ConnectAsync(cancellationToken); 116 | } 117 | } 118 | } 119 | 120 | private ArraySegment CreateTextSegment(WebSocketMessageContext context) 121 | { 122 | if (context == null) 123 | { 124 | throw new ArgumentNullException(nameof(context)); 125 | } 126 | 127 | object connectionId = string.Empty; 128 | if (context.Header.TryGetValue(NCSConstants.ConnectionId, out connectionId)) 129 | { 130 | var id = connectionId as string; 131 | if (string.IsNullOrEmpty(id)) 132 | { 133 | throw new InvalidOperationException(nameof(connectionId)); 134 | } 135 | } 136 | else 137 | { 138 | context.Header.Add(NCSConstants.ConnectionId, ConnectionId); 139 | } 140 | 141 | return context.ToSegment(); 142 | } 143 | 144 | public async Task SendAsync(WebSocketMessageContext context) 145 | { 146 | var segments = CreateTextSegment(context); 147 | await _webSocket.SendAsync(segments, WebSocketMessageType.Text, true, CancellationToken.None); 148 | } 149 | 150 | public async Task SendBinaryAsync(byte[] bytes) 151 | { 152 | var segments = new ArraySegment(bytes, 0, bytes.Count()); 153 | await _webSocket.SendAsync(segments, WebSocketMessageType.Binary, true, CancellationToken.None); 154 | } 155 | 156 | internal void Close(ClientWebSocketReceiverContext context) 157 | { 158 | context.WebSocket.Abort(); 159 | } 160 | 161 | internal void Close(string statusDescription) 162 | { 163 | _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, statusDescription, CancellationToken.None); 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets.ProxyClient/ClientWebSocketConnectorOfT.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using NetCoreStack.WebSockets.Interfaces; 3 | using System; 4 | 5 | namespace NetCoreStack.WebSockets.ProxyClient 6 | { 7 | public class ClientWebSocketConnectorOfT : ClientWebSocketConnector, 8 | IWebSocketConnector where TInvocator : IClientWebSocketCommandInvocator 9 | { 10 | private readonly IClientInvocatorContextFactory _invocatorContextFactory; 11 | 12 | public override ClientInvocatorContext InvocatorContext { get; } 13 | 14 | public ClientWebSocketConnectorOfT(IServiceProvider serviceProvider, 15 | IClientInvocatorContextFactory invocatorContextFactory, 16 | IStreamCompressor compressor, 17 | ILoggerFactory loggerFactory) 18 | : base(serviceProvider, compressor, loggerFactory) 19 | { 20 | _invocatorContextFactory = invocatorContextFactory ?? throw new ArgumentNullException(nameof(invocatorContextFactory)); 21 | InvocatorContext = _invocatorContextFactory.CreateInvocatorContext(); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets.ProxyClient/ClientWebSocketReceiver.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using NetCoreStack.WebSockets.Internal; 3 | using System; 4 | using System.IO; 5 | using System.Net.WebSockets; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace NetCoreStack.WebSockets.ProxyClient 10 | { 11 | public class ClientWebSocketReceiver 12 | { 13 | private readonly IServiceProvider _serviceProvider; 14 | private readonly ClientWebSocketReceiverContext _context; 15 | private readonly Action _closeCallback; 16 | private readonly Action _handshakeCallback; 17 | private readonly ILogger _logger; 18 | 19 | public ClientWebSocketReceiver(IServiceProvider serviceProvider, 20 | ClientWebSocketReceiverContext context, 21 | Action closeCallback, 22 | Action handshakeCallback = null) 23 | { 24 | _serviceProvider = serviceProvider; 25 | _context = context; 26 | _closeCallback = closeCallback; 27 | _handshakeCallback = handshakeCallback; 28 | _logger = context.LoggerFactory.CreateLogger(); 29 | } 30 | 31 | public async Task ReceiveAsync(CancellationToken cancellationToken) 32 | { 33 | var buffer = new byte[NCSConstants.ChunkSize]; 34 | var result = await _context.WebSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken); 35 | while (!result.CloseStatus.HasValue) 36 | { 37 | if (result.MessageType == WebSocketMessageType.Text) 38 | { 39 | byte[] inputs = null; 40 | using (var ms = new MemoryStream()) 41 | { 42 | while (!result.EndOfMessage) 43 | { 44 | await ms.WriteAsync(buffer, 0, result.Count); 45 | result = await _context.WebSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken); 46 | } 47 | 48 | await ms.WriteAsync(buffer, 0, result.Count); 49 | inputs = ms.ToArray(); 50 | } 51 | try 52 | { 53 | var context = result.ToContext(inputs); 54 | if (context.Command == WebSocketCommands.Handshake) 55 | { 56 | _context.ConnectionId = context.Value?.ToString(); 57 | _handshakeCallback?.Invoke(_context.ConnectionId); 58 | } 59 | var invocator = _context.GetInvocator(_serviceProvider); 60 | if (invocator != null) 61 | { 62 | await invocator.InvokeAsync(context); 63 | } 64 | } 65 | catch (Exception ex) 66 | { 67 | _logger.LogWarning(ex, "{0} An error occurred for message type: {1}", NCSConstants.WarningSymbol, WebSocketMessageType.Text); 68 | } 69 | 70 | try 71 | { 72 | result = await _context.WebSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken); 73 | } 74 | catch (WebSocketException ex) 75 | { 76 | _logger.LogInformation("ClientWebSocketReceiver[Proxy] {0} has close status for connection: {1}", ex?.WebSocketErrorCode, _context.ConnectionId); 77 | _closeCallback?.Invoke(_context); 78 | return; 79 | } 80 | } 81 | 82 | if (result.MessageType == WebSocketMessageType.Binary) 83 | { 84 | byte[] binaryResult = null; 85 | using (var ms = new MemoryStream()) 86 | { 87 | while (!result.EndOfMessage) 88 | { 89 | await ms.WriteAsync(buffer, 0, result.Count); 90 | result = await _context.WebSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken); 91 | } 92 | 93 | await ms.WriteAsync(buffer, 0, result.Count); 94 | binaryResult = ms.ToArray(); 95 | } 96 | try 97 | { 98 | var context = await result.ToBinaryContextAsync(_context.Compressor, binaryResult); 99 | var invocator = _context.GetInvocator(_serviceProvider); 100 | if (invocator != null) 101 | { 102 | await invocator.InvokeAsync(context); 103 | } 104 | } 105 | catch (Exception ex) 106 | { 107 | _logger.LogWarning(ex, "ClientWebSocketReceiver {0} Invocator error occurred for message type: {1}", NCSConstants.WarningSymbol, WebSocketMessageType.Binary); 108 | } 109 | result = await _context.WebSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken); 110 | } 111 | } 112 | 113 | await _context.WebSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, cancellationToken); 114 | _logger.LogInformation("ClientWebSocketReceiver[Proxy] {0} has close status for connection: {1}", result.CloseStatus, _context.ConnectionId); 115 | _closeCallback?.Invoke(_context); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets.ProxyClient/ConnectorHostPair.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NetCoreStack.WebSockets.ProxyClient 4 | { 5 | internal class ConnectorHostPair : IEquatable 6 | { 7 | public string ConnectorName { get; } 8 | public string HostAddress { get; } 9 | public Type Invocator { get; } 10 | 11 | public string Key { get; } 12 | 13 | public ConnectorHostPair(string connectorname, string hostAddress, Type invocator) 14 | { 15 | ConnectorName = connectorname ?? throw new ArgumentNullException(nameof(connectorname)); 16 | HostAddress = hostAddress ?? throw new ArgumentNullException(nameof(hostAddress)); 17 | Invocator = invocator ?? throw new ArgumentNullException(nameof(invocator)); 18 | 19 | Key = $"{ConnectorName}|{HostAddress}|{Invocator.GetHashCode()}"; 20 | } 21 | 22 | public bool Equals(ConnectorHostPair other) 23 | { 24 | return other != null && other.Key.Equals(Key, StringComparison.OrdinalIgnoreCase); 25 | } 26 | 27 | public override bool Equals(object obj) 28 | { 29 | return Equals(obj as ConnectorHostPair); 30 | } 31 | 32 | public override int GetHashCode() 33 | { 34 | return Key.GetHashCode(); 35 | } 36 | 37 | public static bool operator ==(ConnectorHostPair left, ConnectorHostPair right) 38 | { 39 | if ((object)left == null) 40 | { 41 | return ((object)right == null); 42 | } 43 | else if ((object)right == null) 44 | { 45 | return ((object)left == null); 46 | } 47 | 48 | return left.Equals(right); 49 | } 50 | 51 | public static bool operator !=(ConnectorHostPair left, ConnectorHostPair right) 52 | { 53 | return !(left == right); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets.ProxyClient/DefaultClientInvocatorContextFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | using System; 3 | 4 | namespace NetCoreStack.WebSockets.ProxyClient 5 | { 6 | internal class DefaultClientInvocatorContextFactory : IClientInvocatorContextFactory 7 | where TInvocator : IClientWebSocketCommandInvocator 8 | { 9 | private readonly ProxyOptions _proxyOptions; 10 | 11 | public DefaultClientInvocatorContextFactory(IOptions> options) 12 | { 13 | if (options == null) 14 | { 15 | throw new ArgumentNullException(nameof(options)); 16 | } 17 | 18 | _proxyOptions = options.Value; 19 | } 20 | 21 | public ClientInvocatorContext CreateInvocatorContext() 22 | { 23 | var context = new ClientInvocatorContext(_proxyOptions.Invocator, _proxyOptions.ConnectorName, _proxyOptions.WebSocketHostAddress); 24 | 25 | if (_proxyOptions.OnConnectedAsync != null) 26 | { 27 | context.OnConnectedAsync = _proxyOptions.OnConnectedAsync; 28 | } 29 | 30 | if (_proxyOptions.OnDisconnectedAsync != null) 31 | { 32 | context.OnDisconnectedAsync = _proxyOptions.OnDisconnectedAsync; 33 | } 34 | 35 | return context; 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets.ProxyClient/Extensions/ApplicationBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Hosting; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace NetCoreStack.WebSockets.ProxyClient 10 | { 11 | public static class ApplicationBuilderExtensions 12 | { 13 | private static void ThrowIfServiceNotRegistered(IServiceProvider applicationServices) 14 | { 15 | var service = applicationServices.GetService(); 16 | if (service == null) 17 | throw new InvalidOperationException(string.Format("Required services are not registered - are you missing a call to AddProxyWebSockets?")); 18 | } 19 | 20 | public static IApplicationBuilder UseProxyWebSockets(this IApplicationBuilder app, CancellationToken cancellationToken = default(CancellationToken)) 21 | { 22 | ThrowIfServiceNotRegistered(app.ApplicationServices); 23 | var appLifeTime = app.ApplicationServices.GetService(); 24 | IList connectors = InvocatorFactory.GetConnectors(app.ApplicationServices); 25 | foreach (var connector in connectors) 26 | { 27 | InvocatorsHelper.EnsureHostPair(connector.InvocatorContext); 28 | appLifeTime.ApplicationStopping.Register(OnShutdown, connector); 29 | Task.Factory.StartNew(async () => await connector.ConnectAsync(cancellationToken), TaskCreationOptions.LongRunning); 30 | } 31 | 32 | return app; 33 | } 34 | 35 | private static void OnShutdown(object state) 36 | { 37 | try 38 | { 39 | var connector = state as ClientWebSocketConnector; 40 | if (connector != null) 41 | { 42 | connector.Close(nameof(OnShutdown)); 43 | } 44 | } 45 | catch (Exception) 46 | { 47 | 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets.ProxyClient/Extensions/ClientWebSocketReceiverExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NetCoreStack.WebSockets.ProxyClient 4 | { 5 | public static class ClientWebSocketReceiverExtensions 6 | { 7 | public static IClientWebSocketCommandInvocator GetInvocator(this ClientWebSocketReceiverContext context, IServiceProvider serviceProvider) 8 | { 9 | if (context.InvocatorContext != null) 10 | { 11 | var instance = serviceProvider.GetService(context.InvocatorContext.Invocator); 12 | if (instance != null && instance is IClientWebSocketCommandInvocator) 13 | { 14 | return (IClientWebSocketCommandInvocator)instance; 15 | } 16 | } 17 | 18 | return null; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets.ProxyClient/Extensions/ConsoleApplicationBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace NetCoreStack.WebSockets.ProxyClient 7 | { 8 | public static class ConsoleApplicationBuilderExtensions 9 | { 10 | public static IServiceProvider UseProxyWebSocket(this IServiceProvider serviceProvider, CancellationToken cancellationToken = default(CancellationToken)) 11 | { 12 | IList connectors = InvocatorFactory.GetConnectors(serviceProvider); 13 | foreach (var connector in connectors) 14 | { 15 | Task.Run(async () => await connector.ConnectAsync(cancellationToken)); 16 | } 17 | 18 | return serviceProvider; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets.ProxyClient/Extensions/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.DependencyInjection.Extensions; 3 | using Microsoft.Extensions.Logging; 4 | using NetCoreStack.WebSockets.Interfaces; 5 | using NetCoreStack.WebSockets.Internal; 6 | using System; 7 | 8 | namespace NetCoreStack.WebSockets.ProxyClient 9 | { 10 | public static class ServiceCollectionExtensions 11 | { 12 | private static void AddProxyWebSocketsInternal(IServiceCollection services) 13 | { 14 | if (services == null) 15 | { 16 | throw new ArgumentNullException(nameof(services)); 17 | } 18 | 19 | services.TryAddSingleton(); 20 | services.TryAdd(ServiceDescriptor.Singleton()); 21 | services.TryAdd(ServiceDescriptor.Singleton()); 22 | services.TryAdd(ServiceDescriptor.Transient()); 23 | } 24 | 25 | public static ProxyWebSocketsBuilder AddProxyWebSockets(this IServiceCollection services) 26 | { 27 | AddProxyWebSocketsInternal(services); 28 | return new ProxyWebSocketsBuilder(services); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets.ProxyClient/IClientInvocatorContextFactory.cs: -------------------------------------------------------------------------------- 1 | namespace NetCoreStack.WebSockets.ProxyClient 2 | { 3 | public interface IClientInvocatorContextFactory where TInvocator : IClientWebSocketCommandInvocator 4 | { 5 | ClientInvocatorContext CreateInvocatorContext(); 6 | } 7 | } -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets.ProxyClient/IClientWebSocketCommandInvocator.cs: -------------------------------------------------------------------------------- 1 | namespace NetCoreStack.WebSockets.ProxyClient 2 | { 3 | public interface IClientWebSocketCommandInvocator : IWebSocketCommandInvocator 4 | { 5 | } 6 | } -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets.ProxyClient/IWebSocketConnector.cs: -------------------------------------------------------------------------------- 1 | using System.Net.WebSockets; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace NetCoreStack.WebSockets.ProxyClient 6 | { 7 | public interface IWebSocketConnector 8 | { 9 | string ConnectionId { get; } 10 | WebSocketState WebSocketState { get; } 11 | Task ConnectAsync(CancellationToken cancellationToken); 12 | Task SendAsync(WebSocketMessageContext context); 13 | Task SendBinaryAsync(byte[] bytes); 14 | ClientInvocatorContext InvocatorContext { get; } 15 | } 16 | 17 | public interface IWebSocketConnector : IWebSocketConnector where TInvocator : IClientWebSocketCommandInvocator 18 | { 19 | } 20 | } -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets.ProxyClient/InvocatorFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace NetCoreStack.WebSockets.ProxyClient 5 | { 6 | internal static class InvocatorFactory 7 | { 8 | internal static IList Invocators { get; } 9 | 10 | static InvocatorFactory() 11 | { 12 | Invocators = new List(); 13 | } 14 | 15 | internal static IList GetConnectors(IServiceProvider serviceProvider) 16 | { 17 | IList connectors = new List(); 18 | var connectorHandlerType = typeof(IWebSocketConnector<>); 19 | foreach (var item in Invocators) 20 | { 21 | Type[] args = { item }; 22 | var genericConnectorHandler = connectorHandlerType.MakeGenericType(args); 23 | var instance = (IWebSocketConnector)serviceProvider.GetService(genericConnectorHandler); 24 | if (instance != null) 25 | { 26 | connectors.Add(instance); 27 | } 28 | } 29 | 30 | return connectors; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets.ProxyClient/InvocatorsHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace NetCoreStack.WebSockets.ProxyClient 6 | { 7 | public static class InvocatorsHelper 8 | { 9 | private static readonly InvocatorTypes _invocators = new InvocatorTypes(StringComparer.OrdinalIgnoreCase); 10 | 11 | private class InvocatorTypes : Dictionary 12 | { 13 | public InvocatorTypes(IEqualityComparer equalityComparer) 14 | :base(equalityComparer) 15 | { 16 | 17 | } 18 | } 19 | 20 | public static void EnsureHostPair(Type invocator, string connectorName, string hostAddress) 21 | { 22 | var connectorHostPair = new ConnectorHostPair(connectorName, hostAddress, invocator); 23 | var key = connectorHostPair.Key; 24 | 25 | if (_invocators.TryGetValue(key, out ConnectorHostPair value)) 26 | { 27 | // throw new InvalidOperationException($"\"{connectorName}\" is already registered with same Host and Invocator"); 28 | return; 29 | } 30 | 31 | _invocators.Add(key, connectorHostPair); 32 | } 33 | 34 | public static void EnsureHostPair(ClientInvocatorContext context) 35 | { 36 | if (context == null) 37 | { 38 | throw new ArgumentNullException(nameof(context)); 39 | } 40 | 41 | var invocator = context.Invocator; 42 | var connectorName = context.ConnectorName; 43 | var hostAddress = context.HostAddress; 44 | 45 | EnsureHostPair(invocator, connectorName, hostAddress); 46 | } 47 | 48 | public static List GetInvocators() 49 | { 50 | return _invocators.Select(p => p.Value.Invocator).ToList(); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets.ProxyClient/NetCoreStack.WebSockets.ProxyClient.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets.ProxyClient/ProxyClientMarkerService.cs: -------------------------------------------------------------------------------- 1 | namespace NetCoreStack.WebSockets.ProxyClient 2 | { 3 | /// 4 | /// Marker service 5 | /// 6 | public class ProxyClientMarkerService 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets.ProxyClient/ProxyLogHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using NetCoreStack.WebSockets.ProxyClient; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Net.WebSockets; 6 | using System.Text.Json; 7 | 8 | namespace NetCoreStack.WebSockets.Internal 9 | { 10 | public static class ProxyLogHelper 11 | { 12 | public static void Log(ILoggerFactory loggerFactory, ClientInvocatorContext context, string message, Exception ex = null) 13 | { 14 | LogLevel logLevel = LogLevel.Debug; 15 | if (ex != null) 16 | logLevel = LogLevel.Error; 17 | 18 | var logger = loggerFactory.CreateLogger(); 19 | var content = $"{message}=={ex?.Message}"; 20 | 21 | logger.Log(logLevel, 22 | new EventId((int)WebSocketState.Aborted, nameof(WebSocketState.Aborted)), 23 | context, 24 | ex, 25 | (msg, exception) => { 26 | 27 | var values = new Dictionary(); 28 | values.Add("Message", content); 29 | values.Add(nameof(context.HostAddress), context.HostAddress); 30 | values.Add(nameof(context.ConnectorName), context.ConnectorName); 31 | return JsonSerializer.Serialize(values); 32 | }); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets.ProxyClient/ProxyOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.WebSockets; 3 | using System.Threading.Tasks; 4 | 5 | namespace NetCoreStack.WebSockets.ProxyClient 6 | { 7 | public class ProxyOptions : SocketsOptions where TInvocator : IClientWebSocketCommandInvocator 8 | { 9 | public string ConnectorName { get; set; } 10 | public string WebSocketHostAddress { get; set; } 11 | public Func OnConnectedAsync { get; set; } 12 | public Func OnDisconnectedAsync { get; set; } 13 | 14 | public ProxyOptions() 15 | { 16 | ConnectorName = ""; 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets.ProxyClient/ProxyWebSocketsBuilder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Options; 3 | 4 | namespace NetCoreStack.WebSockets.ProxyClient 5 | { 6 | public class ProxyWebSocketsBuilder 7 | { 8 | private readonly IServiceCollection _services; 9 | 10 | public ProxyWebSocketsBuilder(IServiceCollection services) 11 | { 12 | _services = services; 13 | } 14 | 15 | private void RegisterInternal() 16 | where TInvocator : IClientWebSocketCommandInvocator 17 | { 18 | var invocatorType = typeof(TInvocator); 19 | InvocatorFactory.Invocators.Add(invocatorType); 20 | _services.AddTransient(invocatorType); 21 | _services.AddSingleton, ClientWebSocketConnectorOfT>(); 22 | } 23 | 24 | /// 25 | /// 26 | /// 27 | /// 28 | /// 29 | /// Unique host address 30 | /// 31 | public ProxyWebSocketsBuilder Register(string connectorName, string hostAddress) 32 | where TInvocator : IClientWebSocketCommandInvocator 33 | { 34 | var invocatorType = typeof(TInvocator); 35 | InvocatorsHelper.EnsureHostPair(invocatorType, connectorName, hostAddress); 36 | 37 | RegisterInternal(); 38 | 39 | var proxyOptions = new ProxyOptions 40 | { 41 | ConnectorName = connectorName, 42 | WebSocketHostAddress = hostAddress 43 | }; 44 | 45 | _services.AddSingleton(Options.Create(proxyOptions)); 46 | _services.AddSingleton, DefaultClientInvocatorContextFactory>(); 47 | return this; 48 | } 49 | 50 | public ProxyWebSocketsBuilder Register() 51 | where TInvocator : IClientWebSocketCommandInvocator 52 | where TContextFactory : IClientInvocatorContextFactory 53 | { 54 | 55 | RegisterInternal(); 56 | 57 | _services.AddSingleton(typeof(IClientInvocatorContextFactory), typeof(TContextFactory)); 58 | 59 | return this; 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets.ProxyClient/Types/ClientInvocatorContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.WebSockets; 3 | using System.Threading.Tasks; 4 | 5 | namespace NetCoreStack.WebSockets.ProxyClient 6 | { 7 | public class ClientInvocatorContext : InvocatorContext 8 | { 9 | public string ConnectorName { get; } 10 | public WebSocketSupportedSchemes Scheme { get; } 11 | public string HostAddress { get; } 12 | public string UriPath { get; } 13 | public string Query { get; } 14 | public string ConnectorKey { get; } 15 | public Uri Uri { get; } 16 | 17 | public Func OnConnectedAsync { get; set; } 18 | 19 | public Func OnDisconnectedAsync { get; set; } 20 | 21 | public ClientInvocatorContext(Type invocator, string connectorName, string hostAddress, 22 | WebSocketSupportedSchemes scheme = WebSocketSupportedSchemes.WS, 23 | string uriPath = "", 24 | string query = "", 25 | Func onConnectedAsync = null, 26 | Func onDisconnectedAsync = null) 27 | :base(invocator) 28 | { 29 | ConnectorName = connectorName ?? throw new ArgumentNullException(nameof(connectorName)); 30 | HostAddress = hostAddress ?? throw new ArgumentNullException(nameof(hostAddress)); 31 | Scheme = scheme; 32 | UriPath = uriPath; 33 | Query = query; 34 | 35 | OnConnectedAsync = onConnectedAsync; 36 | OnDisconnectedAsync = onDisconnectedAsync; 37 | 38 | var schemeStr = Scheme == WebSocketSupportedSchemes.WS ? "ws" : "wss"; 39 | var uriBuilder = new UriBuilder(new Uri($"{schemeStr}://{HostAddress}")); 40 | if (!string.IsNullOrEmpty(UriPath)) 41 | { 42 | uriBuilder.Path = UriPath; 43 | } 44 | 45 | if (!string.IsNullOrEmpty(Query)) 46 | { 47 | uriBuilder.Query = Query; 48 | } 49 | 50 | Uri = uriBuilder.Uri; 51 | ConnectorKey = $"{ConnectorName}|{HostAddress}|{Invocator.GetHashCode()}"; 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets.ProxyClient/Types/ClientWebSocketReceiverContext.cs: -------------------------------------------------------------------------------- 1 | namespace NetCoreStack.WebSockets.ProxyClient 2 | { 3 | public class ClientWebSocketReceiverContext : WebSocketReceiverContextBase 4 | { 5 | public ClientInvocatorContext InvocatorContext { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/ConnectionManagerOfT.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using NetCoreStack.WebSockets.Interfaces; 3 | using System; 4 | 5 | namespace NetCoreStack.WebSockets 6 | { 7 | public class ConnectionManagerOfT : ConnectionManager, IConnectionManager where TInvocator : IServerWebSocketCommandInvocator 8 | { 9 | private readonly IServerInvocatorContextFactory _invocatorContextFactory; 10 | 11 | public override InvocatorContext InvocatorContext { get; } 12 | 13 | public ConnectionManagerOfT(IServiceProvider serviceProvider, 14 | IServerInvocatorContextFactory invocatorContextFactory, 15 | IStreamCompressor compressor, 16 | IHandshakeStateTransport initState, 17 | IHeaderProvider headerProvider, ILoggerFactory loggerFactory) 18 | : base(serviceProvider, compressor, initState, headerProvider, loggerFactory) 19 | { 20 | _invocatorContextFactory = invocatorContextFactory ?? throw new ArgumentNullException(nameof(invocatorContextFactory)); 21 | InvocatorContext = _invocatorContextFactory.CreateInvocatorContext(); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/DefaultHandshakeStateTransport.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | 4 | namespace NetCoreStack.WebSockets 5 | { 6 | public class DefaultHandshakeStateTransport : IHandshakeStateTransport 7 | { 8 | public Task> GetStateAsync() 9 | { 10 | var dictionary = new Dictionary(); 11 | return Task.FromResult>(dictionary); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/DefaultHeaderProvider.cs: -------------------------------------------------------------------------------- 1 | using NetCoreStack.WebSockets.Internal; 2 | using System.Collections.Generic; 3 | 4 | namespace NetCoreStack.WebSockets 5 | { 6 | public class DefaultHeaderProvider : IHeaderProvider 7 | { 8 | public void Invoke(IDictionary header) 9 | { 10 | if (header == null) 11 | { 12 | return; 13 | } 14 | 15 | if (!header.TryGetValue(NCSConstants.WSFQN, out object host)) 16 | { 17 | header.Add(NCSConstants.WSFQN, FQNHelper.Name); 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/EnumExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace NetCoreStack.WebSockets 6 | { 7 | public static class EnumExtensions 8 | { 9 | public static IEnumerable GetUniqueFlags(this Enum flags) 10 | { 11 | ulong flag = 1; 12 | foreach (var value in Enum.GetValues(flags.GetType()).Cast()) 13 | { 14 | ulong bits = Convert.ToUInt64(value); 15 | while (flag < bits) 16 | { 17 | flag <<= 1; 18 | } 19 | 20 | if (flag == bits && flags.HasFlag(value)) 21 | { 22 | yield return value; 23 | } 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/Extensions/ByteExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using static NetCoreStack.WebSockets.Internal.NCSConstants; 4 | 5 | namespace NetCoreStack.WebSockets 6 | { 7 | internal static class ByteExtensions 8 | { 9 | public static Tuple Split(this byte[] input) 10 | { 11 | if (input == null) 12 | { 13 | throw new ArgumentNullException(nameof(input)); 14 | } 15 | 16 | var value = Splitter.First(); 17 | var index = Array.IndexOf(input, value, 1); 18 | if (index == -1) 19 | { 20 | throw new InvalidOperationException($"Invalid data format! " + 21 | $"Check the splitter pattern exist: \"{Splitter}\""); 22 | } 23 | 24 | var header = new ArraySegment(input, 0, index); 25 | var body = new ArraySegment(input, (index + 1), input.Length - (index + 1)); 26 | 27 | return new Tuple(header.ToArray(), body.ToArray()); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/Extensions/SocketApplicationBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using NetCoreStack.WebSockets.Internal; 3 | using System; 4 | using System.Threading; 5 | 6 | namespace NetCoreStack.WebSockets 7 | { 8 | public static class SocketApplicationBuilderExtensions 9 | { 10 | public static IApplicationBuilder UseNativeWebSockets(this IApplicationBuilder app, CancellationToken cancellationToken = default(CancellationToken)) 11 | { 12 | if (app == null) 13 | { 14 | throw new ArgumentNullException(nameof(app)); 15 | } 16 | 17 | app.UseWebSockets(); 18 | 19 | app.UseMiddleware(cancellationToken); 20 | 21 | return app; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/Extensions/SocketServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.DependencyInjection.Extensions; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.Extensions.Options; 5 | using NetCoreStack.WebSockets.Interfaces; 6 | using NetCoreStack.WebSockets.Internal; 7 | using System; 8 | 9 | namespace NetCoreStack.WebSockets 10 | { 11 | public static class SocketServiceCollectionExtensions 12 | { 13 | public static void AddNativeWebSockets(this IServiceCollection services) 14 | where TInvocator : IServerWebSocketCommandInvocator 15 | { 16 | if (services == null) 17 | { 18 | throw new ArgumentNullException(nameof(services)); 19 | } 20 | 21 | services.TryAdd(ServiceDescriptor.Singleton()); 22 | services.TryAdd(ServiceDescriptor.Singleton()); 23 | services.TryAdd(ServiceDescriptor.Singleton()); 24 | services.TryAdd(ServiceDescriptor.Transient()); 25 | 26 | services.AddSingleton(Options.Create(new ServerSocketOptions())); 27 | services.AddSingleton, DefaultServerInvocatorContextFactory>(); 28 | services.AddSingleton, ConnectionManagerOfT>(); 29 | 30 | services.AddSingleton(resolver => { 31 | return (IConnectionManager)resolver.GetService>(); 32 | }); 33 | 34 | services.AddTransient(typeof(TInvocator)); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/Extensions/WebSocketExtensions.cs: -------------------------------------------------------------------------------- 1 | using NetCoreStack.WebSockets.Interfaces; 2 | using NetCoreStack.WebSockets.Internal; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Net.WebSockets; 7 | using System.Text; 8 | using System.Text.Json; 9 | using System.Threading.Tasks; 10 | 11 | namespace NetCoreStack.WebSockets 12 | { 13 | public static class WebSocketExtensions 14 | { 15 | public static WebSocketMessageContext ToContext(this WebSocketReceiveResult result, byte[] input) 16 | { 17 | if (result == null) 18 | { 19 | throw new ArgumentNullException(nameof(result)); 20 | } 21 | 22 | if (input == null) 23 | { 24 | throw new ArgumentNullException(nameof(input)); 25 | } 26 | 27 | var content = Encoding.UTF8.GetString(input, 0, input.Length); 28 | WebSocketMessageContext webSocketContext = new WebSocketMessageContext(); 29 | try 30 | { 31 | webSocketContext = JsonSerializer.Deserialize(content); 32 | } 33 | catch (Exception ex) 34 | { 35 | webSocketContext.Command = WebSocketCommands.DataSend; 36 | webSocketContext.Value = content; 37 | webSocketContext.MessageType = result.MessageType; 38 | } 39 | 40 | webSocketContext.Length = input.Length; 41 | return webSocketContext; 42 | } 43 | 44 | public static async Task ToBinaryContextAsync(this WebSocketReceiveResult result, 45 | IStreamCompressor compressor, 46 | byte[] input) 47 | { 48 | if (result == null) 49 | { 50 | throw new ArgumentNullException(nameof(result)); 51 | } 52 | 53 | var content = input.Split(); 54 | byte[] header = content.Item1; 55 | byte[] body = content.Item2; 56 | 57 | var webSocketContext = new WebSocketMessageContext(); 58 | bool isCompressed = GZipHelper.IsGZipBody(body); 59 | if (isCompressed) 60 | { 61 | body = await compressor.DeCompressAsync(body); 62 | } 63 | 64 | using (var ms = new MemoryStream(header)) 65 | using (var sr = new StreamReader(ms)) 66 | { 67 | var data = await sr.ReadToEndAsync(); 68 | if (data != null) 69 | { 70 | try 71 | { 72 | webSocketContext.Header = JsonSerializer.Deserialize>(data); 73 | } 74 | catch (Exception ex) 75 | { 76 | webSocketContext.Header = new Dictionary 77 | { 78 | ["Exception"] = ex.Message, 79 | ["Unknown"] = "Unknown binary message!" 80 | }; 81 | } 82 | } 83 | } 84 | 85 | using (var ms = new MemoryStream(body)) 86 | using (var sr = new StreamReader(ms)) 87 | { 88 | var data = await sr.ReadToEndAsync(); 89 | webSocketContext.Value = data; 90 | } 91 | 92 | webSocketContext.Length = input.Length; 93 | webSocketContext.MessageType = WebSocketMessageType.Binary; 94 | webSocketContext.Command = WebSocketCommands.DataSend; 95 | 96 | return webSocketContext; 97 | } 98 | 99 | public static ArraySegment ToSegment(this WebSocketMessageContext webSocketContext) 100 | { 101 | if (webSocketContext == null) 102 | { 103 | throw new ArgumentNullException(nameof(webSocketContext)); 104 | } 105 | 106 | var content = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(webSocketContext)); 107 | return new ArraySegment(content, 0, content.Length); 108 | } 109 | 110 | public static MemoryStream ToMemoryStream(this WebSocketMessageContext webSocketContext) 111 | { 112 | if (webSocketContext == null) 113 | { 114 | throw new ArgumentNullException(nameof(webSocketContext)); 115 | } 116 | 117 | return new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(webSocketContext))); 118 | } 119 | 120 | public static string GetConnectionId(this WebSocketMessageContext context) 121 | { 122 | if (context == null) 123 | { 124 | throw new ArgumentNullException(nameof(context)); 125 | } 126 | 127 | object connectionId = null; 128 | if (context.Header.TryGetValue(NCSConstants.ConnectionId, out connectionId)) 129 | { 130 | return connectionId.ToString(); 131 | } 132 | 133 | throw new ArgumentOutOfRangeException(nameof(connectionId)); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/Extensions/WebSocketReceiverExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NetCoreStack.WebSockets 4 | { 5 | public static class WebSocketReceiverExtensions 6 | { 7 | public static IServerWebSocketCommandInvocator GetInvocator(this WebSocketReceiverContext context, IServiceProvider serviceProvider) 8 | { 9 | if (context.InvocatorContext != null) 10 | { 11 | var instance = serviceProvider.GetService(context.InvocatorContext.Invocator); 12 | if (instance != null && instance is IServerWebSocketCommandInvocator) 13 | { 14 | return (IServerWebSocketCommandInvocator)instance; 15 | } 16 | } 17 | 18 | return null; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/Interfaces/IConnectionManager.cs: -------------------------------------------------------------------------------- 1 | using NetCoreStack.WebSockets.Internal; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Net.WebSockets; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace NetCoreStack.WebSockets 9 | { 10 | public interface IConnectionManager 11 | { 12 | ConcurrentDictionary Connections { get; } 13 | 14 | Task ConnectAsync(WebSocket webSocket, string connectionId, string connectorName = "", CancellationToken cancellationToken = default(CancellationToken)); 15 | 16 | /// 17 | /// Text message broadcaster 18 | /// 19 | /// Data 20 | /// 21 | Task BroadcastAsync(WebSocketMessageContext context); 22 | 23 | /// 24 | /// Text message broadcaster 25 | /// 26 | /// Byte content 27 | /// 28 | Task BroadcastAsync(byte[] inputs); 29 | 30 | /// 31 | /// Binary message broadcaster 32 | /// 33 | /// 34 | /// Extra properties, header 35 | /// 36 | Task BroadcastBinaryAsync(byte[] input, IDictionary properties = null); 37 | 38 | /// 39 | /// Binary message broadcaster with inline serializer 40 | /// 41 | /// WebSocket message context 42 | /// 43 | Task BroadcastBinaryAsync(WebSocketMessageContext context); 44 | 45 | /// 46 | /// Send text message to specified connection 47 | /// 48 | /// 49 | /// 50 | /// 51 | Task SendAsync(string connectionId, WebSocketMessageContext context); 52 | 53 | /// 54 | /// Send binary message to specified connection 55 | /// 56 | /// 57 | /// Data 58 | /// Additional transport data 59 | /// Compression status of the data, default value is true 60 | /// 61 | Task SendBinaryAsync(string connectionId, byte[] input, IDictionary properties); 62 | 63 | /// 64 | /// Close the specified connection 65 | /// 66 | /// 67 | void CloseConnection(string connectionId); 68 | } 69 | 70 | public interface IConnectionManager where TInvocator : IServerWebSocketCommandInvocator 71 | { 72 | 73 | } 74 | } -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/Interfaces/IHandshakeStateTransport.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | 4 | namespace NetCoreStack.WebSockets 5 | { 6 | public interface IHandshakeStateTransport 7 | { 8 | Task> GetStateAsync(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/Interfaces/IHeaderProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace NetCoreStack.WebSockets 4 | { 5 | public interface IHeaderProvider 6 | { 7 | void Invoke(IDictionary header); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/Interfaces/IInvocatorContext.cs: -------------------------------------------------------------------------------- 1 | namespace NetCoreStack.WebSockets.Interfaces 2 | { 3 | interface IInvocatorContext 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/Interfaces/IServerInvocatorContextFactory.cs: -------------------------------------------------------------------------------- 1 | namespace NetCoreStack.WebSockets 2 | { 3 | public interface IServerInvocatorContextFactory where TInvocator : IServerWebSocketCommandInvocator 4 | { 5 | InvocatorContext CreateInvocatorContext(); 6 | } 7 | } -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/Interfaces/IServerWebSocketCommandInvocator.cs: -------------------------------------------------------------------------------- 1 | namespace NetCoreStack.WebSockets 2 | { 3 | public interface IServerWebSocketCommandInvocator : IWebSocketCommandInvocator 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/Interfaces/IStreamCompressor.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace NetCoreStack.WebSockets.Interfaces 4 | { 5 | public interface IStreamCompressor 6 | { 7 | Task CompressAsync(byte[] input); 8 | 9 | Task DeCompressAsync(byte[] input); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/Interfaces/IWebSocketCommandInvocator.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace NetCoreStack.WebSockets 4 | { 5 | public interface IWebSocketCommandInvocator 6 | { 7 | Task InvokeAsync(WebSocketMessageContext context); 8 | } 9 | } -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/Internal/DefaultClientInvocatorContextFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | using System; 3 | 4 | namespace NetCoreStack.WebSockets 5 | { 6 | internal class DefaultServerInvocatorContextFactory : IServerInvocatorContextFactory 7 | where TInvocator : IServerWebSocketCommandInvocator 8 | { 9 | private readonly ServerSocketOptions _options; 10 | 11 | public DefaultServerInvocatorContextFactory(IOptions> options) 12 | { 13 | if (options == null) 14 | { 15 | throw new ArgumentNullException(nameof(options)); 16 | } 17 | 18 | _options = options.Value; 19 | } 20 | 21 | public InvocatorContext CreateInvocatorContext() 22 | { 23 | return new InvocatorContext(_options.Invocator); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/Internal/FQNHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NetCoreStack.WebSockets.Internal 4 | { 5 | internal static class FQNHelper 6 | { 7 | internal static readonly string Name = Environment.MachineName; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/Internal/GZipHelper.cs: -------------------------------------------------------------------------------- 1 | namespace NetCoreStack.WebSockets.Internal 2 | { 3 | public static class GZipHelper 4 | { 5 | /// 6 | /// Checks the first two bytes in a GZIP file, which must be 31 and 139. 7 | /// 8 | public static bool IsGZipBody(byte[] arr) 9 | { 10 | return arr.Length >= 2 && 11 | arr[0] == 31 && 12 | arr[1] == 139; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/Internal/GZipStreamCompressor.cs: -------------------------------------------------------------------------------- 1 | using NetCoreStack.WebSockets.Interfaces; 2 | using System.IO; 3 | using System.IO.Compression; 4 | using System.Threading.Tasks; 5 | 6 | namespace NetCoreStack.WebSockets.Internal 7 | { 8 | public class GZipStreamCompressor : IStreamCompressor 9 | { 10 | public async Task CompressAsync(byte[] input) 11 | { 12 | using (MemoryStream memory = new MemoryStream()) 13 | { 14 | using (GZipStream gzip = new GZipStream(memory, CompressionMode.Compress, true)) 15 | { 16 | await gzip.WriteAsync(input, 0, input.Length); 17 | } 18 | return memory.ToArray(); 19 | } 20 | } 21 | 22 | public async Task DeCompressAsync(byte[] input) 23 | { 24 | using (GZipStream stream = new GZipStream(new MemoryStream(input), CompressionMode.Decompress)) 25 | { 26 | byte[] buffer = new byte[NCSConstants.ChunkSize]; 27 | using (MemoryStream memory = new MemoryStream()) 28 | { 29 | int count = 0; 30 | do 31 | { 32 | count = await stream.ReadAsync(buffer, 0, NCSConstants.ChunkSize); 33 | if (count > 0) 34 | { 35 | await memory.WriteAsync(buffer, 0, count); 36 | } 37 | } 38 | while (count > 0); 39 | return memory.ToArray(); 40 | } 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/Internal/LogHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Net.WebSockets; 5 | using System.Text.Json; 6 | 7 | namespace NetCoreStack.WebSockets.Internal 8 | { 9 | internal class LogHelper 10 | { 11 | internal static void Log(WebSocketReceiverContext context, string message, Exception ex = null) 12 | { 13 | LogLevel logLevel = LogLevel.Debug; 14 | if (ex != null) 15 | logLevel = LogLevel.Error; 16 | 17 | var logger = context.LoggerFactory.CreateLogger(); 18 | var content = $"{message}=={ex?.Message}"; 19 | 20 | object state = new 21 | { 22 | context.ConnectionId 23 | }; 24 | 25 | logger.Log(logLevel, 26 | new EventId((int)WebSocketState.Aborted, nameof(WebSocketState.Aborted)), 27 | state, 28 | ex, 29 | (msg, exception) => { 30 | var values = new Dictionary(); 31 | values.Add("Message", content); 32 | return JsonSerializer.Serialize(values); 33 | }); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/Internal/NCSConstants.cs: -------------------------------------------------------------------------------- 1 | namespace NetCoreStack.WebSockets.Internal 2 | { 3 | public static class NCSConstants 4 | { 5 | public static byte[] Splitter = new byte[] { 0x1f }; 6 | public const int ChunkSize = 1024 * 4; 7 | public const string WSFQN = "X-NetCoreStack-WSHost"; 8 | public const string CompressedKey = "GZipCompressed"; 9 | public const string ConnectorName = "ConnectorName"; 10 | public const string ConnectionId = "ConnectionId"; 11 | 12 | // Symbols 13 | public static readonly string CheckMarkSymbol = ((char)0x2713).ToString(); 14 | public static readonly string WarningSymbol = ((char)0x26A0).ToString(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/Internal/WebSocketMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.Extensions.Logging; 3 | using Microsoft.Extensions.Primitives; 4 | using NetCoreStack.WebSockets.Interfaces; 5 | using System; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace NetCoreStack.WebSockets.Internal 10 | { 11 | public class WebSocketMiddleware 12 | { 13 | private readonly RequestDelegate _next; 14 | private CancellationToken _cancellationToken; 15 | 16 | public WebSocketMiddleware(RequestDelegate next, CancellationToken cancellationToken) 17 | { 18 | _next = next; 19 | _cancellationToken = cancellationToken; 20 | } 21 | 22 | public async Task Invoke(HttpContext httpContext, 23 | IConnectionManager manager, 24 | IStreamCompressor compressor, 25 | ILoggerFactory loggerFactory) 26 | { 27 | if (httpContext.WebSockets.IsWebSocketRequest) 28 | { 29 | string connectionId = string.Empty; 30 | string connectorName = string.Empty; 31 | StringValues headerValue = ""; 32 | if (httpContext.Request.Headers.TryGetValue(NCSConstants.ConnectorName, out headerValue)) 33 | { 34 | connectorName = headerValue.ToString(); 35 | } 36 | if (httpContext.Request.Headers.TryGetValue(NCSConstants.ConnectionId, out headerValue)) 37 | { 38 | connectionId = headerValue.ToString(); 39 | } 40 | 41 | if (string.IsNullOrEmpty(connectorName)) 42 | { 43 | if (httpContext.Request.Query.ContainsKey(NCSConstants.ConnectorName)) 44 | { 45 | connectorName = httpContext.Request.Query[NCSConstants.ConnectorName]; 46 | } 47 | } 48 | if (string.IsNullOrEmpty(connectionId)) 49 | { 50 | if (httpContext.Request.Query.ContainsKey(NCSConstants.ConnectionId)) 51 | { 52 | connectionId = httpContext.Request.Query[NCSConstants.ConnectionId]; 53 | Guid connectionIdGuid = Guid.Empty; 54 | if (!Guid.TryParse(connectionId, out connectionIdGuid)) 55 | { 56 | connectionId = string.Empty; 57 | } 58 | } 59 | } 60 | 61 | var webSocket = await httpContext.WebSockets.AcceptWebSocketAsync(); 62 | if (string.IsNullOrEmpty(connectionId)) 63 | { 64 | connectionId = Guid.NewGuid().ToString("N"); 65 | } 66 | 67 | await manager.ConnectAsync(webSocket, connectionId: connectionId, connectorName: connectorName, cancellationToken: _cancellationToken); 68 | } 69 | else 70 | { 71 | await _next(httpContext); 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/Internal/WebSocketReceiver.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System; 3 | using System.IO; 4 | using System.Net.WebSockets; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace NetCoreStack.WebSockets.Internal 9 | { 10 | public class WebSocketReceiver 11 | { 12 | private readonly IServiceProvider _serviceProvider; 13 | private readonly WebSocketReceiverContext _context; 14 | private readonly Action _closeCallback; 15 | private readonly ILoggerFactory _loggerFactory; 16 | private readonly ILogger _logger; 17 | 18 | public WebSocketReceiver(IServiceProvider serviceProvider, 19 | WebSocketReceiverContext context, 20 | Action closeCallback, 21 | ILoggerFactory loggerFactory) 22 | { 23 | _serviceProvider = serviceProvider; 24 | _context = context; 25 | _closeCallback = closeCallback; 26 | _loggerFactory = loggerFactory; 27 | _logger = _loggerFactory.CreateLogger(); 28 | } 29 | 30 | public async Task ReceiveAsync(CancellationToken cancellationToken) 31 | { 32 | var buffer = new byte[NCSConstants.ChunkSize]; 33 | var result = await _context.WebSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken); 34 | while (!result.CloseStatus.HasValue) 35 | { 36 | if (result.MessageType == WebSocketMessageType.Text) 37 | { 38 | byte[] inputs = null; 39 | using (var ms = new MemoryStream()) 40 | { 41 | while (!result.EndOfMessage) 42 | { 43 | await ms.WriteAsync(buffer, 0, result.Count); 44 | result = await _context.WebSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken); 45 | } 46 | 47 | await ms.WriteAsync(buffer, 0, result.Count); 48 | inputs = ms.ToArray(); 49 | } 50 | try 51 | { 52 | var context = result.ToContext(inputs); 53 | var invocator = _context.GetInvocator(_serviceProvider); 54 | if (invocator != null) 55 | { 56 | await invocator.InvokeAsync(context); 57 | } 58 | } 59 | catch (Exception ex) 60 | { 61 | LogHelper.Log(_context, "Error", ex); 62 | } 63 | result = await _context.WebSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken); 64 | } 65 | 66 | if (result.MessageType == WebSocketMessageType.Binary) 67 | { 68 | byte[] binaryResult = null; 69 | using (var ms = new MemoryStream()) 70 | { 71 | while (!result.EndOfMessage) 72 | { 73 | await ms.WriteAsync(buffer, 0, result.Count); 74 | result = await _context.WebSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken); 75 | } 76 | 77 | await ms.WriteAsync(buffer, 0, result.Count); 78 | binaryResult = ms.ToArray(); 79 | } 80 | try 81 | { 82 | var context = await result.ToBinaryContextAsync(_context.Compressor, binaryResult); 83 | var invocator = _context.GetInvocator(_serviceProvider); 84 | if (invocator != null) 85 | { 86 | await invocator.InvokeAsync(context); 87 | } 88 | } 89 | catch (Exception ex) 90 | { 91 | LogHelper.Log(_context, "Error", ex); 92 | } 93 | result = await _context.WebSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken); 94 | } 95 | } 96 | 97 | await _context.WebSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, cancellationToken); 98 | _logger.LogInformation("WebSocketReceiver[Server] {0} has close status for connection: {1}", result.CloseStatus, _context.ConnectionId); 99 | _closeCallback?.Invoke(_context); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/Internal/WebSocketTransport.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.WebSockets; 3 | 4 | namespace NetCoreStack.WebSockets.Internal 5 | { 6 | public class WebSocketTransport : IDisposable 7 | { 8 | public WebSocket WebSocket { get; private set; } 9 | public string ConnectionId { get; } 10 | public string ConnectorName { get; } 11 | 12 | public WebSocketTransport(WebSocket webSocket, string connectionId, string connectorName) 13 | { 14 | ConnectionId = connectionId; 15 | WebSocket = webSocket; 16 | ConnectorName = connectorName; 17 | } 18 | 19 | public void ReConnect(WebSocket webSocket) 20 | { 21 | WebSocket = webSocket; 22 | } 23 | 24 | public void Dispose() 25 | { 26 | WebSocket.Dispose(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/NetCoreStack.WebSockets.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/Types/InvocatorContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NetCoreStack.WebSockets 4 | { 5 | public class InvocatorContext 6 | { 7 | public Type Invocator { get; } 8 | 9 | public InvocatorContext(Type invocator) 10 | { 11 | Invocator = invocator ?? throw new ArgumentNullException(nameof(invocator)); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/Types/JsonObject.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace NetCoreStack.WebSockets 4 | { 5 | public abstract class JsonObject 6 | { 7 | public JsonObject() 8 | { 9 | } 10 | 11 | public IDictionary ToJson() 12 | { 13 | Dictionary dictionary = new Dictionary(); 14 | Serialize(dictionary); 15 | return dictionary; 16 | } 17 | 18 | protected abstract void Serialize(IDictionary value); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/Types/ServerSocketOptions.cs: -------------------------------------------------------------------------------- 1 | namespace NetCoreStack.WebSockets 2 | { 3 | public class ServerSocketOptions : SocketsOptions where TInvocator : IServerWebSocketCommandInvocator 4 | { 5 | } 6 | } -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/Types/SocketsOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NetCoreStack.WebSockets 4 | { 5 | public abstract class SocketsOptions where TInvocator : IWebSocketCommandInvocator 6 | { 7 | public Type Invocator { get; } 8 | 9 | public SocketsOptions() 10 | { 11 | Invocator = typeof(TInvocator); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/Types/WebSocketMessageContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.WebSockets; 4 | 5 | namespace NetCoreStack.WebSockets 6 | { 7 | public class WebSocketMessageContext 8 | { 9 | public WebSocketMessageType MessageType { get; set; } 10 | 11 | public WebSocketCommands? Command { get; set; } 12 | 13 | public object Value { get; set; } 14 | 15 | public IDictionary Header { get; set; } 16 | 17 | public int Length { get; set; } 18 | 19 | public string CommandText 20 | { 21 | get 22 | { 23 | if (Command.HasValue) 24 | { 25 | return Command.ToString(); 26 | } 27 | 28 | return string.Empty; 29 | } 30 | } 31 | 32 | public WebSocketMessageContext() 33 | { 34 | Command = WebSocketCommands.DataSend; 35 | Header = new Dictionary(StringComparer.OrdinalIgnoreCase); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/Types/WebSocketReceiverContext.cs: -------------------------------------------------------------------------------- 1 | namespace NetCoreStack.WebSockets 2 | { 3 | public class WebSocketReceiverContext : WebSocketReceiverContextBase 4 | { 5 | public InvocatorContext InvocatorContext { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/Types/WebSocketReceiverContextBase.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using NetCoreStack.WebSockets.Interfaces; 3 | using System.Net.WebSockets; 4 | 5 | namespace NetCoreStack.WebSockets 6 | { 7 | public abstract class WebSocketReceiverContextBase 8 | { 9 | public string ConnectionId { get; set; } 10 | public WebSocket WebSocket { get; set; } 11 | public ILoggerFactory LoggerFactory { get; set; } 12 | public IStreamCompressor Compressor { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/Types/WebSocketSupportedSchemes.cs: -------------------------------------------------------------------------------- 1 | namespace NetCoreStack.WebSockets 2 | { 3 | public enum WebSocketSupportedSchemes 4 | { 5 | WS = 0, 6 | WSS = 1 7 | } 8 | } -------------------------------------------------------------------------------- /src/NetCoreStack.WebSockets/WebSocketCommands.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NetCoreStack.WebSockets 4 | { 5 | [Flags] 6 | public enum WebSocketCommands : byte 7 | { 8 | Connect = 1, 9 | DataSend = 2, 10 | Handshake = 4, 11 | All = 7 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/Common.Libs/ApplicationVariables.cs: -------------------------------------------------------------------------------- 1 | namespace Common.Libs 2 | { 3 | public static class ApplicationVariables 4 | { 5 | public static int CacheReady { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/Common.Libs/CacheHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace Common.Libs 5 | { 6 | public static class CacheHelper 7 | { 8 | public static IDictionary CacheKeys 9 | { 10 | get 11 | { 12 | var dict = new Dictionary() 13 | { 14 | [nameof(CacheItem2)] = new CacheItemDescriptor { Type = typeof(CacheItem2), Weight = CacheItemWeights.MiddleWeight }, 15 | [nameof(CacheItem1)] = new CacheItemDescriptor { Type = typeof(CacheItem1), Weight = CacheItemWeights.HeavyWeight }, 16 | [nameof(CacheItem3)] = new CacheItemDescriptor { Type = typeof(CacheItem3), Weight = CacheItemWeights.MiddleWeight }, 17 | }; 18 | 19 | var keys = dict.OrderBy(x => x.Key).ToDictionary(t => t.Key, t => t.Value); 20 | return keys; 21 | } 22 | } 23 | 24 | public static CacheItemDescriptor GetDescriptor(string key) 25 | { 26 | if (CacheKeys.TryGetValue(key, out CacheItemDescriptor descriptor)) 27 | { 28 | return descriptor; 29 | } 30 | 31 | return null; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/Common.Libs/CacheItem1.cs: -------------------------------------------------------------------------------- 1 | namespace Common.Libs 2 | { 3 | public class CacheItem1 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/Common.Libs/CacheItem2.cs: -------------------------------------------------------------------------------- 1 | namespace Common.Libs 2 | { 3 | public class CacheItem2 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/Common.Libs/CacheItem3.cs: -------------------------------------------------------------------------------- 1 | namespace Common.Libs 2 | { 3 | public class CacheItem3 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/Common.Libs/CacheItemDescriptor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | namespace Common.Libs 3 | { 4 | public class CacheItemDescriptor 5 | { 6 | public Type Type { get; set; } 7 | public CacheItemWeights Weight { get; set; } 8 | 9 | public CacheItemDescriptor() 10 | { 11 | Weight = CacheItemWeights.LightWeight; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/Common.Libs/CacheItemWeights.cs: -------------------------------------------------------------------------------- 1 | namespace Common.Libs 2 | { 3 | public enum CacheItemWeights 4 | { 5 | LightWeight = 0, 6 | MiddleWeight = 1, 7 | HeavyWeight = 2 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/Common.Libs/Common.Libs.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/Common.Libs/InMemoryCacheExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Common.Libs 2 | { 3 | public static class InMemoryCacheExtensions 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/Common.Libs/InMemoryCacheProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Caching.Memory; 2 | using System; 3 | 4 | namespace Common.Libs 5 | { 6 | public class InMemoryCacheProvider 7 | { 8 | private readonly IMemoryCache _memoryCache; 9 | 10 | public InMemoryCacheProvider(IMemoryCache memoryCache) 11 | { 12 | _memoryCache = memoryCache; 13 | } 14 | 15 | public object SetObject(string key, object value, MemoryCacheEntryOptions options) 16 | { 17 | if (string.IsNullOrEmpty(key)) 18 | { 19 | throw new ArgumentNullException(nameof(key)); 20 | } 21 | 22 | if (value == null) 23 | { 24 | throw new ArgumentNullException("Cache value is null"); 25 | } 26 | 27 | if (options == null) 28 | { 29 | throw new ArgumentNullException(nameof(options)); 30 | } 31 | 32 | var entryOptions = new MemoryCacheEntryOptions 33 | { 34 | AbsoluteExpiration = options.AbsoluteExpiration, 35 | Priority = options.Priority 36 | }; 37 | 38 | _memoryCache.Set(key, value, entryOptions); 39 | return value; 40 | } 41 | 42 | public object GetObject(string key) 43 | { 44 | return _memoryCache.Get(key); 45 | } 46 | 47 | public T GetObject(string key) 48 | { 49 | return (T)_memoryCache.Get(key); 50 | } 51 | 52 | public void Remove(string key) 53 | { 54 | _memoryCache.Remove(key); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/Common.Libs/TypeCoreExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | 5 | namespace Common.Libs 6 | { 7 | public static class TypeCoreExtensions 8 | { 9 | public static IList CreateElementTypeAsGenericList(this Type elementType) 10 | { 11 | return (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(new Type[] { elementType })); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/Common.Libs/WebSocketHeaderNames.cs: -------------------------------------------------------------------------------- 1 | namespace Common.Libs 2 | { 3 | public static class WebSocketHeaderNames 4 | { 5 | public const string ConnectionId = "ConnectionId"; 6 | public const string CacheItemKey = "CacheItemKey"; 7 | public const string AgentConnectionId = "AgentConnectionId"; 8 | public const string CacheRequest = "CacheRequest"; 9 | public const string CacheInvalidate = "CacheInvalidate"; 10 | public const string CacheUpdated = "CacheUpdated"; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/ConsoleAppProxyClient/ConsoleAppProxyClient.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | Exe 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/ConsoleAppProxyClient/DataStreamingInvocator.cs: -------------------------------------------------------------------------------- 1 | using NetCoreStack.WebSockets; 2 | using NetCoreStack.WebSockets.ProxyClient; 3 | using System; 4 | using System.Text.Json; 5 | using System.Threading.Tasks; 6 | 7 | namespace ConsoleAppProxyClient 8 | { 9 | public class DataStreamingInvocator : IClientWebSocketCommandInvocator 10 | { 11 | public async Task InvokeAsync(WebSocketMessageContext context) 12 | { 13 | var values = await Task.Run(() => JsonSerializer.Serialize(context)); 14 | Console.WriteLine(values); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/ConsoleAppProxyClient/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using NetCoreStack.WebSockets.ProxyClient; 3 | using System; 4 | 5 | namespace ConsoleAppProxyClient 6 | { 7 | public static class ApplicationVariables 8 | { 9 | public static int TaskCount = 0; 10 | public static int SocketReady = 0; 11 | } 12 | 13 | public class Program 14 | { 15 | private static IServiceProvider _resolver = null; 16 | 17 | public static void Main(string[] args) 18 | { 19 | var services = new ServiceCollection(); 20 | 21 | var connectorName = $"ConsoleApp-{Environment.MachineName}"; 22 | 23 | services.AddProxyWebSockets() 24 | .Register(connectorName, "localhost:7803"); 25 | 26 | _resolver = services.BuildServiceProvider(); 27 | 28 | _resolver.UseProxyWebSocket(); 29 | 30 | Console.ReadLine(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/NetCoreStack.WebSockets.Tests/AgentsWebSocketCommandInvocator.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace NetCoreStack.WebSockets.Tests 4 | { 5 | public class AgentsWebSocketCommandInvocator : IServerWebSocketCommandInvocator 6 | { 7 | private readonly IConnectionManager _connectionManager; 8 | public AgentsWebSocketCommandInvocator(IConnectionManager connectionManager) 9 | { 10 | _connectionManager = connectionManager; 11 | } 12 | 13 | public async Task InvokeAsync(WebSocketMessageContext context) 14 | { 15 | if (context.Command == WebSocketCommands.Connect) 16 | { 17 | 18 | } 19 | 20 | // Sending incoming data from Backend zone to the Clients (Browsers) 21 | await _connectionManager.BroadcastAsync(context); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/NetCoreStack.WebSockets.Tests/AnotherEndpointWebSocketCommandInvocator.cs: -------------------------------------------------------------------------------- 1 | using Common.Libs; 2 | using NetCoreStack.WebSockets.ProxyClient; 3 | using System.Threading.Tasks; 4 | 5 | namespace NetCoreStack.WebSockets.Tests 6 | { 7 | public class AnotherEndpointWebSocketCommandInvocator : IClientWebSocketCommandInvocator 8 | { 9 | private readonly IConnectionManager _connectionManager; 10 | private readonly InMemoryCacheProvider _cacheProvider; 11 | 12 | public AnotherEndpointWebSocketCommandInvocator(IConnectionManager connectionManager, InMemoryCacheProvider cacheProvider) 13 | { 14 | _connectionManager = connectionManager; 15 | _cacheProvider = cacheProvider; 16 | } 17 | 18 | private Task InternalMethodAsync() 19 | { 20 | return Task.CompletedTask; 21 | } 22 | 23 | public async Task InvokeAsync(WebSocketMessageContext context) 24 | { 25 | await Task.CompletedTask; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/NetCoreStack.WebSockets.Tests/CustomInvocatorContextFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using NetCoreStack.WebSockets.ProxyClient; 3 | 4 | namespace NetCoreStack.WebSockets.Tests 5 | { 6 | public class CustomInvocatorContextFactory : IClientInvocatorContextFactory 7 | { 8 | private readonly IHostingEnvironment _hostingEnvironment; 9 | 10 | public CustomInvocatorContextFactory(IHostingEnvironment hostingEnvironment) 11 | { 12 | _hostingEnvironment = hostingEnvironment; 13 | } 14 | 15 | public ClientInvocatorContext CreateInvocatorContext() 16 | { 17 | return new ClientInvocatorContext(typeof(AnotherEndpointWebSocketCommandInvocator), 18 | "TestMachineName", 19 | "localhost:5003"); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/NetCoreStack.WebSockets.Tests/CustomWebSocketCommandInvocator.cs: -------------------------------------------------------------------------------- 1 | using Common.Libs; 2 | using NetCoreStack.WebSockets.ProxyClient; 3 | using System.Net.WebSockets; 4 | using System.Threading.Tasks; 5 | 6 | namespace NetCoreStack.WebSockets.Tests 7 | { 8 | public class CustomWebSocketCommandInvocator : IClientWebSocketCommandInvocator 9 | { 10 | private readonly IConnectionManager _connectionManager; 11 | private readonly InMemoryCacheProvider _cacheProvider; 12 | 13 | public CustomWebSocketCommandInvocator(IConnectionManager connectionManager, 14 | InMemoryCacheProvider cacheProvider) 15 | { 16 | _connectionManager = connectionManager; 17 | _cacheProvider = cacheProvider; 18 | } 19 | 20 | private Task InternalMethodAsync() 21 | { 22 | return Task.CompletedTask; 23 | } 24 | 25 | public async Task InvokeAsync(WebSocketMessageContext context) 26 | { 27 | await Task.CompletedTask; 28 | 29 | if (context.MessageType == WebSocketMessageType.Text) 30 | { 31 | await _connectionManager.BroadcastAsync(context); 32 | return; 33 | } 34 | 35 | if (context.MessageType == WebSocketMessageType.Binary) 36 | { 37 | 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/NetCoreStack.WebSockets.Tests/NetCoreStack.WebSockets.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /test/NetCoreStack.WebSockets.Tests/ProxyBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Hosting; 5 | using Microsoft.Extensions.Hosting.Internal; 6 | using Microsoft.Extensions.Logging; 7 | using NetCoreStack.WebSockets.ProxyClient; 8 | using System; 9 | using System.IO; 10 | using System.Threading; 11 | using Xunit; 12 | 13 | namespace NetCoreStack.WebSockets.Tests 14 | { 15 | public class ProxyBuilderTests 16 | { 17 | private readonly IServiceCollection _services; 18 | private readonly IApplicationBuilder _appBuilder; 19 | private readonly ILoggerFactory _loggerFactory; 20 | private readonly IHostEnvironment _hostingEnvironment; 21 | 22 | protected IServiceProvider Services => _appBuilder.ApplicationServices; 23 | 24 | public ProxyBuilderTests() 25 | { 26 | _services = new ServiceCollection(); 27 | _loggerFactory = new LoggerFactory(); 28 | _hostingEnvironment = new HostingEnvironment 29 | { 30 | ApplicationName = typeof(ProxyBuilderTests).Namespace, 31 | ContentRootPath = Directory.GetCurrentDirectory(), 32 | EnvironmentName = Environments.Development 33 | }; 34 | 35 | 36 | // WebSockets for Browsers - User Agent ( browser clients ) 37 | _services.AddNativeWebSockets(); 38 | 39 | // Client WebSocket - Proxy connections 40 | var builder = _services.AddProxyWebSockets(); 41 | 42 | var connectorname = $"TestWebApp-{Environment.MachineName}"; 43 | builder.Register(connectorname, "localhost:7803"); 44 | 45 | // localhost:5003 46 | builder.Register(); 47 | 48 | var serviceStarting = new ManualResetEvent(false); 49 | var lifetimeStart = new ManualResetEvent(false); 50 | var lifetimeContinue = new ManualResetEvent(false); 51 | 52 | IHostApplicationLifetime applicationLifetime = new ApplicationLifetime(new Logger(_loggerFactory)); 53 | _services.AddSingleton(applicationLifetime); 54 | 55 | _services.AddSingleton(_hostingEnvironment); 56 | 57 | _appBuilder = new ApplicationBuilder(_services.BuildServiceProvider()); 58 | } 59 | 60 | [Fact] 61 | public void CustomInvocatorRegistryTest() 62 | { 63 | var customWebSocketCommandInvocator = Services.GetService>(); 64 | Assert.IsType>(customWebSocketCommandInvocator); 65 | 66 | var context = customWebSocketCommandInvocator.InvocatorContext; 67 | Assert.Equal($"TestWebApp-{Environment.MachineName}", context.ConnectorName); 68 | Assert.Equal("localhost:7803", context.HostAddress); 69 | 70 | _appBuilder.UseProxyWebSockets(); 71 | } 72 | 73 | [Fact] 74 | public void AnotherEndpointWebSocketCommandInvocator() 75 | { 76 | var customWebSocketCommandInvocator = Services.GetService>(); 77 | Assert.IsType>(customWebSocketCommandInvocator); 78 | 79 | var context = customWebSocketCommandInvocator.InvocatorContext; 80 | Assert.Equal("TestMachineName", context.ConnectorName); 81 | Assert.Equal("localhost:5003", context.HostAddress); 82 | 83 | _appBuilder.UseProxyWebSockets(); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /test/ServerTestApp/Controllers/DiscoveryController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Routing; 3 | using Microsoft.Extensions.Caching.Distributed; 4 | using Microsoft.Extensions.Logging; 5 | using NetCoreStack.WebSockets; 6 | using NetCoreStack.WebSockets.Internal; 7 | using ServerTestApp.Models; 8 | using System; 9 | using System.Linq; 10 | using System.Net.WebSockets; 11 | using System.Text; 12 | using System.Text.Json; 13 | using System.Threading; 14 | using System.Threading.Tasks; 15 | 16 | namespace ServerTestApp.Controllers 17 | { 18 | [Route("api/[controller]")] 19 | public class DiscoveryController : Controller 20 | { 21 | private readonly ILoggerFactory _loggerFactory; 22 | private readonly IConnectionManager _connectionManager; 23 | private readonly IDistributedCache _distrubutedCache; 24 | 25 | public DiscoveryController(IConnectionManager connectionManager, 26 | IDistributedCache distrubutedCache, 27 | ILoggerFactory loggerFactory) 28 | { 29 | _connectionManager = connectionManager; 30 | _distrubutedCache = distrubutedCache; 31 | _loggerFactory = loggerFactory; 32 | } 33 | 34 | [HttpGet] 35 | public IActionResult Get() 36 | { 37 | return Json(new { processorCount = Environment.ProcessorCount }); 38 | } 39 | 40 | [HttpPost(nameof(SendAsync))] 41 | public async Task SendAsync([FromBody]SimpleModel model) 42 | { 43 | if (model != null) 44 | { 45 | var echo = $"Echo from server '{model.Key}' - {DateTime.Now}"; 46 | var obj = new { message = echo }; 47 | var webSocketContext = new WebSocketMessageContext { Command = WebSocketCommands.DataSend, Value = obj }; 48 | await _connectionManager.BroadcastAsync(webSocketContext); 49 | } 50 | 51 | return Ok(); 52 | } 53 | 54 | [HttpPost(nameof(SendTextAsync))] 55 | public async Task SendTextAsync([FromBody]SimpleModel model) 56 | { 57 | if (model != null) 58 | { 59 | var echo = $"Echo from server '{model.Key}' - {DateTime.Now}"; 60 | var obj = new { message = echo }; 61 | var webSocketContext = new WebSocketMessageContext { Command = WebSocketCommands.DataSend, Value = obj }; 62 | 63 | var str = JsonSerializer.Serialize(webSocketContext); 64 | 65 | var bytes = Encoding.UTF8.GetBytes(str); 66 | await _connectionManager.BroadcastAsync(bytes); 67 | } 68 | 69 | return Ok(); 70 | } 71 | 72 | [HttpPost(nameof(BroadcastBinaryAsync))] 73 | public async Task BroadcastBinaryAsync([FromBody]SimpleModel model) 74 | { 75 | if (model != null) 76 | { 77 | var bytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(model)); 78 | await _connectionManager.BroadcastBinaryAsync(bytes, new RouteValueDictionary(new { Id = 1, SomeProperty = "Some value" })); 79 | } 80 | 81 | return Ok(); 82 | } 83 | 84 | [HttpPost(nameof(SendBinaryAsync))] 85 | public async Task SendBinaryAsync() 86 | { 87 | var routeValueDictionary = new RouteValueDictionary(new { Key = "SomeKey" }); 88 | var bytes = Encoding.UTF8.GetBytes("Hello World"); 89 | await _connectionManager.BroadcastBinaryAsync(bytes, routeValueDictionary); 90 | return Ok(); 91 | } 92 | 93 | [HttpGet(nameof(GetConnections))] 94 | public IActionResult GetConnections() 95 | { 96 | var connections = _connectionManager.Connections 97 | .Select(x => new 98 | { 99 | id = x.Value.ConnectionId, 100 | name = x.Value.ConnectorName, 101 | state = x.Value.WebSocket?.State.ToString() 102 | }).OrderBy(o => o.name).ToList(); 103 | 104 | return Json(connections); 105 | } 106 | 107 | [HttpGet(nameof(CloseConnection))] 108 | public async Task CloseConnection([FromQuery]string connectionId) 109 | { 110 | if(_connectionManager.Connections.TryGetValue(connectionId, out WebSocketTransport webSocketTransport)) 111 | { 112 | await webSocketTransport.WebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Server close", CancellationToken.None); 113 | return Json(new { status = 1 }); 114 | } 115 | 116 | return Json(new { status = 0 }); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /test/ServerTestApp/DistributedCacheExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Routing; 2 | using Microsoft.Extensions.Caching.Distributed; 3 | using NetCoreStack.WebSockets; 4 | using System; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using static Common.Libs.CacheHelper; 8 | 9 | namespace ServerTestApp 10 | { 11 | public static class DistributedCacheExtensions 12 | { 13 | private static byte[] GetCompressedBinaryObject(this IDistributedCache cache, string key) 14 | { 15 | var bytes = cache.Get(key); 16 | 17 | if (bytes == null) 18 | throw new ArgumentOutOfRangeException($"Cache not found: {key}"); 19 | 20 | return bytes; 21 | } 22 | 23 | public static async Task SendCache(this IDistributedCache cache, IConnectionManager connectionManager, string connectionId) 24 | { 25 | var keys = CacheKeys.Keys.Select(x => x).OrderBy(x => x); 26 | foreach (var key in keys) 27 | { 28 | try 29 | { 30 | var values = cache.GetCompressedBinaryObject(key); 31 | await connectionManager.SendBinaryAsync(connectionId, values, new RouteValueDictionary(new { Key = key })); 32 | } 33 | catch (Exception ex) 34 | { 35 | throw ex; 36 | } 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/ServerTestApp/Models/Models.cs: -------------------------------------------------------------------------------- 1 | namespace ServerTestApp.Models 2 | { 3 | public class SimpleModel 4 | { 5 | public string ConnectionId { get; set; } 6 | public string Key { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/ServerTestApp/MyHandshakeStateTransport.cs: -------------------------------------------------------------------------------- 1 | using NetCoreStack.WebSockets; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | namespace ServerTestApp 6 | { 7 | public class MyHandshakeStateTransport : IHandshakeStateTransport 8 | { 9 | public Task> GetStateAsync() 10 | { 11 | var dictionary = new Dictionary(); 12 | return Task.FromResult>(dictionary); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/ServerTestApp/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | using System.Threading.Tasks; 4 | 5 | 6 | namespace ServerTestApp 7 | { 8 | public class Program 9 | { 10 | public static Task Main(string[] args) 11 | => BuildHost(args).RunAsync(); 12 | 13 | public static IHost BuildHost(string[] args) => CreateHostBuilder(args).Build(); 14 | 15 | public static IHostBuilder CreateHostBuilder(string[] args) => 16 | Host.CreateDefaultBuilder(args) 17 | .ConfigureWebHostDefaults(webBuilder => webBuilder 18 | .UseStartup()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/ServerTestApp/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:7803/", 8 | "sslPort": 0 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": false, 15 | "launchUrl": "swagger", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "ServerTestApp": { 21 | "commandName": "Project", 22 | "launchBrowser": true, 23 | "launchUrl": "swagger", 24 | "applicationUrl": "http://localhost:7803/", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /test/ServerTestApp/ServerTestApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /test/ServerTestApp/ServerWebSocketCommandInvocator.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Caching.Distributed; 2 | using NetCoreStack.WebSockets; 3 | using System.Diagnostics; 4 | using System.Threading.Tasks; 5 | 6 | namespace ServerTestApp 7 | { 8 | public class ServerWebSocketCommandInvocator : IServerWebSocketCommandInvocator 9 | { 10 | private readonly IConnectionManager _connectionManager; 11 | private readonly IDistributedCache _cache; 12 | public ServerWebSocketCommandInvocator(IDistributedCache cache, IConnectionManager connectionManager) 13 | { 14 | _cache = cache; 15 | _connectionManager = connectionManager; 16 | } 17 | 18 | public async Task InvokeAsync(WebSocketMessageContext context) 19 | { 20 | await Task.CompletedTask; 21 | Debug.WriteLine("Context length: {0}", context.Length); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/ServerTestApp/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | using Microsoft.Extensions.Logging; 8 | using NetCoreStack.WebSockets; 9 | 10 | namespace ServerTestApp 11 | { 12 | public class Startup 13 | { 14 | public Startup(IConfiguration configuration) 15 | { 16 | Configuration = configuration; 17 | } 18 | 19 | public IConfiguration Configuration { get; } 20 | 21 | public void ConfigureServices(IServiceCollection services) 22 | { 23 | services.AddMemoryCache(); 24 | services.AddDistributedRedisCache(options => 25 | { 26 | options.Configuration = "localhost"; 27 | options.InstanceName = "RedisInstance"; 28 | }); 29 | 30 | services.AddTransient(); 31 | 32 | // Add NetCoreStack Native Socket Services. 33 | services.AddNativeWebSockets(); 34 | 35 | services.AddOpenApiDocument(); 36 | 37 | services.AddControllers(); 38 | } 39 | 40 | public void Configure(IApplicationBuilder app) 41 | { 42 | var appLifeTime = app.ApplicationServices.GetService(); 43 | app.UseNativeWebSockets(appLifeTime.ApplicationStopped); 44 | 45 | app.UseOpenApi(config => { 46 | config.PostProcess = (document, request) => 47 | { 48 | document.Info.Version = "v1"; 49 | document.Info.Title = $"Platform API"; 50 | }; 51 | }); // serve OpenAPI/Swagger documents 52 | 53 | app.UseSwaggerUi3(); // serve Swagger UI 54 | 55 | app.UseReDoc(); // serve ReDoc UI 56 | 57 | app.UseRouting(); 58 | 59 | app.UseEndpoints(routes => 60 | { 61 | routes.MapControllers(); 62 | }); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/ServerTestApp/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/WebClientTestApp/AgentsWebSocketCommandInvocator.cs: -------------------------------------------------------------------------------- 1 | using NetCoreStack.WebSockets; 2 | using System.Threading.Tasks; 3 | 4 | namespace WebClientTestApp 5 | { 6 | public class AgentsWebSocketCommandInvocator : IServerWebSocketCommandInvocator 7 | { 8 | private readonly IConnectionManager _connectionManager; 9 | public AgentsWebSocketCommandInvocator(IConnectionManager connectionManager) 10 | { 11 | _connectionManager = connectionManager; 12 | } 13 | 14 | public async Task InvokeAsync(WebSocketMessageContext context) 15 | { 16 | if (context.Command == WebSocketCommands.Connect) 17 | { 18 | 19 | } 20 | 21 | // Sending incoming data from Backend zone to the Clients (Browsers) 22 | // await _connectionManager.BroadcastAsync(context); 23 | 24 | await _connectionManager.SendAsync(context.GetConnectionId(), context); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/WebClientTestApp/AnotherEndpointWebSocketCommandInvocator.cs: -------------------------------------------------------------------------------- 1 | using Common.Libs; 2 | using Microsoft.Extensions.Logging; 3 | using NetCoreStack.WebSockets; 4 | using NetCoreStack.WebSockets.ProxyClient; 5 | using System.Threading.Tasks; 6 | 7 | namespace WebClientTestApp 8 | { 9 | public class AnotherEndpointWebSocketCommandInvocator : IClientWebSocketCommandInvocator 10 | { 11 | private readonly IConnectionManager _connectionManager; 12 | private readonly ILogger _logger; 13 | private readonly InMemoryCacheProvider _cacheProvider; 14 | 15 | public AnotherEndpointWebSocketCommandInvocator(IConnectionManager connectionManager, InMemoryCacheProvider cacheProvider, ILogger logger) 16 | { 17 | _connectionManager = connectionManager; 18 | _cacheProvider = cacheProvider; 19 | _logger = logger; 20 | } 21 | 22 | private Task InternalMethodAsync() 23 | { 24 | return Task.CompletedTask; 25 | } 26 | 27 | public async Task InvokeAsync(WebSocketMessageContext context) 28 | { 29 | await Task.CompletedTask; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/WebClientTestApp/BroadcastMessageContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WebClientTestApp 4 | { 5 | public class BroadcastMessageContext 6 | { 7 | public string Message { get; set; } 8 | public DateTime DateTime { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/WebClientTestApp/ClientExceptionFilterAttribute.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.Filters; 2 | using System.Threading.Tasks; 3 | 4 | namespace WebClientTestApp 5 | { 6 | public class ClientExceptionFilterAttribute : ExceptionFilterAttribute 7 | { 8 | public override Task OnExceptionAsync(ExceptionContext context) 9 | { 10 | return base.OnExceptionAsync(context); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/WebClientTestApp/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace WebClientTestApp 2 | { 3 | public static class Constants 4 | { 5 | public const string ConnectorName = "WebClientSPA"; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/WebClientTestApp/Controllers/ChatController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using NetCoreStack.WebSockets; 3 | using System.Linq; 4 | 5 | namespace WebClientTestApp.Controllers 6 | { 7 | public class ChatController : Controller 8 | { 9 | private readonly IConnectionManager _connectionManager; 10 | 11 | public ChatController(IConnectionManager connectionManager) 12 | { 13 | _connectionManager = connectionManager; 14 | } 15 | 16 | [HttpPost] 17 | public IActionResult Broadcast([FromBody]BroadcastMessageContext context) 18 | { 19 | var messageContext = new WebSocketMessageContext 20 | { 21 | Command = WebSocketCommands.DataSend, 22 | Value = context 23 | }; 24 | 25 | _connectionManager.BroadcastAsync(messageContext); 26 | return Json(messageContext); 27 | } 28 | 29 | public IActionResult GetConnections() 30 | { 31 | var connections = _connectionManager.Connections 32 | .Select(x => new 33 | { 34 | id = x.Value.ConnectionId, 35 | name = x.Value.ConnectorName, 36 | state = x.Value.WebSocket?.State.ToString() 37 | }).OrderBy(o => o.name).ToList(); 38 | 39 | return Json(connections); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/WebClientTestApp/Controllers/DiscoveryController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using NetCoreStack.WebSockets; 3 | using NetCoreStack.WebSockets.ProxyClient; 4 | using System; 5 | using System.Threading.Tasks; 6 | 7 | namespace WebClientTestApp.Controllers 8 | { 9 | public class DiscoveryController : Controller 10 | { 11 | private readonly IWebSocketConnector _connector; 12 | public DiscoveryController(IWebSocketConnector connector) 13 | { 14 | _connector = connector; 15 | } 16 | 17 | [HttpGet] 18 | public IActionResult Status() 19 | { 20 | return Ok(new { _connector.WebSocketState }); 21 | } 22 | 23 | [HttpGet] 24 | public async Task KeepAlive() 25 | { 26 | await _connector.SendAsync(new WebSocketMessageContext 27 | { 28 | Command = WebSocketCommands.DataSend, 29 | Value = new { Id = 1, Name = "Hello World!", DateTime = DateTime.Now } 30 | }); 31 | 32 | return Ok(); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/WebClientTestApp/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace WebClientTestApp.Controllers 4 | { 5 | public class HomeController : Controller 6 | { 7 | public IActionResult Index() 8 | { 9 | return View(); 10 | } 11 | 12 | public IActionResult ReConnectTest() 13 | { 14 | return View(); 15 | } 16 | 17 | public IActionResult About() 18 | { 19 | ViewData["Message"] = "Your application description page."; 20 | 21 | return View(); 22 | } 23 | 24 | public IActionResult Contact() 25 | { 26 | ViewData["Message"] = "Your contact page."; 27 | 28 | return View(); 29 | } 30 | 31 | public IActionResult Error() 32 | { 33 | return View(); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/WebClientTestApp/CustomInvocatorContextFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using NetCoreStack.WebSockets.ProxyClient; 3 | using System; 4 | 5 | namespace WebClientTestApp 6 | { 7 | public class CustomInvocatorContextFactory : IClientInvocatorContextFactory 8 | { 9 | private readonly IHostingEnvironment _hostingEnvironment; 10 | 11 | public CustomInvocatorContextFactory(IHostingEnvironment hostingEnvironment) 12 | { 13 | _hostingEnvironment = hostingEnvironment; 14 | } 15 | 16 | public ClientInvocatorContext CreateInvocatorContext() 17 | { 18 | return new ClientInvocatorContext(typeof(AnotherEndpointWebSocketCommandInvocator), 19 | Environment.MachineName + "-WCT", 20 | "localhost:5003", 21 | uriPath: "ws", 22 | query: "connectionId=" + Guid.NewGuid().ToString("N")); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/WebClientTestApp/CustomWebSocketCommandInvocator.cs: -------------------------------------------------------------------------------- 1 | using Common.Libs; 2 | using Microsoft.Extensions.Logging; 3 | using NetCoreStack.WebSockets; 4 | using NetCoreStack.WebSockets.ProxyClient; 5 | using System.Collections.Generic; 6 | using System.Net.WebSockets; 7 | using System.Threading.Tasks; 8 | 9 | namespace WebClientTestApp 10 | { 11 | public class CustomWebSocketCommandInvocator : IClientWebSocketCommandInvocator 12 | { 13 | private readonly IConnectionManager _connectionManager; 14 | private readonly IWebSocketConnector _webSocketConnector; 15 | private readonly ILogger _logger; 16 | private readonly InMemoryCacheProvider _cacheProvider; 17 | 18 | public CustomWebSocketCommandInvocator(IConnectionManager connectionManager, 19 | IWebSocketConnector webSocketConnector, 20 | InMemoryCacheProvider cacheProvider, 21 | ILogger logger) 22 | { 23 | _connectionManager = connectionManager; 24 | _webSocketConnector = webSocketConnector; 25 | _cacheProvider = cacheProvider; 26 | _logger = logger; 27 | } 28 | 29 | private Task InternalMethodAsync() 30 | { 31 | return Task.CompletedTask; 32 | } 33 | 34 | public async Task InvokeAsync(WebSocketMessageContext context) 35 | { 36 | if (context.MessageType == WebSocketMessageType.Text) 37 | { 38 | 39 | } 40 | 41 | if (context.MessageType == WebSocketMessageType.Binary) 42 | { 43 | if (context.Header is Dictionary stateDictionary) 44 | { 45 | 46 | } 47 | } 48 | 49 | await _connectionManager.BroadcastAsync(context); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/WebClientTestApp/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | using System.Threading.Tasks; 4 | 5 | namespace WebClientTestApp 6 | { 7 | public class Program 8 | { 9 | public static Task Main(string[] args) 10 | => BuildHost(args).RunAsync(); 11 | 12 | public static IHost BuildHost(string[] args) => CreateHostBuilder(args).Build(); 13 | 14 | public static IHostBuilder CreateHostBuilder(string[] args) => 15 | Host.CreateDefaultBuilder(args) 16 | .ConfigureWebHostDefaults(webBuilder => webBuilder 17 | .UseStartup()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/WebClientTestApp/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:7812/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "environmentVariables": { 14 | "ASPNETCORE_ENVIRONMENT": "Development" 15 | } 16 | }, 17 | "WebClientTestApp": { 18 | "commandName": "Project", 19 | "launchBrowser": true, 20 | "launchUrl": "http://localhost:5000", 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /test/WebClientTestApp/Startup.cs: -------------------------------------------------------------------------------- 1 | using Common.Libs; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Logging; 7 | using NetCoreStack.WebSockets; 8 | using NetCoreStack.WebSockets.ProxyClient; 9 | using System; 10 | 11 | namespace WebClientTestApp 12 | { 13 | public class Startup 14 | { 15 | public Startup(IConfiguration configuration) 16 | { 17 | Configuration = configuration; 18 | } 19 | 20 | public IConfiguration Configuration { get; } 21 | 22 | public void ConfigureServices(IServiceCollection services) 23 | { 24 | services.AddMemoryCache(); 25 | services.AddSingleton(); 26 | 27 | // WebSockets for Browsers - User Agent ( javascript clients ) 28 | services.AddNativeWebSockets(); 29 | 30 | // Client WebSocket - Proxy connections 31 | var builder = services.AddProxyWebSockets(); 32 | 33 | var connectorname = $"TestWebApp-{Environment.MachineName}"; 34 | builder.Register(connectorname, "localhost:7803"); 35 | 36 | // Runtime context factory 37 | // builder.Register(); 38 | 39 | services.AddControllersWithViews(); 40 | } 41 | 42 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) 43 | { 44 | if (env.IsDevelopment()) 45 | { 46 | app.UseDeveloperExceptionPage(); 47 | } 48 | else 49 | { 50 | app.UseExceptionHandler("/Home/Error"); 51 | } 52 | 53 | app.UseStaticFiles(); 54 | 55 | // Proxy (Domain App) Client WebSocket - DMZ to API side connections 56 | app.UseProxyWebSockets(); 57 | 58 | // User Agent WebSockets for Browsers 59 | app.UseNativeWebSockets(); 60 | 61 | app.UseRouting(); 62 | 63 | app.UseEndpoints(endpoints => 64 | { 65 | endpoints.MapDefaultControllerRoute(); 66 | }); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/WebClientTestApp/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Home Page"; 3 | } 4 | 5 | 24 | 25 |
26 |
27 |
28 |
29 |
30 | ConnectionId: 31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | 45 |
46 | 47 |
48 |
49 |
50 |
51 | 52 |
53 | 54 |
55 |
56 |
57 |
58 |
59 | 60 |
61 |
62 |
63 |
64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |
FromToData
80 |
81 |
82 | 83 | @section scripts { 84 | 214 | } -------------------------------------------------------------------------------- /test/WebClientTestApp/Views/Home/ReConnectTest.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Home Page"; 3 | } 4 | 5 | 24 | 25 |
26 |
27 |
28 |
29 |
30 | ConnectionId: 31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | 45 |
46 | 47 |
48 |
49 |
50 |
51 | 52 |
53 | 54 |
55 |
56 |
57 |
58 |
59 | 60 |
61 |
62 |
63 |
64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |
FromToData
80 |
81 |
82 | 83 | @section scripts { 84 | 85 | 214 | } -------------------------------------------------------------------------------- /test/WebClientTestApp/Views/Process/Index.cshtml: -------------------------------------------------------------------------------- 1 | @* 2 | For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860 3 | *@ 4 | @{ 5 | } 6 | -------------------------------------------------------------------------------- /test/WebClientTestApp/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Error"; 3 | } 4 | 5 |

Error.

6 |

An error occurred while processing your request.

7 | 8 |

Development Mode

9 |

10 | Swapping to Development environment will display more detailed information about the error that occurred. 11 |

12 |

13 | Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application. 14 |

15 | -------------------------------------------------------------------------------- /test/WebClientTestApp/Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | @ViewData["Title"] - Test App 1 7 | 8 | 9 | 10 | 28 |
29 | @RenderBody() 30 |
31 | 32 | 33 | 34 | 35 | @RenderSection("scripts", required: false) 36 | 37 | 38 | -------------------------------------------------------------------------------- /test/WebClientTestApp/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using WebClientTestApp 2 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 3 | -------------------------------------------------------------------------------- /test/WebClientTestApp/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /test/WebClientTestApp/WebClientTestApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/WebClientTestApp/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "CustomWebSocketCommandInvocator": { 3 | "ConnectorName": null, 4 | "WebSocketHostAddress": "localhost:7803" 5 | }, 6 | "Logging": { 7 | "IncludeScopes": false, 8 | "LogLevel": { 9 | "Default": "Debug", 10 | "System": "Information", 11 | "Microsoft": "Information" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/WebClientTestApp/wwwroot/_references.js: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | /// 6 | /// 7 | -------------------------------------------------------------------------------- /test/WebClientTestApp/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetCoreStack/WebSockets/df14771cf84daeccd4ebf0889cdd49e79ea6e818/test/WebClientTestApp/wwwroot/favicon.ico -------------------------------------------------------------------------------- /test/WebClientTestApp/wwwroot/js/cookie.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * JavaScript Cookie v2.1.3 3 | * https://github.com/js-cookie/js-cookie 4 | * 5 | * Copyright 2006, 2015 Klaus Hartl & Fagner Brack 6 | * Released under the MIT license 7 | */ 8 | ; (function (factory) { 9 | var registeredInModuleLoader = false; 10 | if (typeof define === 'function' && define.amd) { 11 | define(factory); 12 | registeredInModuleLoader = true; 13 | } 14 | if (typeof exports === 'object') { 15 | module.exports = factory(); 16 | registeredInModuleLoader = true; 17 | } 18 | if (!registeredInModuleLoader) { 19 | var OldCookies = window.Cookies; 20 | var api = window.Cookies = factory(); 21 | api.noConflict = function () { 22 | window.Cookies = OldCookies; 23 | return api; 24 | }; 25 | } 26 | }(function () { 27 | function extend() { 28 | var i = 0; 29 | var result = {}; 30 | for (; i < arguments.length; i++) { 31 | var attributes = arguments[i]; 32 | for (var key in attributes) { 33 | result[key] = attributes[key]; 34 | } 35 | } 36 | return result; 37 | } 38 | 39 | function init(converter) { 40 | function api(key, value, attributes) { 41 | var result; 42 | if (typeof document === 'undefined') { 43 | return; 44 | } 45 | 46 | // Write 47 | 48 | if (arguments.length > 1) { 49 | attributes = extend({ 50 | path: '/' 51 | }, api.defaults, attributes); 52 | 53 | if (typeof attributes.expires === 'number') { 54 | var expires = new Date(); 55 | expires.setMilliseconds(expires.getMilliseconds() + attributes.expires * 864e+5); 56 | attributes.expires = expires; 57 | } 58 | 59 | // We're using "expires" because "max-age" is not supported by IE 60 | attributes.expires = attributes.expires ? attributes.expires.toUTCString() : ''; 61 | 62 | try { 63 | result = JSON.stringify(value); 64 | if (/^[\{\[]/.test(result)) { 65 | value = result; 66 | } 67 | } catch (e) { } 68 | 69 | if (!converter.write) { 70 | value = encodeURIComponent(String(value)) 71 | .replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent); 72 | } else { 73 | value = converter.write(value, key); 74 | } 75 | 76 | key = encodeURIComponent(String(key)); 77 | key = key.replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent); 78 | key = key.replace(/[\(\)]/g, escape); 79 | 80 | var stringifiedAttributes = ''; 81 | 82 | for (var attributeName in attributes) { 83 | if (!attributes[attributeName]) { 84 | continue; 85 | } 86 | stringifiedAttributes += '; ' + attributeName; 87 | if (attributes[attributeName] === true) { 88 | continue; 89 | } 90 | stringifiedAttributes += '=' + attributes[attributeName]; 91 | } 92 | return (document.cookie = key + '=' + value + stringifiedAttributes); 93 | } 94 | 95 | // Read 96 | 97 | if (!key) { 98 | result = {}; 99 | } 100 | 101 | // To prevent the for loop in the first place assign an empty array 102 | // in case there are no cookies at all. Also prevents odd result when 103 | // calling "get()" 104 | var cookies = document.cookie ? document.cookie.split('; ') : []; 105 | var rdecode = /(%[0-9A-Z]{2})+/g; 106 | var i = 0; 107 | 108 | for (; i < cookies.length; i++) { 109 | var parts = cookies[i].split('='); 110 | var cookie = parts.slice(1).join('='); 111 | 112 | if (cookie.charAt(0) === '"') { 113 | cookie = cookie.slice(1, -1); 114 | } 115 | 116 | try { 117 | var name = parts[0].replace(rdecode, decodeURIComponent); 118 | cookie = converter.read ? 119 | converter.read(cookie, name) : converter(cookie, name) || 120 | cookie.replace(rdecode, decodeURIComponent); 121 | 122 | if (this.json) { 123 | try { 124 | cookie = JSON.parse(cookie); 125 | } catch (e) { } 126 | } 127 | 128 | if (key === name) { 129 | result = cookie; 130 | break; 131 | } 132 | 133 | if (!key) { 134 | result[name] = cookie; 135 | } 136 | } catch (e) { } 137 | } 138 | 139 | return result; 140 | } 141 | 142 | api.set = api; 143 | api.get = function (key) { 144 | return api.call(api, key); 145 | }; 146 | api.getJSON = function () { 147 | return api.apply({ 148 | json: true 149 | }, [].slice.call(arguments)); 150 | }; 151 | api.defaults = {}; 152 | 153 | api.remove = function (key, attributes) { 154 | api(key, '', extend(attributes, { 155 | expires: -1 156 | })); 157 | }; 158 | 159 | api.withConverter = init; 160 | 161 | return api; 162 | } 163 | 164 | return init(function () { }); 165 | })); -------------------------------------------------------------------------------- /test/WebClientTestApp/wwwroot/js/reconnecting-websocket.min.js: -------------------------------------------------------------------------------- 1 | !function(a,b){"function"==typeof define&&define.amd?define([],b):"undefined"!=typeof module&&module.exports?module.exports=b():a.ReconnectingWebSocket=b()}(this,function(){function a(b,c,d){function l(a,b){var c=document.createEvent("CustomEvent");return c.initCustomEvent(a,!1,!1,b),c}var e={debug:!1,automaticOpen:!0,reconnectInterval:1e3,maxReconnectInterval:3e4,reconnectDecay:1.5,timeoutInterval:2e3};d||(d={});for(var f in e)this[f]="undefined"!=typeof d[f]?d[f]:e[f];this.url=b,this.reconnectAttempts=0,this.readyState=WebSocket.CONNECTING,this.protocol=null;var h,g=this,i=!1,j=!1,k=document.createElement("div");k.addEventListener("open",function(a){g.onopen(a)}),k.addEventListener("close",function(a){g.onclose(a)}),k.addEventListener("connecting",function(a){g.onconnecting(a)}),k.addEventListener("message",function(a){g.onmessage(a)}),k.addEventListener("error",function(a){g.onerror(a)}),this.addEventListener=k.addEventListener.bind(k),this.removeEventListener=k.removeEventListener.bind(k),this.dispatchEvent=k.dispatchEvent.bind(k),this.open=function(b){h=new WebSocket(g.url,c||[]),b||k.dispatchEvent(l("connecting")),(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","attempt-connect",g.url);var d=h,e=setTimeout(function(){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","connection-timeout",g.url),j=!0,d.close(),j=!1},g.timeoutInterval);h.onopen=function(){clearTimeout(e),(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onopen",g.url),g.protocol=h.protocol,g.readyState=WebSocket.OPEN,g.reconnectAttempts=0;var d=l("open");d.isReconnect=b,b=!1,k.dispatchEvent(d)},h.onclose=function(c){if(clearTimeout(e),h=null,i)g.readyState=WebSocket.CLOSED,k.dispatchEvent(l("close"));else{g.readyState=WebSocket.CONNECTING;var d=l("connecting");d.code=c.code,d.reason=c.reason,d.wasClean=c.wasClean,k.dispatchEvent(d),b||j||((g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onclose",g.url),k.dispatchEvent(l("close")));var e=g.reconnectInterval*Math.pow(g.reconnectDecay,g.reconnectAttempts);setTimeout(function(){g.reconnectAttempts++,g.open(!0)},e>g.maxReconnectInterval?g.maxReconnectInterval:e)}},h.onmessage=function(b){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onmessage",g.url,b.data);var c=l("message");c.data=b.data,k.dispatchEvent(c)},h.onerror=function(b){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onerror",g.url,b),k.dispatchEvent(l("error"))}},1==this.automaticOpen&&this.open(!1),this.send=function(b){if(h)return(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","send",g.url,b),h.send(b);throw"INVALID_STATE_ERR : Pausing to reconnect websocket"},this.close=function(a,b){"undefined"==typeof a&&(a=1e3),i=!0,h&&h.close(a,b)},this.refresh=function(){h&&h.close()}}return a.prototype.onopen=function(){},a.prototype.onclose=function(){},a.prototype.onconnecting=function(){},a.prototype.onmessage=function(){},a.prototype.onerror=function(){},a.debugAll=!1,a.CONNECTING=WebSocket.CONNECTING,a.OPEN=WebSocket.OPEN,a.CLOSING=WebSocket.CLOSING,a.CLOSED=WebSocket.CLOSED,a}); 2 | -------------------------------------------------------------------------------- /test/WebClientTestApp2/AgentsWebSocketCommandInvocator.cs: -------------------------------------------------------------------------------- 1 | using NetCoreStack.WebSockets; 2 | using System.Threading.Tasks; 3 | 4 | namespace WebClientTestApp2 5 | { 6 | public class AgentsWebSocketCommandInvocator : IServerWebSocketCommandInvocator 7 | { 8 | private readonly IConnectionManager _connectionManager; 9 | public AgentsWebSocketCommandInvocator(IConnectionManager connectionManager) 10 | { 11 | _connectionManager = connectionManager; 12 | } 13 | 14 | public async Task InvokeAsync(WebSocketMessageContext context) 15 | { 16 | if (context.Command == WebSocketCommands.Connect) 17 | { 18 | 19 | } 20 | 21 | // Sending incoming data from Backend zone to the Clients (Browsers) 22 | await _connectionManager.BroadcastAsync(context); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/WebClientTestApp2/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace WebClientTestApp2 2 | { 3 | public static class Constants 4 | { 5 | public const string ConnectorName = "WebClientSPA2"; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/WebClientTestApp2/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace WebClientTestApp2.Controllers 4 | { 5 | public class HomeController : Controller 6 | { 7 | public IActionResult Index() 8 | { 9 | return View(); 10 | } 11 | 12 | public IActionResult ReConnectTest() 13 | { 14 | return View(); 15 | } 16 | 17 | public IActionResult About() 18 | { 19 | ViewData["Message"] = "Your application description page."; 20 | 21 | return View(); 22 | } 23 | 24 | public IActionResult Contact() 25 | { 26 | ViewData["Message"] = "Your contact page."; 27 | 28 | return View(); 29 | } 30 | 31 | public IActionResult Error() 32 | { 33 | return View(); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/WebClientTestApp2/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | using System.Threading.Tasks; 4 | 5 | namespace WebClientTestApp2 6 | { 7 | public class Program 8 | { 9 | public static Task Main(string[] args) 10 | => BuildHost(args).RunAsync(); 11 | 12 | public static IHost BuildHost(string[] args) => CreateHostBuilder(args).Build(); 13 | 14 | public static IHostBuilder CreateHostBuilder(string[] args) => 15 | Host.CreateDefaultBuilder(args) 16 | .ConfigureWebHostDefaults(webBuilder => webBuilder 17 | .UseStartup()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/WebClientTestApp2/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:17217/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "WebClientTestApp2": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "launchUrl": "http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /test/WebClientTestApp2/Startup.cs: -------------------------------------------------------------------------------- 1 | using Common.Libs; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | using Microsoft.Extensions.Logging; 8 | using NetCoreStack.WebSockets; 9 | using NetCoreStack.WebSockets.ProxyClient; 10 | using System; 11 | 12 | namespace WebClientTestApp2 13 | { 14 | public class Startup 15 | { 16 | public Startup(IConfiguration configuration) 17 | { 18 | Configuration = configuration; 19 | } 20 | 21 | public IConfiguration Configuration { get; } 22 | 23 | public void ConfigureServices(IServiceCollection services) 24 | { 25 | services.AddMemoryCache(); 26 | services.AddSingleton(); 27 | 28 | var connectorname = $"TestApp2-{Environment.MachineName}"; 29 | 30 | // WebSocket - Proxy 31 | var builder = services.AddProxyWebSockets() 32 | .Register(connectorname, "localhost:7803"); 33 | 34 | builder.Register(connectorname, "localhost:5000"); 35 | 36 | // WebSockets for Browsers - User Agent ( javascript clients ) 37 | services.AddNativeWebSockets(); 38 | 39 | services.AddControllersWithViews(); 40 | } 41 | 42 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) 43 | { 44 | if (env.IsDevelopment()) 45 | { 46 | app.UseDeveloperExceptionPage(); 47 | } 48 | else 49 | { 50 | app.UseExceptionHandler("/Home/Error"); 51 | } 52 | 53 | app.UseStaticFiles(); 54 | 55 | // Proxy (Domain App) Client WebSocket - DMZ to API side connections 56 | app.UseProxyWebSockets(); 57 | 58 | // User Agent WebSockets for Browsers 59 | app.UseNativeWebSockets(); 60 | 61 | app.UseRouting(); 62 | 63 | app.UseEndpoints(endpoints => 64 | { 65 | endpoints.MapDefaultControllerRoute(); 66 | }); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/WebClientTestApp2/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Home Page"; 3 | } 4 | 5 | 24 | 25 |
26 |
27 |
28 |
29 |
30 | ConnectionId: 31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | 45 |
46 | 47 |
48 |
49 |
50 |
51 | 52 |
53 | 54 |
55 |
56 |
57 |
58 |
59 | 60 |
61 |
62 |
63 |
64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |
FromToData
80 |
81 |
82 | 83 | @section scripts { 84 | 209 | } -------------------------------------------------------------------------------- /test/WebClientTestApp2/Views/Home/ReConnectTest.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Home Page"; 3 | } 4 | 5 | 24 | 25 |
26 |
27 |
28 |
29 |
30 | ConnectionId: 31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | 45 |
46 | 47 |
48 |
49 |
50 |
51 | 52 |
53 | 54 |
55 |
56 |
57 |
58 |
59 | 60 |
61 |
62 |
63 |
64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |
FromToData
80 |
81 |
82 | 83 | @section scripts { 84 | 85 | 209 | } -------------------------------------------------------------------------------- /test/WebClientTestApp2/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Error"; 3 | } 4 | 5 |

Error.

6 |

An error occurred while processing your request.

7 | 8 |

Development Mode

9 |

10 | Swapping to Development environment will display more detailed information about the error that occurred. 11 |

12 |

13 | Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application. 14 |

15 | -------------------------------------------------------------------------------- /test/WebClientTestApp2/Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | @ViewData["Title"] - Test App 2 7 | 8 | 9 | 10 | 28 |
29 | @RenderBody() 30 |
31 | 32 | 33 | 34 | 35 | @RenderSection("scripts", required: false) 36 | 37 | 38 | -------------------------------------------------------------------------------- /test/WebClientTestApp2/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using WebClientTestApp2 2 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 3 | -------------------------------------------------------------------------------- /test/WebClientTestApp2/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /test/WebClientTestApp2/WebClientTestApp2.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/WebClientTestApp2/WebSocketCommandInvocator.cs: -------------------------------------------------------------------------------- 1 | using Common.Libs; 2 | using Microsoft.Extensions.Caching.Memory; 3 | using NetCoreStack.WebSockets; 4 | using NetCoreStack.WebSockets.ProxyClient; 5 | using System.Collections.Generic; 6 | using System.Net.WebSockets; 7 | using System.Threading.Tasks; 8 | 9 | namespace WebClientTestApp2 10 | { 11 | public class WebSocketCommandInvocator : IClientWebSocketCommandInvocator 12 | { 13 | private readonly IConnectionManager _connectionManager; 14 | private readonly InMemoryCacheProvider _cacheProvider; 15 | 16 | public WebSocketCommandInvocator(IConnectionManager connectionManager, InMemoryCacheProvider cacheProvider) 17 | { 18 | _connectionManager = connectionManager; 19 | _cacheProvider = cacheProvider; 20 | } 21 | 22 | private Task InternalMethodAsync() 23 | { 24 | return Task.CompletedTask; 25 | } 26 | 27 | public async Task InvokeAsync(WebSocketMessageContext context) 28 | { 29 | if (context.MessageType == WebSocketMessageType.Text) 30 | { 31 | 32 | } 33 | 34 | if (context.MessageType == WebSocketMessageType.Binary) 35 | { 36 | var header = context.Header as Dictionary; 37 | if (header != null) 38 | { 39 | object key = null; 40 | if(header.TryGetValue(WebSocketHeaderNames.CacheItemKey, out key)) 41 | { 42 | await Task.Run(() => _cacheProvider.SetObject(key.ToString(), 43 | context.Value, 44 | new MemoryCacheEntryOptions { Priority = CacheItemPriority.NeverRemove })); 45 | } 46 | } 47 | var length = context.Length; 48 | double size = (length / 1024f) / 1024f; 49 | context.Value = $"{size} MB <>"; 50 | } 51 | 52 | // Sending incoming data from Backend zone to the Clients (Browsers) 53 | await _connectionManager.BroadcastAsync(context); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/WebClientTestApp2/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/WebClientTestApp2/wwwroot/_references.js: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | /// 6 | /// 7 | -------------------------------------------------------------------------------- /test/WebClientTestApp2/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetCoreStack/WebSockets/df14771cf84daeccd4ebf0889cdd49e79ea6e818/test/WebClientTestApp2/wwwroot/favicon.ico -------------------------------------------------------------------------------- /test/WebClientTestApp2/wwwroot/js/cookie.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * JavaScript Cookie v2.1.3 3 | * https://github.com/js-cookie/js-cookie 4 | * 5 | * Copyright 2006, 2015 Klaus Hartl & Fagner Brack 6 | * Released under the MIT license 7 | */ 8 | ; (function (factory) { 9 | var registeredInModuleLoader = false; 10 | if (typeof define === 'function' && define.amd) { 11 | define(factory); 12 | registeredInModuleLoader = true; 13 | } 14 | if (typeof exports === 'object') { 15 | module.exports = factory(); 16 | registeredInModuleLoader = true; 17 | } 18 | if (!registeredInModuleLoader) { 19 | var OldCookies = window.Cookies; 20 | var api = window.Cookies = factory(); 21 | api.noConflict = function () { 22 | window.Cookies = OldCookies; 23 | return api; 24 | }; 25 | } 26 | }(function () { 27 | function extend() { 28 | var i = 0; 29 | var result = {}; 30 | for (; i < arguments.length; i++) { 31 | var attributes = arguments[i]; 32 | for (var key in attributes) { 33 | result[key] = attributes[key]; 34 | } 35 | } 36 | return result; 37 | } 38 | 39 | function init(converter) { 40 | function api(key, value, attributes) { 41 | var result; 42 | if (typeof document === 'undefined') { 43 | return; 44 | } 45 | 46 | // Write 47 | 48 | if (arguments.length > 1) { 49 | attributes = extend({ 50 | path: '/' 51 | }, api.defaults, attributes); 52 | 53 | if (typeof attributes.expires === 'number') { 54 | var expires = new Date(); 55 | expires.setMilliseconds(expires.getMilliseconds() + attributes.expires * 864e+5); 56 | attributes.expires = expires; 57 | } 58 | 59 | // We're using "expires" because "max-age" is not supported by IE 60 | attributes.expires = attributes.expires ? attributes.expires.toUTCString() : ''; 61 | 62 | try { 63 | result = JSON.stringify(value); 64 | if (/^[\{\[]/.test(result)) { 65 | value = result; 66 | } 67 | } catch (e) { } 68 | 69 | if (!converter.write) { 70 | value = encodeURIComponent(String(value)) 71 | .replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent); 72 | } else { 73 | value = converter.write(value, key); 74 | } 75 | 76 | key = encodeURIComponent(String(key)); 77 | key = key.replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent); 78 | key = key.replace(/[\(\)]/g, escape); 79 | 80 | var stringifiedAttributes = ''; 81 | 82 | for (var attributeName in attributes) { 83 | if (!attributes[attributeName]) { 84 | continue; 85 | } 86 | stringifiedAttributes += '; ' + attributeName; 87 | if (attributes[attributeName] === true) { 88 | continue; 89 | } 90 | stringifiedAttributes += '=' + attributes[attributeName]; 91 | } 92 | return (document.cookie = key + '=' + value + stringifiedAttributes); 93 | } 94 | 95 | // Read 96 | 97 | if (!key) { 98 | result = {}; 99 | } 100 | 101 | // To prevent the for loop in the first place assign an empty array 102 | // in case there are no cookies at all. Also prevents odd result when 103 | // calling "get()" 104 | var cookies = document.cookie ? document.cookie.split('; ') : []; 105 | var rdecode = /(%[0-9A-Z]{2})+/g; 106 | var i = 0; 107 | 108 | for (; i < cookies.length; i++) { 109 | var parts = cookies[i].split('='); 110 | var cookie = parts.slice(1).join('='); 111 | 112 | if (cookie.charAt(0) === '"') { 113 | cookie = cookie.slice(1, -1); 114 | } 115 | 116 | try { 117 | var name = parts[0].replace(rdecode, decodeURIComponent); 118 | cookie = converter.read ? 119 | converter.read(cookie, name) : converter(cookie, name) || 120 | cookie.replace(rdecode, decodeURIComponent); 121 | 122 | if (this.json) { 123 | try { 124 | cookie = JSON.parse(cookie); 125 | } catch (e) { } 126 | } 127 | 128 | if (key === name) { 129 | result = cookie; 130 | break; 131 | } 132 | 133 | if (!key) { 134 | result[name] = cookie; 135 | } 136 | } catch (e) { } 137 | } 138 | 139 | return result; 140 | } 141 | 142 | api.set = api; 143 | api.get = function (key) { 144 | return api.call(api, key); 145 | }; 146 | api.getJSON = function () { 147 | return api.apply({ 148 | json: true 149 | }, [].slice.call(arguments)); 150 | }; 151 | api.defaults = {}; 152 | 153 | api.remove = function (key, attributes) { 154 | api(key, '', extend(attributes, { 155 | expires: -1 156 | })); 157 | }; 158 | 159 | api.withConverter = init; 160 | 161 | return api; 162 | } 163 | 164 | return init(function () { }); 165 | })); -------------------------------------------------------------------------------- /test/WebClientTestApp2/wwwroot/js/reconnecting-websocket.min.js: -------------------------------------------------------------------------------- 1 | !function(a,b){"function"==typeof define&&define.amd?define([],b):"undefined"!=typeof module&&module.exports?module.exports=b():a.ReconnectingWebSocket=b()}(this,function(){function a(b,c,d){function l(a,b){var c=document.createEvent("CustomEvent");return c.initCustomEvent(a,!1,!1,b),c}var e={debug:!1,automaticOpen:!0,reconnectInterval:1e3,maxReconnectInterval:3e4,reconnectDecay:1.5,timeoutInterval:2e3};d||(d={});for(var f in e)this[f]="undefined"!=typeof d[f]?d[f]:e[f];this.url=b,this.reconnectAttempts=0,this.readyState=WebSocket.CONNECTING,this.protocol=null;var h,g=this,i=!1,j=!1,k=document.createElement("div");k.addEventListener("open",function(a){g.onopen(a)}),k.addEventListener("close",function(a){g.onclose(a)}),k.addEventListener("connecting",function(a){g.onconnecting(a)}),k.addEventListener("message",function(a){g.onmessage(a)}),k.addEventListener("error",function(a){g.onerror(a)}),this.addEventListener=k.addEventListener.bind(k),this.removeEventListener=k.removeEventListener.bind(k),this.dispatchEvent=k.dispatchEvent.bind(k),this.open=function(b){h=new WebSocket(g.url,c||[]),b||k.dispatchEvent(l("connecting")),(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","attempt-connect",g.url);var d=h,e=setTimeout(function(){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","connection-timeout",g.url),j=!0,d.close(),j=!1},g.timeoutInterval);h.onopen=function(){clearTimeout(e),(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onopen",g.url),g.protocol=h.protocol,g.readyState=WebSocket.OPEN,g.reconnectAttempts=0;var d=l("open");d.isReconnect=b,b=!1,k.dispatchEvent(d)},h.onclose=function(c){if(clearTimeout(e),h=null,i)g.readyState=WebSocket.CLOSED,k.dispatchEvent(l("close"));else{g.readyState=WebSocket.CONNECTING;var d=l("connecting");d.code=c.code,d.reason=c.reason,d.wasClean=c.wasClean,k.dispatchEvent(d),b||j||((g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onclose",g.url),k.dispatchEvent(l("close")));var e=g.reconnectInterval*Math.pow(g.reconnectDecay,g.reconnectAttempts);setTimeout(function(){g.reconnectAttempts++,g.open(!0)},e>g.maxReconnectInterval?g.maxReconnectInterval:e)}},h.onmessage=function(b){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onmessage",g.url,b.data);var c=l("message");c.data=b.data,k.dispatchEvent(c)},h.onerror=function(b){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onerror",g.url,b),k.dispatchEvent(l("error"))}},1==this.automaticOpen&&this.open(!1),this.send=function(b){if(h)return(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","send",g.url,b),h.send(b);throw"INVALID_STATE_ERR : Pausing to reconnect websocket"},this.close=function(a,b){"undefined"==typeof a&&(a=1e3),i=!0,h&&h.close(a,b)},this.refresh=function(){h&&h.close()}}return a.prototype.onopen=function(){},a.prototype.onclose=function(){},a.prototype.onconnecting=function(){},a.prototype.onmessage=function(){},a.prototype.onerror=function(){},a.debugAll=!1,a.CONNECTING=WebSocket.CONNECTING,a.OPEN=WebSocket.OPEN,a.CLOSING=WebSocket.CLOSING,a.CLOSED=WebSocket.CLOSED,a}); 2 | --------------------------------------------------------------------------------