需求
项目中需要服务端主动向客户端发送通知消息
后端是.net6的webapi项目,前端是vue3全家桶项目,前后端分离
这里使用signalR来实现,官网:ASP.NET Core SignalR 概述 | Microsoft Learn
概念
signalr会自动选择最合适的连接方法,所以使用signalr是优于websocket的。
中心Hub:Hub 是一种高级管道,允许客户端和服务器相互调用方法。服务端需要创建Hub来发送消息,客户端需要创建Hub来接受消息。
服务端
首先在配置文件program.cs中添加如下代码
//signalr
builder.Services.AddSignalR().AddJsonProtocol(options =>
{
//加配置可以传给客户端对象,否则只能传字符串
options.PayloadSerializerOptions.PropertyNamingPolicy = null;
});
//signalr路由
app.MapHub<MessageHub>("/messageHub");
配置signalr路由之后,前端才能根据路由地址来访问服务端进行实时通信
然后创建一个中心类和消息类
using Microsoft.AspNetCore.SignalR;
namespace cunzhi.net.Controllers.Hubs
{
/// <summary>
/// signalR中心(发送通知给客户端)
/// </summary>
public class MessageHub : Hub
{
}
public class alertMessage
{
public int Id { get; set; }
public string Name { get; set; }
}
}
最后在Controller层中注入中心,并调用发送消息到客户端的方法(服务层当然也可以注入)
(这里我们编写一个test接口,通过随意方式触发这个接口,就可以模拟一次服务端向客户端发送信息)
[Route("api/factory/[controller]")]
[ApiController]
[Authorize]
public class TestController: ControllerBase
{
private readonly IHubContext<MessageHub> _hubContext;
/// <summary>
///
/// </summary>
/// <param name="service"></param>
public TestController(IHubContext<MessageHub> hubContext)
{
_hubContext = hubContext;
}
/// <summary>
/// 测试服务端发送消息给客户端
/// </summary>
/// <returns></returns>
[HttpGet("test")]
public async Task TestAsync(){
var message=new AlertMessage{
Id=0,
Name="测试发送消息"
};
await _hubContext.Clients.All.SendAsync("sendAlert", message);
}
}
客户端
这里创建一个useMessageHub.js文件来存放相关业务逻辑
前后端通过同一个方法名称“sendAlert”以及参数来匹配
import * as signalR from "@microsoft/signalr"
//如果需要身份验证 .withUrl("/messageHub", {accessTokenFactory: () => sessionStorage.getItem("token")})
const connection = new signalR.HubConnectionBuilder()
.withUrl("/messageHub")//跨域需要使用绝对地址
.configureLogging(signalR.LogLevel.Information)
.build();
async function start() {
try {
await connection.start();
console.log("SignalR Connected.");
} catch (err) {
console.log(err);
setTimeout(start, 10000);//错误重连
}
}
connection.onclose(async error => {
console.log('error', error)
//断线重连 error是空的话则是手动断开,不需要重连
if (!!error) await start()
})
//开始signalr连接
const connect = async () => {
await start()
}
//调用服务端方法
async function send(methodName, param) {
try {
await connection.invoke(methodName, param);
} catch (err) {
console.error(err);
}
}
//获取服务端长链接的推送信息
connection.on('sendAlert', (message) => {
console.log(message)
})
//断开连接
const disconnect = async ()=>{
await connection.stop()
}
export {
connection,
connect,
send,
disconnect,
}
我们在某个页面上调用一下start()方法,然后主动触发一下test接口,就可以打印服务端发送的消息了。
不过笔者这里因为要在on方法中使用vuex(useStore),所以将useMessageHub的代码全部放在了首页(Home.vue),因为单独的js无法像组件一样使用useStore和useRouter,不知道有没有好的解决办法
注意在vue.config.js里面配置好代理地址,例如我这里加上以下代理即可
'/messageHub':{
target: 'http://localhost:5548',
ws: true, //代理websockets
changeOrigin: true, // 虚拟的站点需要更管origin
}
否则需要使用其他跨域的处理方法,微软官方文档也十分详细,这里不再展开。
nginx配置websocket需要额外加配置
#启用http长连接支持websocket
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
向指定客户端发送信息
signalr自带用户和组的概念,而且每个连接都有一个唯一的connectionId
这里先新建一个类用来存储每个连接和用户信息
/// <summary>
/// 连接到中心的用户
/// </summary>
public class HubUser
{
/// <summary>
/// 连接ID
/// </summary>
[Key]
public string ConnectionID { get; set; } = String.Empty;
/// <summary>
/// 用户id
/// </summary>
public int UserId { get; set; }
/// <summary>
/// 姓名
/// </summary>
public string name { get; set; }
}
然后新建一个类用来存储所有连接信息
/// <summary>
/// 存储Hub的状态
/// </summary>
public static class HubState
{
/// <summary>
/// 创建用户集合,用于存储所有链接的用户数据
/// </summary>
public static List<HubUser> Users = new List<HubUser>();
}
重写hub的连接和断开连接方法,分别加上添加用户到HubState和删除HubState里面用户的业务逻辑
/// <summary>
/// signalR中心(发送通知给客户端)
/// </summary>
public class MessageHub : Hub
{
/// <summary>
/// 重写连接事件 添加连接用户
/// </summary>
/// <returns></returns>
public override Task OnConnectedAsync()
{
//这里可以使用Context.User来获取连接的用户信息
//...
var user = HubState.Users.SingleOrDefault(x => x.ConnectionID == Context?.ConnectionId);
if (user == null)
{
var newUser = new HubUser
{
ConnectionID = Context?.ConnectionId ?? string.Empty,
UserId = 0,//根据实际业务逻辑赋值
WorkerName = "",//根据实际业务逻辑赋值
};
HubState.Users.Add(newUser);
}
return base.OnConnectedAsync();
}
/// <summary>
/// 重写断开事件 移除断开用户
/// </summary>
/// <param name="exception"></param>
/// <returns></returns>
public override Task OnDisconnectedAsync(Exception? exception)
{
var user = HubState.Users.SingleOrDefault(x => x.ConnectionID == Context.ConnectionId);
if (user != null)
{
HubState.Users.Remove(user);
}
return base.OnDisconnectedAsync(exception);
}
}
业务层需要发送消息时,先根据业务逻辑找到HubState.Users中需要接受消息的客户端,然后再发送消息即可。
例如可以根据名称找到connectionId,然后给指定的connectionId发送消息
var message = "测试指定消息接收对象";
var receiveUser = HubState.Users.FirstOrDefault(x => x.Name == "张三");
if (receiveUser != null)
{
await _hubContext.Clients.Client(receiveUser.ConnectionID).SendAsync("sendAlert", message);
}