Signal R

Posted by Kerwen Blog on January 26, 2022

ASP.NET Core SignalR 是一个开放源代码库,可用于简化向应用添加实时 Web 功能。 实时 Web 功能使服务器端代码能够将内容推送到客户端。
SignalR 支持以下用于处理实时通信的技术(按正常回退的顺序):

WebSockets
Server-Sent Events
长轮询
SignalR 自动选择服务器和客户端能力范围内的最佳传输方法。

Server

  1. 创建一个新的ASP.NET Core Web Api工程, 工程名:SignalRServer, .net 版本选5.0
    .Net 6.0 拿掉了Startup,启动项的写法有所不同,这个稍后再介绍。
  2. Nuget添加SignalR引用
    img

  3. 添加新的文件夹Service, 在Services下添加新的接口文件ISignalRHub.cs
    在此接口文件里定义我们想在SignalR Client端要接收的消息

    1
    2
    3
    4
    5
    6
    7
    
     namespace SignalRServer.Services
     {
         public interface ISignalRHub
         {
             Task ReceiveMessage(string data);
         }
     }
    
  4. 在Services下添加新的类SignalRHub.cs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
     using Microsoft.AspNetCore.SignalR;
     namespace SignalRServer.Services
     {
         public class SignalRHub : Hub<ISignalRHub>
         {
             public void GenerateData()
             {
                 var stopwatch = new Stopwatch();
                 stopwatch.Start();
    
                 Clients.Caller.ReceiveMessage("Start to receive data from SignalR Server.");
                 int progressPercentage = 0;
                 var random = new Random();
                 for (int i = 10; i > 0; i--)
                 {
                     int waitTimeMilliseconds = random.Next(100, 2500);
                     Thread.Sleep(waitTimeMilliseconds);
                     progressPercentage = progressPercentage + 10;
                     Clients.Caller.ReceiveMessage(progressPercentage.ToString());
                 }
                 stopwatch.Stop();
                 Clients.Caller.ReceiveMessage("End to receive data from SignalR Server.");
             }
         }
     }
    
  5. 修改Startup.cs,添加SignalR的引用,并配置CORS

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    
     public void ConfigureServices(IServiceCollection services)
     {
         services.AddCors(options =>
         {
             options.AddPolicy("CorsPolicy", builder => builder
             .WithOrigins("http://localhost:4200") // the Angular app url
             .AllowAnyMethod()
             .AllowAnyHeader()
             .AllowCredentials());
         });
         services.AddControllers();
         services.AddSwaggerGen(c =>
         {
             c.SwaggerDoc("v1", new OpenApiInfo { Title = "SignalRServer", Version = "v1" });
         });
            
         services.AddSignalR();
     }
    
     // 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.UseSwagger();
             app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "SignalRServer v1"));
         }
    
         app.UseHttpsRedirection();
    
         app.UseRouting();
         app.UseCors("CorsPolicy");
    
         app.UseAuthorization();
    
         app.UseEndpoints(endpoints =>
         {
             endpoints.MapControllers();
             endpoints.MapHub<SignalRHub>("/signalrdemo");  
         });
     }
    

