在C#中使用RabbitMQ做个简单的发送邮件小项目 _

前言

好久没有做项目了,这次做一个发送邮件的小项目。发邮件是一个比较耗时的操作,之前在我的个人博客里面回复评论和友链申请是会通过发送邮件来通知对方的,不过当时只是简单的进行了异步操作。那么这次来使用RabbitMQ去统一发送邮件,我的想法是通过调用邮件发送接口,将请求发送到队列。然后在队列中接收并执行邮件发送操作。本文采用简单的点对点模式:

在点对点模式中,只会有一个消费者进行消费。

架构图

image

简单描述下项目结构。项目主要分为生产者、RabbitMQ、消费者这3个对象。

  • 生产者(Publisher):负责将邮件发送请求发送到RabbitMQ的队列中。

  • RabbitMQ服务器:作为消息中间件,用于接收并存储生产者发送的消息。

  • 消费者(Consumer):从RabbitMQ的队列中接收邮件发送请求,并执行实际的邮件发送操作。

项目结构

  • RabbitMQEmailProject

  • EamilApiProject 生产者

  • Controllers 控制器

  • Service 服务

  • RabiitMQClient 消费者

  • Program 主程序

  • Model 实体类

开始编码(一阶段)

首先我们先简单的将生产者和消费者代码完成,让生产者能够发送消息,消费者能够接受并处理消息。代码有点多,不过注释也多很容易看懂。给生产者和消费者都安装上用于处理RabiitMQ连接的Nuget包:

dotnet add package RabbitMQ.Client

生产者

EamilApiProject

配置文件

appsetting.json

"RabbitMQ": {  "Hostname": "localhost",  "Port": "5672",  "Username": "guest",  "Password": "guest"  
}

控制器

[ApiController]  
[Route("[controller]")]  
public class SendEmailController : ControllerBase  
{  private readonly EmailService _emailService;  public SendEmailController(EmailService emailService)  {       _emailService = emailService;  }  [HttpPost(Name = "SendEmail")]  public IActionResult Post([FromBody] EmailDto emailRequest)  {        _emailService.SendEamil(emailRequest);  return Ok("邮件已发送");  }
}

服务

RabbitMQ连接服务

public class RabbitMqConnectionFactory :IDisposable  
{  private readonly RabbitMqSettings _settings;  private IConnection _connection;  public RabbitMqConnectionFactory (IOptions<RabbitMqSettings> settings)  {       _settings = settings.Value;  }  public IModel CreateChannel()  {        if (_connection == null || _connection.IsOpen == false)  {            var factory = new ConnectionFactory()  {  HostName = _settings.Hostname,  UserName = _settings.Username,  Password = _settings.Password  };  _connection = factory.CreateConnection();  }  return _connection.CreateModel();  }  public void Dispose()  {        if (_connection != null)  {            if (_connection.IsOpen)  {               _connection.Close();  }            _connection.Dispose();  }    }
}

发送邮件服务

public class EmailService
{private readonly RabbitMqConnectionFactory _connectionFactory;public EmailService(RabbitMqConnectionFactory connectionFactory){_connectionFactory = connectionFactory;}public void SendEamil(EmailDto emailDto){using var channel = _connectionFactory.CreateChannel();var properties = channel.CreateBasicProperties();properties.Persistent = true;//消息持久化var message = JsonConvert.SerializeObject(emailDto);var body = Encoding.UTF8.GetBytes(message);channel.BasicPublish( string.Empty, "email_queue", properties, body);}
}

注册服务

builder.Services.Configure<RabbitMqSettings>(builder.Configuration.GetSection("RabbitMQ"));
builder.Services.AddSingleton<RabbitMqConnectionFactory >();
builder.Services.AddTransient<EmailService>();

实体

Model

public class EmailDto  
{  /// <summary>  /// 邮箱地址  /// </summary>  public string Email { get; set; }  /// <summary>  /// 主题  /// </summary>  public string Subject { get; set; }  /// <summary>  /// 内容  /// </summary>  public string Body { get; set; }  
}

public class RabbitMqSettings  
{  public string Hostname { get; set; }  public string Port { get; set; }  public string Username { get; set; }  public string Password { get; set; }  
}

消费者

RabiitMQClient

static void Main(string[] args)  
{  var factory = new ConnectionFactory { HostName = "localhost", Port = 5672, UserName = "guest", Password = "guest" };  using var connection = factory.CreateConnection();  using var channel = connection.CreateModel();  channel.QueueDeclare(queue: "email_queue",  durable: true,//是否持久化  exclusive: false,//是否排他  autoDelete: false,//是否自动删除  arguments: null);//参数  //这里可以设置prefetchCount的值,表示一次从队列中取多少条消息,默认是1,可以根据需要设置  //这里设置了prefetchCount为1,表示每次只取一条消息,然后处理完后再确认收到,这样可以保证消息的顺序性  //global是否全局  channel.BasicQos(prefetchSize: 0, prefetchCount: 1, global: false);  Console.WriteLine(" [*] 正在等待消息...");  //创建消费者  var consumer = new EventingBasicConsumer(channel);  //注册事件处理方法  consumer.Received += (model, ea) =>  {  byte[] body = ea.Body.ToArray();  var message = Encoding.UTF8.GetString(body);  var email = JsonConvert.DeserializeObject<EmailDto>(message);  Console.WriteLine(" [x] 发送邮件 {0}", email.Email);  //处理完消息后,确认收到  //multiple是否批量确认  channel.BasicAck(deliveryTag: ea.DeliveryTag, multiple: false);  };    //开始消费  //queue队列名  //autoAck是否自动确认,false表示手动确认  //consumer消费者  channel.BasicConsume(queue: "email_queue",  autoAck: false,  consumer: consumer);  Console.WriteLine(" 按任意键退出");  Console.ReadLine();  
}	

一阶段测试效果

一阶段就是消费者和生产者能正常运行。

image

image

可以看到生产者发送邮件之后,消费者能够正常消费请求。那么开始二阶段,将邮件发送代码完成,并实现能够通过队列处理邮件发送。对于邮件发送失败就简单的做下处理,相对较好的解决方案就是使用死信队列,将发送失败的消息放到死信队列处理。

简单的创建一个用于发送邮件的类,这里使用MailKit库发送邮件。

public class EmailService  
{  private readonly SmtpClient client;  public EmailService(SmtpClient client)  {  this.client = client;  }  public async Task SendEmailAsync(string from, string to, string subject, string body)  {try{await client.ConnectAsync("smtp.163.com", 465, SecureSocketOptions.SslOnConnect); // 认证  await client.AuthenticateAsync("zy1767992919@163.com", "");  // 创建一个邮件消息  var message = new MimeMessage(); message.From.Add(new MailboxAddress("发件人名称", from));  message.To.Add(new MailboxAddress("收件人名称", to));  message.Subject = subject;  // 设置邮件正文  message.Body = new TextPart("html")  {  Text = body  };  // 发送邮件  var response =await client.SendAsync(message);  // 断开连接  await client.DisconnectAsync(true);  }catch (Exception ex){// 断开连接  await client.DisconnectAsync(true);  throw new EmailServiceException("邮件发送失败", ex);  }}  
}  public class EmailServiceFactory  
{  public EmailService CreateEmailService()  {  var client = new SmtpClient();  return new EmailService(client);  }  
}  
public class EmailServiceException : Exception  
{  public EmailServiceException(string message) : base(message)  {  }  public EmailServiceException(string message, Exception innerException) : base(message, innerException)  {  }  
}  

接下来我们在消费者中调用邮件发送方法即可,如果不使用死信队列,我们只需要在事件处理代码加上邮件发送逻辑就行了。

consumer.Received += async (model, ea) =>
{byte[] body = ea.Body.ToArray();var message = Encoding.UTF8.GetString(body);var email = JsonConvert.DeserializeObject<EmailDto>(message);// 创建一个EmailServiceFactory实例var emailServiceFactory = new EmailServiceFactory();  // 使用EmailServiceFactory创建一个EmailService实例  var emailService = emailServiceFactory.CreateEmailService();  // 调用EmailService的SendEmailAsync方法来发送电子邮件  string from = "zy1767992919@163.com"; // 发件人地址  string to = email.Email; // 收件人地址  string subject = email.Subject; // 邮件主题  string emailbody = email.Body; // 邮件正文  try  {  await emailService.SendEmailAsync(from, to, subject, emailbody);  Console.WriteLine(" [x] 发送邮件 {0}", email.Email);}  catch (Exception ex)  {  Console.WriteLine(" [x] 发送邮件失败 " + ex.Message);  //这里可以记录日志//可以使用BasicNack方法,重新回到队列,重新消费}  //处理完消息后,确认收到//multiple是否批量确认channel.BasicAck(deliveryTag: ea.DeliveryTag, multiple: false);
};

在上面中可以将发送失败的邮件重新放队列,多试几次,这里就不做多余的介绍了。

完成效果展示

一封正确的邮件

ok,现在展示邮件发送Demo的完整展示。首先我们来写一个正确的邮箱地址进行发送:

image

image

image

可以看到当我们发送请求之后,消费者正常消费了这条请求,同时邮件发送服务也正常执行。

多条发送邮件请求

那么接下来,我们通过Api测试工具,一次性发送多条邮件请求。其中包含正确的邮箱地址、错误的邮箱地址,看看消费者能不能正常消费呢~这里简单的发送3条请求,2封正确的邮件地址,一封错误的,看看2封正常邮件地址的能不能正常发送出去。

这里有个问题,如果我填的邮件格式是正确的但是这个邮件地址是不存在的,他是能正常发送过去的,然后会被邮箱服务器退回来,这里不知道该怎么判断是否发送成功。所以我这的错误地址是格式就不对的邮件地址,用来模拟因为网络原因或者其他原因导致的邮件发送不成功。

image

image

image

image

可以看到3条请求都成功了,并且消费者接收到并正确消费了。2条正确邮件也收到了,1条错误的邮件也捕获到了。

总结

本文通过使用RabiitMQ点对点模式来完成一个发送邮件的小项目,通过队列去处理邮件发送。通过RabbitMQ.Client库去连接RabbitMQ服务器。使用MailKit库发送邮件。通过使用RabbitMQ来避免邮件发送请求时间长的问题,同时能在消费者中重试、记录发送失败的邮件,来统一发送、统一处理。不足点就是被退回的邮件不知道该如何处理。可优化点:

  • 可以使用WorkQueues工作队列队列模式将消息分发给多个消费者,适用于消息量较大的情况。

  • 可以使用死信队列处理发送失败的邮件

文章转载自:妙妙屋(zy)

原文链接:https://www.cnblogs.com/ZYPLJ/p/18279034

体验地址:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/39258.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vue中路由来回切换页面直接卡死

今天发现一个很严重的问题&#xff0c;项目好不容易做好了&#xff0c;结果页面多了&#xff0c;切换之后卡死。页面所有的交互效果都失效了。 排查了许久的错误原因最后发现原来是路由名称重复了。 如上图当页面跳转到riskdetails详细页面之后&#xff0c;框架则被这个详情页…

随机森林R语言预测工具

随机森林&#xff08;Random Forest&#xff09;是一种基于决策树的集成学习方法&#xff0c;它通过构建多个决策树并集成它们的预测结果来提高预测的准确性。在R语言中&#xff0c;我们可以使用randomForest包来构建和训练随机森林模型。以下是对随机森林的详细介绍以及使用R语…

java高仿真数据生成器-需要的拿去

java高仿真数据生成器源码-需要的拿去 nit-random-tools 介绍&#xff1a;高仿真数据生成器 逆天开源 java 证号码, 姓名&#xff0c;职业, 日期&#xff0c;手机号 生成器 功能列表 编号功能描述class1号 生成器NitIdcardGenerator2姓名 生成器NitChineseNameGenerator3职…

node.lib下载失败,手动下载并配置

在无网络环境&#xff0c;或者网络不好的环境&#xff0c;node.lib会下载失败&#xff0c;此时可手动下载并进行配置。 我们以 node16.17.0 为例&#xff1a; 下载地址 分别下载node.lib和headers https://registry.npmmirror.com/-/binary/node/v16.17.0/win-x64/node.lib…

目标检测算法的技术革新与应用案例

引言 目标检测作为计算机视觉领域中的一项关键技术&#xff0c;近年来取得了显著进展。从传统的基于特征的方法到如今的深度学习算法&#xff0c;目标检测技术在准确性、速度和鲁棒性上均实现了大幅提升。本文将深入探讨目标检测算法的技术原理、发展历程、最新进展以及实际应…

HarmonyOS--开发者证书考试地址

初级证书&#xff1a;华为开发者学堂 高级证书&#xff1a;华为开发者学堂 对应课程&#xff1a;华为开发者学堂

Linux rpm与yum

一、rpm包管理 rpm用于互联网下载包的打包及安装工具&#xff0c;它包含在某些Linux分发版中。它生成具有.RPM扩展名的文件。RPM是RedHat Package Manager (RedHat软件包管理工具&#xff09;的缩写&#xff0c;类似windows的setup.exe&#xff0c;这一文件格式名称虽然打上了R…

办理北京公司注销流程和步骤说明

公司的生命周期是多变的&#xff0c;有时候&#xff0c;业务可能会结束或者出现其他原因&#xff0c;需要注销公司。注销公司是一个复杂的法律过程&#xff0c;需要遵循一系列的步骤和提交特定的材料。下面我们将详细介绍北京注销公司的流程以及需要准备的材料&#xff0c;以帮…

《等保测评实战指南:从评估到加固的全程解析》

在当今数字化时代&#xff0c;信息安全已成为企业生存与发展的基石。随着网络攻击手段的不断演变和复杂度的提升&#xff0c;信息系统等级保护&#xff08;简称“等保”&#xff09;作为国家信息安全保障体系的重要组成部分&#xff0c;其重要性日益凸显。《等保测评实战指南&a…

私有云统一多云管理平台主要服务内容

私有云统一多云管理平台&#xff0c;作为企业IT架构现代化的关键组成部分&#xff0c;旨在为企业提供高效、灵活、安全的云计算资源管理解决方案。这类平台通过整合和优化不同云环境(包括私有云、公有云、混合云)的管理&#xff0c;帮助企业打破云孤岛&#xff0c;实现资源的统…

clickhouse-client 数据导入导出

ClickHouse提供了clickhouse-client客户端可用于数据的快速导入导出 官方文档&#xff1a; Inserting Data from a File JSONL 格式 导出 clickhouse-client -h 127.0.0.1 --port 9000 -u default --password XXX -d default \--query "SELECT * from default.doc_typ…

【游戏引擎之路】登神长阶(五)

5月20日-6月4日&#xff1a;攻克2D物理引擎。 6月4日-6月13日&#xff1a;攻克《3D数学基础》。 6月13日-6月20日&#xff1a;攻克《3D图形教程》。 6月21日-6月22日&#xff1a;攻克《Raycasting游戏教程》。 6月23日-6月30日&#xff1a;攻克《Windows游戏编程大师技巧》。 …

【Qwen2部署实战】Qwen2初体验:用Transformers打造智能聊天机器人

系列篇章&#x1f4a5; No.文章1【Qwen部署实战】探索Qwen-7B-Chat&#xff1a;阿里云大型语言模型的对话实践2【Qwen2部署实战】Qwen2初体验&#xff1a;用Transformers打造智能聊天机器人3【Qwen2部署实战】探索Qwen2-7B&#xff1a;通过FastApi框架实现API的部署与调用4【Q…

从任意用户注册到任意密码重置

写在最前面一句话 To be or not to be ,it‘s a question . 哎呀&#xff0c;放错台词了&#xff0c;应该是 true or false , 在最近的测试中遇到了一个很有趣的点 “将 false 改为true ”就可以成功绕过验证码了。 T rue or false &#xff1f;&#xff1f;&#xff1f; …

Oracle PL / SQL包

在实践中&#xff0c;您很少创建独立的存储函数或过程。 相反&#xff0c;你会使用一个包。 包可以一起组织相关的功能和过程&#xff0c;例如创建库&#xff0c;但在PL / SQL中&#xff0c;库被称为包。 PL / SQL包有两个部分&#xff1a; 包规格包装体 包规范是包的公共…

使用fabric8操作k8s

文章目录 一、引入fabric包二、认证1、使用config文件认证2、使用oauthtoken认证 三、pod的查询和遍历四、命名空间的创建和删除五、deployment的创建和删除部分参数说明1、resourceRequirements2、containerPorts3、envVarList4、volumeMounts和volumeList5、nodeAffinity 六、…

「51媒体」企业举行新闻发布会,如何邀请媒体到场报道

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 媒体宣传加速季&#xff0c;100万补贴享不停&#xff0c;一手媒体资源&#xff0c;全国100城线下落地执行。详情请联系胡老师。 企业举行新闻发布会时&#xff0c;邀请媒体到场报道是一个…

MySQL常用操作命令大全

文章目录 一、连接与断开数据库1.1 连接数据库1.2 选择数据库1.3 断开数据库 二、数据库操作2.1 创建数据库2.2 查看数据库列表2.3 删除数据库 三、表操作3.1 创建表3.2 查看表结构3.3 修改表结构3.3.1 添加列3.3.2 删除列3.3.3 修改列数据类型 3.4 删除表 四、数据操作4.1 插入…

day62--若依框架(基础应用篇)

若依搭建 若依版本 官方 若依官方针对不同开发需求提供了多个版本的框架&#xff0c;每个版本都有其独特的特点和适用场景&#xff1a; 前后端混合版本&#xff1a;RuoYi结合了SpringBoot和Bootstrap的前端开发框架&#xff0c;适合快速构建传统的Web应用程序&#xff0c;其…

【Arm技术日:为AI终端准备了哪些新基石?】

过去一年&#xff0c;移动终端设备的长足进步令人赞叹&#xff0c;例如人工智能 (AI) 从手机到笔记本电脑的巨大创新&#xff0c;并诞生了“新一代 AI 手机”和 AIPC。据IDC预测&#xff0c;2024年全球新一代AI手机的出货量将达到1.7亿部&#xff0c;占智能手机市场总量的近15%…