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

news/2024/7/7 19:51:22/文章来源:https://www.cnblogs.com/ZYPLJ/p/18279034

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

前言

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

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

对于常用的RabbitMQ队列模式不了解的可以查看往期文章:

  • .NET 中使用RabbitMQ初体验 - 妙妙屋(zy) - 博客园 (cnblogs.com) https://www.cnblogs.com/ZYPLJ/p/17572104.html
  • ZY知识库 · ZY - 在.NET Core中使用RabbitMQ (pljzy.top) https://pljzy.top/blog/post/fa670520e3df2839.html

架构图

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

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

  • .NET中使用RabbitMQ延时队列和死信队列 - 妙妙屋(zy) - 博客园 (cnblogs.com)
  • ZY知识库 · ZY - RabbitMQ延时队列和死信队列 (pljzy.top)

开始编码(二阶段)

简单的创建一个用于发送邮件的类,这里使用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工作队列队列模式将消息分发给多个消费者,适用于消息量较大的情况。
  • 可以使用死信队列处理发送失败的邮件

参考链接

  • RabbitMQ tutorial - Work Queues | RabbitMQ https://www.rabbitmq.com/tutorials/tutorial-two-dotnet
  • .NET 中使用RabbitMQ初体验 - 妙妙屋(zy) - 博客园 (cnblogs.com) https://www.cnblogs.com/ZYPLJ/p/17572104.html
  • ZY知识库 · ZY - RabbitMQ延时队列和死信队列 (pljzy.top) https://pljzy.top/blog/post/8a8b75ca23896940.html

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

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

相关文章

【进阶篇】Java 项目中对使用递归的理解分享

笔者在最近的项目开发中,遇到了两个父子关系紧密相关的场景:评论树结构、部门树结构。具体的需求如:找出某条评论下的所有子评论id集合,找出某个部门下所有的子部门id集合。【进阶篇】Java 项目中对使用递归的理解分享 目录【进阶篇】Java 项目中对使用递归的理解分享前言一…

C#/.NET/.NET Core编程技巧练习集(学习,实践干货)

DotNet Exercises介绍 DotNetGuide专栏C#/.NET/.NET Core编程常用语法、算法、技巧、中间件、类库练习集,配套详细的文章教程讲解,助你快速掌握C#/.NET/.NET Core各种编程常用语法、算法、技巧、中间件、类库等等。GitHub开源地址:https://github.com/YSGStudyHards/DotNetE…

2024年6月文章一览

2024年6月编程人总共更新了5篇文章: 1.2024年5月文章一览 2.《编译原理》阅读笔记:p18 3.《编译原理》阅读笔记:p19-p24 4.《编译原理》阅读笔记:p25-p32 5.《Programming from the Ground Up》阅读笔记:p1-p18 6月再挖一个坑,开始《Programming from the Ground Up》的学…

【论文阅读】自动驾驶光流任务 DeFlow: Decoder of Scene Flow Network in Autonomous Driving

再一次轮到讲自己的paper!耶,宣传一下自己的工作,顺便完成中文博客的解读 方便大家讨论。Title PictureReference and pictures paper: https://arxiv.org/abs/2401.16122 code: https://github.com/KTH-RPL/DeFlow b站视频: https://www.bilibili.com/video/BV1GH4y1w7LQ1.…

利用SonarCloud和Azure DevOps提升代码质量

利用SonarCloud和Azure DevOps提升代码质量 在软件开发过程中,代码质量是至关重要的。为了确保代码的清洁和安全性,我们可以使用SonarCloud——一个基于云的代码质量与安全服务。SonarCloud不仅对开源项目免费,还为私有项目提供了14天的免费试用。本文将指导您如何将SonarCl…

免费申请一张SSL证书-包含100个域名-挑战!!

挑战一下,申请一张包含100个域名的证书 首先,我们访问来此加密网站,进入登录页面,输入我的账号密码。登录后,咱们就可以开始申请证书,首先说一下,咱账号是SVIP哦,只有SVIP才可以申请包含100个域名的证书。为了方便验证,咱么先添加一下域名接口,这样可以自动配置验证信…

.net6+ 在单文件应用程序中获取程序集位置

一般来说,获取执行程序集的位置,您可以调用: var executableDirectory = System.Reflection.Assembly.GetExecutingAssembly().Location;如果发布为单个文件, 会提示如下警告 warning IL3000: System.Reflection.Assembly.Location always returns an empty string for assemb…

