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

前言

好久没有做项目了,这次做一个发送邮件的小项目。发邮件是一个比较耗时的操作,之前在我的个人博客里面回复评论和友链申请是会通过发送邮件来通知对方的,不过当时只是简单的进行了异步操作。

那么这次来使用RabbitMQ去统一发送邮件,我的想法是通过调用邮件发送接口,将请求发送到队列。然后在队列中接收并执行邮件发送操作。

本文采用简单的点对点模式:

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

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

架构图

简单描述下项目结构。项目主要分为生产者、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();
}

一阶段测试效果

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



可以看到生产者发送邮件之后,消费者能够正常消费请求。那么开始二阶段,将邮件发送代码完成,并实现能够通过队列处理邮件发送。

对于邮件发送失败就简单的做下处理,相对较好的解决方案就是使用死信队列,将发送失败的消息放到死信队列处理,我这里就不用死信队列,对于死信队列感兴趣的可以查看往期文章:

开始编码(二阶段)

简单的创建一个用于发送邮件的类,这里使用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的完整展示。

首先我们来写一个正确的邮箱地址进行发送:





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

多条发送邮件请求

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

这里简单的发送3条请求,2封正确的邮件地址,一封错误的,看看2封正常邮件地址的能不能正常发送出去。

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







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

总结

本文通过使用RabiitMQ点对点模式来完成一个发送邮件的小项目,通过队列去处理邮件发送。

通过RabbitMQ.Client库去连接RabbitMQ服务器。

使用MailKit库发送邮件。

通过使用RabbitMQ来避免邮件发送请求时间长的问题,同时能在消费者中重试、记录发送失败的邮件,来统一发送、统一处理。

不足点就是被退回的邮件不知道该如何处理。

可优化点:

  • 可以使用WorkQueues工作队列队列模式将消息分发给多个消费者,适用于消息量较大的情况。
  • 可以使用死信队列处理发送失败的邮件

参考链接

在C#中使用RabbitMQ做个简单的发送邮件小项目的更多相关文章

  1. 最近做的一个Spring Boot小项目,欢迎大家访问 http://39.97.115.152/

    最近做的一个Spring Boot小项目,欢迎大家访问 http://39.97.115.152/,帮忙找找bug,网站里有源码地址 网站说明 甲壳虫社区(Beetle Community) 一个开源 ...

  2. 参照实验室这边案例做一个简单的maven webapp项目

    流程 : 首先写出一个简单的前端页面. 之后写配置文件.dao.domain等等,注意这里可以使用generator进行自动配置 实验室这边配置文件如下: 其实主要的配置文件就分为6“个”. appl ...

  3. springboot + rabbitmq 做智能家居,我也没想到会这么简单

    本文收录在个人博客:www.chengxy-nds.top,共享技术资源,共同进步 前一段有幸参与到一个智能家居项目的开发,由于之前都没有过这方面的开发经验,所以对智能硬件的开发模式和技术栈都颇为好奇 ...

  4. 简单介绍下怎么在spring中使用RabbitMQ

    这篇文章主要介绍了简单了解如何在spring中使用RabbitMQ,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 常见的消息中间件产品: (1)Ac ...

  5. .NET 环境中使用RabbitMQ

    在企业应用系统领域,会面对不同系统之间的通信.集成与整合,尤其当面临异构系统时,这种分布式的调用与通信变得越发重要.其次,系统中一般会有很多对实时性要求不高的但是执行起来比较较耗时的地方,比如发送短信 ...

  6. .NET 环境中使用RabbitMQ(转)

    在企业应用系统领域,会面对不同系统之间的通信.集成与整合,尤其当面临异构系统时,这种分布式的调用与通信变得越发重要.其次,系统中一般会有很多对实时性要求不高的但是执行起来比较较耗时的地方,比如发送短信 ...

  7. 转:.NET 环境中使用RabbitMQ

    原文来自于:http://blog.jobbole.com/83819/ 原文出处: 寒江独钓   欢迎分享原创到伯乐头条 在企业应用系统领域,会面对不同系统之间的通信.集成与整合,尤其当面临异构系统 ...

  8. 在Node.js中使用RabbitMQ系列二 任务队列

    在上一篇文章在Node.js中使用RabbitMQ系列一 Hello world我有使用一个任务队列,不过当时的场景是将消息发送给一个消费者,本篇文章我将讨论有多个消费者的场景. 其实,任务队列最核心 ...

  9. .NET 环境中使用RabbitMQ 转发 http://www.cnblogs.com/yangecnu/p/4227535.html

    .NET 环境中使用RabbitMQ   在企业应用系统领域,会面对不同系统之间的通信.集成与整合,尤其当面临异构系统时,这种分布式的调用与通信变得越发重要.其次,系统中一般会有很多对实时性要求不高的 ...

  10. 二、消息队列之如何在C#中使用RabbitMQ

    1.什么是RabbitMQ.详见 http://www.rabbitmq.com/. 作用就是提高系统的并发性,将一些不需要及时响应客户端且占用较多资源的操作,放入队列,再由另外一个线程,去异步处理这 ...

