分享程式代碼相關筆記
目前文章總數:157 篇
最後更新:2024年 12月 07日
上篇上篇 完成 SLB + Session Sticky 配置,最後遺留下 2台Web機器的 SignalR 無法互通問題。
此問題MSDN提出第2個建議解法,常態性解決 Server 實際部署受到環境影響的問題
※假設使用 SignalR Server 註冊到另一台 SignalR Server 會無法通過 SLB + Session Sticky 配置。
因為 SLB 會動態分配到 Server,有可能自己連到自己造成 Crash
我們目標是啟用 Redis Backplane 模式,管理所有連接的 SignalR Server,當需要推播時,由 Redis Server 完成推播給所有用戶的方法
1. Web資源檔 | : | 將 SignalR 8.0.0.js 下載,讓 .cshtml 引用,使前端網站可以註冊 SignalR 服務 |
2. 資料模型 | : | 保存於 Redis 的 DataEntity |
3. Redis 業務邏輯 | : | 獨立 Redis 的業務邏輯,並且在初始化時注入為 SingalTon 使其全域共用 |
4. 初始化配置 | : | 依賴注入 Reids、SignalR 等配置 |
5. 配置 | : | 每個 Server 為了辨識,增加自己的代號、使用的 Port 號、Redis Server 位置 |
6. SignalR Hub | : | Web伺服器實現 SignalR ,並且提供前端 Publish 與後端 Subscribe |
7. 前端頁面 | : | 提供聊天室註冊 SignalR 並且可發送訊息,與接收訊息功能 |
可從微軟給定的CDN SignalR 8.0.0 的位置下載,並放進自己的專案中,可在不對外,並且只能在內部網路的狀況下使用 SignalR
SignalR 8.0.0 下載
保存於 Redis 中的資料結構,紀錄用戶發送紀錄
※在 Database Backplane 中是存在 Table 中
public class SignalRMessagesEntity
{
public string UserName { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
public int SiteValues { get; set; }
public long CreateTime { get; set; }
}
關鍵在 GetDb 實現了從 Redis 中指定資料庫進行存、取
public class RedisService
{
private readonly IConnectionMultiplexer _redis;
private readonly ISubscriber _redisSubscriber;
public RedisService(IConnectionMultiplexer redis)
{
_redis = redis;
_redisSubscriber = _redis.GetSubscriber();
}
public IDatabase GetDb(int dbIndex = 0) => _redis.GetDatabase(dbIndex);
public ISubscriber GetSubscriber() => _redis.GetSubscriber();
public void Publish(RedisChannel channel, RedisValue message)
{
_redisSubscriber.Publish(channel, message);
}
}
在 Program.cs 初始化配置中,將 Redis 連線設定
在 1-2. 中配置 Prefix 做隔離,避免其他服務被影響
※AddStackExchangeRedis 配置時,就自動啟用 Redis Backplane
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
// 1. 添加 SignalR - 並且啟用 Redis BackPlane (AddStackExchangeRedis 已經內建)
var redisConnection = builder.Configuration.GetConnectionString("RedisConnection");
builder.Services.AddSingleton<IConnectionMultiplexer>(ConnectionMultiplexer.Connect(redisConnection));
builder.Services.AddSignalR().AddStackExchangeRedis(redisConnection, options => {
//1-2. 重要:為了讓 Redis 某個DB內可以辨識彼此的 Channel 可增加 Prefix 做隔離
options.Configuration.ChannelPrefix = "MyApp";
});
builder.Services.AddControllers();
// 1-3. 注入RedisService 為 Singleton 使其持久化
builder.Services.AddSingleton<RedisService, RedisService>();
var app = builder.Build();
...... 略
app.UseEndpoints(endpoints =>
{
//3. 配置 SignalR 路由
endpoints.MapHub<UpdateHub>("UpdateHub");
});
...... 略
app.Run();
appsettings.json 增加 SiteNumber、ConnectionStrings 完成辨識 Server 與 Redis 連線字串的工作
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"SiteNumber": "1",
"Site": "WebStie Port: XXXX",
"AllowedHosts": "*",
"ConnectionStrings": {
"RedisConnection": "127.0.0.1:6379,abortConnect=False,connectRetry=3,connectTimeout=3000,defaultDatabase=0,syncTimeout=3000,responseTimeout=3000"
}
}
依序將 1. ~ 6. 項 SignalR 的推播、資料保存的工作
public class UpdateHub : Microsoft.AspNetCore.SignalR.Hub
{
private readonly RedisService _redisService;
private readonly IConfiguration _configure;
private static string _Site = string.Empty;
private static string _RedisKey = "MyRadisSignalR";
private int _siteNumber = 0;
public UpdateHub(RedisService redisService)
{
_redisService = redisService;
_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()
{
int startIndex = 0;
int endIndex = -1;
// 2. 從 Redis 中獲取聊天室的歷史訊息列表
var chatHistory = await _redisService.GetDb(0).SortedSetRangeByRankAsync(_RedisKey, startIndex, endIndex);
// 3. 發送聊天室的歷史訊息給新連接的用戶
foreach (var message in chatHistory)
{
await Clients.Caller.SendAsync("ReceiveMessage", message.ToString());
}
await base.OnConnectedAsync();
}
//事件名稱SendUpdate 行為:回傳message
public async Task SendUpdate(string message)
{
await Clients.All.SendAsync("SendUpdate", message);
}
/// <summary>
/// 接收前端傳送訊息
/// </summary>
public async Task SendMessage(string user, string message)
{
//4. 將前端傳來的訊息轉為 Json
var dataEntity = new SignalRMessagesEntity() {
Message = message,
SiteValues = _siteNumber,
UserName = user,
CreateTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
};
var jsonData= JsonConvert.SerializeObject(dataEntity);
//5. 寫入 Redis 保存資料
await _redisService.GetDb(0).SortedSetAddAsync(_RedisKey, jsonData, dataEntity.CreateTime);
//6. 這裡只要直接推播即可, Redis 的 Stack已經BackPlane
await Clients.All.SendAsync("ReceiveMessage", jsonData.ToString());
}
}
前端實現基本的 SignalR 連線工作,在 “ReciveMessage” 訂閱工作中
取得 Redis 的聊天室歷史資料,並且轉換日期格式
<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. 監聽 SendUpdate 事件
connection.on("SendUpdate", (message) => {
const updateContainer = document.getElementById("updateContainer");
updateContainer.innerHTML += `<p>${message.message}</p>`;
});
// 6. 訂閱可接收訊息
connection.on("ReceiveMessage", function (jsonMessage) {
const updateContainer = document.getElementById("updateContainer");
// 6-2. 接收到後端SignalR Server 回傳聊天室歷史訊息
var message = JSON.parse(jsonMessage);
// 6-3. 將資料時間轉為可讀格式 格式為 yyyy-MM-ddTHH:mm:ss
var date = new Date(message.CreateTime * 1000); //※乘以 1000 將秒轉換為毫秒
var dateString = date.toISOString().replace("T", " ").substr(0, 19);
updateContainer.innerHTML += `<p>[站點${message.SiteValues} ${dateString}] ${message.UserName}:${message.Message}</p>`;
});
// 7. 啟動
connection.start()
.then(() => {
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>
選擇一個自己熟悉的 Redis 工具,這邊用 AnotherRedisDesktopManager 檢視 Redis 內的資料
比對 Step 4:資料模型 中的資料結構,在 Redis 中保存 Json 格式的字串
將範例代碼部署到2個 Web 資料夾中
Web1的 appsetting.json 如下
Web2的 appsetting.json 如下
對 Web1 輸入指令
dotnet SingalRWebsiteUseScaleOutAndBackPlateRedisExample.dll --urls=http://localhost:6001
對 Web2 輸入指令
dotnet SingalRWebsiteUseScaleOutAndBackPlateRedisExample.dll--urls=http://localhost:6002
啟動 Nginx
※這邊的配置是基於上一篇 SLB + Session Sticky
進入配置好的首頁,並且開啟兩個分頁(第1個分頁連 web1 ; 第2個分頁連 web2)
輸入一些簡單文字,可以發現完成 SLB + Session Sticky 下的 SignalR 聊天室互動
未來要部署多個機器時,只要改一下 appsetting.json 的代號,即可部署完成
資料表的變化如下: