实战 Net Core 集成SlgnaIR RabbitMQ 实现实时消息通知

最近想给博客网站添加留言问答,以及订阅功能
留言问答,订阅功能:博客网站当用户留言或者订阅的时候,会实时通知到后台管理系统。
流程图
开发难点:
在开发中遇到的问题
1数据丢失,数据堆积,重复消费
2 线程如何托管
消费者
发送者
3.消息通知如何区分
4.后台通知如何针对不同角色不同通知
这些问题,都会进行解决
准备工作
web api项目, 做接收请求
RabbitMQ服务 做发布订阅消息
web项目          做接收消息,显示信息。
web api准备工作:暴露一个接口做收集消息,用户发布到Mq队列中(比较简单)
RabbitMQ准备工作:
我这里是用的docker发布的RabbitMQ 使用docker 发布真的简单强烈推荐使用docker
(如何安装RabbitMQ自行百度教程,很全面,操作也简单)
web项目准备工作:
主要是要集成到SlgnaIR
第一步,创建一个继承与Hab的类
using Microsoft.AspNetCore.SignalR;
using Newtonsoft.Json;
using System.Diagnostics;
using Utility.Common;
namespace Utility.Message
{
   public class MessageHub : Hub
   {
       public async Task SendMessage(string data, string message, string  groupName)
       {
           //给前端发送消息,通过组的方式发送 ,我把订阅消息分为一组,问答分为一组来区分
           await Clients.Group(groupName).SendMessage("SendMessage", data,  message);
       }
       /// <summary>
       /// 客户端连接的时候调用
       /// </summary>
       /// <returns></returns>
       public override Task OnConnectedAsync()
       {
           try
           {
               //我的业务比较简单,我的消息通知只通知到管理员也就是admin
               if (Context.User.FindFirst("role").Value.Contains("admin"))
               {
                   Groups.AddToGroupAsync(Context.ConnectionId,  BasicParam.SignaIRSubscribeGroup);
                   Groups.AddToGroupAsync(Context.ConnectionId,  BasicParam.SignaIRLogGroup);
               }
           }
           catch (Exception)
           {
           }
           return Task.CompletedTask;
       }
   }
}
在Program.cs 中开启SignIR的服务
前端配置
1,添加SignaIR客户端Js文件
下载
通过客户端下载
页面应用
添加到自己要显示消息的页面
效果
开始上代码
创建 IMessageService接口 并实现它 (我这里只做了订阅的实现
using Model.BaseService.Subscibe;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IBussiness.BaseService.Message
{
    public interface IMessageService
    {
        //发布订阅消息
        bool SendSubscribeMes(SubscribeModel subscribeModel);
        //接收订阅消息
        Task ReceiveSubscribeMes();
    }
}
在SendSubscribeMes订阅消息 以防MQ服务的挂掉等问题导致数据丢失,可以把数据先持久化到磁盘上, 我是直接先保存一份到数据库,如果Rabbit接收到数据,就把数据再删除掉
using BaseDataProvide;
using Bussiness.BaseService.Subscribe;
using Bussiness.Queue;
using IBussiness.BaseService.Message;
using IBussiness.BaseService.Subscribe;
using IBussiness.Queue;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using Model.BaseService.Subscibe;
using Model.Queue;
using RabbitMq.RabbitMQ;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Utility.Common;
using Utility.Message;
namespace Bussiness.BaseService.Message
{
    public class MessageService : IMessageService
    {
        private readonly IMqService _mqService;
        private readonly IQueueService _queueService;
        private readonly IHubContext<MessageHub> _hubContext;
        private readonly ISubscribeService _subscribeService;
        private readonly ILogger _logger;
        private readonly ISqlContext _sqlContext;
        #region 队列参数
        #endregion
        public MessageService(
            IMqService mqService,
            IQueueService queueService,
            IHubContext<MessageHub> hubContext,
            ISubscribeService subscribeService,
            ILoggerFactory loggerFactory,
            ISqlContext sqlContext
            )
        {
            _mqService = mqService;
            _queueService = queueService;
            _hubContext = hubContext;
            _subscribeService = subscribeService;
            _logger = loggerFactory.CreateLogger<MessageService>();
            _sqlContext = sqlContext;
        }
        public Task ReceiveSubscribeMes()
        {
           
            Func<BasicDeliverEventArgs,bool> businessAction = (data) =>
            {  
                var isBool = true;
                try
                {
                    var datas = Encoding.UTF8.GetString(data.Body.Span);
                    using (var conn=_sqlContext.Connection)
                    {
                        //订阅消息存储数据库
                        try
                        {
                            SubscribeModel model =  JsonHelper.ToObject<SubscribeModel>(datas);
                            SubscribeEntity entity = GetSubscribeEntity(model);
                            conn.Ado.BeginTran();
                            isBool = _subscribeService.CreateSubscribeMes(entity,  conn);
                            //确认队列被使用
                             _queueService.ConsumptionQueue(Guid.Parse(data.BasicProperties.MessageId), conn);
                            conn.Ado.CommitTran();
                        }
                        catch (Exception ex)
                        {
                            _logger.LogError(ex, "Receive SubscribeMes:" +  ex.Message);
                            conn.Ado.RollbackTran();
                        }
                    }
                    
                   
                    //redis 记录订阅消息
                    //向客户端发送消息
                     _hubContext.Clients.Groups(BasicParam.SignaIRSubscribeGroup).SendAsync("SendMessage", datas, MessageType.SubscribeMessage);
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Receive SubscribeMes:" + ex.Message);
                }
                return isBool;
            };
            Action<BasicDeliverEventArgs> failureAction = (data) =>
            {
            };
            _mqService.ConsumptionMsg(BasicParam.SubscribeQueueExchangeStr,  businessAction, failureAction, BasicParam.QueueRoutingKey,  BasicParam.SubscribeQueueStr, ExchangeType.Fanout);
            return Task.CompletedTask;
        }
        public bool SendSubscribeMes(SubscribeModel subscribeModel)
        {
            bool isbool = true;
            if (subscribeModel==null)
            {
                return false;
            }
            string exchangeName = BasicParam.SubscribeQueueExchangeStr;
            string queueName= BasicParam.SubscribeQueueStr;
            string routingKey = BasicParam.QueueRoutingKey;
            byte isPersistence = BasicParam.QueuePersistence;
            //记录队列
            try
            {
                var queueEntiy = GetQueueEntity<SubscribeModel>(subscribeModel,  exchangeName, queueName, routingKey, isPersistence == 2 ? true : false);
                _queueService.CreateQueue(queueEntiy);
                Action<BasicAckEventArgs> successMethod = (data) =>
                {
                    _queueService.ConfirmQueueEntry(queueEntiy);
                };
                Action<BasicNackEventArgs> failureMethod = (data) =>
                {
                    _queueService.CreateBadQueue(queueEntiy);
                };
                _mqService.ConfirmSenMsg(
                    QueueID: queueEntiy.QueueID,
                    exchangeName: exchangeName,
                    msgEntity: subscribeModel,
                    successMethod: successMethod,
                    failureMethod: failureMethod,
                    routingkey: routingKey,
                    queueName,
                    exchangeType: ExchangeType.Fanout,
                    deliveryMode: isPersistence
                    );
            }
            catch (Exception)
            {
                return false;
            }
            return isbool;
        }
        #region 帮助方法
        /// <summary>
        /// 获取队列实体
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="data"></param>
        /// <param name="exchangeName"></param>
        /// <param name="queueName"></param>
        /// <param name="routingKey"></param>
        /// <param name="isPersistence"></param>
        /// <returns></returns>
        private QueueEntity GetQueueEntity<T>(T data,string exchangeName,string  queueName,string routingKey,bool isPersistence)
        {
            QueueEntity queueEntity = new QueueEntity();
            queueEntity.QueueID = Guid.NewGuid();
            queueEntity.DataJson=JsonHelper.ToJson(data);
            queueEntity.ExchangeName = exchangeName;
            queueEntity.QueueName = queueName;
            queueEntity.RoutingKey = routingKey;
            queueEntity.IsConsumption = false;
            queueEntity.IsQueue = false;
            queueEntity.IsPersistence = isPersistence;
            queueEntity.CreateBy = "vueApi";
            queueEntity.CreateDate = DateTime.Now;
            return queueEntity;
        }
        private SubscribeEntity GetSubscribeEntity(SubscribeModel model)
        {
            SubscribeEntity subscribeEntity = new SubscribeEntity();
            subscribeEntity.SubscribeUserName = model.SubscribeUserName;
            subscribeEntity.Email= model.Email;
            subscribeEntity.Status = false; //表示还没有成功订阅
            subscribeEntity.IsDeleted = false;
            subscribeEntity.CreateBy = "BryantLogAdminSystem";
            subscribeEntity.CreateDate =DateTime.Now;
            return subscribeEntity;
        }
        #endregion
    }
    public enum MessageType
    {
        SubscribeMessage,
        LogMessage,
    }
}
创建IMqService接口  并实现它  (用来发送消息和消费消息)
using RabbitMQ.Client.Events;
using RabbitMQ.Client;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace RabbitMq.RabbitMQ
{
    public interface IMqService
    {
        // 消费消息
        Task ConsumptionMsg(string exchangeName, Func<BasicDeliverEventArgs,bool>  businessAction, Action<BasicDeliverEventArgs> failureAction, string routingkey =  null, string queueName = null,string exchangeType= ExchangeType.Direct);
        //发送消息
        Task ConfirmSenMsg<TEntity>(Guid QueueID, string exchangeName, TEntity  msgEntity, Action<BasicAckEventArgs> successMethod = null,  Action<BasicNackEventArgs> failureMethod = null, string routingkey = null, string  queueName = null, string exchangeType = ExchangeType.Direct, byte deliveryMode =  2, string msgTimeOut = null);
    }
}
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System.Text;
using System.Threading;
using Utility.Common;
using static System.Net.Mime.MediaTypeNames;
namespace RabbitMq.RabbitMQ
{
    /// <summary>
    ///  RabbitMQ消息队列处理
    /// </summary>
    public class MqService : IMqService
    {
        #region 连接Mq配置
        /// <summary>
        /// RabbitMQ地址
        /// </summary>
        private string HostUrl = ConfigurationHelper.Get("RabbitMQServcerStr");   //ConfigurationManager.AppSettings["RabbitMQHostName"];
        /// <summary>
        /// 账号
        /// </summary>
        private string UserName = ConfigurationHelper.Get("RabbitMQUserStr");    //ConfigurationManager.AppSettings["RabbitMQUserName"];
        /// <summary>
        /// 密码
        /// </summary>
        private string Password = ConfigurationHelper.Get("RabbitMQPassWordStr");     // ConfigurationManager.AppSettings["RabbitMQPassword"];
        /// 连接配置
        private ConnectionFactory connfactory { get; set; } //创建一个工厂连接对象
        private readonly int QueueMaxCount = 50000;
        #endregion
        private readonly ILogger _logger;
        public MqService(ILoggerFactory loggerFactory)
        {
            if (connfactory == null)
            {
                var uri = new Uri(HostUrl);
                connfactory = new ConnectionFactory();
                connfactory.Endpoint = new AmqpTcpEndpoint(uri);
                connfactory.UserName = UserName;
                connfactory.Password = Password;
                connfactory.AutomaticRecoveryEnabled = true;//网络故障自动连接恢复
            }
            _logger = loggerFactory.CreateLogger<MqService>();
        }
        /// <summary>
        /// Confirm 模式 发送消息
        /// </summary>
        /// <param name="exchangeName">交换机名称</param>
        /// <param name="queueName">队列名称</param>
        /// <param name="routingkey">路由名称</param>
        public Task ConfirmSenMsg<TEntity>(Guid QueueID, string exchangeName, TEntity msgEntity, Action<BasicAckEventArgs> successMethod = null, Action<BasicNackEventArgs> failureMethod = null, string routingkey = null, string queueName = null, string exchangeType = ExchangeType.Direct, byte deliveryMode = 2, string msgTimeOut = null)
        {
           
            return Task.Run(() =>
            {
                bool isStop = false;
                var mqId = QueueID;
                CancellationTokenSource cts = new CancellationTokenSource();
                CancellationToken token = cts.Token;
                token.Register(() =>
                {
                    System.Console.WriteLine("监听到取消事件");
                    return;
                });
                using (IConnection conn = connfactory.CreateConnection()) //创建一个连接
                {
                    using (IModel channel = conn.CreateModel()) //创建一个Channel
                    {
                        channel.ConfirmSelect();//开启消息确认应答模式 ;当Channel设置成confirm模式时,发布的每一条消息都会获得一个唯一的deliveryTag  ;deliveryTag在basicPublish执行的时候加1           
                        try
                        {
                            channel.ExchangeDeclare(exchangeName, exchangeType, true, false);
                        }
                        catch (Exception ex)
                        {
                            _logger.LogError(ex, ex.Message);
                        }
                        if (exchangeType == ExchangeType.Direct)
                        {
                            if (queueName == null || routingkey == null)
                            {
                            }
                            Dictionary<string, object> dic = new Dictionary<string, object>() { { "x-max-length", QueueMaxCount } }; //设定这个队列的最大容量为50000条消息
                            channel.QueueDeclare(queueName, true, false, false, arguments: dic);
                            channel.QueueBind(queueName, exchangeName, routingkey, arguments: dic);
                        }
                        if (exchangeType == ExchangeType.Topic)
                        {
                            if (routingkey == null)
                            {
                                _logger.LogError("RabbitMq routingkey==null ");
                            }
                        }
                        /*-------------Return机制:不可达的消息消息监听--------------*/
                        //这个事件就是用来监听我们一些不可达的消息的内容的:比如某些情况下,如果我们在发送消息时,当前的exchange不存在或者指定的routingkey路由不到,这个时候如果要监听这种不可达的消息,就要使用 return
                        EventHandler<BasicReturnEventArgs> evreturn = new EventHandler<BasicReturnEventArgs>((o, basic) =>
                        {
                            var rc = basic.ReplyCode; //消息失败的code
                            var rt = basic.ReplyText; //描述返回原因的文本。
                            var msg = basic.Body.ToArray(); //失败消息的内容
                                                            //在这里我们可能要对这条不可达消息做处理,比如是否重发这条不可达的消息呀,或者这条消息发送到其他的路由中呀,等等  
                            _logger.LogError("Rabbit Error:ReplyCode:" + rc + ";ReplyText:" + rt + ";Body:" + msg);
                            cts.Cancel();
                        });
                        channel.BasicReturn += evreturn;
                        //消息发送成功的时候进入到这个事件:即RabbitMq服务器告诉生产者,我已经成功收到了消息
                        EventHandler<BasicAckEventArgs> BasicAcks = new EventHandler<BasicAckEventArgs>((o, basic) =>
                        {
                            if (successMethod != null)
                            {
                                successMethod(basic);
                            }
                            cts.Cancel();
                        });
                        //消息发送失败的时候进入到这个事件:即RabbitMq服务器告诉生产者,你发送的这条消息我没有成功的投递到Queue中,或者说我没有收到这条消息。
                        EventHandler<BasicNackEventArgs> BasicNacks = new EventHandler<BasicNackEventArgs>((o, basic) =>
                        {
                            //MQ服务器出现了异常,可能会出现Nack的情况
                            if (successMethod != null)
                            {
                                failureMethod(basic);
                            }
                            _logger.LogError("调用了Nacks;DeliveryTag:" + basic.DeliveryTag.ToString() + ";Multiple:" + basic.Multiple.ToString() + "时间:" + DateTime.Now.ToString());
                            cts.Cancel();
                        });
                        channel.BasicAcks += BasicAcks;
                        channel.BasicNacks += BasicNacks;
                        //--------------------------------
                        IBasicProperties props = channel.CreateBasicProperties();
                        props.DeliveryMode = deliveryMode; //1:非持久化 2:持续久化 (即:当值为2的时候,我们一个消息发送到服务器上之后,如果消息还没有被消费者消费,服务器重启了之后,这条消息依然存在)
                        props.Persistent = true;
                        props.ContentEncoding = "UTF-8"; //注意要大写
                        if (msgTimeOut != null) { props.Expiration = msgTimeOut; }; //消息过期时间:单位毫秒
                        props.MessageId = mqId.ToString(); //设定这条消息的MessageId(每条消息的MessageId都是唯一的)
                        string message = Newtonsoft.Json.JsonConvert.SerializeObject(msgEntity);
                        var msgBody = Encoding.UTF8.GetBytes(message); //发送的消息必须是二进制的
                                                                       //记住:如果需要EventHandler<BasicReturnEventArgs>事件监听不可达消息的时候,一定要将mandatory设为true
                        channel.BasicPublish(exchange: exchangeName, routingKey: routingkey, mandatory: true, basicProperties: props, body: msgBody);
                        try
                        {
                            while (true)
                            {
                                token.ThrowIfCancellationRequested();
                            }
                        }
                        catch (OperationCanceledException)
                        {
                            Console.WriteLine("Queue OK");
                        }
                    }
                }
            });
        }
        public Task ConsumptionMsg(string exchangeName, Func<BasicDeliverEventArgs, bool> businessAction, Action<BasicDeliverEventArgs> failureAction, string routingkey = null, string queueName = null, string exchangeType = ExchangeType.Direct)
        {
            return Task.Run(() =>
            {
                using (IConnection conn = connfactory.CreateConnection())
                {
                    using (IModel channel = conn.CreateModel())
                    {
                        //channel.ConfirmSelect()消费端是不需要去指定消息的确认应答模式的,消费端本身就是监听
                        channel.BasicQos(0, 1, false);
                        channel.ExchangeDeclare(exchangeName, exchangeType, durable: true, autoDelete: false, arguments: null);
                        channel.QueueDeclare(queueName, durable: true, autoDelete: false, exclusive: false, arguments: null);
                        channel.QueueBind(queueName, exchangeName, routingKey: routingkey); //交换机与队列进行绑定,并指定了他们的路由
                        EventingBasicConsumer consumer = new EventingBasicConsumer(channel); //创建一个消费者                    
                        consumer.Received += (o, basic) =>//EventHandler<BasicDeliverEventArgs>类型事件
                        {
                            try
                            {
                                var isBool = businessAction(basic);
                                if (isBool)
                                {
                                    //手动ACK确认分两种:BasicAck:肯定确认 和 BasicNack:否定确认
                                    channel.BasicAck(deliveryTag: basic.DeliveryTag, multiple: false);//这种情况是消费者告诉RabbitMQ服务器,我已经确认收到了消息
                                }
                                else
                                {
                                    failureAction(basic);
                                    channel.BasicNack(deliveryTag: basic.DeliveryTag, multiple: false, requeue: false);//这种情况是消费者告诉RabbitMQ服务器,因为某种原因我无法立即处理这条消息,这条消息重新回到队列,或者丢弃吧.requeue: false表示丢弃这条消息,为true表示重回队列
                                }
                            }
                            catch (Exception ex)
                            {
                                failureAction(basic);
                                //requeue:被拒绝的是否重新入队列;true:重新进入队列 fasle:抛弃此条消息
                                //multiple:是否批量.true:将一次性拒绝所有小于deliveryTag的消息
                                _logger.LogError(ex, "Consumption Msg:" + ex.Message);
                                channel.BasicNack(deliveryTag: basic.DeliveryTag, multiple: false, requeue: false);//这种情况是消费者告诉RabbitMQ服务器,因为某种原因我无法立即处理这条消息,这条消息重新回到队列,或者丢弃吧.requeue: false表示丢弃这条消息,为true表示重回队列
                            }
                        };
                        channel.BasicConsume(queueName, autoAck: false, consumer: consumer);//第二个参数autoAck设为true为自动应答,false为手动ack ;这里一定要将autoAck设置false,告诉MQ服务器,发送消息之后,消息暂时不要删除,等消费者处理完成再说
                        Console.ReadLine();
                    }
                }
            });
        }
    }
}
 1.我这里在发送消息的时候做了Confirm 确认机制,并且持久化了Mq的交换机队列数据,并且在消费消息的时候手动确认,这样保证了消息不回丢失,消息没有设置过期时间,消息堆积也不会丢失。
2.添加了CancellationTokenSource 来监视队列发送的状态,避免了在调用api时候线程提前结束,导致Mq的回调确认机制正常运行
在web后台挂载接收MQ服务消息
创建继承BackgroundService类来做后台挂载类
using Bussiness.BaseService.Message;
using IBussiness.BaseService.Message;
using Utility.Common;
namespace BryantLogAdmin.Common
{
    public class MesBackgroundService : BackgroundService
    {
        private readonly IMessageService _messageService;
        public MesBackgroundService(IMessageService messageService)
        {
            _messageService = messageService;
        }
        /// <summary>
        /// 当web服务启动时运行
        /// </summary>
        /// <param name="stoppingToken"></param>
        /// <returns></returns>
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            //异步运行接收订阅消息服务
            await Task.Run(() => _messageService.ReceiveSubscribeMes());
        }
    }
}
在Program.cs中申明
OK,主要实现大致就是这些,其中有业务的服务就不用介绍了看效果
第一步调用api发送消息
查看数据是否存储到数据库
查看页面显示是否正常
红色圈中的数字从0变成了1表示接受正常
再查看有没有把数据添加到消费超过的队列表中
添加成功 ,我这里做了操作 是如果消费成功了的话就把队列表的消息转移到成功消费的队列表中
OK这里就成功实现的,
代码没有做什么封装 简单的实现了一下,慢慢完善
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
WebSocket 和 RabbitMQ 可以结合使用来实现实时消息推送。WebSocket 是一种基于 TCP 的协议,它允许在客户端和服务器之间建立持久的双向通信通道。而 RabbitMQ 是一个消息代理和队列管理系统,可以实现消息的可靠传输和分发。 下面是使用 WebSocket 和 RabbitMQ 实现实时消息推送的一般步骤: 1. 配置 WebSocket 服务器:在后端应用程序中,你需要配置一个 WebSocket 服务器,用于接收和处理客户端的 WebSocket 连接请求。可以使用 Spring Boot 中的 Spring WebSocket 或其他 WebSocket 框架进行配置。 2. 配置 RabbitMQ:在后端应用程序中,你需要配置 RabbitMQ 的连接信息,并创建一个或多个交换机和队列。可以使用 RabbitMQ 的 Java 客户端库进行配置。 3. 监听 RabbitMQ 消息:在后端应用程序中,你需要监听 RabbitMQ 中指定队列的消息。当有新的消息到达时,通过 WebSocket 服务器将消息推送给客户端。 4. 前端连接 WebSocket:在前端应用程序中,你需要使用 JavaScript 的 WebSocket API 连接到后端的 WebSocket 服务器。 5. 接收消息并更新 UI:在前端应用程序中,当接收到 WebSocket 服务器推送的消息时,你可以在界面上实时展示或处理这些消息。 通过结合使用 WebSocket 和 RabbitMQ,你可以实现实时、双向的消息通信,并将消息推送给多个客户端。这种方式适用于需要实时更新消息的应用场景,如聊天应用、实时监控等。需要根据具体的技术栈和需求进行相应的配置和开发。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值