集成RocketChat至现有的.Net项目中,为ChatGPT铺路

文章目录

    • 前言
    • 项目搭建
      • 后端
      • 前端
    • 代理账号
    • 鉴权方式介绍
    • 登录校验模块
      • 前端鉴权方式
    • 后端鉴权方式
    • 登录委托
    • 使用登录委托
    • 处理聊天消息
      • 前端鉴权方式
      • 后端校验方式
    • 项目地址

前言

今天我们来聊一聊一个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 = newMsgRawlet 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 = newMsgRawif (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.eventNamelet 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/17357.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

我发布了自己第一个由ChatGPT辅助开发的开源项目goattribute

需求产生 前两天在工作过程中又遇到了一直以来困惑我的一个问题&#xff0c;就是Go配置项的管理问题。 在开发一个新项目的时候&#xff0c;往往涉及到配置项的管理。个人小项目可能会通过配置文件来传入、环境变量来传入&#xff0c;也可能通过命令行参数来传入&#xff0c;公…

阿里自爆性能优化100+小技巧,Github已获赞68.7K

随着互联网飞速的发展&#xff0c;从4G到5G的全面过渡&#xff0c;深度学习性能优化&#xff0c;已经变成一个越来越重要的话题&#xff0c;从面试时的面试题都可以看出来了&#xff0c;所以今天就来分享一份Java性能优化100小技巧&#xff01; 本性能优化手册包含内容&#x…

在虚拟机上测试rm -rf 命令,自爆了

切记不要在任何正常机器上使用rm -rf / 或 rm -rf / * 命令&#xff01;&#xff01;&#xff01; 系统&#xff1a;CentOS-7-x86_64-DVD-2003.iso 开始测试&#xff1a; 现在很多操作系统&#xff0c;已经默认拒绝在 / 目录下执行递归删除操作了&#xff0c;这减小了一些风险。…

【UE】三步创建自动追踪自爆可造成伤害的敌人

效果 可以看到造成伤害时在右上角打印玩家当前的生命值 步骤 1. 首先拖入导航网格体边界体积 2. 首先复制一份“ThirdPersonCharacter”&#xff0c;命名为“ExplodingAI” 打开“ExplodingAI”&#xff0c;删除事件图表中所有节点 添加一个panw感应组件 在事件图表中添加如…

字节跳动技术总监自爆:mysql创建库books

算法 ⼏道常⻅的字符串算法题总结最⻓公共前缀回⽂串两数相加翻转链表链表中倒数第k个节点删除链表的倒数第N个节点合并两个排序的链表剑指offer部分编程题跳台阶问题变态跳台阶问题⼆维数组查找替换空格题⽬描述&#xff1a;数值的整数次⽅调整数组顺序使奇数位于偶数前⾯链表…

字节跳动技术总监自爆:mongodbmysql配合使用

算法 ⼏道常⻅的字符串算法题总结最⻓公共前缀回⽂串两数相加翻转链表链表中倒数第k个节点删除链表的倒数第N个节点合并两个排序的链表剑指offer部分编程题跳台阶问题变态跳台阶问题⼆维数组查找替换空格题⽬描述&#xff1a;数值的整数次⽅调整数组顺序使奇数位于偶数前⾯链表…

字节跳动技术总监自爆:微服务架构技术栈

一、对Kafka的认识 1.Kafka的基本概念 2.安装与配置 3.生产与消费 4.服务端参数配置 二、生产者 1.客户端开发 2.原理分析 3.重要的生产者参数 三、消费者 1.消费者与消费组

字节跳动技术总监自爆:大学javaweb课程

一、前言 最近刚读完一本书&#xff1a;《Netty、Zookeeper、Redis 并发实战》&#xff0c;个人觉得 Netty 部分是写得很不错的&#xff0c;读完之后又对 Netty 进行了一波很好的复习&#xff08;之前用 spring boot netty zookeeper 模仿 dubbo 做 rpc 框架&#xff0c;那时…

惊艳,阿里自爆用480页讲清楚了44种微服务架构设计模式

微服务架构设计 微服务的概念虽然直观易懂&#xff0c;但“细节是魔鬼”&#xff0c;微服务在实操落地的环节中存在诸多挑战。微服务也是可以成为企业转型的强力催化剂&#xff01; 随着网络基础设施的高速发展&#xff0c;以及越来越多的企业和组织需要通过互联网提供服务&a…

Pytorch 深度学习实战教程(六):仝卓自爆,快本打码。

本文 GitHub https://github.com/Jack-Cherish/PythonPark 已收录&#xff0c;有技术干货文章&#xff0c;整理的学习资料&#xff0c;一线大厂面试经验分享等&#xff0c;欢迎 Star 和 完善。 一、人脸识别 人脸识别是一门比较成熟的技术。 它的身影随处可见&#xff0c;刷脸…

阿里自爆十万字Java面试手抄本

金九银十就要来了&#xff0c;近两年企业越来越不好做&#xff0c;导致面试时对程序员的要求越来越高&#xff0c;越来越挑剔&#xff0c;除了掌握扎实的专业技能之外&#xff0c;你还需要一份Java程序员面试题汇总&#xff0c;才能在万千面试者中杀出重围&#xff0c;成功拿下…

玻璃绝缘子自爆图像数据集(3k多张,无标签)

下载地址&#xff1a; 玻璃绝缘子自爆图像数据集&#xff08;3k多张&#xff0c;无标签&#xff09;

2-9自报家底相关函数以及应用

1.自报家底 我们本篇文章&#xff0c;将讨论什么是自报家底&#xff0c;相关的函数又有哪些&#xff0c;它的基本使用是怎么样的&#xff0c;为什么自爆家底能够把所有能暴露的都暴露出来&#xff0c;它的原理是什么&#xff0c;我们又如何把用户名、数据库名&#xff0c;甚至…

【虚幻引擎】UE4/UE5科大讯飞文字合成语音

一、链接地址 链接&#xff1a;https://pan.baidu.com/s/15Qoc48x3DLpw4eW1qHXInQ 提取码&#xff1a;jqpx B站视频链接&#xff1a;https://space.bilibili.com/449549424?spm_id_from333.1007.0.0 二、案例介绍 第一步&#xff1a;首先进入讯飞开放平台注册一个账号&…

虚幻引擎C++开发学习(三)

这一章&#xff0c;我们要实现一个俯视视角的坦克小游戏&#xff0c;玩家可以操作坦克在地图中移动。敌人是固定的&#xff0c;但是具备一定的AI&#xff0c;可以瞄准玩家&#xff0c;并且在玩家进入攻击范围后&#xff0c;对玩家进行攻击。如果玩家被摧毁&#xff0c;则游戏结…

在虚幻引擎中创建大气的HIMIL电影作品

今天瑞云渲染小编给大家带来了关于电影制片人Tiziano Fioriti展示了《H I M I L》项目背后的工作流程&#xff0c;解释了人工智能是如何用于细节的&#xff0c;并谈到了设置火光的问题。 介绍 大家好&#xff0c;我叫Tiziano Fioriti&#xff0c;是来自意大利的自由电影制作人…

颠覆游戏开发,虚幻引擎 UE5 正式发布

整理 | 章雨铭 责编 | 屠敏 出品 | CSDN&#xff08;ID&#xff1a;CSDNnews&#xff09; 4月6日&#xff0c;UE5正式发布&#xff01; 体验完UE5的新功能后&#xff0c;3D游戏狂热爱好者感叹&#xff1a;“惊呆了&#xff0c;老铁&#xff01;”美工直呼&#xff1a;”工作量…

虚幻4引擎将至!从虚幻看游戏引擎发展

3D游戏引擎是个什么玩意 泡泡网显卡频道5月23日 在某游戏中的一个场景中&#xff0c;玩家控制的角色躲藏在屋子里&#xff0c;敌人正在屋子外面搜索玩家。突然&#xff0c;玩家控制的是一个穿迷彩服的士兵&#xff0c;突然碰倒了桌子上的一个杯子&#xff0c;杯子坠地发出破碎…

UE DTMqtt 虚幻引擎 Mqtt 客户端插件说明

目录 CreateMqttClient Connect Subscribe UnSubscribe Publish Disconnect BindConnectedDelegate BindConnectionLostDelegate BindMessageDelegate CreateMqttClient 创建一个Mqtt客户端对象 Connect 链接Mqtt服务器Subscribe 订阅消息频道UnSubscribe 取消订阅频道…

Unreal Engine 虚幻引擎 接入第三方SDK

前言 虚幻引擎对SDK接入有着一套专门的方式&#xff0c;本文主要描述了如何给使用虚幻引擎开发的项目接入第三方SDK,并分享了接入微信SDK的分享等基础功能的实践过程&#xff0c;还涉及到调试和提高整合SDK效率的一些方法。 SDK意义 SDK 广义是指Software Development Kit 即 应…