[實作] C# Windows Service - Step02. 加入SignalR

Client端使用Angular 10 來實作SignalR

Server端則使用Windows Service來實作

目的:

client會發送一組sid通知server

server會針對有該組sid的connid做持續廣播

 

 

 

Server端

1. 

透過nuget安裝

Microsoft.AspNet.SignalR.SelfHost

Microsoft.Owin.Cors

 

2. 新增Startup.cs

using Microsoft.AspNet.SignalR;
using Microsoft.Owin.Cors;
using Owin;

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.UseCors(CorsOptions.AllowAll);
        app.Map("/signalr", map =>
        {
            map.UseCors(CorsOptions.AllowAll);
            var hubConfiguration = new HubConfiguration()
            {
                EnableDetailedErrors = true,
                EnableJSONP = true
            };

            map.RunSignalR(hubConfiguration);
        });
    }
}

 

3. 新增BaseHub.cs

using Microsoft.AspNet.SignalR;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace WindowsServiceSample.Signalr
{
    public class BaseHub : Hub
    {
        /// <summary>
        /// 紀錄目前已連結的 Client 資料[key = ConnID, valie = sID]
        /// </summary>
        public static Dictionary<string, string> CurrClients = new Dictionary<string, string>();

        /// <summary>
        /// 複寫OnConnected
        /// </summary>
        /// <returns></returns>
        public override Task OnConnected()
        {            
            return base.OnConnected();
        }

        /// <summary>
        /// 複寫OnReconnected
        /// </summary>
        /// <returns></returns>
        public override Task OnReconnected()
        {            
            return base.OnReconnected();
        }

        /// <summary>
        /// 斷線
        /// </summary>
        /// <returns></returns>
        public override Task OnDisconnected(bool stop)
        {
            Remove(Context.ConnectionId);
            return base.OnDisconnected(stop);
        }


        /// <summary>
        /// 斷線時移除該使用者
        /// </summary>
        /// <param name="connectionId"></param>
        private void Remove(string connectionId)
        {
            lock (CurrClients)
            {
                foreach (var item in CurrClients.ToArray())
                {
                    if (item.Key == connectionId)
                    {
                        CurrClients.Remove(item.Key);
                    }
                }
            }
        }
    }
}

 

4. 新增BroadcastHub.cs

using Microsoft.AspNet.SignalR.Hubs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace WindowsServiceSample.Signalr
{
    [HubName("broadcastHub")]
    public class BroadcastHub : BaseHub
    {
        public class Para
        {
            public string sID { set; get; }
        }

        public void Register(Para para)
        {
            CurrClients.Add(Context.ConnectionId, para.sID);
        }
    }
}

 

5. 修改Service1.cs

/// <summary>
/// 服務啟動
/// </summary>
protected override void OnStart(string[] args)
{
    //初始化SignalR
    InitializeSelfHosting();
}

/// <summary>
/// 初始化SignalR
/// </summary>
public void InitializeSelfHosting()
{
    const string url = "http://localhost:8080";
    WebApp.Start(url);

    _Timer = new Timer(1000) { AutoReset = true };
    _Timer.Elapsed += Timer_Elapsed;
    _Timer.Start();
}

