首页 > 编程语言 >在C#中使用RabbitMQ做个简单的发送邮件小项目

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

时间:2024-07-02 09:02:25浏览次数:1  
标签:string C# RabbitMQ 发送 队列 public 邮件

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

前言

好久没有做项目了,这次做一个发送邮件的小项目。发邮件是一个比较耗时的操作,之前在我的个人博客里面回复评论和友链申请是会通过发送邮件来通知对方的,不过当时只是简单的进行了异步操作。
那么这次来使用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("[email protected]", "");  

			// 创建一个邮件消息  
			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 = "[email protected]"; // 发件人地址  
	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工作队列队列模式将消息分发给多个消费者,适用于消息量较大的情况。
  • 可以使用死信队列处理发送失败的邮件

参考链接

标签:string,C#,RabbitMQ,发送,队列,public,邮件
From: https://www.cnblogs.com/ZYPLJ/p/18279034

相关文章

  • 掌握 C# 中的空合并运算符
    高效处理空值是软件开发中的常见要求。C#提供了强大的工具来管理空值,包括空值合并运算符(??)。本文探讨了空值合并运算符、其优点以及它如何简化和增强您的代码。目录空合并运算符简介空合并运算符的基本用法与空条件运算符结合链接空合并运算符空合并赋值运算符实例传......
  • C# 中的并发和并行
    介绍并发和并行是现代编程中的关键概念,可帮助开发人员创建高效、响应迅速、高性能的应用程序。在C#中,这些概念尤其重要,因为该语言对多线程和异步编程提供了强大的支持。本文介绍了C#中的并发和并行,包括关键概念、优点和实际示例。并发C#中的并发涉及同时管理多个任......
  • C++文件输入输出
    参考博文:https://blog.csdn.net/houbincarson/article/details/136327765/*文件输入输出fstream有三个文件流类:std::ifstream:用于从文件中读取数据的输入流对象。std::ofstream:用于向文件中写入数据的输出流对象。std::fstream:用于读写文件的输入输出流对象。*/#include<f......
  • Oracle 上机
    --1.(3分)查找每个部门的最高工资员工编号及其下属信息。selecte2.empno,e1.*fromempe1join(select*fromempwhere(deptno,sal)in(selectdeptno,max(sal)fromempgroupbydeptno))e2one1.mgr=e2.empno;/*2.(5分)有成绩表如下(使用with子查询):准考证号科......
  • Oracle day15
    /*createtablef0307(idnumber,productnamevarchar2(100),parentidnumber);insertintof0307values(1,'汽车',null);insertintof0307values(2,'车身',1);insertintof0307values(3,'发动机',1);insertintof0307values(4......
  • camunda多租户技术架构介绍和测试验证
    多租户考虑的是单个Camunda安装应该为多个租户提供服务的情况。对于每个租户,应做出一定的隔离保证。例如,一个租户的流程实例不应干扰另一租户的流程实例。多租户可以通过两种不同的方式实现。一种方法是每个租户使用一个流程引擎。另一种方法是仅使用一个流程引擎并将数据与租......
  • Oracle day14
    /*createtablef0810(idnumber,timesvarchar2(50));insertintof0810values(1,'2019-12-2511:01');insertintof0810values(2,'2019-12-2511:03');insertintof0810values(3,'2019-12-2511:05');insertintof0810values(4,......
  • camunda数据库表结构详细说明
    本文基于Camunda7.19.0版本,介绍Camunda开源工作流引擎的数据库架构和ER模型,Camunda7.19.0共49张表,包括了BPMN流程引擎、DMN规则引擎、CMMN引擎、历史数据、用户身份等方面的表结构定义,以及表与表之间的关联关系。1、camunda数据库结构综述Camunda流程引擎的数据库架构由多个表组......
  • C#面:实现产生一个int数组,长度为100,并向其中随机插入1-100,并且不能重复
    对生成的数组排序,需要支持升序、降序两种顺序usingSystem;usingSystem.Collections.Generic;classProgram{staticvoidMain(string[]args){Randomrandom=newRandom();HashSet<int>set=newHashSet<int>();while(set.C......
  • 用质因数求解最大公约数(gcd)和最小公倍数(lcm)
    用质因数求解最大公约数(gcd)思路分析:1、质因数:(素因数或质因子)他指的是能整除给定正整数的质数。例如:36可以分解为223*3,其中2和3就是质因数。2、质因数求解最大公约数:对每个数进行质因数分解;找出所有数的共有质因数,并取每个共有质因数的最低次幂;将这些最低次幂的质因......