From 202a5c0115076e8958c366b7d705adc27d8ee5fb Mon Sep 17 00:00:00 2001 From: Tao Li Date: Mon, 17 Aug 2020 10:52:32 +0800 Subject: [PATCH 1/2] Adding an experimental feature to reload client when runtime instance graceful shutdown.(Serverless mode client sample) --- .../MessagePublisher/MessagePublisher.csproj | 15 ++ .../MessagePublisher/MessagePubulisher.cs | 126 ++++++++++++++ .../MessagePublisher/Program.cs | 136 +++++++++++++++ .../MessagePublisher/README.md | 90 ++++++++++ .../Controllers/NegotiateController.cs | 46 +++++ .../NegotiationServer.csproj | 13 ++ .../NegotiationServer/Program.cs | 23 +++ .../Properties/launchSettings.json | 30 ++++ .../NegotiationServer/README.md | 84 ++++++++++ .../NegotiationServer/Startup.cs | 47 ++++++ .../appsettings.Development.json | 9 + .../NegotiationServer/appsettings.json | 10 ++ samples/ServerlessReloading/README.md | 71 ++++++++ .../SignalRClient/Program.cs | 49 ++++++ .../SignalRClient/README.md | 62 +++++++ .../SignalRClient/SignalRClient.csproj | 16 ++ .../SignalRClient/StableConnection.cs | 158 ++++++++++++++++++ .../SignalRClient/TypeClass.cs | 38 +++++ 18 files changed, 1023 insertions(+) create mode 100644 samples/ServerlessReloading/MessagePublisher/MessagePublisher.csproj create mode 100644 samples/ServerlessReloading/MessagePublisher/MessagePubulisher.cs create mode 100644 samples/ServerlessReloading/MessagePublisher/Program.cs create mode 100644 samples/ServerlessReloading/MessagePublisher/README.md create mode 100644 samples/ServerlessReloading/NegotiationServer/Controllers/NegotiateController.cs create mode 100644 samples/ServerlessReloading/NegotiationServer/NegotiationServer.csproj create mode 100644 samples/ServerlessReloading/NegotiationServer/Program.cs create mode 100644 samples/ServerlessReloading/NegotiationServer/Properties/launchSettings.json create mode 100644 samples/ServerlessReloading/NegotiationServer/README.md create mode 100644 samples/ServerlessReloading/NegotiationServer/Startup.cs create mode 100644 samples/ServerlessReloading/NegotiationServer/appsettings.Development.json create mode 100644 samples/ServerlessReloading/NegotiationServer/appsettings.json create mode 100644 samples/ServerlessReloading/README.md create mode 100644 samples/ServerlessReloading/SignalRClient/Program.cs create mode 100644 samples/ServerlessReloading/SignalRClient/README.md create mode 100644 samples/ServerlessReloading/SignalRClient/SignalRClient.csproj create mode 100644 samples/ServerlessReloading/SignalRClient/StableConnection.cs create mode 100644 samples/ServerlessReloading/SignalRClient/TypeClass.cs diff --git a/samples/ServerlessReloading/MessagePublisher/MessagePublisher.csproj b/samples/ServerlessReloading/MessagePublisher/MessagePublisher.csproj new file mode 100644 index 00000000..f13caee8 --- /dev/null +++ b/samples/ServerlessReloading/MessagePublisher/MessagePublisher.csproj @@ -0,0 +1,15 @@ + + + + Exe + netcoreapp3.1 + ReloadingSample + + + + + + + + + diff --git a/samples/ServerlessReloading/MessagePublisher/MessagePubulisher.cs b/samples/ServerlessReloading/MessagePublisher/MessagePubulisher.cs new file mode 100644 index 00000000..4905beb3 --- /dev/null +++ b/samples/ServerlessReloading/MessagePublisher/MessagePubulisher.cs @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Threading.Tasks; +using System.Net.Http; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Azure.SignalR.Management; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System.Text; +using System.Net.Http.Headers; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; + +namespace Microsoft.Azure.SignalR.Samples.Management +{ + public class MessagePublisher + { + private const string Target = "Target"; + private const string HubName = "ManagementSampleHub"; + private readonly string _connectionString; + private readonly ServiceTransportType _serviceTransportType; + private IServiceHubContext _hubContext; + // reload connection string + private readonly string connectionString = "Endpoint=http://localhost;AccessKey=ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGH;Port=8081;Version=1.0;"; + + + public MessagePublisher(string connectionString, ServiceTransportType serviceTransportType) + { + _connectionString = connectionString; + _serviceTransportType = serviceTransportType; + } + + public async Task InitAsync() + { + var serviceManager = new ServiceManagerBuilder().WithOptions(option => + { + option.ConnectionString = _connectionString; + option.ServiceTransportType = _serviceTransportType; + }).Build(); + + _hubContext = await serviceManager.CreateHubContextAsync(HubName, new LoggerFactory()); + } + + public Task ManageUserGroup(string command, string userId, string groupName) + { + switch (command) + { + case "add": + return _hubContext.UserGroups.AddToGroupAsync(userId, groupName); + case "remove": + return _hubContext.UserGroups.RemoveFromGroupAsync(userId, groupName); + default: + Console.WriteLine($"Can't recognize command {command}"); + return Task.CompletedTask; + } + } + + public Task SendMessages(string command, string receiver, string message) + { + var jMsg = new + { + msgType = "0", + content = message + }; + var uMsg = JsonConvert.SerializeObject(jMsg); + switch (command) + { + case "broadcast": + return _hubContext.Clients.All.SendAsync(Target, uMsg); + case "user": + var userId = receiver; + return _hubContext.Clients.User(userId).SendAsync(Target, uMsg); + case "users": + var userIds = receiver.Split(','); + return _hubContext.Clients.Users(userIds).SendAsync(Target, uMsg); + case "group": + var groupName = receiver; + return _hubContext.Clients.Group(groupName).SendAsync(Target, uMsg); + case "groups": + var groupNames = receiver.Split(','); + return _hubContext.Clients.Groups(groupNames).SendAsync(Target, uMsg); + case "reload": + HttpClient restClient = new HttpClient(); + var msg = new + { + Target = Target, + Arguments = new string[] { connectionString } + }; + + // generate token + string key = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGH"; + var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)) + { + KeyId = key.GetHashCode().ToString() + }; + SigningCredentials credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); + + JwtSecurityTokenHandler JwtTokenHandler = new JwtSecurityTokenHandler(); + var token = JwtTokenHandler.CreateJwtSecurityToken( + issuer: null, + audience: "http://localhost/api/v1/reload", + notBefore: DateTime.Now, + expires: DateTime.Now.AddHours(1), + issuedAt: DateTime.Now, + signingCredentials: credentials) ; + string tk = JwtTokenHandler.WriteToken(token); + + restClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tk); + restClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + string json = JsonConvert.SerializeObject(msg); + var data = new StringContent(json, Encoding.UTF8, "application/json"); + // Call reload rest api on the old service to start reloading connection + string url = "http://localhost:8080/api/v1/reload"; + + return restClient.PostAsync(url, data); + default: + Console.WriteLine($"Can't recognize command {command}"); + return Task.CompletedTask; + } + } + + public Task DisposeAsync() => _hubContext?.DisposeAsync(); + } +} \ No newline at end of file diff --git a/samples/ServerlessReloading/MessagePublisher/Program.cs b/samples/ServerlessReloading/MessagePublisher/Program.cs new file mode 100644 index 00000000..056cf967 --- /dev/null +++ b/samples/ServerlessReloading/MessagePublisher/Program.cs @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Azure.SignalR.Management; +using Microsoft.Extensions.CommandLineUtils; +using Microsoft.Extensions.Configuration; + +namespace Microsoft.Azure.SignalR.Samples.Management +{ + class Program + { + static void Main(string[] args) + { + var app = new CommandLineApplication(); + app.FullName = "Azure SignalR Management Sample: Message Publisher"; + app.HelpOption("--help"); + app.Description = "Message publisher using Azure SignalR Service Management SDK."; + + var connectionStringOption = app.Option("-c|--connectionstring", "Set connection string.", CommandOptionType.SingleValue, true); + var serviceTransportTypeOption = app.Option("-t|--transport", "Set service transport type. Options: |. Default value: transient. Transient: calls REST API for each message. Persistent: Establish a WebSockets connection and send all messages in the connection.", CommandOptionType.SingleValue, true); // todo: description + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddUserSecrets() + .AddEnvironmentVariables() + .Build(); + + + app.OnExecute(async () => + { + var connectionString = connectionStringOption.Value() ?? configuration["Azure:SignalR:ConnectionString"]; + + if (string.IsNullOrEmpty(connectionString)) + { + MissOptions(); + return -1; + } + + ServiceTransportType serviceTransportType; + if (string.IsNullOrEmpty(serviceTransportTypeOption.Value())) + { + serviceTransportType = ServiceTransportType.Transient; + } + else + { + serviceTransportType = Enum.Parse(serviceTransportTypeOption.Value(), true); + } + + var publisher = new MessagePublisher(connectionString, serviceTransportType); + await publisher.InitAsync(); + + await StartAsync(publisher); + + return 0; + }); + + app.Execute(args); + } + + private static async Task StartAsync(MessagePublisher publisher) + { + Console.CancelKeyPress += async (sender, e) => + { + await publisher.DisposeAsync(); + Environment.Exit(0); + }; + + ShowHelp(); + + try + { + while (true) + { + var argLine = Console.ReadLine(); + if (argLine == null) + { + continue; + } + var args = argLine.Split(' '); + + if (args.Length == 2 && args[0].Equals("broadcast")) + { + Console.WriteLine($"broadcast message '{args[1]}'"); + await publisher.SendMessages(args[0], null, args[1]); + } + else if (args.Length == 4 && args[0].Equals("send")) + { + await publisher.SendMessages(args[1], args[2], args[3]); + Console.WriteLine($"{args[0]} message '{args[3]}' to '{args[2]}'"); + } + else if (args.Length == 4 && args[0] == "usergroup") + { + await publisher.ManageUserGroup(args[1], args[2], args[3]); + var preposition = args[1] == "add" ? "to" : "from"; + Console.WriteLine($"{args[1]} user '{args[2]}' {preposition} group '{args[3]}'"); + } + else if (args.Length == 1 && args[0].Equals("reload")) + { + await publisher.SendMessages(args[0], null, null); + Console.WriteLine($"{args[0]}"); + } + else + { + Console.WriteLine($"Can't recognize command {argLine}"); + } + } + } + finally + { + await publisher.DisposeAsync(); + } + } + + private static void ShowHelp() + { + Console.WriteLine( + "*********Usage*********\n" + + "send user \n" + + "send users \n" + + "send group \n" + + "send groups \n" + + "usergroup add \n" + + "usergroup remove \n" + + "broadcast \n" + + "reload\n" + + "***********************"); + } + + private static void MissOptions() + { + Console.WriteLine("Miss required options: Connection string and Hub must be set"); + } + } +} \ No newline at end of file diff --git a/samples/ServerlessReloading/MessagePublisher/README.md b/samples/ServerlessReloading/MessagePublisher/README.md new file mode 100644 index 00000000..b56f3005 --- /dev/null +++ b/samples/ServerlessReloading/MessagePublisher/README.md @@ -0,0 +1,90 @@ +Message Publisher +========= + +This sample shows how to use [Microsoft.Azure.SignalR.Management](https://www.nuget.org/packages/Microsoft.Azure.SignalR.Management) to publish messages to SignalR clients that connect to Azure SignalR Service. + +## Build from Scratch + +### Add Management SDK to your project + +``` +dotnet add package Microsoft.Azure.SignalR.Management -v 1.0.0-* +``` + +### Create instance of `IServiceManager` + +The `IServiceManager` is able to manage your Azure SignalR Service from your connection string. + +```c# +var serviceManager = new ServiceManagerBuilder() + .WithOptions(option => + { + option.ConnectionString = ""; + }) + .Build(); +``` + +### Create instance of `IServiceHubContext` + +The `IServiceHubContext` is used to publish messages to a specific hub. + +```C# +var hubContext = await serviceManager.CreateHubContextAsync(""); +``` + +### Publish messages to a specific hub + +Once you create the `hubContext`, you can use it to publish messages to a given hub. + +```C# +// broadcast +hubContext.Clients.All.SendAsync("", "", "", ...); + +// send to a user +hubContext.Clients.User("").SendAsync("", "", "", ...); + +// send to users +hubContext.Clients.Users().SendAsync("", "", "", ...); + +// send to a group +hubContext.Clients.Group("").SendAsync("", "", "", ...); + +// send to groups +hubContext.Clients.Group().SendAsync("", "", "", ...); + +// add a user to a group +hubContext.UserGroups.AddToGroupAsync("", ""); + +// remove a user from a group +hubContext.UserGroups.RemoveFromGroupAsync("", ""); + +... +``` + +All features can be found [here](). + +### Reload connection to another new service platform + +```C# +// Use specialized Json Web Token with userId claim +restClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tk); + +// Serialize reloading message for transimisstion +string json = JsonConvert.SerializeObject(msg); + +// Call reload rest api on the old service to start reloading connection +string url = "http://localhost:8080/api/v1/reload"; + +// Post REST API to reload connections on a specific service instance +restClient.PostAsync(url, data); +``` + +### Dispose the instance of `IServiceHubContext` + +```c# +await hubContext.DisposeAsync(); +``` + +## Full Sample + +The full message publisher sample can be found [here](.). The usage of this sample can be found [here](). \ No newline at end of file diff --git a/samples/ServerlessReloading/NegotiationServer/Controllers/NegotiateController.cs b/samples/ServerlessReloading/NegotiationServer/Controllers/NegotiateController.cs new file mode 100644 index 00000000..d323311b --- /dev/null +++ b/samples/ServerlessReloading/NegotiationServer/Controllers/NegotiateController.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Security.Claims; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.SignalR.Management; +using Microsoft.Extensions.Configuration; + +namespace NegotiationServer.Controllers +{ + [ApiController] + public class NegotiateController : ControllerBase + { + private readonly IServiceManager _serviceManager; + + public NegotiateController(IConfiguration configuration) + { + var connectionString = configuration["Azure:SignalR:ConnectionString"]; + // var connectionString = "Endpoint=http://localhost;AccessKey=ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGH;Port=8080;Version=1.0;"; for localhost reloading + _serviceManager = new ServiceManagerBuilder() + .WithOptions(o => o.ConnectionString = connectionString) + .Build(); + } + + [HttpPost("{hub}/negotiate")] + public ActionResult Index(string hub, string user) + { + if (string.IsNullOrEmpty(user)) + { + return BadRequest("User ID is null or empty."); + } + + IList cs = new List(); + cs.Add(new Claim("isFirstConnection", "True")); + + return new JsonResult(new Dictionary() + { + { "url", _serviceManager.GetClientEndpoint(hub) }, + { "accessToken", _serviceManager.GenerateClientAccessToken(hub, user, cs) } + }); + } + } + + +} diff --git a/samples/ServerlessReloading/NegotiationServer/NegotiationServer.csproj b/samples/ServerlessReloading/NegotiationServer/NegotiationServer.csproj new file mode 100644 index 00000000..6ebb3623 --- /dev/null +++ b/samples/ServerlessReloading/NegotiationServer/NegotiationServer.csproj @@ -0,0 +1,13 @@ + + + + netcoreapp3.1 + ReloadingSample + + + + + + + + diff --git a/samples/ServerlessReloading/NegotiationServer/Program.cs b/samples/ServerlessReloading/NegotiationServer/Program.cs new file mode 100644 index 00000000..fd4e60db --- /dev/null +++ b/samples/ServerlessReloading/NegotiationServer/Program.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace NegotiationServer +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/samples/ServerlessReloading/NegotiationServer/Properties/launchSettings.json b/samples/ServerlessReloading/NegotiationServer/Properties/launchSettings.json new file mode 100644 index 00000000..c25960c7 --- /dev/null +++ b/samples/ServerlessReloading/NegotiationServer/Properties/launchSettings.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:31190", + "sslPort": 44372 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "negotiate", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "NegotiationServer": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "negotiate", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/ServerlessReloading/NegotiationServer/README.md b/samples/ServerlessReloading/NegotiationServer/README.md new file mode 100644 index 00000000..5e5f55ed --- /dev/null +++ b/samples/ServerlessReloading/NegotiationServer/README.md @@ -0,0 +1,84 @@ +# Negotiation Server + +This sample shows how to use [Microsoft.Azure.SignalR.Management](https://www.nuget.org/packages/Microsoft.Azure.SignalR.Management) to host negotiation endpoint for SignalR clients. + +> You can use [Azure Functions]() or other similar product instead to provide a totally serverless environment. +> +> For details what is negotiation and why we need a negotiation endpoint can be found [here](). + +## Build from Scratch + +### create a webapi app + +``` +dotnet new webapi +``` + +### Add Management SDK to your project + +``` +dotnet add package Microsoft.Azure.SignalR.Management -v 1.0.0-* +``` + +### Create a controller for negotiation + +```C# +namespace NegotiationServer.Controllers +{ + [ApiController] + public class NegotiateController : ControllerBase + { + ... + } +} +``` + +### Create instance of `IServiceManger` + +`IServiceManager` provides methods to generate client endpoints and access tokens for SignalR clients to connect to Azure SignalR Service. Add this constructor to the `NegotiateController` class. + +``` +private readonly IServiceManager _serviceManager; + +public NegotiateController(IConfiguration configuration) +{ + var connectionString = configuration["Azure:SignalR:ConnectionString"]; + _serviceManager = new ServiceManagerBuilder() + .WithOptions(o => o.ConnectionString = connectionString) + .Build(); +} +``` + +### Provide Negotiation Endpoint + +In the `NegotiateController` class, provide the negotiation endpoint `//negotiate?user=`. + +We use the `_serviceManager` to generate a client endpoint and an access token and return to SignalR client following [Negotiation Protocol](https://github.com/aspnet/SignalR/blob/master/specs/TransportProtocols.md#post-endpoint-basenegotiate-request), which will redirect the SignalR client to the service. + +Here we add a special claim to let service recognize is this connection is first connection for this client for the use of reloading feature. + +> You only need to provide a negotiation endpoint, since SignalR clients will reach the `//negotiate` endpoint for redirecting, if you provide a hub endpoint `/` to SignalR clients. + +```C# +[HttpPost("{hub}/negotiate")] +public ActionResult Index(string hub, string user) +{ + if (string.IsNullOrEmpty(user)) + { + return BadRequest("User ID is null or empty."); + } + + IList cs = new List(); + cs.Add(new Claim("isFirstConnection", "True")); + + return new JsonResult(new Dictionary() + { + { "url", _serviceManager.GetClientEndpoint(hub) }, + { "accessToken", _serviceManager.GenerateClientAccessToken(hub, user) } + }); +} +``` + +## Full Sample + +The full negotiation server sample can be found [here](.). The usage of this sample can be found [here](). \ No newline at end of file diff --git a/samples/ServerlessReloading/NegotiationServer/Startup.cs b/samples/ServerlessReloading/NegotiationServer/Startup.cs new file mode 100644 index 00000000..b743b6c9 --- /dev/null +++ b/samples/ServerlessReloading/NegotiationServer/Startup.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace NegotiationServer +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + //app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} diff --git a/samples/ServerlessReloading/NegotiationServer/appsettings.Development.json b/samples/ServerlessReloading/NegotiationServer/appsettings.Development.json new file mode 100644 index 00000000..8983e0fc --- /dev/null +++ b/samples/ServerlessReloading/NegotiationServer/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/samples/ServerlessReloading/NegotiationServer/appsettings.json b/samples/ServerlessReloading/NegotiationServer/appsettings.json new file mode 100644 index 00000000..d9d9a9bf --- /dev/null +++ b/samples/ServerlessReloading/NegotiationServer/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/samples/ServerlessReloading/README.md b/samples/ServerlessReloading/README.md new file mode 100644 index 00000000..f548a193 --- /dev/null +++ b/samples/ServerlessReloading/README.md @@ -0,0 +1,71 @@ +Azure SignalR Service Management SDK Sample +================================= + +This sample shows the use of Azure SignalR Service Management SDK. + +* Message Publisher: shows how to publish messages to SignalR clients or graceful reloading to another service platform using Management SDK. +* Negotiation Server: shows how to negotiate client from you app server to Azure SignalR Service using Management SDK. +* SignalR Client: is a tool to start multiple SignalR clients(supporting reloading feature) and these clients listen messages for this sample. + +## Run the sample + +### Start the negotiation server + +``` +cd NegotitationServer +dotnet user-secrets set Azure:SignalR:ConnectionString "" +dotnet run +``` + +### Start SignalR clients + +``` +cd SignalRClient +dotnet run +``` + +> Parameters: +> +> - -h|--hubEndpoint: Set hub endpoint. Default value: "". +> - -u|--user: Set user ID. Default value: "User". You can set multiple users like this: "-u user1 -u user2". + +### Start message publisher + +``` +cd MessagePublisher +dotnet run + +``` + +> Parameters: +> +> -c|--connectionstring: Set connection string. +> -t|--transport: Set service transport type. Options: |. Default value: transient. Transient: calls REST API for each message. Persistent: Establish a WebSockets connection and send all messages in the connection. + +Once the message publisher get started, use the command to send message + +``` +send user +send users +send group +send groups +usergroup add +usergroup remove +broadcast +reload +``` + For example, type `broadcast hello`, and press keyboard `enter` to publish messages. + +You will see `User: gets message from service: 'hello'` from your SignalR client tool. + +### Use `user-secrets` to specify Connection String + +You can run `dotnet user-secrets set Azure:SignalR:ConnectionString ""` in the root directory of the sample. After that, you don't need the option `-c ""` anymore. + +## Build Management Sample from Scratch + +The following links are guides for building 3 components of this management sample from scratch. + +* [Message Publisher](./MessagePublisher/README.md) +* [Negotiation Server](./NegotiationServer/README.md) +* [SignalR Client](./SignalRClient/README.md) \ No newline at end of file diff --git a/samples/ServerlessReloading/SignalRClient/Program.cs b/samples/ServerlessReloading/SignalRClient/Program.cs new file mode 100644 index 00000000..a4fd26b7 --- /dev/null +++ b/samples/ServerlessReloading/SignalRClient/Program.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.CommandLineUtils; + +namespace SignalRClient +{ + class Program + { + private const string DefaultHubEndpoint = "http://localhost:5000/ManagementSampleHub"; + private const string Target = "Target"; + private const string DefaultUser = "User"; + + static void Main(string[] args) + { + var app = new CommandLineApplication(); + app.FullName = "Azure SignalR Management Sample: SignalR Client Tool"; + app.HelpOption("--help"); + + var hubEndpointOption = app.Option("-h|--hubEndpoint", $"Set hub endpoint. Default value: {DefaultHubEndpoint}", CommandOptionType.SingleValue, true); + var userIdOption = app.Option("-u|--userIdList", "Set user ID list", CommandOptionType.MultipleValue, true); + + app.OnExecute(async () => + { + var hubEndpoint = hubEndpointOption.Value() ?? DefaultHubEndpoint; + var userIds = userIdOption.Values != null && userIdOption.Values.Count > 0 ? userIdOption.Values : new List() { "User" }; + + var connections = (from userId in userIds + select new StableConnection(hubEndpoint, userId)).ToList(); + + await Task.WhenAll(from conn in connections + select conn.StartAsync()); + + Console.WriteLine($"{connections.Count} Client(s) started..."); + Console.ReadLine(); + + await Task.WhenAll(from conn in connections + select conn.StopAsync()); + return 0; + }); + + app.Execute(args); + } + } +} \ No newline at end of file diff --git a/samples/ServerlessReloading/SignalRClient/README.md b/samples/ServerlessReloading/SignalRClient/README.md new file mode 100644 index 00000000..efc58afb --- /dev/null +++ b/samples/ServerlessReloading/SignalRClient/README.md @@ -0,0 +1,62 @@ +# SignalR Client + +This sample shows how to use SignalR clients to connect Azure SignalR Service without using a web server that host a SignalR hub. The SignalrR client support reloading connection with the use of various types of messages in TypeClass.cs. + +## Build from Scratch + +### Add Management SDK to your project + +``` +dotnet add package Microsoft.Azure.SignalR.Management -v 1.0.0-* +``` + +### Connect SignalR clients to a hub endpoint with user ID + +Here because we need client to support reloading feature, so we actually build a more powerful StableConnection class on the basis of HubConnection class. + +```C# +var connections = (from userId in userIds +select new StableConnection(hubEndpoint, userId)).ToList(); +``` + +### Handle connection closed event in StableConnection class + +Sometimes SignalR clients may be disconnected by Azure SignalR Service, the `Closed` event handler will be useful to figure out the reason. + +```C# +connection.Closed += async ex => +{ + // handle exception here + ... +}; +``` + +### Handle SignalR client callback in StableConnection class + +```C# +connection.On("", ( , , ...) => +{ + // handle received arguments + ... +}); +``` + +### Establish connection to Azure SignalR Service + +```C# +await connection.StartAsync(); +``` + +Once your SignalR clients connect to the service, it is able to listen messages. + +### Stop SignalR clients + +You can stop the client connection anytime you want. + +```C# +await connection.StopAsync(); +``` + +## Full Sample + +The full refined message publisher version(supporting reloading feature) sample can be found [here](.). The usage of this sample can be found [here](). \ No newline at end of file diff --git a/samples/ServerlessReloading/SignalRClient/SignalRClient.csproj b/samples/ServerlessReloading/SignalRClient/SignalRClient.csproj new file mode 100644 index 00000000..e5c3ab5e --- /dev/null +++ b/samples/ServerlessReloading/SignalRClient/SignalRClient.csproj @@ -0,0 +1,16 @@ + + + + Exe + netcoreapp3.1 + + + + + + + + + + + diff --git a/samples/ServerlessReloading/SignalRClient/StableConnection.cs b/samples/ServerlessReloading/SignalRClient/StableConnection.cs new file mode 100644 index 00000000..be5c8854 --- /dev/null +++ b/samples/ServerlessReloading/SignalRClient/StableConnection.cs @@ -0,0 +1,158 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR.Client; +using Newtonsoft.Json; + +namespace SignalRClient +{ + internal class BufferConnection + { + internal HubConnection conn; + internal Queue buffer; // Used to store message received from new connection before old connection ending + internal bool active; // Is this connection has already been active or not. + internal string addr; // Connected service's url + internal int finish; // A flag represents finish state, 2 means both two sides have received barrier msg + + internal BufferConnection(string url) + { + addr = url; + conn = new HubConnectionBuilder().WithUrl(url).Build(); + buffer = new Queue(); + active = true; + finish = 0; + } + + internal BufferConnection(string url, Action opt) + { + addr = url; + conn = new HubConnectionBuilder().WithUrl(url, opt).WithAutomaticReconnect().Build(); + buffer = new Queue(); + active = false; + finish = 0; + } + + } + internal class StableConnection + { + private BufferConnection curconn; + private Queue conns; // Backup connections, there may be consecutive reloading event + private Channel chan; + private Thread t; + private const string DefaultHubEndpoint = "http://localhost:5000/ManagementSampleHub"; + private const string Target = "Target"; + private const string DefaultUser = "User"; + + internal StableConnection(string hubEndpoint = DefaultHubEndpoint, string userId = DefaultUser) + { + var url = hubEndpoint.TrimEnd('/') + $"?user={userId}"; + + curconn = new BufferConnection(url); + + Bind(curconn); + + conns = new Queue(); + chan = Channel.CreateUnbounded(); + + t = new Thread(() => readFromChannel(DefaultUser)); + t.Start(); + } + + private async void readFromChannel(string userId) + { + // print received messages if any + while (await chan.Reader.WaitToReadAsync()) + while (chan.Reader.TryRead(out string item)) + Console.WriteLine($"{userId}: gets message from {curconn.addr}: '{item}'"); + } + + private void Bind(BufferConnection bc) + { + bc.conn.On(Target, async (string message) => + { + Message msg = JsonConvert.DeserializeObject(message); + + // 0 : Messages + // 1 : ReloadMessage + // 2 : FinMessage + // 3 : AckMessage + if (msg.msgType == "0" && bc.active) + { + if (bc.active) chan.Writer.TryWrite(msg.content); + else + { + // If not active yet, put message to the buffer + bc.buffer.Enqueue(msg.content); + } + } + else if (msg.msgType == "1") + { + ReloadMessage rmsg = JsonConvert.DeserializeObject(msg.content); + Console.WriteLine("My url is" + rmsg.url); + BufferConnection backup_conn = new BufferConnection(rmsg.url, options => + { + options.AccessTokenProvider = () => Task.FromResult(rmsg.token); + }); + conns.Enqueue(backup_conn); + + Bind(backup_conn); + + await backup_conn.conn.StartAsync(); + // Send barrier msg with endConnID to both old and new conns + await bc.conn.SendAsync("Barrier", bc.conn.ConnectionId, backup_conn.conn.ConnectionId); + await backup_conn.conn.SendAsync("Barrier", bc.conn.ConnectionId, backup_conn.conn.ConnectionId); + } else if (msg.msgType == "2") + { + bc.active = false; + FinMessage fmsg = JsonConvert.DeserializeObject(msg.content); + + if (fmsg.from == bc.conn.ConnectionId) + { + bc.finish++; + } + } else if (msg.msgType == "3") + { + Console.WriteLine(bc.conn.ConnectionId + " Received message 3"); + AckMessage amsg = JsonConvert.DeserializeObject(msg.content); + if (amsg.connID == bc.conn.ConnectionId) + { + bc.finish++; + } + } + + // If both client and old service have received each other's barrier message + if (bc.finish == 2) + { + if (conns.Count == 0) return; + curconn = conns.Dequeue(); + // Write new connection's buffering messages to the channel + while (curconn.buffer.Count>0) chan.Writer.TryWrite(curconn.buffer.Dequeue()); + curconn.active = true; + await bc.conn.StopAsync(); + + } + }); + + bc.conn.Closed += ex => + { + Console.WriteLine(ex); + return Task.CompletedTask; + }; + } + + internal async Task StartAsync() + { + await curconn.conn.StartAsync(); + } + + internal async Task StopAsync() + { + await curconn.conn.StopAsync(); + } + } +} \ No newline at end of file diff --git a/samples/ServerlessReloading/SignalRClient/TypeClass.cs b/samples/ServerlessReloading/SignalRClient/TypeClass.cs new file mode 100644 index 00000000..c17df858 --- /dev/null +++ b/samples/ServerlessReloading/SignalRClient/TypeClass.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +namespace SignalRClient +{ + public class Message + { + public string msgType { get; set; } + // 0 : just messages + // 1 : ReloadMessage + // 2 : FinMessage + // 3 : AckMessage + + public string content { get; set; } + } + + // Used to notify client that you need to reload connection with new url and token + public class ReloadMessage + { + public string url { get; set; } + public string token { get; set; } + } + + // Used as Barrier Message + public class FinMessage + { + public string from { get; set; } + public string to { get; set; } + } + + // When old service has received the Barrier message sent by the client, it will send this AckMessage to the client + public class AckMessage + { + // This connection is over. + public string connID { get; set; } + } +} From cff29d17749bf79206684d2b851a9bddded1baf2 Mon Sep 17 00:00:00 2001 From: Tao Li Date: Mon, 17 Aug 2020 11:33:22 +0800 Subject: [PATCH 2/2] Adding an experimental feature to reload client when runtime instance graceful shutdown.(Serverless mode client sample) --- samples/ServerlessReloading/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/ServerlessReloading/README.md b/samples/ServerlessReloading/README.md index f548a193..2070ea0c 100644 --- a/samples/ServerlessReloading/README.md +++ b/samples/ServerlessReloading/README.md @@ -1,9 +1,9 @@ -Azure SignalR Service Management SDK Sample +Azure SignalR Service Connection Reloading Sample(Service Graceful Shutdown). ================================= -This sample shows the use of Azure SignalR Service Management SDK. +This sample shows the use of Azure SignalR Service Management SDK to simulate service graceful shutdown and client connection reloading under serverless mode. -* Message Publisher: shows how to publish messages to SignalR clients or graceful reloading to another service platform using Management SDK. +* Message Publisher: shows how to publish messages to SignalR clients or to simulate service's graceful shutdown with REST api using Management SDK. * Negotiation Server: shows how to negotiate client from you app server to Azure SignalR Service using Management SDK. * SignalR Client: is a tool to start multiple SignalR clients(supporting reloading feature) and these clients listen messages for this sample. @@ -42,7 +42,7 @@ dotnet run > -c|--connectionstring: Set connection string. > -t|--transport: Set service transport type. Options: |. Default value: transient. Transient: calls REST API for each message. Persistent: Establish a WebSockets connection and send all messages in the connection. -Once the message publisher get started, use the command to send message +Once the message publisher get started, use the command to send message. If you want to switch all connections from one service instance to another, just enter 'reload' ``` send user