需求

项目中需要服务端主动向客户端发送通知消息

后端是.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);
            }

Logo

前往低代码交流专区

更多推荐