Hola habrahabr. Actualmente estoy trabajando en un motor de chat basado en la biblioteca
SignalR . Además del fascinante proceso de inmersión en el mundo de las aplicaciones en tiempo real, también tuve que enfrentar una serie de desafíos técnicos. Sobre uno de ellos, quiero compartir con ustedes en este artículo.
Introduccion
Qué es SignalR: es una especie de fachada sobre
WebSockets ,
sondeo largo , tecnologías de
eventos de envío de servidor . Gracias a esta fachada, puede trabajar de manera uniforme con cualquiera de estas tecnologías y no preocuparse por los detalles. Además, gracias a la tecnología Long Polling, puede ayudar a los clientes que, por alguna razón, no pueden trabajar en sockets web, como IE-8. La fachada está representada por una API de alto nivel basada en
RPC . Además, SignalR ofrece construir comunicaciones de acuerdo con el principio de "editor-suscriptor", que en la terminología API se llama grupos. Esto se discutirá más a fondo.
Desafíos
Quizás lo más interesante en la programación es la capacidad de resolver problemas no estándar. Y hoy designaremos una de estas tareas y consideraremos su solución.
En la era del desarrollo de ideas de escalado y, en primer lugar, horizontal, el principal desafío es la necesidad de tener más de un servidor. Y los desarrolladores de la biblioteca indicada ya han respondido a esta llamada, se puede encontrar una descripción de la solución en
MSDN . En resumen, se propone, utilizando el principio editor-suscriptor, sincronizar las llamadas entre servidores. Cada servidor se suscribe a un bus compartido y todos los comandos
enviados desde este servidor se envían primero al bus. Además, el comando se aplica a todos los servidores y solo a los clientes:

