@
前言
今天我們來聊一聊一個Paas的方案,如何整合到一個既有的專案中。
以其中一個需求為例子:在產品專案中,加入IM(即時通訊)功能,開始徒手擼程式碼,會發現工作量很大,去github找開源專案,結果也可能事與願違:功能不夠強大,或者用不同的語言編寫的,編譯出來程式集無法整合到專案中。
可能當下最好的方案是利用獨立的聊天功能元件,作為專案的中介軟體(Paas方案)。
- 元件是獨立部署,獨立執行的,功能的穩定性,搭建速度快,
- 作為基礎設施服務,可以用在其他專案中,並且專案中的對接作為抽象層,可隨時替換現有元件。
這個聊天元件就是RocketChat。
RocketChat 是一款免費,開源的聊天軟體平臺。
其主要功能是:群組聊天、相互通訊、私密聊群、桌面通知、檔案上傳、語音/影片、截圖等,實現了使用者之間的實時訊息轉換。
https://github.com/RocketChat/Rocket.Chat
它本身是使用Meteor全棧框架以JavaScript開發的Web聊天伺服器。本身帶有一個精美的web端,甚至有開源的App端。
整合到一個既有的專案中我們是需要做減法的,然而在實際對接中,我們仍然需要解決一些問題:
首先是Rocket.Chat自己有一套獨立的使用者系統,其中登入鑑權邏輯,這一部分是我們不需要的。
第二是Rocket.Chat聊天功能依賴這個使用者系統,需要簡化流程同步使用者資訊,只保留使用者,不需要許可權,角色。
準備工作:搭建Rocket.Chat服務
Rocket.Chat有兩套Api,一個是基於https的REST Api,和一個基於wss的Realtime API, https://developer.rocket.chat/reference/api/realtime-api
這兩個Api都需要鑑權。
解決這個有兩套方案,一個是透過完全的後端接管,兩個Api都經過後端專案進行轉發,另一個是後端只接管REST Api, Realtime API和Rocket.Chat服務直接通訊
專案搭建
後端
新建一個.Net 6 Abp專案後,新增AbpBoilerplate.RocketChat庫,AbpBoilerplate.RocketChat的具體介紹請參考https://blog.csdn.net/jevonsflash/article/details/128342430
dotnet add package AbpBoilerplate.RocketChat
在Domain層中建立IM專案,建立Authorization目錄存放與IM鑑權相關的程式碼,ImWebSocket目錄用於存放處理Realtime API相關的程式碼.
在搭建Rocket.Chat環節,還記得有一個設定管理員的步驟嗎?在AdminUserName和AdminPassword配置中,指定這個管理員的密碼,
管理員用於在使用者未登入時,提供操作的許可權主體,
"Im": {
"Provider": "RocketChat",
"Address": "http://localhost:3000/",
"WebSocketAddress": "ws://localhost:3000/",
"AdminUserName": "super",
"AdminPassword": "123qwe",
"DefaultPassword": "123qwe"
}
前端
用vue2來搭建一個簡單的前端介面,需要用到以下庫
- element-UI庫
- axios
- vuex
- signalr
新建一個vue專案,在package.json中的 "dependencies"新增如下:
"axios": "^0.26.1",
"element-ui": "^2.15.6",
"@microsoft/signalr": "^5.0.6"
"vuex": "^3.6.2"
代理賬號
代理賬號是一個管理員賬號
在程式的啟動時,要登入這個管理員賬號,並儲存Token,程式停止時退出登入這個賬號。
我們需要一個cache儲存管理員賬號的登入資訊(使用者ID和Token)
在Threads目錄下建立ImAdminAgentAuthBackgroundWorker,
並在ImModule中註冊這個後臺任務
private async Task LoginAdminAgent()
{
var userName = rocketChatConfiguration.AdminUserName;
var password = rocketChatConfiguration.AdminPassword;
var loginResult = await imManager.Authenticate(userName, password);
if (loginResult.Success && loginResult.Content != null)
{
var cache = imAdminAgentCache.GetCache("ImAdminAgent");
await cache.SetAsync("UserId", loginResult.Content.Data.UserId);
await cache.SetAsync("AuthToken", loginResult.Content.Data.AuthToken);
await cache.SetAsync("UserName", userName);
}
else
{
throw new UserFriendlyException("無法登入IM服務Admin代理賬號");
}
}
public override async void Stop()
{
base.Stop();
var cache = imAdminAgentCache.GetCache("ImAdminAgent");
var token = (string)cache.Get("AuthToken", (i) => { return string.Empty; });
var userId = (string)cache.Get("UserId", (i) => { return string.Empty; });
if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(userId))
{
return;
}
using (_iocManager.IocContainer.BeginScope()) //extension method
{
_iocManager.Resolve<SessionContextDto>().Token = token;
_iocManager.Resolve<SessionContextDto>().UserId = userId;
_iocManager.Resolve<SessionContextDto>().IsAuthorized = true;
try
{
await imManager.Logout();
}
catch (Exception ex)
{
throw;
}
}
}
SessionContextDto是一個會話上下文物件,在.net專案中,登入校驗成功後寫入,在請求Rocket.Chat的時候讀取,並寫入到請求頭中。
在ImModule的PostInitialize方法中註冊ImAdminAgentAuthBackgroundWorker
public override void PostInitialize()
{
var workerManager = IocManager.Resolve<IBackgroundWorkerManager>();
workerManager.Add(IocManager.Resolve<ImAdminAgentAuthBackgroundWorker>());
}
使用者登入時,需要傳使用者名稱密碼,使用者名稱是跟.net專案中相同的,密碼可以獨立設定,也可以設定約定一個預設密碼,那麼新建使用者和登入的時候,可以不用傳密碼,直接使用預設密碼即可,使用者成功登入後,將使用者ID和Token回傳給前端。
定義傳輸物件類AuthenticateResultDto
public class AuthenticateResultDto
{
public string AccessToken { get; set; }
public string UserId { get; set; }
}
在應用層中建立類ImAppService,建立應用層服務Authenticate,用於使用者登入。
private async Task<AuthenticateResultDto> Authenticate(MatoAppSample.Authorization.Users.User user, string password = null)
{
var loginResult = await _imManager.Authenticate(user.UserName, password);
if (loginResult.Success)
{
var userId = loginResult.Content.Data.UserId;
var token = loginResult.Content.Data.AuthToken;
this.imAuthTokenCache.Set(user.UserName, new ImAuthTokenCacheItem(userId, token), new TimeSpan(1, 0, 0));
}
else
{
this.imAuthTokenCache.Remove(user.UserName);
throw new UserFriendlyException($"登入失敗, {loginResult.Error}");
}
return new AuthenticateResultDto
{
AccessToken = loginResult.Content.Data.AuthToken,
UserId = loginResult.Content.Data.UserId
};
}
鑑權方式介紹
由於Rocket.Chat的Realtime API基於REST API基礎上進行鑑權,在呼叫完成/api/v1/login
介面後,需要在已經建立的Websocket連線中傳送
{
"msg": "method",
"method": "login",
"id": "42",
"params":[
{ "resume": "auth-token" }
]
}
詳見官方文件
在整合RocketChat時,對於Realtime API方案有二:
-
前端鑑權,前端透過Abp登入後,呼叫
/api/v1/login
介面,返回token之後存入前端Token快取中,之後前端將與Rocketchat直接建立websocket聯絡,訂閱的聊天訊息和房間訊息將被直接推送至前端。優點是訊息訂閱推送直接,效率較高,但前端需要同時顧及Abp的鑑權和RocketChat Realtime API鑑權,前端的程式碼邏輯複雜,代理賬號邏輯複雜,後期擴充套件性差。小型專案適合此方式
-
後端鑑權,前端透過Abp登入後,呼叫
/api/v1/login
介面,返回token之後存入後端Token快取中,由後端發起websocket連線,訂閱的聊天訊息和房間訊息將被轉發成signalR訊息傳送給前端,由後端快取過期機制統一管理各連線的生命週期。優點是統一了前端的訊息推送機制,架構更趨於合理,對於多使用者端的大型專案,能夠減少前端不必要的程式碼邏輯。但是後端的程式碼會複雜一些。適合中大型專案。
Realtime API 的前端鑑權
Realtime API 的後端鑑權
登入校驗模組
前端鑑權方式
由於是從小程式,或者web端共用的所以要分別從Header和Cookie中獲取登入資訊,IHttpContextAccessor型別的引數用於從http請求上下文物件中訪問Header或Cookie,
整個流程如下:
建立AuthorizedFrontendWrapper.cs,新建AuthorizationVerification方法,此方法是登入校驗邏輯
private static void AuthorizationVerification(IHttpContextAccessor _httpContextAccessor, bool useAdminIfNotAuthorized, out StringValues? token, out StringValues? userId)
{
var isCommonUserLoginPassed = true;
token = _httpContextAccessor.HttpContext?.Request.Headers["X-Auth-Token"];
userId = _httpContextAccessor.HttpContext?.Request.Headers["X-User-Id"];
if (!ValidateToken(token, userId))
{
token = _httpContextAccessor.HttpContext?.Request.Cookies["chat_token"];
userId = _httpContextAccessor.HttpContext?.Request.Cookies["chat_uid"];
if (!ValidateToken(token, userId))
{
isCommonUserLoginPassed = false;
}
}
var cache = Manager.GetCache("ImAdminAgent");
if (!isCommonUserLoginPassed)
{
if (useAdminIfNotAuthorized)
{
//若不存在則取admin作為主體
token = (string)cache.Get("AuthToken", (i) => { return string.Empty; });
userId = (string)cache.Get("UserId", (i) => { return string.Empty; });
if (!ValidateToken(token, userId))
{
throw new UserFriendlyException("操作未取得IM服務授權, 當前使用者未登入,且初始代理使用者未登入");
}
}
else
{
throw new UserFriendlyException("操作未取得IM服務授權, 當前使用者未登入");
}
}
else
{
if ((string)cache.Get("UserId", (i) => { return string.Empty; }) == userId.Value)
{
token = (string)cache.Get("AuthToken", (i) => { return string.Empty; });
if (!ValidateToken(token, userId))
{
throw new UserFriendlyException("操作未取得IM服務授權, 初始代理使用者未登入");
}
}
}
}
後端鑑權方式
整個流程如下:
建立AuthorizedBackendWrapper.cs,新建AuthorizationVerification方法,登入校驗程式碼如下
public void AuthorizationVerification(out string token, out string userId)
{
User user = null;
try
{
user = userManager.FindByIdAsync(abpSession.GetUserId().ToString()).Result;
}
catch (Exception)
{
}
var userName = user != null ? user.UserName : rocketChatConfiguration.AdminUserName;
var password = user != null ? ImUserDefaultPassword : rocketChatConfiguration.AdminPassword;
var userIdAndToken = imAuthTokenCache.Get(userName, (i) => { return default; });
if (userIdAndToken == default)
{
var loginResult = imManager.Authenticate(userName, password).Result;
if (loginResult.Success && loginResult.Content != null)
{
userId = loginResult.Content.Data.UserId;
token = loginResult.Content.Data.AuthToken;
var imAuthTokenCacheItem = new ImAuthTokenCacheItem(userId, token);
imAuthTokenCache.Set(userName, imAuthTokenCacheItem, new TimeSpan(1, 0, 0));
var userIdentifier = abpSession.ToUserIdentifier();
if (userIdentifier != null)
{
Task.Run(async () =>
{
await Login(imAuthTokenCacheItem, userIdentifier, userName);
});
}
}
else
{
var adminUserName = rocketChatConfiguration.AdminUserName;
var adminPassword = rocketChatConfiguration.AdminPassword;
var adminLoginResult = imManager.Authenticate(adminUserName, adminPassword).Result;
if (adminLoginResult.Success && adminLoginResult.Content != null)
{
userId = adminLoginResult.Content.Data.UserId;
token = adminLoginResult.Content.Data.AuthToken;
if (!ValidateToken(token, userId))
{
throw new UserFriendlyException("操作未取得IM服務授權, 無法登入賬號" + userName);
}
}
else
{
throw new UserFriendlyException("賬號登入失敗:" + adminLoginResult.Error);
}
}
}
else
{
userId = userIdAndToken.UserId;
token = userIdAndToken.Token;
}
if (!ValidateToken(token, userId))
{
throw new UserFriendlyException("操作未取得IM服務授權, 登入失敗");
}
}
登入委託
在AuthorizedFrontendWrapper(或AuthorizedBackendWrapper)中
寫一個登入委託AuthorizedChatAction,用於包裝一個需要登入之後才能使用的操作
public static async Task AuthorizedChatAction(Func<Task> func, IocManager _iocManager)
{
if (_iocManager.IsRegistered<SessionContextDto>())
{
string token, userId;
AuthorizationVerification(out token, out userId);
using (_iocManager.IocContainer.Begin()) //extension method
{
_iocManager.Resolve<SessionContextDto>().Token = token;
_iocManager.Resolve<SessionContextDto>().UserId = userId;
_iocManager.Resolve<SessionContextDto>().IsAuthorized = true;
try
{
await func();
}
catch (Exception ex)
{
throw;
}
}
}
else
{
throw new UserFriendlyException("沒有註冊即時通訊會話上下文物件");
}
}
使用登入委託
我們在建立IM相關方法的時候,需要用AuthorizedFrontendWrapper(或AuthorizedBackendWrapper),來包裝登入校驗的邏輯。
public async Task<bool> DeleteUser(long userId)
{
var user = await _userManager.GetUserByIdAsync(userId);
var result = await AuthorizedBackendWrapper.AuthorizedChatAction(() =>
{
return _imManager.DeleteUser(user.UserName);
}, _iocManager);
if (!result.Success || !result.Content)
{
throw new UserFriendlyException($"刪除失敗, {result.Error}");
}
return result.Content;
}
處理聊天訊息
前端鑑權方式
新建messageHandler_frontend_auth.ts
處理程式
客戶端支援WebSocket的瀏覽器中,在建立socket後,可以透過onopen、onmessage、onclose和onerror四個事件對socket進行響應。
我已經封裝好了一個WebSocket 通訊模組\web\src\utils\socket.ts
,Socket物件是一個WebSocket抽象,後期將擴充套件到uniapp小程式專案上使用的WebSocket。透過這個物件可以方便的進行操作。
建立一個Socket物件wsConnection
,用於接收和傳送基於wss的Realtime API訊息
const wsRequestUrl: string = "ws://localhost:3000/websocket";
const socketOpt: ISocketOption = {
server: wsRequestUrl,
reconnect: true,
reconnectDelay: 2000,
};
const wsConnection: Socket = new Socket(socketOpt);
WebSocket的所有操作都是採用事件的方式觸發的,這樣不會阻塞UI,是的UI有更快的響應時間,有更好的使用者體驗。
連線建立後,客戶端和伺服器就可以透過TCP連線直接交換資料。我們訂閱onmessage事件觸發newMsgHandler處理資訊
wsConnection.$on("message", newMsgHandler);
當連結開啟後,立即傳送{"msg":"connect","version":"1","support":["1","pre2","pre1"]}
報文
wsConnection.$on("open", (newMsg) => {
console.info("WebSocket Connected");
wsConnection.send({
msg: "connect",
version: "1",
support: ["1"],
});
});
建立連結後,會從Rocket.Chat收到connected訊息,此時需要傳送登入請求的訊息到Rocket.Chat
接收到報文
"{"msg":"connected","session":"cMvzWpCNSCR24bwCf"}"
傳送報文
{"msg":"method","method":"login","params":[{"resume":"wY67O8rJFyf2FrqD5vxpQjIUs5tdThmyfW_VaA7MrsG"}],"id":"1"}
接下來,在newMsgHandler方法中,根據msg型別,處理一系列的訊息
const newMsgHandler: Function = (newMsgRaw) => {
if (!getIsNull(newMsgRaw)) {
if (newMsgRaw.msg == "ping") {
wsConnection.send({
msg: "pong",
});
} else if (newMsgRaw.msg == "connected") {
let newMsg: ConnectedWsDto = newMsgRaw
let session = newMsg.session;
if (
wsConnection.isConnected
) {
wsConnection.send({
msg: "method",
method: "login",
params: [
{
resume: UserModule.chatToken,
},
],
id: "1",
});
}
} else if (newMsgRaw.msg == "added") {
subEvent("stream-notify-user", "message");
subEvent("stream-notify-user", "subscriptions-changed");
subEvent("stream-notify-user", "rooms-changed");
} else if (newMsgRaw.msg == "changed") {
let newMsg: SubChangedWsDto = newMsgRaw
if (newMsg.collection == "stream-notify-user") {
let fields = newMsg.fields;
if (fields.eventName.indexOf("/") != -1) {
let id = fields.eventName.split('/')[0];
let eventName = fields.eventName.split('/')[1];
if (eventName == "subscriptions-changed") {
let args = fields.args;
let msg: ISubscription = null;
let method: string;
args.forEach((arg) => {
if (typeof arg == "string") {
if (arg == "remove" || arg == "insert") {
method = arg;
}
}
else if (typeof arg == "object") {
msg = arg
}
});
$EventBus.$emit("getRoomSubscriptionChangedNotification", { msg, method });
}
else if (eventName == "rooms-changed") {
let args = fields.args;
let msg: RoomMessageNotificationDto = null;
args.forEach((arg) => {
if (typeof arg == "object") {
msg = arg
}
});
$EventBus.$emit("getRoomMessageNotification", msg.lastMessage);
}
}
else {
let id = fields.eventName
}
}
else if (newMsg.collection == "stream-room-messages") {
let fields = newMsg.fields;
let id = fields.eventName
let msg: MessageItemDto = fields.args;
$EventBus.$emit("getRoomMessageNotification", msg);
}
}
}
}
store/chat.ts檔案中,定義了ChatState用於儲存聊天資訊,當有訊息收到,或者房間資訊變更時,更新這些儲存物件
export interface IChatState {
currentChannel: ChannelDto;
channelList: Array<ChannelDto>;
currentMessage: MessageDto;
}
後端校驗方式
Login時將生成webSocket物件,併傳送connect訊息
public async Task Login(ImAuthTokenCacheItem imAuthTokenCacheItem, UserIdentifier userIdentifier, string userName)
{
using (var webSocket = new ClientWebSocket())
{
webSocket.Options.RemoteCertificateValidationCallback = delegate { return true; };
var url = Flurl.Url.Combine(rocketChatConfiguration.WebSocketHost, "websocket");
await webSocket.ConnectAsync(new Uri(url), CancellationToken.None);
if (webSocket.State == WebSocketState.Open)
{
var model = new ImWebSocketConnectRequest()
{
Msg = "connect",
Version = "1",
Support = new string[] { "1" }
};
var jsonStr = JsonConvert.SerializeObject(model);
var sendStr = Encoding.UTF8.GetBytes(jsonStr);
await webSocket.SendAsync(sendStr, WebSocketMessageType.Text, true, CancellationToken.None);
await Echo(webSocket, imAuthTokenCacheItem, userIdentifier, userName);
}
}
}
每次接收指令時,將判斷快取中的Token值是否合法,若不存在,或過期(session變化),將主動斷開websocket連線
在接收Realtime API訊息後,解析方式同前端鑑權邏輯
在拿到資料後,做signalR轉發。
private async Task Echo(WebSocket webSocket, ImAuthTokenCacheItem imAuthTokenCacheItem, UserIdentifier userIdentifier, string userName)
{
JsonSerializerSettings serializerSettings = new JsonSerializerSettings()
{
NullValueHandling = NullValueHandling.Ignore
};
var buffer = new byte[1024 * 4];
var receiveResult = await webSocket.ReceiveAsync(
new ArraySegment<byte>(buffer), CancellationToken.None);
string session=string.Empty;
ImAuthTokenCacheItem im;
while (!receiveResult.CloseStatus.HasValue)
{
im = imAuthTokenCache.GetOrDefault(userName);
if (im == null)
{
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure,
"快取超時自動退出",
CancellationToken.None);
Console.WriteLine(userName + "超時主動斷開IM連線");
break;
}
else
{
if (!string.IsNullOrEmpty(session) && im.Session!=session)
{
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure,
"快取更新自動退出",
CancellationToken.None);
Console.WriteLine(userName + "快取更新主動斷開IM連線");
break;
}
}
var text = Encoding.UTF8.GetString(buffer.AsSpan(0, receiveResult.Count));
if (!string.IsNullOrEmpty(text))
{
dynamic response = JsonConvert.DeserializeObject<dynamic>(text);
if (response.msg == "ping")
{
var model = new ImWebSocketCommandRequest()
{
Msg = "pong",
};
var jsonStr = JsonConvert.SerializeObject(model, serializerSettings);
var sendStr = Encoding.UTF8.GetBytes(jsonStr);
await webSocket.SendAsync(sendStr, WebSocketMessageType.Text, true, CancellationToken.None);
}
if (response.msg == "connected")
{
session = response.session;
var model = new ImWebSocketCommandRequest()
{
Msg = "method",
Method = "login",
Params = new object[]{
new {
resume = imAuthTokenCacheItem.Token,
}
},
Id = "1"
};
imAuthTokenCacheItem.Session = session;
imAuthTokenCache.Set(userName, imAuthTokenCacheItem, new TimeSpan(1, 0, 0));
var jsonStr = JsonConvert.SerializeObject(model, serializerSettings);
var sendStr = Encoding.UTF8.GetBytes(jsonStr);
await webSocket.SendAsync(sendStr, WebSocketMessageType.Text, true, CancellationToken.None);
}
else if (response.msg == "added")
{
await SubEvent(webSocket, imAuthTokenCacheItem, "stream-notify-user", "message");
await SubEvent(webSocket, imAuthTokenCacheItem, "stream-notify-user", "subscriptions-changed");
await SubEvent(webSocket, imAuthTokenCacheItem, "stream-notify-user", "rooms-changed");
}
else if (response.msg == "changed")
{
var newMsg = response;
if (newMsg.collection == "stream-notify-user")
{
var fields = newMsg.fields;
var fullEventName = fields.eventName.ToString();
if (fullEventName.IndexOf("/") != -1)
{
var id = fullEventName.Split('/')[0];
var eventName = fullEventName.Split('/')[1];
if (eventName == "subscriptions-changed")
{
var args = fields.args;
dynamic msg = null;
var method = string.Empty;
foreach (var arg in args as IEnumerable<dynamic>)
{
if (arg.ToString() == "remove" || arg.ToString() == "insert")
{
method = arg.ToString();
}
else
{
msg = arg;
}
}
await signalREventPublisher.PublishAsync(userIdentifier, "getRoomSubscriptionChangedNotification", new { msg, method });
}
else if (eventName == "rooms-changed")
{
var args = fields.args;
dynamic msg = null;
var method = string.Empty;
foreach (var arg in args as IEnumerable<dynamic>)
{
if (arg.ToString() == "updated")
{
method = arg.ToString();
}
else
{
msg = arg;
}
};
var jobject = msg.lastMessage as JObject;
await signalREventPublisher.PublishAsync(userIdentifier, "getRoomMessageNotification", jobject);
}
}
else
{
var id = fields.eventName;
}
}
}
else if (response.collection == "stream-room-messages")
{
var fields = response.fields;
var id = fields.eventName;
var msg = fields.args;
var jobject = msg as JObject;
await signalREventPublisher.PublishAsync(userIdentifier, "getRoomMessageNotification", jobject);
}
}
try
{
receiveResult = await webSocket.ReceiveAsync(
new ArraySegment<byte>(buffer), CancellationToken.None);
}
catch (Exception ex)
{
Console.WriteLine(userName + "異常斷開IM連線");
break;
}
}
try
{
await webSocket.CloseAsync(
receiveResult.CloseStatus.Value,
receiveResult.CloseStatusDescription,
CancellationToken.None);
}
catch (Exception ex)
{
}
imAuthTokenCache.Remove(userName);
}
private async Task SubEvent(WebSocket webSocket, ImAuthTokenCacheItem imAuthTokenCacheItem, string name, string type)
{
var eventstr = $"{imAuthTokenCacheItem.UserId}/${type}";
var id = RandomHelper.GetRandom(100000).ToString().PadRight(5, '0');
var model = new ImWebSocketCommandRequest()
{
Msg = "sub",
Params = new object[]{eventstr,
new {
useCollection= false,
args = new string[]{ }
}
},
Id = id,
Name = name,
};
var jsonStr = JsonConvert.SerializeObject(model);
var sendStr = Encoding.UTF8.GetBytes(jsonStr);
await webSocket.SendAsync(sendStr, WebSocketMessageType.Text, true, CancellationToken.None);
}
SignalREventPublisher.cs 中的PublishAsync,將訊息轉發給對應的使用者。
public async Task PublishAsync(IUserIdentifier userIdentifier, string method, object message)
{
try
{
var onlineClients = _onlineClientManager.GetAllByUserId(userIdentifier);
foreach (var onlineClient in onlineClients)
{
var signalRClient = _hubContext.Clients.Client(onlineClient.ConnectionId);
if (signalRClient == null)
{
Logger.Debug("Can not get user " + userIdentifier.ToUserIdentifier() + " with connectionId " + onlineClient.ConnectionId + " from SignalR hub!");
continue;
}
await signalRClient.SendAsync(method, message);
}
}
catch (Exception ex)
{
Logger.Warn("Could not send notification to user: " + userIdentifier.ToUserIdentifier());
Logger.Warn(ex.ToString(), ex);
}
}
前端程式碼則要簡單得多
新建messageHandler_backend_auth.ts
處理程式
import * as signalR from "@microsoft/signalr";
建立一個HubConnection物件hubConnection
,用於接收SignalR訊息
const baseURL = "http://localhost:44311/"; // url = base url + request url
const requestUrl = "signalr";
let header = {};
if (UserModule.token) {
header = {
"X-XSRF-TOKEN": UserModule.token,
Authorization: "Bearer " + UserModule.token,
};
}
//signalR config
const hubConnection: signalR.HubConnection = new signalR.HubConnectionBuilder()
.withUrl(baseURL + requestUrl, {
headers: header,
accessTokenFactory: () => getAccessToken(),
transport: signalR.HttpTransportType.WebSockets,
logMessageContent: true,
logger: signalR.LogLevel.Trace,
})
.withAutomaticReconnect()
.withHubProtocol(new signalR.JsonHubProtocol())
.build();
我們只需要響應後端程式中定義好的signalR訊息的methodName就可以了
hubConnection.on("getRoomMessageNotification", (n: MessageItemDto) => {
console.info(n.msg)
if (ChatModule.currentChannel._id != n.rid) {
ChatModule.increaseChannelUnread(n.rid);
} else {
if (n.t == null) {
n.from =
n.u.username == UserModule.userName
? constant.MSG_FROM_SELF
: constant.MSG_FROM_OPPOSITE;
} else {
n.from = constant.MSG_FROM_SYSTEM;
}
ChatModule.appendMessage(n);
}
});
hubConnection.on("getRoomSubscriptionChangedNotification", (n) => {
console.info(n.method, n.msg)
if (n.method == "insert") {
console.info(n.msg + "has been inserted!");
ChatModule.insertChannel(n.msg);
}
else if (n.method == "update") {
}
});
至此,完成了所有的整合工作。
此文目的是介紹一種思路,使用快取生命週期管理的相關機制,規避第三方使用者系統對現有專案的使用者系統的影響。舉一反三,可以用到其他Paas的方案整合中。最近ChatGPT很火,可惜沒時間研究怎麼接入,有閒工夫的同學們可以嘗試著寫一個ChatGPT聊天機器人,歡迎大家評論留言!
最終效果如圖