在C#中使用RabbitMQ做個簡單的發送郵件小項目

来源:https://www.cnblogs.com/ZYPLJ/p/18279034
-Advertisement-
Play Games

在C#中使用RabbitMQ做個簡單的發送郵件小項目 前言 好久沒有做項目了,這次做一個發送郵件的小項目。發郵件是一個比較耗時的操作,之前在我的個人博客裡面回覆評論和友鏈申請是會通過發送郵件來通知對方的,不過當時只是簡單的進行了非同步操作。 那麼這次來使用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("[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工作隊列隊列模式將消息分發給多個消費者,適用於消息量較大的情況。
  • 可以使用死信隊列處理髮送失敗的郵件

參考鏈接


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、WTM是什麼 WalkingTec.Mvvm框架(簡稱WTM)最早開發與2013年,基於Asp.net MVC3 和 最早的Entity Framework, 當初主要是為瞭解決公司內部開發效率低,代碼風格不統一的問題。2017年9月,將代碼移植到了.Net Core上,併進行了深度優化和重構, ...
  • 實現了一個支持長短按得按鈕組件,單擊可以觸發Click事件,長按可以觸發LongPressed事件,長按鬆開時觸發LongClick事件。還可以和自定義外觀相結合,實現自定義的按鈕外形。 ...
  • WPF的按鈕提供了Template模板,可以通過修改Template模板中的內容對按鈕的樣式進行自定義。結合資源字典,可以將自定義資源在xaml視窗、自定義控制項或者整個App當中調用 ...
  • 在 Visual Studio 中,至少可以創建三種不同類型的類庫: 類庫(.NET Framework) 類庫(.NET 標準) 類庫 (.NET Core) 雖然第一種是我們多年來一直在使用的,但一直感到困惑的一個主要問題是何時使用 .NET Standard 和 .NET Core 類庫類型。 ...
  • 之前也分享過 Swashbuckle.AspNetCore 的使用,不過版本比較老了,本次演示用的示例版本為 .net core 8.0,從安裝使用開始,到根據命名空間分組顯示,十分的有用 ...
  • 1.簡單使用實例 1.1 添加log4net.dll的引用。 在NuGet程式包中搜索log4net並添加,此次我所用版本為2.0.17。如下圖: 1.2 添加配置文件 右鍵項目,添加新建項,搜索選擇應用程式配置文件,命名為log4net.config,步驟如下圖: 1.2.1 log4net.co ...
  • 最近在微軟商店,官方上架了新款Win11風格的WPF版UI框架【WPF Gallery Preview 1.0.0.0】,這款應用引入了前沿的Fluent Design UI設計,為用戶帶來全新的視覺體驗。 ...
  • 當你使用Edge等瀏覽器或系統軟體播放媒體時,Windows控制中心就會出現相應的媒體信息以及控制播放的功能,如圖。 SMTC (SystemMediaTransportControls) 是一個Windows App SDK (舊為UWP) 中提供的一個API,用於與系統媒體交互。接入SMTC的好 ...
一周排行
    -Advertisement-
    Play Games
  • 通過WPF的按鈕、文本輸入框實現了一個簡單的SpinBox數字輸入用戶組件並可以通過數據綁定數值和步長。本文中介紹了通過Xaml代碼實現自定義組件的佈局,依賴屬性的定義和使用等知識點。 ...
  • 以前,我看到一個朋友在對一個系統做初始化的時候,通過一組魔幻般的按鍵,調出來一個隱藏的系統設置界面,這個界面在常規的菜單或者工具欄是看不到的,因為它是一個後臺設置的關鍵界面,不公開,同時避免常規用戶的誤操作,它是作為一個超級管理員的入口功能,這個是很不錯的思路。其實Winform做這樣的處理也是很容... ...
  • 一:背景 1. 講故事 前些天有位朋友找到我,說他的程式每次關閉時就會自動崩潰,一直找不到原因讓我幫忙看一下怎麼回事,這位朋友應該是第二次找我了,分析了下 dump 還是挺經典的,拿出來給大家分享一下吧。 二:WinDbg 分析 1. 為什麼會崩潰 找崩潰原因比較簡單,用 !analyze -v 命 ...
  • 在一些報表模塊中,需要我們根據用戶操作的名稱,來動態根據人員姓名,更新報表的簽名圖片,也就是電子手寫簽名效果,本篇隨筆介紹一下使用FastReport報表動態更新人員簽名圖片。 ...
  • 最新內容優先發佈於個人博客:小虎技術分享站,隨後逐步搬運到博客園。 創作不易,如果覺得有用請在Github上為博主點亮一顆小星星吧! 博主開始學習編程於11年前,年少時還只會使用cin 和cout ,給單片機點點燈。那時候,類似async/await 和future/promise 模型的認知還不是 ...
  • 之前在阿裡雲ECS 99元/年的活動實例上搭建了一個測試用的MINIO服務,以前都是直接當基礎設施來使用的,這次準備自己學一下S3相容API相關的對象存儲開發,因此有了這個小工具。目前僅包含上傳功能,後續計劃開發一個類似圖床的對象存儲應用。 ...
  • 目錄簡介快速入門安裝 NuGet 包實體類User資料庫類DbFactory增刪改查InsertSelectUpdateDelete總結 簡介 NPoco 是 PetaPoco 的一個分支,具有一些額外的功能,截至現在 github 星數 839。NPoco 中文資料沒多少,我是被博客園群友推薦的, ...
  • 前言 前面使用 Admin.Core 的代碼生成器生成了通用代碼生成器的基礎模塊 分組,模板,項目,項目模型,項目欄位的基礎功能,本篇繼續完善,實現最核心的模板生成功能,並提供生成預覽及代碼文件壓縮下載 準備 首先清楚幾個模塊的關係,如何使用,簡單畫一個流程圖 前面完成了基礎的模板組,模板管理,項目 ...
  • 假設需要實現一個圖標和文本結合的按鈕 ,普通做法是 直接重寫該按鈕的模板; 如果想作為通用的呢? 兩種做法: 附加屬性 自定義控制項 推薦使用附加屬性的形式 第一種:附加屬性 創建Button的附加屬性 ButtonExtensions 1 public static class ButtonExte ...
  • 在C#中,委托是一種引用類型的數據類型,允許我們封裝方法的引用。通過使用委托,我們可以將方法作為參數傳遞給其他方法,或者將多個方法組合在一起,從而實現更靈活的編程模式。委托類似於函數指針,但提供了類型安全和垃圾回收等現代語言特性。 基本概念 定義委托 定義委托需要指定它所代表的方法的原型,包括返回類 ...