在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. VUE+element页面按钮调用dialog

    VUE+element通过按钮调用普通弹框(弹框页面独立出一个dialog页面,非在同一个页面文件里) 代码如下 <el-dialog> <el-button type=" ...

  2. VMware虚拟机安装Centos-7.9

    VMware虚拟机安装Centos-7.9 创作不易,点赞关注一下吧 1.安装VMware Workstation Pro 大家根据自己的实际情况安装合适版本的VMware Workstation P ...

  3. 「IT运维迷宫」那些让人头疼的常见问题与破局之道

    在数字化浪潮汹涌的今天,IT运维如同一座错综复杂的迷宫,稍有不慎便可能迷失方向.作为企业运营的幕后英雄,运维团队常常面临着各种突如其来的挑战.本文将带你深入探索IT运维中的那些常见"坑&qu ...

  4. 在线自动加字幕!超快完成Pr的字幕制作!

    在线自动加字幕!超快完成Pr的字幕制作! https://www.bilibili.com/video/BV1T441167eX/?spm_id_from=333.788.videocard.3 ht ...

  5. IPv6 — 基于邻居发现协议的通信方式

    目录 文章目录 目录 前文列表 IPv6 的通信方式 NDP(Neighbor Discovery Protocol,邻居发现协议) IPv6 地址自动配置 无状态自动配置概述 前文列表 <IP ...

  6. 定了!航天科技AIRIOT 物联网平台新品发布会,6月6日北京见!

    AIRIOT新品发布会预告 航天科技定档6月6日举办AIRIOT新品发布会,诚邀大家共同见证4.0版本的创新与赋能! 活动地点:北京雍和航星科技园. 现场参会请通过下方长图二维码进行报名! 亦可预约直 ...

  7. 模拟重装Kubernetes(k8s)集群:删除k8s集群然后重装

    目录 一.系统环境 二.前言 三.重装Kubernetes集群 3.1 环境介绍 3.2 删除k8s所有节点(node) 3.3 kubeadm初始化 3.4 添加worker节点到k8s集群 3.5 ...

  8. 基于docker的cicd

    基于docker的cicd 飞书链接: https://dlk2qiw7lh.feishu.cn/docs/doccnyu14HTaamVzASIXreCSNjf 相关软件 链接:https://pa ...

  9. 分布式定理--CAP定理

    cap定理指的是,在一个分布式系统中,只能满足cap中的两项. C consistency 一致性 A availability 可用性 P partition tolerance 分区可容错性 -- ...

  10. 01-布局扩展-BFC完成圣杯布局

    <!DOCTYPE html>   <html lang="en">   <head>   <meta charset="UTF ...