1、什麼是SignalR
ASP.NET SignalR 是一個面向 ASP.NET 開發人員的庫,可簡化嚮應用程式新增實時 Web 功能的過程。 實時 Web 功能是讓伺服器程式碼在可用時立即將內容推播到連線的使用者端,而不是讓伺服器等待使用者端請求新資料。
SignalR使用的三種底層傳輸技術分別是Web Socket, Server Sent Events 和 Long Polling, 它讓你更好的關注業務問題而不是底層傳輸技術問題。
WebSocket是最好的最有效的傳輸方式, 如果瀏覽器或Web伺服器不支援它的話(IE10之前不支援Web Socket), 就會降級使用SSE, 實在不行就用Long Polling。
(現在也很難找到不支援WebSocket的瀏覽器了,所以我們一般定義必須使用WebSocket)
2、我們做一個聊天室,實現一下SignalR前後端通訊
由簡入深,先簡單實現一下
2.1 伺服器端Net5
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; using System; using System.Threading.Tasks; namespace ServerSignalR.Models { public class ChatRoomHub:Hub { public override Task OnConnectedAsync()//連線成功觸發 { return base.OnConnectedAsync(); } public Task SendPublicMsg(string fromUserName,string msg)//給所有client傳送訊息 { string connId = this.Context.ConnectionId; string str = $"[{DateTime.Now}]{connId}\r\n{fromUserName}:{msg}"; return this.Clients.All.SendAsync("ReceivePublicMsg",str);//傳送給ReceivePublicMsg方法,這個方法由SignalR機制自動建立 } } }
Startup新增
static string _myAllowSpecificOrigins = "MyAllowSpecificOrigins"; public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "ServerSignalR", Version = "v1" }); }); services.AddSignalR(); services.AddCors(options => { options.AddPolicy(_myAllowSpecificOrigins, policy => { policy.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.UseSwagger(); app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "ServerSignalR v1")); } app.UseCors(_myAllowSpecificOrigins); app.UseHttpsRedirection(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); endpoints.MapHub<ChatRoomHub>("/Hubs/ChatRoomHub"); }); }
2.2 前端Angular
引入包
npm i --save @microsoft/signalr
ts:
import { Component, OnInit } from '@angular/core'; import * as signalR from '@microsoft/signalr'; import { CookieService } from 'ngx-cookie-service'; @Component({ selector: 'app-home', templateUrl: './home.component.html', styleUrls: ['./home.component.scss'] }) export class HomeComponent implements OnInit { msg = ''; userName='kxy' public messages: string[] = []; public hubConnection: signalR.HubConnection; constructor( private cookie: CookieService ) {this.hubConnection=new signalR.HubConnectionBuilder() .withUrl('https://localhost:44313/Hubs/ChatRoomHub', { skipNegotiation:true,//跳過三個協定協商 transport:signalR.HttpTransportType.WebSockets,//定義使用WebSocket協定通訊 } ) .withAutomaticReconnect() .build(); this.hubConnection.on('ReceivePublicMsg',msg=>{ this.messages.push(msg); console.log(msg); }); } ngOnInit(): void { } JoinChatRoom(){ this.hubConnection.start() .catch(res=>{ this.messages.push('連線失敗'); throw res; }).then(x=>{ this.messages.push('連線成功'); }); } SendMsg(){ if(!this.msg){ return; } this.hubConnection.invoke('SendPublicMsg', this.userName,this.msg); } }
這樣就簡單實現了SignalR通訊!!!
有一點值得記錄一下
問題:強制啟用WebSocket協定,有時候發生錯誤會被遮蔽,只是提示找不到/連線不成功
解決:可以先不跳過協商,偵錯完成後再跳過
3、引入Jwt進行許可權驗證
安裝Nuget包:Microsoft.AspNetCore.Authentication.JwtBearer
Net5的,注意包版本選擇5.x,有對應關係
Startup定義如下
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.OpenApi.Models; using ServerSignalR.Models; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Text; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using JwtHelperCore; namespace ServerSignalR { 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. static string _myAllowSpecificOrigins = "MyAllowSpecificOrigins"; public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "ServerSignalR", Version = "v1" }); }); services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.RequireHttpsMetadata = false;//是否需要https options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = false,//是否驗證Issuer ValidateAudience = false,//是否驗證Audience ValidateLifetime = true,//是否驗證失效時間 ValidateIssuerSigningKey = true,//是否驗證SecurityKey IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("VertivSecurityKey001")),//拿到SecurityKey }; options.Events = new JwtBearerEvents()//從url獲取token { OnMessageReceived = context => { if (context.HttpContext.Request.Path.StartsWithSegments("/Hubs/ChatRoomHub"))//判斷存取路徑 { var accessToken = context.Request.Query["access_token"];//從請求路徑獲取token if (!string.IsNullOrEmpty(accessToken)) context.Token = accessToken;//將token寫入上下文給Jwt中介軟體驗證 } return Task.CompletedTask; } }; } ); services.AddSignalR(); services.AddCors(options => { options.AddPolicy(_myAllowSpecificOrigins, policy => { policy.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.UseSwagger(); app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "ServerSignalR v1")); } app.UseCors(_myAllowSpecificOrigins); app.UseHttpsRedirection(); app.UseRouting(); //Token 授權、認證 app.UseErrorHandling();//自定義的處理錯誤資訊中介軟體 app.UseAuthentication();//判斷是否登入成功 app.UseAuthorization();//判斷是否有存取目標資源的許可權 app.UseEndpoints(endpoints => { endpoints.MapHub<ChatRoomHub>("/Hubs/ChatRoomHub"); endpoints.MapControllers(); }); } } }
紅色部分為主要關注程式碼!!!
因為WebSocket無法自定義header,token資訊只能通過url傳輸,由後端獲取並寫入到上下文
認證特性使用方式和http請求一致:
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; using System; using System.Linq; using System.Threading.Tasks; namespace ServerSignalR.Models { [Authorize]//jwt認證 public class ChatRoomHub:Hub { public override Task OnConnectedAsync()//連線成功觸發 { return base.OnConnectedAsync(); } public Task SendPublicMsg(string msg)//給所有client傳送訊息 { var roles = this.Context.User.Claims.Where(x => x.Type.Contains("identity/claims/role")).Select(x => x.Value).ToList();//獲取角色 var fromUserName = this.Context.User.Identity.Name;//從token獲取登入人,而不是傳入(前端ts方法的傳入引數也需要去掉) string connId = this.Context.ConnectionId; string str = $"[{DateTime.Now}]{connId}\r\n{fromUserName}:{msg}"; return this.Clients.All.SendAsync("ReceivePublicMsg",str);//傳送給ReceivePublicMsg方法,這個方法由SignalR機制自動建立 } } }
然後ts新增
constructor( private cookie: CookieService ) { var token = this.cookie.get('spm_token'); this.hubConnection=new signalR.HubConnectionBuilder() .withUrl('https://localhost:44313/Hubs/ChatRoomHub', { skipNegotiation:true,//跳過三個協定協商 transport:signalR.HttpTransportType.WebSockets,//定義使用WebSocket協定通訊 accessTokenFactory:()=> token.slice(7,token.length)//會自動新增Bearer頭部,我這裡已經有Bearer了,所以需要截掉 } ) .withAutomaticReconnect() .build(); this.hubConnection.on('ReceivePublicMsg',msg=>{ this.messages.push(msg); console.log(msg); }); }
4、私聊
Hub
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; using System; using System.Collections.Generic; using System.Threading.Tasks; namespace ServerSignalR.Models { [Authorize]//jwt認證 public class ChatRoomHub:Hub { private static List<UserModel> _users = new List<UserModel>(); public override Task OnConnectedAsync()//連線成功觸發 { var userName = this.Context.User.Identity.Name;//從token獲取登入人 _users.Add(new UserModel(userName, this.Context.ConnectionId)); return base.OnConnectedAsync(); } public override Task OnDisconnectedAsync(Exception exception) { var userName = this.Context.User.Identity.Name;//從token獲取登入人 _users.RemoveRange(_users.FindIndex(x => x.UserName == userName), 1); return base.OnDisconnectedAsync(exception); } public Task SendPublicMsg(string msg)//給所有client傳送訊息 { var fromUserName = this.Context.User.Identity.Name; //var ss = this.Context.User!.FindFirst(ClaimTypes.Name)!.Value; string str = $"[{DateTime.Now}]\r\n{fromUserName}:{msg}"; return this.Clients.All.SendAsync("ReceivePublicMsg",str);//傳送給ReceivePublicMsg方法,這個方法由SignalR機制自動建立 } public Task SendPrivateMsg(string destUserName, string msg) { var fromUser = _users.Find(x=>x.UserName== this.Context.User.Identity.Name); var toUser = _users.Find(x=>x.UserName==destUserName); string str = $""; if (toUser == null) { msg = $"使用者{destUserName}不線上"; str = $"[{DateTime.Now}]\r\n系統提示:{msg}"; return this.Clients.Clients(fromUser.WebScoketConnId).SendAsync("ReceivePrivateMsg", str); } str = $"[{DateTime.Now}]\r\n{fromUser.UserName}-{destUserName}:{msg}"; return this.Clients.Clients(fromUser.WebScoketConnId,toUser.WebScoketConnId).SendAsync("ReceivePrivateMsg", str); } } }
TS:
//加一個監聽 this.hubConnection.on('ReceivePublicMsg', msg => { this.messages.push('公屏'+msg); console.log(msg); }); this.hubConnection.on('ReceivePrivateMsg',msg=>{ this.messages.push('私聊'+msg); console.log(msg); }); //加一個傳送 if (this.talkType == 1) this.hubConnection.invoke('SendPublicMsg', this.msg); if (this.talkType == 3){ console.log('11111111111111'); this.hubConnection.invoke('SendPrivateMsg',this.toUserName, this.msg); }
5、在控制器中使用Hub上下文
Hub連結預設30s超時,正常情況下Hub只會進行通訊,而不再Hub裡進行復雜業務運算
如果涉及複雜業務計算後傳送通訊,可以將Hub上下文注入外部控制器,如
namespace ServerSignalR.Controllers { //[Authorize] public class HomeController : Controller { private IHubContext<ChatRoomHub> _hubContext; public HomeController(IHubContext<ChatRoomHub> hubContext) { _hubContext = hubContext; } [HttpGet("Welcome")] public async Task<ResultDataModel<bool>> Welcome() { await _hubContext.Clients.All.SendAsync("ReceivePublicMsg", "歡迎"); return new ResultDataModel<bool>(true); } } }
至此,感謝關注!!