什么是SignalR
ASP.NET Core SignalR 是一个开源库,它简化了向应用程序添加实时 web 功能的功能。 实时 Web 功能使服务器端代码能够即时将内容推送到客户端。
ASP.NET Core SignalR 的一些功能:
- 自动管理连接
- 同时向所有连接的客户端发送消息。 例如,聊天室
- 向特定客户端或客户端组发送消息
- 可缩放以处理不断增加的流量
SignalR 支持以下用于处理实时通信的技术:
- WebSockets
- 服务器发送的事件
- 长轮询
其中Web Socket仅支持比较现代的浏览器, Web服务器也不能太老.
而 Server Sent Events 情况可能好一点, 但是也存在同样的问题.
所以SignalR采用了回落机制, SignalR有能力去协商支持的传输类型.
Web Socket是最好的最有效的传输方式, 如果浏览器或Web服务器不支持它的话, 就会降级使用SSE, 实在不行就用Long Polling.
一旦建立连接, SignalR就会开始发送keep alive消息, 来检查连接是否还正常. 如果有问题, 就会抛出异常.
因为SignalR是抽象于三种传输方式的上层, 所以无论底层采用的哪种方式, SignalR的用法都是一样的.
SignalR - 集线器(Hub)
SignalR中, 我们主要需要做的事情就是继承Hub
类, 来和客户端互相发送消息; 可以察觉出, SignalR服务器是一个消息中心, 客户端将消息发送给SignalR服务器, 然后有我们来处理这些消息, 可以将这些消息广播出去, 也可以将某个客户端发过来的消息转发给另一个客户端, 实现两个客户端之间的通信;
开始使用SignalR - CountHub
这里将完成一个简单的使用SignalR服务端和客户端实时通信的例子:
- 客户端发起连接请求
- 服务器端接受请求, 并向该客户端发出计数, 从0到10
- 当计数到10, 服务端调用客户端Finished方法, 客户端Finished关闭连接
服务端:
建立asp.net core项目, 选择空模板
新建 CountService
类 和 CountHub
类
public class CountService { private int _count; public int GetLastestCount() => _count++; }
public class CountHub : Hub { private readonly CountService _countService; public CountHub(CountService countService, ILoggerFactory loggerFactory) { _countService=countService; } public async Task GetLastestCount() { IClientProxy client = Clients.Caller; int count; do { count = _countService.GetLastestCount(); await Task.Delay(1000); await client.SendAsync("ReceiveUpdate", $"ConnectionId: {Context.ConnectionId}, count: {count}"); } while (count < 10); await client.SendAsync("Finished"); } }
在Startup类中注册Service和配置Hub路由
services.AddScoped<CountService>();
services.AddSignalR();
endpoints.MapHub<CountHub>("/countHub");
Startup
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using SingleRStudy.Hubs; using SingleRStudy.Services; namespace SingleR { public class Startup { // This method gets called by the runtime. Use this method to add services to the container. // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { services.AddScoped<CountService>(); services.AddSignalR(); services.AddCors(options => { options.AddPolicy("NgClientPolicy", p => { p.WithOrigins("http://localhost:4200") .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); }); }); } // 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.UseCors("NgClientPolicy"); app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapHub<CountHub>("/countHub"); }); } } }
由于我这边使用了Angular来作为客户端, 所以在Startup中同时配置了跨域.
客户端(Angular)
通过npm引入signalr: npm i @microsoft/signalr @types/node
导入signalr: import * as signalR from '@microsoft/signalr/'
完整的如下:
ChatComponent
import { Component, OnInit } from '@angular/core'; import * as signalR from '@microsoft/signalr/' /** * 创建连接 */ const connection = new signalR.HubConnectionBuilder() .withUrl('//localhost:5000/countHub') .build(); @Component({ selector: 'app-chat', templateUrl: './chat.component.html', styleUrls: ['./chat.component.css'] }) export class ChatComponent implements OnInit { constructor() { } async ngOnInit() { connection.on('ReceiveUpdate', (message: string) => { console.log(message); }); connection.on('Finished', () => { console.log('count finished'); connection.stop(); }); connection.onclose(error => { console.error('signalR connection closed. error: ', error); }); // 开始通信 await connection.start().catch(error => { console.error(error); }); if(connection.state === signalR.HubConnectionState.Connected) await connection.send('GetLastestCount', 'aaa'); } }
客户端定义了ReceiveUpdate
和 Finished
可以让服务端调用的方法.
ReceiveUpdate
方法, 将方法参数在控制台打印出来;
Finished
方法则用来关闭连接.
运行结果:
开始使用SignalR - ChatRoom
服务端:
建立 ChatHub
类
ChatHub
using Microsoft.AspNetCore.SignalR; namespace SignalRStudy.Hubs { public class ChatHub : Hub { public async void SendMessage(string username, string message) { await Clients.All.SendAsync("ReceiveMessage", username, message); } } }
Startup中要配置一下:
app.UseEndpoints(endpoints => { ... endpoints.MapHub<ChatHub>("/chat"); });
服务端很简单, 就是把收到的消息转发给所有连接着的客户端
客户端(Angular):
ChatService
ChatService
import { Injectable, EventEmitter } from '@angular/core'; import * as signalr from '@microsoft/signalr' import { Observable, of, Subscribable, Subscriber } from 'rxjs'; const connection = new signalr.HubConnectionBuilder() .withUrl('http://localhost:5000/chat') .build(); @Injectable() export class ChatService { receivedMessage$ = new Observable<any>(observer => { connection.on('ReceiveMessage', (username: string, message: string) => { observer.next({ username, message }); }); }); username: string = '匿名用户'; constructor() { // connection.on('ReceiveMessage', this.receiveMessage); } async startChat() { await connection.start(); } async sendMessage(message: string) { // 等待连接或断开连接操作完成 while(connection.state === signalr.HubConnectionState.Connecting || connection.state === signalr.HubConnectionState.Disconnecting); // 如果未连接服务器, 则尝试进行连接 if(connection.state === signalr.HubConnectionState.Disconnected) { await connection.start().catch(err => console.error('signalr failed to connect to server.')); } if(connection.state === signalr.HubConnectionState.Connected) { await connection.send('SendMessage', this.username, message); } } }
ChatService中处理了SignalR交互的逻辑, 组件可以通过订阅ReceivedMessage$
来获取最新的消息...
下面放一下相关组件的代码:
chat.component.ts
import { Component, OnInit, ViewChild, ElementRef } from '@angular/core'; import { ChatService } from '../../services/chat.service'; @Component({ selector: 'app-chat', templateUrl: './chat.component.html', styleUrls: ['./chat.component.css'] }) export class ChatComponent implements OnInit { messageToSend: string = ''; receivedMessages: any[] = []; @ViewChild('messageBox', { static: true }) messageBox:ElementRef; constructor( private chatServ: ChatService ) { } async ngOnInit() { this.chatServ.username = 'laggage'; await this.chatServ.startChat(); this.chatServ.receivedMessage$.subscribe(r => { this.receivedMessages.push(r); // 滚动条滑动到最底部, 等待5ms是为了等待angular渲染完界面, 否则可能无法滚动到底部 setTimeout(() => { let ele = this.messageBox.nativeElement as HTMLDivElement; ele.scrollTop = ele.scrollHeight; }, 5); }); } get username() { return this.chatServ.username; } set username(value: string) { if(value != this.chatServ.username) this.chatServ.username = value; } sendMessage() { this.chatServ.sendMessage(this.messageToSend); this.messageToSend = ''; } }
chat.component.html
<div id="wrapper"> <!-- chat works --> <div id="message-receive-area"> <div id="message-container" #messageBox class="px-3 py-2 overflow-auto"> <div class="message-item jumbotron p-0 px-3 py-2 mb-3" *ngFor="let message of receivedMessages"> <span> {{message.username}}说: </span> <span class="d-block"> {{message.message}} </span> </div> </div> </div> <div id="message-send-area" class="container-fluid mx-0 row"> <div id="write-message" class="col col-8 pl-0"> <textarea name="message" class="h-100 w-100" [(ngModel)]="messageToSend"></textarea> </div> <div class="col col-4 overflow-hidden pr-0"> <div class="mb-3"> <label for=""> 用户名 <input type="text" class="w-100" name="username" placeholder="用户名" [(ngModel)]="username"> </label> </div> <div class="w-100"> <button class="w-100 overflow-hidden" (click)="sendMessage()">Send</button> </div> </div> </div> </div>
chat.component.css
#message-receive-area { height: 60vh; padding: .6rem; } #message-container { border: 1px solid black; height: 100%; } #message-send-area { height: 40vh; padding: .6rem; }
先这样...