随机推荐

  1. 使用.NET查询日出日落时间

    在WPF中,通过资源文件实现主题切换是个常见的功能,有不少文章介绍了如何实现手动切换主题.那如何实现自动切换主题呢?通常有两种机制:一是跟随系统明暗主题切换,二是像手机操作系统那样根据日出日落时间自动 ...

  2. FTP主动模式和被动模式(2)

    防火墙对FTP的影响 ASPF 多通道协议 应用层程序有些使用的是单通道协议,有些使用的是多通道协议. 单通道协议 例如http协议,整个协议交互过程中,服务端和客户端只建立一个连接,并且服务端固定使 ...

  3. 【WebForms王者归来】在 ASP.NET Core 中运行 WebForms 业务代码,99%相似度!

    1. 先说结论 我们为 ASP.NET Core 带来了全新的 WebForms 开发模式,可以让 20 年前的 WebForms 业务代码在最新的 ASP.NET Core 框架中运行,代码相似度9 ...

  4. 物联网平台在AIoT领域8大场景应用

    物联网平台技术在AIoT智慧物联领域的应用越来越深入,尤其是在智慧城市建设项目中,提供了强有力的技术底座工具支撑.ToG的项目需要"门当户对"的服务商具备完善的资质和靠谱的技术服务 ...

  5. 记一次Idea无法打开记录(idea升级)

    记一次Idea无法打开记录 前言,本来今天是打算升级Idea,然后体验一波的,结果升级完之后,发现无法打开idea(双击之后并没有任何打开的反应). 原因排查,打开idea所在目录,找到idea.ba ...

  6. Android 12(S) MultiMedia Learning(七)NuPlayer GenericSource

    本节来看一下NuPlayer Source中的GenericSource,GenericSource主要是用来播放本地视频的,接下来着重来看以下5个方法: prepare,start,pause,se ...

  7. vscode开发一个luaIDE插件

    基础知识 环境准备 node.js 下载后下一步下一步即可安装成功,推荐LTS版本 yeoman 脚手架工具,也就是快速帮你新建一个插件所需的目录的工具,在工作目录下cmd,输入下列命令即可安装 np ...

  8. itestwork(爱测试) 开源一站式接口测试&敏捷测试工作站 9.0.2Rc2发布

    (一)itest 简介 itest work (爱测试)  一站式工作站让测试变得简单.敏捷,"好用.好看,好敏捷" ,是itest wrok 追求的目标.itest work 包 ...

  9. Hive 在工作中的调优总结

    总结了一下在以往工作中,对于Hive SQL调优的一些实际应用,是日常积累的一些优化技巧,如有出入,欢迎在评论区留言探讨~ 一.EXPLAIN 查看执行计划 二.建表优化 2.1 分区 分区表基本操作 ...

  10. LINQ to Entities does not recognize the method 'System.String ToString()' method

    LINQ to Entities does not recognize the method 'System.String ToString()' method, and this method ca ...