在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. 《最新出炉》系列入门篇-Python+Playwright自动化测试-42-强大的可视化追踪利器Trace Viewer

    1.简介 在我们日常执行自动化测试工作的过程中,经常会遇到一些偶发性的bug,但是因为bug是偶发性的,我们不一定每次执行都能复现,所以我们在测试执行的时候,追踪用例执行就变得非常重要了.playwr ...

  2. ITIL4之四维模型

    ITIL4的一个核心概念.它定义了四个维度(图中的1~4),这四个维度旨在确保组织能够在多方面考虑其服务提供,从而更有效地创造和交付价值. 四维模型的整合流程 确立价值载体:明确信息服务提供商的价值主 ...

  3. Canvas简历编辑器-我的剪贴板里究竟有什么数据

    Canvas图形编辑器-我的剪贴板里究竟有什么数据 在这里我们先来聊聊我们究竟应该如何操作剪贴板,也就是我们在浏览器的复制粘贴事件,并且在此基础上聊聊我们在Canvas图形编辑器中应该如何控制焦点以及 ...

  4. Vben-admin---ApiSelect Invalid prop: type check failed for prop "onUpdate:value". Expected Function, got Array

    在basicFrom组件里添加一个ApiSelect, <template #localSearch="{ model, field }"> <ApiSelect ...

  5. vue3.4中defineModel中默认值是复杂数据类型 (注意!!!)

    const drillFields = defineModel<string[]>('drillFields', { get(val) { return reactive(val || [ ...

  6. PageOffice调用本地office实现多人在线同时编辑Word文档

    说明:多人同时在线编辑文件大多数会出现文档相互覆盖的问题,后保存的文档会覆盖先保存的文档.pageoffice的这个功能可以用程序控制不同用户打开文件后,编辑Word文档中属于自己的区域,并且不会互相 ...

  7. 通过 Wireshark 解密 Kerberos 票据

    前言 在使用 Wireshark 分析 Active Directory 的 Kerberos 的流量时,会遇到加密票据的情况,这对进一步探究 AD 下的漏洞篡改事件的详细过程造成了影响.在查询资料时 ...

  8. MQTT 实践总结

    QMQX 文档:https://www.emqx.io/docs/zh/latest/ MQTT 入门:https://www.emqx.com/zh/mqtt-guide 通过案例理解 MQTT 主 ...

  9. django中实现Markdown

    分为两个部分: ------------后台使用Markdown编辑 ------------前端使用Markdown展示 准备工作: pip install django-mdeditor # 用于 ...

  10. SwiftUI(二)- 页面导航NavigationLink和Sheet窗口(模态视图)

    NavigationLink 官方文档对NavigationLink的定义: A button that triggers a navigation presentation when pressed ...