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

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

参考链接

posted @   妙妙屋(zy)  阅读(1382)  评论(10编辑  收藏  举报
相关博文:
阅读排行:
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· 易语言 —— 开山篇
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
  1. 1 够爱(翻自 曾沛慈) 是我呀卡司宝贝
  2. 2 老人と海 ヨルシカ
  3. 3 生生世世爱 黄霄雲
  4. 4 希望有羽毛和翅膀 imzat
生生世世爱 - 黄霄雲
00:00 / 00:00
An audio error has occurred, player will skip forward in 2 seconds.

作词 : 杨漩予

作曲 : 林毅

爱 还没来

天地间风云忽然变

有情有义的人都要回来

爱 总会来

生死注定的来世再爱

都等了太久哭尽无奈

rap

是谁站在三清山门外

回首看桃溪花正开

一入岁月江湖中来

再随之去追星辰山海

到底是那恩怨过往

或是爱恨情长 怎么收场

这些年的故事和秘密

都在逆水寒里珍藏

爱恨纠缠的生生世世

爱恨纠缠的生生世世

心底执着的信念为你存在

多遥远的路都阻挡不住

再次拥有没距离的温度

失去自由的生生世世

有爱不懂相拥错过了最爱

送一剑祝福再默默相助

恐怕没以后不自觉留退路

爱 还没来

爱 还没来

天地间风云忽然变

有情有义的人都要回来

爱 总会来

生死注定的来世再爱

都等了太久哭尽无奈

rap

遇过金风细雨楼的刀太快

也遇过六分半堂惊雷开

身处这江湖风雨事

却不知可曾有故人来

轻挥一剑千山过

再回望轻舟多澎湃

我欲乘风逍遥去

纵览天地入我怀

从此我心自在

爱恨纠缠的生生世世

爱恨纠缠的生生世世

心底执着的信念为你存在

多遥远的路都阻挡不住

再次拥有没距离的温度

失去自由的生生世世

有爱不懂相拥错过了最爱

送一剑祝福再默默相助

恐怕没以后不自觉留退路

为情所困的生生世世

为情所困的生生世世

伤也被伤命中成双的伤害

等不到日出一个人孤独

让星光代替我伴你远途

黑白轮回的生生世世

彻底放开成全永远的依赖

是乱世英雄或凡间俗梦

爱不离爱是把这感动留住

爱不离爱是把这感动留住

原唱 : 吴雨霏

OP原始版权人:北京大石音乐版权有限公司、CHANCES CREATIVE MUSIC LTD

SP代理权利人:北京大石音乐版权有限公司、百代音乐版权代理(北京)有限公司

编曲改编:1AN孙毅然

改编词(Rap词):焦糖

Rapper:阿茹汗

笛子实录:水玥儿

混音&母带工程师:王嘉屏

和声:曾雪祁

配唱制作人:沈小力

录音师:邢铜

录音棚:55TEC Studio

出品:逆水寒

企划营销:微梦传媒

点击右上角即可分享
微信分享提示