(set+拓扑排序) CF1572A Book

题意:思路: 每本书之间很明显存在拓扑关系,由此想到拓扑排序。使用set对图进行拓扑排序,将阅读次数小的放在前面,若阅读次数相同则按照阅读章节编号排序。假设第 x 章在第 y 章理解之后就能理解,若 x 大于 y 则本次阅读就可以理解 x 章,否则需要下一次才能理解第 x 章。 …

【github报错解决】Failed to connect to github.com port 443: Couldnt connect to server

今天使用github push时候报错的 解决办法: 1、查看本机代理 路径:设置->网络和Internet->代理->地址:端口 2、用git cmd修改配置,port就用刚刚代理端口值git config --global http.proxy http://127.0.0.1:[port] git config --global https.proxy http://127.0.0…

LVGL组件

LVGL组件的使用 目录LVGL组件的使用1 . 父和子对象2. 部件的基本属性及设置3. 图解:还有其他样式可以设置2. 组件分类 1 . 父和子对象2. 部件的基本属性及设置 /******** 设置打小 ********/ /******** 注意:设置部件位置时,坐标原点在父对象的左上角 ********/ lv_…

Maven 笔记

开发工具:Maven相关内容笔记# Maven简介 Maven的本质是一个项目管理工具,将项目开发和管理过程抽象成一个项目对象模型(POM)) 这玩意儿是使用Java开发的,所以采用的就是Java的思想:面向对象 POM (Project Object Model):项目对象模型Maven的作用:项目构建:提供标准的、…

202406月度小结

这个学期比上个学期有意思得多了。但是为什么我只写了6月小结,没有写3月4月5月小结呢?因为那时候还没有想到用博客写这种小结形式的随笔啦…… 横向对比,这四个月都挺有意思的。但是由于我太容易忘记事情了……我的记忆是有限的,先把6月发生的趣事赶紧记录下来比较好。 昨天…

大模型技术方向Task1笔记

赛题概要 一、赛事背景 在当今数字化时代,企业积累了丰富的对话数据,这些数据不仅是客户与企业之间交流的记录,更是隐藏着宝贵信息的宝库。在这个背景下,群聊对话分角色要素提取成为了企业营销和服务的一项重要策略。 群聊对话分角色要素提取的理念是基于企业对话数据的深度…

[LeetCode] 122. Best Time to Buy and Sell Stock II

medium是你的谎言. class Solution:def maxProfit(self, prices: List[int]) -> int:#1if len(prices) == 1:return 0#elsemax_profit = 0min_price = prices[0]for i, element in enumerate(prices):#find a min_price if element <= min_price:min_price = elementelse…

【esp32 学习笔记】esp-idf学会调用组件管理——以button

简单不看版——esp-idf组件管理步骤 在ESP-IDF 组件管理器网页(https://components.espressif.com/)搜索我们需要的组件,比如【button】,然后 点开相应的组件,比如 espressif/button 组件。 【关键步骤】复制相关组件界面上配置组件的命令 ,形如:idf.py add-dependency &…

idea创建javaweb项目

1.新建project项目 2.添加依赖<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>6.0.10</version> </dependency>…

Linguistics-English-Would, Should, and Could: How to Use Them Correctly

https://7esl.com/would-should-could/ Key Takeaways“Would” is used for hypotheticals and future possibilities that may not occur. “Should” implies advice, expectation, or probability. “Could” expresses past ability or present possibility.Home Knowled…

[LeetCode] 121. Best Time to Buy and Sell Stock

想清楚了确实算是简单题. class Solution:def maxProfit(self, prices: List[int]) -> int:#1if len(prices) == 1:return 0#elsemax_profit = 0min_price = prices[0]for i, element in enumerate(prices):if element <= min_price:min_price = elementelif element - m…

Druid数据库加密实现

Druid数据库连接池中的密码加密功能提供了以下几个主要好处: 增强安全性:最显著的好处是提高了数据库系统的安全性。明文存储数据库密码容易造成安全隐患,一旦配置文件泄露,数据库可能遭受未授权访问。通过加密数据库密码,即使配置文件落入未经授权的人员手中,他们也无法…

17-移动端适配

移动端适配01 单位的分类02 相对单位相对的对象 2.1 em 永远都是相对于自身的font-size <!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, …