分享程式代碼相關筆記
目前文章總數:157 篇
最後更新:2024年 12月 07日
上篇上篇 完成 SLB + Session Sticky 配置,最後遺留下 2台Web機器的 SignalR 無法互通問題。
基於 .Net 提供 3 種解法,常態性解決 Server 實際部署受到環境影響的問題,這篇是第 4 種即使非 .Net 也常常使用的一種做法 RabbitMQ Backplane
※假設使用 SignalR Server 註冊到另一台 SignalR Server 會無法通過 SLB + Session Sticky 配置。
因為 SLB 會動態分配到 Server,有可能自己連到自己造成 Crash
我們目標是啟用 RabbitMQ Backplane 模式,管理所有連接的 SignalR Server,當需要推播時,由 RabbitMQ Exchange 的 FanOut 模式完成推播給所有用戶。
※RabbitMQ Windows 安裝可參考此篇
1. Web資源檔 | : | 將 SignalR 8.0.0.js 下載,讓 .cshtml 引用,使前端網站可以註冊 SignalR 服務 |
2. 背景服務 | : | 註冊 RabbitMQ 的接收事件,進行推播 SignalR 資訊 |
3. RabbitMQ | : | RabbitMQ 實際邏輯、釋放資源時的事件 |
4. SignalR Hub | : | SignalR Hub 開出的接口,並且提供前端 Publish 與後端 Subscribe |
5. 前端頁面 | : | 提供聊天室註冊 SignalR 並且可發送訊息,與接收訊息功能 |
6. 配置 | : | 每個 Server 為了辨識,增加自己的代號、使用的 Port 號、RabbitMQ Server 位置 |
7. 初始化配置 | : | 依賴注入 SiganlR、RabbitMQ、背景服務 |
可從微軟給定的CDN SignalR 8.0.0 的位置下載,並放進自己的專案中,可在不對外,並且只能在內部網路的狀況下使用 SignalR
SignalR 8.0.0 下載
建立一個 PageBackroundUpdaterService ,在初始化配置時引用
關鍵在 _rabbitMqService.StartReceiving() 註冊了事件,當有 RabbitMQ 消息推送時可以收到資料。
public class PageBackroundUpdaterService : BackgroundService
{
private readonly IHubContext<UpdateHub> _hubContext;
private readonly IConfiguration _configure;
private readonly RabbitMqService _rabbitMqService;
// 1. 配置變數,版本號、間隔時間
private int _siteNumber = 0;
public PageBackroundUpdaterService(IHubContext<UpdateHub> hubContext,
RabbitMqService rabbitMqService)
{
_rabbitMqService = rabbitMqService;
_hubContext = hubContext;
_configure = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build();
//2. 求出自己站點編號的 2 ^ (SiteNumber-1) 值 EX: 編號1=1 / 編號2=2 / 編號3=4
_siteNumber = (int)Math.Pow(2, (_configure.GetValue("SiteNumber", 1) - 1));
//3-1. 設定 RabbitMQ 消費者(Consumer)的工作
_rabbitMqService.StartReceiving(publishMessage => {
//3-2. 從 RabbitMQ 收到訊息後,推播給自己 Server 下的所有用戶
Task.Run(() => _hubContext.Clients.Group("groupName").SendAsync("ReceiveMessage", publishMessage));
});
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
}
}
RabbitMqService.cs 檔案實現了以下:
SendMessage | : | 生產者:發送訊息到 RabbitMQ 的 Exchange 上 |
StartReceiving | : | 消費者:從 RabbitMQ 接收生產者推送的資料 |
Dispose | : | 結束網站生命週期時釋放資源,並且呼叫 RabbitMqServiceHostedService.cs 的方法 |
public class RabbitMqService
{
private readonly IConnection _connection;
private readonly IModel _channel;
private readonly string _exchangeName = "my-fanout-exchange";
/// <summary>
/// 1-1. 建立基本資訊
/// </summary>
public RabbitMqService()
{
var factory = new ConnectionFactory
{
HostName = "127.0.0.1", // RabbitMQ 主機名
UserName = "admin",
Password = "123fff",
Port = 5672 // RabbitMQ 連接埠
};
// 1-2. 初始化 RabbitMQ 連線
_connection = factory.CreateConnection();
_channel = _connection.CreateModel();
// 1-3. 選擇 Fanout 類型的 Exchange ,註冊此 Exchange 的 RabbitMQ 客戶端(Queue),都會收到廣播訊息
_channel.ExchangeDeclare(exchange: _exchangeName, type: ExchangeType.Fanout);
}
/// <summary>
/// 2-1. 增加一個接口,可以將資料發送到 RabbitMQ
/// </summary>
public void SendMessage(string message)
{
// 2-2. 配置傳送到 RabbitMQ 上 Queue 的內容
var body = Encoding.UTF8.GetBytes(message);
// 2-3. FanOut 基本配置 (關注 Exchange 位置)
_channel.BasicPublish(exchange: _exchangeName,
routingKey: string.Empty,
basicProperties: null,
body: body);
}
/// <summary>
/// 3-1. 增加一個接口,可以接收RabbitMQ 訊息
/// </summary>
public void StartReceiving(Action<string> handleMessage)
{
// 3-2. 建立一個匿名委派
var consumer = new EventingBasicConsumer(_channel);
consumer.Received += (sender, ea) =>
{
// 每當從 Exchange 上收到資料時都會自動觸發 Received 事件
var message = Encoding.UTF8.GetString(ea.Body.ToArray());
// 並將資料回傳給 BackgroundService 上的註冊事件
handleMessage(message);
};
// 3-3. 創建一個臨時隊列,並將其與設定的 FanOut Exchange 綁定
// ※臨時佇列 - 釋放資源即刪除,可以動態擴容站點
var queueName = _channel.QueueDeclare().QueueName;
_channel.QueueBind(queue: queueName, exchange: _exchangeName, routingKey: string.Empty);
_channel.BasicConsume(queue: queueName,
autoAck: true,// True: 消費者收到視為處理完畢
consumer: consumer);// 訂閱的結果事件傳給消費者
}
public void Dispose()
{
_channel.Dispose();
_connection.Dispose();
}
}
實現 SignalR Hub 這邊關注於 RabbitMQ 的 Backplane 省略資料庫取歷史數據
關鍵在 _rabbitMqService.SendMessage(combineMessage); 收到前端資料是往 RabbitMQ 發送
public class UpdateHub : Microsoft.AspNetCore.SignalR.Hub
{
private readonly IConfiguration _configure;
private readonly RabbitMqService _rabbitMqService;
private static string _Site = string.Empty;
private int _siteNumber = 0;
public UpdateHub(RabbitMqService rabbitMqService)
{
_rabbitMqService = rabbitMqService;
_configure = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build();
_Site = _configure.GetValue("Site", string.Empty);
//1. 求出自己站點編號的 2 ^ (SiteNumber-1) 值 EX: 編號1=1 / 編號2=2 / 編號3=4
_siteNumber = (int)Math.Pow(2, (_configure.GetValue("SiteNumber", 1) - 1));
}
/// <summary>
/// 建立連接時,將歷史訊息回傳
/// </summary>
/// <returns></returns>
public override async Task OnConnectedAsync()
{
var getData = FakeHistoryMessage();
foreach (var message in getData)
{
await Clients.Caller.SendAsync("ReceiveMessage", message);
}
await base.OnConnectedAsync();
}
/// <summary>
/// 提供前端讓用戶添加到群組
/// </summary>
public async Task AddToGroup(string groupName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
}
/// <summary>
/// 接收前端傳送訊息
/// </summary>
public async Task SendMessage(string user, string message)
{
var dateTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
var combineMessage = $@"[站點{_siteNumber} {dateTime}] {user}: {message}";
_rabbitMqService.SendMessage(combineMessage);
}
/// <summary>
/// 偽造歷史資料 ※實務資料源可為 Redis / Mysql / SqlServer / MongoDB .....
/// </summary>
private List<string> FakeHistoryMessage()
{
return new List<string>()
{
"[站點1 2024-6-16 05:32:35] Louis: History 1",
"[站點2 2024-6-16 05:32:35] MilkTeaGreen: History 2",
"[站點1 2024-6-16 05:33:17] Louis: History 3",
"[站點2 2024-6-16 05:33:52] MilkTeaGreen: History 4",
};
}
}
進入頁面時,與 Server 的 SignalR 連結,並且加入到正確的群組上
<script>
// 3. 預設頁面值
var updateContainer = document.getElementById("updateContainer");
updateContainer.innerHTML = `<p>New update: 初始化 </p>`;
// 4. 創建 SignalR 連接
const connection = new signalR.HubConnectionBuilder()
.withUrl("UpdateHub", { accessTokenFactory: () => "I am jwtToken" })
.build();
// 5. 訂閱可接收訊息
connection.on("ReceiveMessage", function (message) {
const updateContainer = document.getElementById("updateContainer");
updateContainer.innerHTML += `<p>${message}</p>`;
});
// 6. 啟動
connection.start()
.then(() => {
//7. 連接群組 "groupName"
connection.invoke("AddToGroup", "groupName").catch(function (err) {
return console.error(err.toString());
});
console.log("連接 SignalR 成功");
})
.catch((error) => {
console.log("錯誤訊息:" + error);
});
// 8. 發送訊息到Hub 伺服器上
function sendMessage() {
var user = document.getElementById("userInput").value;
var message = document.getElementById("messageInput").value;
connection.invoke("SendMessage", user, message).catch(function (err) {
console.error("Error invoking SendMessage: " + err.toString());
});
}
</script>
系統的參數化配置,在初始化配置時會使用以下設定值。
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"SiteNumber": "1",
"Site": "WebStie Port: *",
"AllowedHosts": "*",
"RabbitMQ": {
"HostName": "127.0.0.1",
"UserName": "admin",
"Password": "123fff"
}
}
依賴注入背景服務、RabbitMQ 並且啟用 SignalR
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
// 1. 添加 SignalR
builder.Services.AddSignalR();
// 2-1. 注入-增加背景服務 - 輪詢 Push SignalR 讓每台 Server 完成 Backplane 工作
builder.Services.AddHostedService<PageBackroundUpdaterService>();
// 2-2. 注入-RabbitMQ 單例模式
builder.Services.AddSingleton<RabbitMqService>();
// 2-3. 應用程式結束時,釋放 RabbitMQ 連線
builder.Services.AddSingleton<IHostedService, RabbitMqServiceHostedService>();
var app = builder.Build();
...... 略
app.UseEndpoints(endpoints =>
{
// 3. 配置 SignalR 路由
endpoints.MapHub<UpdateHub>("UpdateHub");
});
...... 略
app.Run();
關於 RabbitMQ 的發布/訂閱 模式,可以參考官網,官網文件寫的很詳盡
這篇範例是基於官方文件實現 Asp.net Core WEB 下 RabbitMQ Backplane 方法
如果成功實現代碼,可以在自己的 RabbitMQ UI Manage 上,看到自定義的 Exchange
為了方便辨識,範例代碼定義以下:
my-fanout-exchange
將範例代碼部署到2個 Web 資料夾中
Web1的 appsetting.json 如下
Web2的 appsetting.json 如下
對 Web1 輸入指令
dotnet SingalRWebsiteUseRabbitMQSignalRServiceBackPlateExample.dll --urls=http://localhost:6001
對 Web2 輸入指令
dotnet SingalRWebsiteUseRabbitMQSignalRServiceBackPlateExample.dll --urls=http://localhost:6002
啟動 Nginx
※這邊的配置是基於上一篇 SLB + Session Sticky
進入配置好的首頁,並且開啟兩個分頁(第1個分頁連 web1 ; 第2個分頁連 web2)
輸入一些簡單文字,可以發現完成 SLB + Session Sticky 下的 SignalR 聊天室互動
未來要部署多個機器時,只要改一下 appsetting.json 的代號,即可部署完成