SignalR WebSocket通讯机制

2023-07-10,,

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);
}
}
}

  

  至此,感谢关注!!

SignalR WebSocket通讯机制的相关教程结束。

《SignalR WebSocket通讯机制.doc》

下载本文的Word格式文档,以方便收藏与打印。