高效的SignalR连接管理

哈Ha哈卜bra 我目前正在开发基于SignalR库的聊天引擎。 除了沉浸在实时应用程序世界中的迷人过程之外,我还必须面对许多技术挑战。 关于其中一个,我想在本文中与您分享。

引言


什么是SignalR-它是WebSockets的一种基础, 长轮询服务器发送事件技术。 由于有了这个立面,您可以统一使用这些技术中的任何一种,而不必担心细节。 此外,借助长轮询技术,您可以支持由于某些原因而无法在Web套接字(例如IE-8)上工作的客户端。 外观由基于RPC的高级API表示。 另外,SignalR提供了根据“发布者-订阅者”的原理来构建通信的功能,在API术语中,这称为组。 这将进一步讨论。

挑战性


编程中最有趣的事情也许是解决非标准问题的能力。 今天,我们将指定这些任务之一,并考虑其解决方案。

在扩展思想,首先是横向思想发展的时代,主要挑战是需要拥有多个服务器。 指示库的开发人员已经可以应付此调用,可以在MSDN上找到解决方案的描述。 简而言之,提出了使用发布者-订阅者原理来同步服务器之间的调用。 每个服务器都订阅共享总线,并且从该服务器发送的所有命令都首先发送到总线。 此外,该命令适用于所有服务器,然后仅适用于客户端:

图片

重要的是要注意,连接到服务器的每个客户端都有其自己的唯一连接标识符-ConnectionId-并且所有消息最终都使用该标识符进行寻址。 因此,每个服务器都存储这些连接。

但是,由于未知原因,SignalR库API不提供对此数据的访问。 在这里,我们面临着访问这些连接的非常尖锐的问题。 这是我们的挑战。

为什么我们需要连接


如前所述,SignalR提供了发布者-订阅者模型。 在这里,消息路由的单位不是ConnectionId,而是一个组。 组是连接的集合。 通过向组发送消息,我们向该组中的所有ConnectionId发送消息。 建立组很方便-将客户端连接到服务器时,我们只需调用AddToGroupAsync API方法:

public override async Task OnConnectedAsync() { foreach (var chat in _options.Chats) await Groups.AddToGroupAsync(ConnectionId, chat); await Groups.AddToGroupAsync(ConnectionId, Client); } 

以及如何离开小组? 开发人员提供API方法RemoveFromGroupAsync

 public override async Task OnDisconnectedAsync(Exception exception) { foreach (var chat in _options.Chats) await Groups.RemoveFromGroupAsync(ConnectionId, chat); await Groups.RemoveFromGroupAsync(ConnectionId, Client); } 

请注意,数据单位为ConnectionId。 但是,从域模型的角度来看,ConnectionId不存在,但是有客户端。 在这方面,将映射到ConnectionId数组的客户端组织(反之亦然)分配给指定库的用户。

离开组时,将需要所有ConnectionId客户端的数组。 但是,这样的数组不存在。 您需要自己组织它。 在水平缩放系统的情况下,任务变得更加有趣。 在这种情况下,部分连接可以在一台服务器上,其余的可以在其他服务器上。

将客户端映射到连接的方法


有关MSDN的整个章节专门讨论此问题。 建议考虑以下方法:

  • 内存中存储
  • “用户组”
  • 永久外部存储

如何跟踪连接?
您可以使用OnConnectedAsyncOnDisconnectedAsync集线器方法跟踪连接。

立即,我注意到不考虑不支持缩放的选项。 这些选项包括将连接存储在服务器内存中的选项。 如果有其他服务器,则无法访问客户端连接。 存储在外部持久性存储中的选择与其缺点相关联,其中包括清理非活动连接的问题。 如果服务器硬重启,则会发生此类连接。 检测和清理这些连接不是一件容易的事。

在上述选项中,“用户组”选项很有趣。 简单性当然有其优势-不需要库和存储库。 同样重要的是这种方法的简单性的结果-可靠性。

但是Redis呢?
顺便说一句,使用Redis存储连接也是一个不好的选择。 组织内存中的数据存在一个严重的问题。 一方面,关键是客户,另一方面,是团队。

“用户组”


什么是“用户组”? 这是SignalR术语中的一个组,其中只有一个客户可以是客户-他本人。 这保证了两件事:

  1. 邮件将仅发送给一个人
  2. 消息将传递到所有人工设备

这将如何帮助我们? 让我提醒您,我们的挑战是解决客户离开小组的问题。 我们需要从一台设备离开该组,其余的也要取消订阅,但是我们没有该客户端的连接列表,只是从中启动退出的那个客户端。

“用户组”是解决此问题的第一步。 第二步是在客户端上构建“镜像”。 是的,是的,镜子。

镜子


从客户端发送到服务器的命令的来源是用户操作。 发布消息-向服务器发送命令:

 this.state.hubConnection .invoke('post', {message, group, nick}) .catch(err => console.error(err)); 

我们会将新帖子通知该群组的所有客户:

 public async Task PostMessage(PostMessage message) { await Clients.Group(message.Group).SendAsync("message", new { Message = message.Message, Group = message.Group, Nick = ClientNick }); } 

但是,必须在所有设备上同步执行许多命令。 如何实现呢? 具有一组连接并在特定客户端上为每个连接执行命令,或者使用下面描述的方法。 通过退出聊天考虑此方法。

从客户端到达的小组将首先进入“用户组”以寻求一种特殊方法,该方法会将其简单地重定向回服务器,即 “ 镜子 。” 因此,不是服务器将取消订阅设备,而是将要求设备本身取消订阅。

这是服务器聊天取消订阅命令的示例:

 public async Task LeaveChat(LeaveChatMessage message) { await Clients.OthersInGroup(message.Group).SendAsync("lost", new ClientCommand { Group = message.Group, Nick = Client }); await Clients.Group(Client).SendAsync("mirror", new MirrorChatCommand { Method = "unsubscribe", Payload = new UnsubscribeChatMessage { Group = message.Group } }); } 

 public async Task Unsubscribe(UnsubscribeChatMessage message) { await Groups.RemoveFromGroupAsync(ConnectionId, message.Group); } 

这是客户端代码:

 connection.on('mirror', (message) => { connection .invoke(message.method, message.payload) .catch(err => console.error(err)); }); 

让我们更详细地检查这里发生了什么:

  1. 客户端发起退订-将“ leave”命令发送到服务器
  2. 服务器将“取消订阅”命令发送到“镜像”上的“用户组”
  3. 该消息将传递到所有客户端设备。
  4. 使用服务器指定的方法将客户端上的消息发送回服务器
  5. 在每台服务器上,该客户端都不再订阅该组

结果,所有设备本身都将退订与其连接的服务器。 每个人都将取消订阅,我们不需要存储任何内容。 如果服务器硬重启,也不会出现任何问题。

那么为什么我们需要连接?


在客户端上具有“用户组”和“镜像”,从而无需使用连接。 亲爱的读者,您对此有何看法? 在评论中分享您的意见。

示例的源代码:

github.com/aesamson/signalr-server
github.com/aesamson/signalr-client

Source: https://habr.com/ru/post/zh-CN470299/


All Articles