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);
- }
- }
- }
至此,感谢关注!!