最近想给博客网站添加留言问答,以及订阅功能
留言问答,订阅功能:博客网站当用户留言或者订阅的时候,会实时通知到后台管理系统。
流程图

开发难点:
在开发中遇到的问题
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{//我的业务比较简单,我的消息通知只通知到管理员也就是adminif (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 队列参数#endregionpublic 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;#endregionprivate 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执行的时候加1try{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路由不到,这个时候如果要监听这种不可达的消息,就要使用 returnEventHandler<BasicReturnEventArgs> evreturn = new EventHandler<BasicReturnEventArgs>((o, basic) =>{var rc = basic.ReplyCode; //消息失败的codevar 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设为truechannel.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这里就成功实现的,
代码没有做什么封装 简单的实现了一下,慢慢完善