Es importante tener en cuenta que cada cliente conectado al servidor tiene su propio identificador de conexión único,
ConnectionId , y todos los mensajes se abordan en última instancia utilizando este identificador. Por lo tanto, cada servidor almacena estas conexiones.
Sin embargo, por razones desconocidas, la API de la biblioteca SignalR no proporciona acceso a estos datos. Y aquí nos enfrentamos a una cuestión muy aguda de acceso a estas conexiones. Este es nuestro desafío.
¿Por qué necesitamos conectarnos?
Como se señaló anteriormente, SignalR ofrece un modelo de editor-suscriptor. Aquí, la unidad de enrutamiento de mensajes no es un
ConnectionId sino un grupo. Un grupo es una colección de conexiones. Al enviar un mensaje a un grupo, enviamos un mensaje a todos los ConnectionId que están en este grupo. Es conveniente crear grupos: al conectar un cliente al servidor, simplemente llamamos al método API
AddToGroupAsync :
public override async Task OnConnectedAsync() { foreach (var chat in _options.Chats) await Groups.AddToGroupAsync(ConnectionId, chat); await Groups.AddToGroupAsync(ConnectionId, Client); }
¿Y cómo dejar el grupo? Los desarrolladores ofrecen el método 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); }
Tenga en cuenta que la unidad de datos es ConnectionId. Sin embargo, desde el punto de vista del modelo de dominio, ConnectionId no existe, pero hay clientes. En este sentido, la organización de la asignación de clientes a la matriz ConnectionId y viceversa se asigna a los usuarios de la biblioteca especificada.
Es la matriz de todos los clientes ConnectionId que se necesita cuando abandona el grupo. Sin embargo, tal matriz no existe. Necesitas organizarlo tú mismo. La tarea se vuelve mucho más interesante en el caso de un sistema a escala horizontal. En este caso, parte de las conexiones pueden estar en un servidor, el resto en otros servidores.
Formas de asignar clientes a conexiones
Una sección completa en
MSDN está dedicada a este problema. Se proponen los siguientes métodos para su consideración:
- Almacenamiento en memoria
- "Grupo de usuarios"
- Almacenamiento externo permanente
¿Cómo rastrear las conexiones?Puede realizar un seguimiento de las conexiones utilizando los métodos del centro OnConnectedAsync y OnDisconnectedAsync .
Inmediatamente, observo que no se consideran las opciones que no admiten la escala. Estos incluyen la opción de almacenar conexiones en la memoria del servidor. No hay acceso a las conexiones del cliente en otros servidores, si hay alguno. La opción de almacenar en almacenamiento externo persistente está asociada con sus inconvenientes, que incluyen el problema de limpiar conexiones inactivas. Dichas conexiones se producen en caso de un reinicio completo del servidor. Detectar y limpiar estas conexiones no es una tarea trivial.
Entre las opciones anteriores, la opción "grupo de usuarios" es interesante. La simplicidad ciertamente se aplica a sus ventajas: no se requieren bibliotecas, repositorios. Igualmente importante es la consecuencia de la simplicidad de este método: la fiabilidad.
¿Pero qué hay de Redis?Por cierto, usar Redis para almacenar conexiones también es una mala opción. Existe un grave problema de organizar los datos en la memoria. Por un lado, la clave es el cliente, por otro, el grupo.
"Grupo de usuarios"
¿Qué es un "grupo de usuarios"? Este es un grupo en terminología de SignalR donde solo un cliente puede ser cliente: él mismo. Esto garantiza 2 cosas:
- Los mensajes se entregarán a una sola persona.
- Los mensajes se enviarán a todos los dispositivos humanos.
¿Cómo nos ayudará esto? Permítame recordarle que nuestro desafío es resolver el problema de dejar al cliente del grupo. Necesitábamos que, dejando el grupo desde un dispositivo, el resto también se daría de baja, pero no teníamos una lista de conexiones para este cliente, excepto para el que iniciamos la salida.
"Grupo de usuarios" es el primer paso para resolver este problema. El segundo paso es construir un "espejo" en el cliente. Sí, sí, espejos.
El espejo
La fuente de los comandos enviados desde el cliente al servidor son las acciones del usuario. Publique un mensaje: envíe un comando al servidor:
this.state.hubConnection .invoke('post', {message, group, nick}) .catch(err => console.error(err));
Y notificamos a todos los clientes del grupo sobre la nueva publicación:
public async Task PostMessage(PostMessage message) { await Clients.Group(message.Group).SendAsync("message", new { Message = message.Message, Group = message.Group, Nick = ClientNick }); }
Sin embargo, se deben ejecutar varios comandos sincrónicamente en todos los dispositivos. ¿Cómo lograr esto? Tenga una matriz de conexiones y ejecute un comando para cada conexión en un cliente específico, o utilice el método que se describe a continuación. Considere este método saliendo del chat.
El equipo que llega desde el cliente primero irá al "grupo de usuarios" para un método especial, que simplemente lo redirigirá al servidor, es decir. "
Espejos ". Por lo tanto, no el servidor cancelará la suscripción de los dispositivos, pero se les pedirá a los dispositivos mismos que se den de baja.
Aquí hay un ejemplo de un comando para darse de baja del chat del servidor:
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); }
Y aquí está el código del cliente:
connection.on('mirror', (message) => { connection .invoke(message.method, message.payload) .catch(err => console.error(err)); });
Examinemos con más detalle lo que está sucediendo aquí:
- El cliente inicia la cancelación de la suscripción: envía el comando "dejar" al servidor
- El servidor envía el comando "cancelar suscripción" al "grupo de usuarios" en el "espejo"
- El mensaje se entrega a todos los dispositivos del cliente.
- Un mensaje en el cliente se envía de vuelta al servidor utilizando el método especificado por el servidor
- En cada servidor, el cliente se da de baja del grupo
Como resultado, todos los dispositivos se darán de baja de los servidores a los que están conectados. Cada uno se dará de baja de la suya y no necesitamos almacenar nada. Tampoco surgirán problemas en el caso de un reinicio completo del servidor.
Entonces, ¿por qué necesitamos conectarnos?
Tener un "grupo de usuarios" y un "espejo" en el cliente elimina la necesidad de trabajar con conexiones. ¿Qué piensan, queridos lectores, sobre esto? Comparte tu opinión en los comentarios.
Código fuente para ejemplos:
github.com/aesamson/signalr-servergithub.com/aesamson/signalr-client