private static void Timer_Elapsed(object sender, ElapsedEventArgs e)
{
    var signalR = GlobalHost.ConnectionManager.GetHubContext<Signalr.BroadcastHub>();

    //發送系統時間給client
    signalR.Clients.All.systemTime(DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"));

    foreach (var item in Signalr.BroadcastHub.CurrClients)
    {
        //發送訊息給特定sID的client
        switch (item.Value)
        {
            case "1001":
                signalR.Clients.Client(item.Key).broadcast("sID = 1001, Good!..." + new Random().Next(1, 10));
                break;
            case "1002":
                signalR.Clients.Client(item.Key).broadcast("sID = 1002, Good!..." + new Random().Next(11, 20));
                break;
            default:
                signalR.Clients.Client(item.Key).broadcast("sID = null, Error!..." + new Random().Next(-10, 0));
                break;
        }
    }
}

 

 

Client端 (當Server端是採用Microsoft.AspNet.SignalR)

1. 安裝 ng2-signalr

npm install ng2-signalr jquery signalr --save

 

2. 新增signal-r-test component

ng g c /page/signal-r-test

 

3. 新增app.resolve.ts

import { Resolve } from '@angular/router';
import { SignalR, ISignalRConnection  } from 'ng2-signalr';
import { Injectable } from '@angular/core';

@Injectable()
export class SignalRResolver implements Resolve<ISignalRConnection > {

  constructor(public signalR: SignalR) { }

  resolve() {
    return this.signalR.connect();
  }
}

 

4. 修改app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { SignalRTestComponent } from './page/signal-r-test/signal-r-test.component';

import { environment } from 'src/environments/environment';
import { SignalRResolver } from './app.resolve';
import { SignalRModule } from 'ng2-signalr';
import { SignalRConfiguration } from 'ng2-signalr';

export function createConfig(): SignalRConfiguration {
  const c = new SignalRConfiguration();
  c.hubName = 'broadcastHub';
  c.qs = {};
  c.url = environment.APIWebSite; // 'http://localhost:8080/'
  c.logging = false;

  // >= v5.0.0
  c.executeEventsInZone = true; // optional, default is true
  c.executeErrorsInZone = false; // optional, default is false
  c.executeStatusChangeInZone = true; // optional, default is true
  return c;
}

@NgModule({
  declarations: [
    AppComponent,
    SignalRTestComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    SignalRModule.forRoot(createConfig),
  ],
  providers: [SignalRResolver],
  bootstrap: [AppComponent]
})
export class AppModule { }

 

5. 修改app-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { SignalRResolver } from './app.resolve';
import { SignalRTestComponent } from './page/signal-r-test/signal-r-test.component';

const routes: Routes = [
  { path: 'SignalR/:sid', component: SignalRTestComponent, resolve: { connection: SignalRResolver } },
];

@NgModule({
  imports: [RouterModule.forRoot(routes, { useHash: true, scrollPositionRestoration: 'enabled' })],
  exports: [RouterModule]
})
export class AppRoutingModule { }

 

6. 修改signal-r-test.component.ts

import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { timer } from 'rxjs';

import { BroadcastEventListener, SignalRConnection, ConnectionStatus } from 'ng2-signalr';

@Component({
  selector: 'app-signal-r-test',
  templateUrl: './signal-r-test.component.html',
  styleUrls: ['./signal-r-test.component.css']
})
export class SignalRTestComponent implements OnInit {
  connection: SignalRConnection;
  sID: string;
  systemTime: string;
  broadcast: string;

  constructor(private activatedRoute: ActivatedRoute) {

    this.connection = activatedRoute.snapshot.data['connection'];
  }

  ngOnInit(): void {
    this.activatedRoute.paramMap.subscribe((params) => {
      this.sID = params.get('sid').toString();
      this.StartSignalR();
    });
  }

  StartSignalR(): void {
    // Socket斷線5秒後自動重連
    this.connection.errors.subscribe(err => {
      console.log('Error while establishing connection... Retrying...');
      timer(5000).subscribe(() => {
        this.connection.start();
        this.connection.status.subscribe(x => {
          if (x.value === 1) {
            console.log('Reconnect...Work!');
            this.ConnectiSignalR();
          }
        });
      });
    });

    this.ConnectiSignalR();
  }

  // 連接SingalR
  ConnectiSignalR(): void {
    // 1.create a listener object
    const onSystemTime = new BroadcastEventListener<any>('systemTime');
    const onBroadcast = new BroadcastEventListener<any>('broadcast');

    // 2.register the listener
    this.connection.listen(onSystemTime);
    this.connection.listen(onBroadcast);

    // 3.subscribe for incoming messages

    onSystemTime.subscribe((res: any) => {
      this.systemTime = res;
    });

    onBroadcast.subscribe((res: any) => {
      this.broadcast = res;
    });

    const data = {
      sID: this.sID,
    };
    this.connection.invoke('Register', data).catch(err => {
      console.log('Error:' + err);
    });
  }

}

 

7. 修改signal-r-test.component.html

<p>signal-r-test works!</p>
<span>SystemTime:{{systemTime}}</span>
<br>
<span>Broadcast:{{broadcast}}</span>

 

8. 執行結果

 

 

 

如果Server為.net core版本的SignalR
參考此篇

 

 

參考


  1. SignalR 2 Server with Console Application ver
  2. [C#]ConsoleApplication 環境下使用 SignalR
  3. 教學課程:SignalR 自我裝載
  4. ASP.NET SignalR Hubs API Guide - Server (C#)
  5. .NET Core with SignalR and Angular – Real-Time Charts