@

前言

今天我们来聊一聊一个Paas的方案,如何集成到一个既有的项目中。

以其中一个需求为例子:在产品项目中,加入IM(即时通信)功能,开始徒手撸代码,会发现工作量很大,去github找开源项目,结果也可能事与愿违:功能不够强大,或者用不同的语言编写的,编译出来程序集无法集成到项目中。

可能当下最好的方案是利用独立的聊天功能组件,作为项目的中间件(Paas方案)。

  1. 组件是独立部署,独立运行的,功能的稳定性,搭建速度快,
  2. 作为基础设施服务,可以用在其他项目中,并且项目中的对接作为抽象层,可随时替换现有组件。

这个聊天组件就是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方案有二:

  1. 前端鉴权,前端通过Abp登录后,调用/api/v1/login接口,返回token之后存入前端Token缓存中,之后前端将与Rocketchat直接建立websocket联系,订阅的聊天消息和房间消息将被直接推送至前端。

    优点是消息订阅推送直接,效率较高,但前端需要同时顾及Abp的鉴权和RocketChat Realtime API鉴权,前端的代码逻辑复杂,代理账号逻辑复杂,后期扩展性差。小型项目适合此方式

  2. 后端鉴权,前端通过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聊天机器人,欢迎大家评论留言!

最终效果如图

项目地址

Github:matoapp-samples

集成RocketChat至现有的.Net项目中,为ChatGPT铺路的更多相关文章

  1. 如何在现有的Vue项目中嵌入 Blazor项目?

    目前官方只提供了angular和react俩种示例,所以本教程将来讲解如何在Vue的现有项目中使用,上期已经做好了react的教材! 准备流程 Vue 项目创建流程 使用Vue创建一个Demo项目 全 ...

  2. 在Java项目中整合Scala

    Scala是一个运行在Java JVM上的面向对象的语言.它支持函数编程,在语法上比Java更加灵活,同时通过Akka库,Scala支持强大的基于Actor的多线程编程.具有这些优势,使得我最近很想在 ...

  3. 如何在Vue项目中使用Typescript

    0.前言 本快速入门指南将会教你如何在Vue项目中使用TypeScript进行开发.本指南非常灵活,它可以将TypeScript集成到现有的Vue项目中任何一个阶段. 1.初始化项目 首先,创建一个新 ...

  4. IOS-在ARC项目中使用非ARC框架或者类库

    1.在ARC项目中使用非ARC框架或者类库 IOS 4引入了Automatic Reference Count(ARC),编译器可以在编译时对obj-c对象进行内存管理. 之前,obj-c的内存管理方 ...

  5. 转:Qt项目中遇到的一些小问题汇总

    链接:http://blog.csdn.net/yangyunfeizj/article/details/7082001    作者:GoatYangYang 公司让负责qt界面开发,但是接触qt又不 ...

  6. 【主流技术】ElasticSearch 在 Spring 项目中的实践

    前言 ElasticSearch简称es,是一个开源的高扩展的分布式全文检索引擎. 它可以近乎实时的存储.检索数据,其扩展性很好,ElasticSearch是企业级应用中较为常见的技术. 下面和大家分 ...

  7. 在Windows上将ReactNative集成到现有的Android项目

    React Natvie的官方文档的 Integrating with Existing Apps 已经很详细地教我们如何将React Natvie集成到现在的Android项目.我根据官方文档的步骤 ...

  8. react native 之 在现有的iOS工程中集成react native

    在现有的iOS工程中集成react native, 或者说将react native引入到iOS 项目,是RN和iOS混合开发的必经之路 参考官网教程:https://reactnative.cn/d ...

  9. ionic+vue+capacitor系列笔记--02项目中集成Capacitor,添加android,ios平台,真机运行项目

    Capacitor是什么? Capacitor是由ionic团队开发的一款跨平台移动应用构建工具,可轻让我们轻松的构建Android.iOS.Electron和Web应用程序. Capacitor是A ...

  10. Atitit.mybatis的测试  以及spring与mybatis在本项目中的集成配置说明

    Atitit.mybatis的测试  以及spring与mybatis在本项目中的集成配置说明 1.1. Mybatis invoke1 1.2. Spring的数据源配置2 1.3. Mybatis ...

随机推荐

  1. Ubuntu 22.04 LTS 安装 0.A.D 实时策略游戏 并汉化

    众所周知,Linux生态中,能玩的正儿八经的大型游戏其实没几个,而 0.A.D 这个游戏就是这其中之一.这是一个类似于帝国时代的实时策略游戏,开源跨平台,这是其官方网站:https://play0ad ...

  2. 三步快速搭建Typora图床(SM.MS+PicGo)

    三步快速搭建Typora图床(基于SM.MS+PicGo) 前言 在有些同学使用Typora的过程中,会发现Typora不像Word一样,在文档脱离本机后依然正常显示图片,自己的tyopora文件在发 ...

  3. 如何取消磁盘的BitLocker加密

    步骤1:打开开始[win]菜单,点击齿轮图标,打开[设置] 步骤2:在Windows设置视窗中点击[更新和安全] 步骤3:点击左侧[设备加密],点击视窗右侧[关闭] 步骤4:将提示是否需要关闭设备加密 ...

  4. 【转载】EXCEL VBA 自定义排序的三种方法

    何谓自定义排序,就是按指定的顺序对数据源进行排序呗.   共分享了三种方法: 第1种方法是系统自带的OrderCustom,优点是代码简洁,缺点是自定义序列有字符长度限制(255个). 第2种方法是字 ...

  5. 你的 GitHub 年度报告「GitHub 热点速览 v.22.52」

    辞旧迎新的日子,又是年度报告满天飞的时候.GitHub 也不落其他平台之后,推出了用户 GitHub Contributions 报告.不知道,今年的你是不是比去年搬了更多的砖呢?在本期的 News ...

  6. Creator 2.x 升级 3.x 基础 API 差异总结

    上一篇我们介绍了 Cocos Creator 2.x 项目升级 3.x 的大流程. 但最后一步,还需要手动将之前 2.x 写的函数注释一处处的放开. 并将 2.x 的代码写法改成 3.x 的,下面我们 ...

  7. sikulix___自动化办公___重复性_机械性_的电脑操作___python脚本___Java运行环境下德jar包完成自动化测试相关___截图编程控制键盘鼠标

    sikulix___自动化办公___重复性_机械性_的电脑操作___python脚本___Java运行环境下德jar包完成自动化测试相关___截图编程控制键盘鼠标 应用场景: 公司内的大佬更改了xml ...

  8. 使用SQL4Automation让CodeSYS连接数据库

      摘要:本文旨在说明面向CodeSYS的数据库连接方案SQL4Automation的使用方法. 1.SQL4Automation简介 1.1.什么是SQL4Automation   SQL4Auto ...

  9. 基于 VScode 搭建 Verilog 自动格式化

    插件 Verilog-HDL/SystemVerilog/Bluespec SystemVerilog SystemVerilog and Verilog Formatter 工具 https://g ...

  10. css边框,盒子模型、浮动、定位

    边框,盒子模型.浮动.定位 一.边框 border-width : 边框宽度 border-style : 边框样式 1.solid 实线 2.none 无边框 3.dotted 点状虚线边框 4.d ...