Client

  1. 创建新的Angular工程ng new SignalRClient
  2. 添加SignalR安装包 npm install @microsoft/signalr
  3. 添加新的Service ng g service services/SignalR
  4. 在SignalR service里添加引用

    1
    2
    
     import * as signalR from '@microsoft/signalr';
     import { ReplaySubject } from 'rxjs';
    
  5. 添加SignalR创建,关闭和Invoke方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    
     export class SignalRService {
    
         readonly APIUrl = 'https://localhost:44349/signalrdemo';
         connection: signalR.HubConnection | undefined;
         receiveMessage: ReplaySubject<string> | undefined;
    
         constructor() {
             this.receiveMessage = new ReplaySubject<string>();
         }
    
         public initiateSignalrConnection(): Promise<void> {
             console.log("Begin to initialize Signalr");
             return new Promise((resolve, reject) => {
             this.connection = new signalR.HubConnectionBuilder()
                 .withUrl(this.APIUrl)
                 .build();
    
             this.SetSignalrClientMethods();
             this.connection.start()
                 .then(() => {
                 console.log(`SignalR connection success! connectionId: ${this.connection?.connectionId}`);
                 resolve();
                 })
                 .catch((error) => {
                 console.log(`Signalr connection error: ${error}`);
                 reject();
                 });
             });
         }
    
         private SetSignalrClientMethods(): void {
             this.connection?.on('ReceiveMessage', (data: string) => {
             this.receiveMessage?.next(data);
             });
         }
    
         public InvokeGenerateData(): Promise<void> | undefined {
             console.log("Begin to invoke server method");
             if (!this.IsSignalRConnected()) {
             return Promise.reject("The signalr connection is disconnected.");
             }
             return this.connection?.invoke('GenerateData');
         }
    
         public CloseSignalrConnection(): void {
             console.log("Begin to close Signalr");
             if (this.connection) {
             this.connection.stop();
             this.connection = undefined;
             }
             this.receiveMessage = undefined;
         }
     }
    
  6. 修改app.component.html,添加三个button

    1
    2
    3
    4
    5
    6
    
     <div>
         <button (click)="InitializeConnection()">Initialize</button>
         <button (click)="ReceiveMessage()">Receive</button>
         <button (click)="StopConnection()">Stop</button>
     </div>
     <p></p>
    
  7. app.component.ts里添加调用函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    
     export class AppComponent {
         title = 'SignalRClient';
         Message: string | undefined;
         constructor(public signalrService: SignalRService) { }
         ngOnInit(): void {
             this.signalrService.receiveMessage?.subscribe((data: string) => {
             console.log(data);
             this.Message += data;
             });
         }
    
         InitializeConnection(): void {
             this.signalrService.initiateSignalrConnection().then(() => {
             this.Message = "Initialzie SignalR succeed.";
             })
             .catch((error) => {
                 this.Message = `Signalr connection error: ${error}`;
             });
         }
    
         ReceiveMessage(): void {
             this.signalrService.InvokeGenerateData()?.then(() => {
             console.log("Invoke AutoDiscovery complete!!!");
             })
             .catch((error) => {
                 console.log(`Invoke AutoDiscovery failed!!!! ${error}`);
             });
         }
    
         StopConnection(): void {
             this.signalrService.CloseSignalrConnection();
         }
     }
    

.net core 6.0 Server

.net core 6.0拿掉了Startup.cs,在Program.cs中配置SignalR

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    using SignalRServer.Services;
    var builder = WebApplication.CreateBuilder(args);

    builder.Services.AddCors(p => p.AddPolicy("corsapp", builder =>
    {
        builder.WithOrigins("http://localhost:4200").AllowAnyMethod().AllowAnyHeader().AllowCredentials();
    }));

    builder.Services.AddControllers();        
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen();
    builder.Services.AddSignalR();


    var app = builder.Build();

    if (app.Environment.IsDevelopment())
    {
        app.UseSwagger();
        app.UseSwaggerUI();
    }
    app.UseCors("corsapp");
    app.UseHttpsRedirection();

    app.UseAuthorization();

    app.MapControllers();
    app.MapHub<SignalRHub>("/signalrdemo");

    app.Run();

指定传输协议

由于SignalR支持多种传输协议,并且是自动选择传输方式,我们可以在Server端打印出当前的连接方式

1
2
3
4
5
6
    public void GenerateData()
    {
        var transportType = Context.Features.Get<IHttpTransportFeature>().TransportType;
        Console.WriteLine(transportType);
        ...
    }

出于某些考虑,我们可能需要强制指定传输方式
在Client端指定:

1
2
3
4
5
    this.connection = new signalR.HubConnectionBuilder()
        .withUrl(this.APIUrl, {
            transport: signalR.HttpTransportType.WebSockets
        })
        .build();

也可以在Server端指定

1
2
3
4
5
6
7
8
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
        endpoints.MapHub<SignalRHub>("/signalrdemo", configureOptions =>
        {
            configureOptions.Transports = HttpTransportType.ServerSentEvents;
        });  
    });

Enalbe WebSocket on IIS

IIS从8.0版本开始支持web socket,需要手动在控制面板里enable
命令行:

1
%SystemRoot%\system32\dism.exe /online /enable-feature /featurename:IIS-WebSockets

Reference

ASP.NET Core SignalR 简介
.NET Core with SignalR and Angular – Real-Time Charts
Real-time Angular 11 Application With SignalR And .NET 5
Real-time Angular Application With SignalR And ASP .NET Core
SignalR开篇以及如何指定传输协议
ASP.NET Core SignalR 的安全注意事项
ASP.NET Core SignalR 配置
WebSocket with SSL
Websocket support is not enabled by default on IIS