Концентратор SignalR может транслировать сообщение всем подключенным клиентам. Но это далеко не единственный способ использования SignalR. Хаб позволяет отправлять сообщения отдельным клиентам. Вы также можете группировать клиентов и отправлять сообщения определенным группам клиентов. И об этом мы сегодня и поговорим.

Но даже если вы рассылаете сообщения всем клиентам, вам не обязательно включать клиента, от которого исходит сообщение. В конце концов, клиент уже знает, что это за сообщение. Таким образом, вы можете исключить его из списка получателей.

Итак, в этой статье затронуты следующие темы:

  • Рассылка сообщений всем клиентам
  • Отправка сообщений определенным клиентам
  • Работа с клиентскими группами.

К концу этой статьи вы узнаете, как выбирать клиентов, которым вы хотите отправлять сообщения из концентратора SignalR.

Предпосылки

Чтобы следовать примерам из этой статьи, вам понадобится следующее:

  • Компьютер с операционной системой Windows, Mac OS или Linux.
  • Подходящая IDE или редактор кода (Visual Studio, JetBrains Rider или VS Code)
  • .NET 6 SDK (или новее)

Кроме того, поскольку мы продолжаем строить поверх приложения, которое мы разработали в предыдущей статье, нам нужен код, который мы написали ранее. Если вы пропустили предыдущую главу, вы можете получить доступ к полному коду из следующего места в репозитории GitHub:

https://github.com/fiodarsazanavets/SignalR-on-.NET-6-the-complete-guide/tree/main/Chapter-04/Part-03/LearningSignalR

Полные примеры кода из этой статьи доступны по следующему адресу в репозитории GitHub, где есть отдельные папки, соответствующие отдельным частям статьи:

https://github.com/fiodarsazanavets/SignalR-on-.NET-6-the-complete-guide/tree/main/Chapter-05

Итак, начнем.

Рассылка сообщений всем клиентам

В предыдущей статье мы уже транслировали сообщения SignalR всем подключенным клиентам. Это было достигнуто путем вызова свойства Clients.All в концентраторе на стороне сервера. Но этот способ передачи сообщения имеет свои ограничения. Что делать, если вы хотите исключить клиента, отправившего сообщение, из списка его получателей?

В SignalR этого можно добиться, используя Clients.Others вместо Clients.All. И это то, что мы сейчас будем реализовывать.

Откройте файл LearningHub.cs в проекте SignalRServer и добавьте в класс следующий метод:

public async Task SendToOthers(string message)
{
    await Clients.Others.ReceiveMessage(message);
}

Итак, как видите, этот метод идентичен BroadcastMessage, который мы использовали ранее, за исключением одной маленькой детали. И теперь нам нужно, чтобы наши клиенты звонили ему.

Применение изменений к клиенту JavaScript

Сначала мы изменим наш клиент JavaScript. И начнем мы с добавления новых элементов управления в разметку страницы. давайте откроем файл Index.cshtml, который находится в папке Home внутри папки Views. Мы найдем элемент div со значением атрибута class, установленным на col-md-4, и вставим в элемент следующую разметку:

<div class="control-group">
    <div>
        <label for="others-message">Message</label>
        <input type="text" id="others-message" name="others-message" />
    </div>
    <button id="btn-others-message">Send to Others</button>
</div>

Теперь нам нужно добавить JavaScript, который будет запускать только что добавленная кнопка для выполнения соответствующего вызова. Для этого мы откроем файл site.js, который находится внутри папки js папки wwwroot. Мы добавим в файл следующий обработчик события click:

$('#btn-others-message').click(function () {
    var message = $('#others-message').val();
    connection.invoke("SendToOthers", message).catch(err => console.error(err.toString()));
});

Наш клиент JavaScript готов. Далее нам нужно применить изменения и к нашему клиенту .NET. Мы не будем изменять все клиенты, так как проекты BlazorClient и DotnetClient основаны на одних и тех же технологиях и используют одни и те же библиотеки. Итак, мы выберем проект DotnetClient для изменения, так как он больше отличается от клиента JavaScript, который мы уже обновили.

Обновление клиента .NET

В проекте DotnetClient откройте файл Program.cs и замените содержимое блока try следующим:

await hubConnection.StartAsync();
var running = true;
while (running)
{
    var message = string.Empty;
Console.WriteLine("Please specify the action:");
    Console.WriteLine("0 - broadcast to all");
    Console.WriteLine("1 - send to others");
    Console.WriteLine("exit - Exit the program");
var action = Console.ReadLine();
Console.WriteLine("Please specify the message:");
    message = Console.ReadLine();
switch (action)
    {
        case "0":
            await hubConnection.SendAsync("BroadcastMessage", message);
            break;
        case "1":
            await hubConnection.SendAsync("SendToOthers", message);
            break;
        case "exit":
            running = false;
            break;
        default:
            Console.WriteLine("Invalid action specified");
            break;
    }
}

Итак, теперь у нас есть два действия — исходный вызов метода концентратора BroadcastMessage и вызов метода SendToOthers. Вы можете выбрать соответствующее действие, введя в консоли 0 или 1.

Теперь оба наших клиента готовы. Давайте запустим наше приложение и посмотрим его в действии.

Тестирование эксклюзивного вещательного функционала

Когда оба ваших приложения будут запущены и запущены, вам потребуется вручную подключить приложение DotnetClient к концентратору SignalR на SignalRServer. Для этого в консоли введите адрес приложения, который можно найти в записи applicationUrl в файле launchSettings.json на SignalRServer проект. Затем добавьте к URL-адресу путь learningHub. Таким образом, если URL-адрес вашего приложения https://localhost:7128, вам необходимо указать адрес https://localhost:7128/learningHub.

Теперь, открыв домашнюю страницу веб-приложения в браузере и подключив консольное приложение к концентратору SignalR, вы можете проверить, как работают разные конечные точки SignalR. Если вы выберете действие, которое запускает метод SendToOthers в концентраторе, ваш исходный клиент не получит сообщение, но ваш другой клиент получит его. Однако если вы активируете метод BroadcastMessage, оба ваших клиента получат сообщение. Это хорошо видно на следующем скриншоте:

Далее вы узнаете, как отправлять сообщения отдельным клиентам, а не просто транслировать их всем.

Отправка сообщений определенным клиентам

Существует множество различных причин, по которым вы хотите отправить сообщение только тому клиенту, который активировал определенный метод. Например, эта функциональность будет использоваться в любом сценарии, где уместна модель обмена сообщениями запрос-ответ. Давайте теперь применим эту способность, чтобы увидеть ее в действии.

Включение сообщений самому себе

Чтобы использовать свойство Caller, мы добавим следующий метод в класс LearningHub проекта SignalRServer:

public async Task SendToCaller(string message)
{
    await Clients.Caller.ReceiveMessage(GetMessageToSend(message));
}

Затем мы добавим следующую разметку к элементу div с классом col-md-4 внутри файла Index.cshtml в Home. внутри папки Views:

<div class="control-group">
    <div>
        <label for="self-message">Message</label>
        <input type="text" id="self-message" name="self-message" />
    </div>
    <button id="btn-self-message">Send to Self</button>
</div>

Затем мы добавим следующий обработчик события click в файл site.js:

$('#btn-self-message').click(function () {
    var message = $('#self-message').val();
    connection.invoke("SendToCaller", message).catch(err => console.error(err.toString()));
});

И, наконец, мы заменим содержимое инструкции `while` внутри файла Program.cs DotnetClient следующим:

var message = string.Empty;
Console.WriteLine("Please specify the action:");
Console.WriteLine("0 - broadcast to all");
Console.WriteLine("1 - send to others");
Console.WriteLine("2 - send to self");
Console.WriteLine("exit - Exit the program");
var action = Console.ReadLine();
Console.WriteLine("Please specify the message:");
message = Console.ReadLine();
switch (action)
{
    case "0":
        await hubConnection.SendAsync("BroadcastMessage", message);
        break;
    case "1":
        await hubConnection.SendAsync("SendToOthers", message);
        break;
    case "2":
        await hubConnection.SendAsync("SendToCaller", message);
        break;
    case "exit":
        running = false;
        break;
    default:
        Console.WriteLine("Invalid action specified");
        break;
}

Теперь, если мы запустим приложения и попытаемся использовать метод SendToCaller с любого из клиентов, мы увидим, что только сам клиент получает сообщение. Другой клиент ничего не получает, как видно на следующем снимке экрана:

Но отправка сообщения самому себе — не единственный способ отправки сообщений отдельным клиентам в концентраторе SignalR. Фактически вы можете идентифицировать конкретного клиента (или несколько клиентов) и отправлять им сообщения. И это то, что мы рассмотрим далее.

Отправка сообщений другим клиентам

SignalR Hub имеет свойство под названием Context. Это свойство представляет контекст текущего подключения и содержит некоторые связанные с ним метаданные. Например, если вы подключаетесь как аутентифицированный пользователь (о чем мы поговорим в следующей статье), вы сможете получить информацию о пользователе из этого свойства.

Одно из свойств ` Context` называется ConnectionId. И это свойство, которое содержит автоматически сгенерированную строку, представляющую уникальный идентификатор текущего подключения клиента. Если вы знаете уникальный идентификатор любого конкретного клиентского соединения, вы сможете отправлять сообщения конкретным клиентам. И это то, что мы будем делать дальше.

Изменение концентратора SignalR

Мы начнем с добавления следующего метода в класс LearningHub проекта SignalRServer:

private string GetMessageToSend(string originalMessage)
{
    return $"User connection id: {Context.ConnectionId}. Message: {originalMessage}";
}

Context.ConnectionId покажет идентификатор подключения текущего клиента, чтобы другие клиенты могли отправлять ему сообщения. Затем мы будем использовать этот метод во всех наших клиентских вызовах, заменив все экземпляры ReceiveMessage(message) следующим:

ReceiveMessage(GetMessageToSend(message))

Затем мы добавим в хаб следующий метод:

public async Task SendToIndividual(string connectionId, string message)
{
    await Clients.Client(connectionId)
            .ReceiveMessage(GetMessageToSend(message));
}

В этом методе мы выбираем конкретного клиента, передавая идентификатор подключения в метод Client свойства Clients. Но мы также можем выбрать более одного клиента для отправки сообщения. Существует также метод Clients для свойства Clients. И этот метод позволяет вам использовать либо одну строку идентификатора соединения, либо их набор. Например, если бы у нас было несколько идентификаторов подключения, которым мы хотели отправить сообщение, мы могли бы сохранить их в переменной connectionIds, представляющей коллекцию C# (например, List<string>). При этом мы могли бы реализовать следующий вызов:

await Clients.Clients(connectionIds)
    .ReceiveMessage(GetMessageToSend(message));

Теперь мы добавим необходимые компоненты для обоих наших клиентов.

Изменение клиентов

В разметку класса Index.cshtml мы вставим следующую разметку рядом с другими контрольными группами:

<div class="control-group">
    <div>
        <label for="individual-message">Message</label>
        <input type="text" id="individual-message" name="individual-message" />
    </div>
    <div>
        <label for="connection-for-message">User connection id:</label>
        <input type="text" id="connection-for-message" name="connection-for-message" />
    </div>
    <button id="btn-individual-message">Send to Specific User</button>
</div>

Затем мы добавим следующий обработчик события click в файл site.js:

$('#btn-individual-message').click(function () {
    var message = $('#individual-message').val();
    var connectionId = $('#connection-for-message').val();
    connection.invoke("SendToIndividual", connectionId, message).catch(err => console.error(err.toString()));
});

Наконец, мы заменим содержимое оператора while в файле Program.cs DotnetClient следующим:

var message = string.Empty;
Console.WriteLine("Please specify the action:");
Console.WriteLine("0 - broadcast to all");
Console.WriteLine("1 - send to others");
Console.WriteLine("2 - send to self");
Console.WriteLine("3 - send to individual");
Console.WriteLine("exit - Exit the program");
var action = Console.ReadLine();
Console.WriteLine("Please specify the message:");
message = Console.ReadLine();
switch (action)
{
    case "0":
        await hubConnection.SendAsync("BroadcastMessage", message);
        break;
    case "1":
        await hubConnection.SendAsync("SendToOthers", message);
        break;
    case "2":
        await hubConnection.SendAsync("SendToCaller", message);
        break;
    case "3":
        Console.WriteLine("Please specify the connection id:");
        var connectionId = Console.ReadLine();
        await hubConnection.SendAsync("SendToIndividual", connectionId, message);
        break;
    case "exit":
        running = false;
        break;
    default:
        Console.WriteLine("Invalid action specified");
        break;
}

Теперь давайте запустим наши приложения, чтобы посмотреть, как они работают.

Просмотр отдельных клиентских сообщений в действии

После запуска наших приложений и подключения приложения DotnetClient к концентратору SignalR мы можем сначала отправить начальное сообщение от любого из клиентов, чтобы получить его идентификатор подключения. Затем мы можем отправить ему сообщение от другого клиента, указав этот идентификатор соединения. Это демонстрирует следующий скриншот:

Но к настоящему времени вы, вероятно, заметили проблему с этим подходом. Несмотря на то, что идентификатор подключения надежно идентифицирует отдельного клиента, с точки зрения пользователя его использование не очень удобно. Кроме того, если клиент отключится, а затем снова подключится, идентификатор подключения изменится, поэтому вы больше не сможете отправлять сообщения клиенту, пока не узнаете обновленное значение идентификатора подключения.

Но есть решение для этого. Вы можете сопоставить идентификатор соединения с каким-нибудь красивым понятным для человека именем. Вы можете использовать свой собственный словарь в бэкенде сервера, что будет лучшим решением при определенных обстоятельствах. Но во многих случаях для этого можно просто использовать встроенный механизм, который представляет собой клиентские группы SignalR.

Работа с клиентскими группами.

В SignalR Hub есть свойство Группы, которое позволяет добавлять клиентов в группу и удалять из нее клиентов. Каждая группа представляет собой отношение «один ко многим» между произвольным именем группы и набором идентификаторов соединения. Свойство Clients базового класса Hub имеет метод Group, который позволяет указать имя группы и отправить сообщение всем клиентам в группа.

Группы полезны во многих сценариях. Вы можете использовать их для назначения всех клиентов определенной категории в группу. Или вы можете просто связать группу с конкретным пользователем. Тогда вы не потеряете след подключенного клиента, представляющего пользователя. Имя пользователя, которое вы укажете, всегда будет соответствовать правильному идентификатору соединения, пока клиент подключен. Кроме того, у отдельного пользователя может быть одновременно подключено несколько клиентов. Например, вы можете использовать одну и ту же платформу социальной сети на нескольких вкладках браузера, одновременно открывая ее в мобильном приложении.

Итак, давайте изменим наш хаб, чтобы увидеть, как можно использовать группы.

Использование групп SignalR внутри концентратора

Мы начнем с добавления следующих методов в класс LearningHub проекта SignalRServer:

public async Task SendToGroup(string groupName, string message)
{
    await Clients.Group(groupName).ReceiveMessage(GetMessageToSend(message));
}
public async Task AddUserToGroup(string groupName)
{
    await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
    await Clients.Caller.ReceiveMessage($"Current user added to {groupName} group");
    await Clients.Others.ReceiveMessage($"User {Context.ConnectionId} added to {groupName} group");
}
public async Task RemoveUserFromGroup(string groupName)
{
    await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
    await Clients.Caller.ReceiveMessage($"Current user removed from {groupName} group");
    await Clients.Others.ReceiveMessage($"User {Context.ConnectionId} removed from {groupName} group");
}

В приведенном выше коде, когда мы добавляем текущего клиента в определенную именованную группу (или удаляем его из определенной группы), мы отправляем сообщения об этом действии всем клиентам. Но сообщение, отправляемое вызывающему клиенту, будет отличаться от того, что отправляется другим клиентам.

Наш хаб готов. Давайте теперь изменим наши клиенты, чтобы они могли использовать группы.

Включение клиентов SignalR для использования групп

Во-первых, мы добавим следующие контрольные группы в соответствующий раздел класса Index.cshtml:

<div class="control-group">
    <div>
        <label for="group-message">Message</label>
        <input type="text" id="group-message" name="group-message" />
    </div>
    <div>
        <label for="group-for-message">Group Name</label>
        <input type="text" id="group-for-message" name="group-for-message" />
    </div>
    <button id="btn-group-message">Send to Group</button>
</div>
<div class="control-group">
    <div>
        <label for="group-to-add">Group Name</label>
        <input type="text" id="group-to-add" name="group-to-add" />
    </div>
    <button id="btn-group-add">Add User to Group</button>
</div>
<div class="control-group">
    <div>
        <label for="group-to-remove">Group Name</label>
        <input type="text" id="group-to-remove" name="group-to-remove" />
    </div>
    <button id="btn-group-remove">Remove User from Group</button>
</div>

Затем мы добавим следующие обработчики click в файл site.js:

$('#btn-group-message').click(function () {
    var message = $('#group-message').val();
    var group = $('#group-for-message').val();
    connection.invoke("SendToGroup", group, message).catch(err => console.error(err.toString()));
});
$('#btn-group-add').click(function () {
    var group = $('#group-to-add').val();
    connection.invoke("AddUserToGroup", group).catch(err => console.error(err.toString()));
});
$('#btn-group-remove').click(function () {
    var group = $('#group-to-remove').val();
    connection.invoke("RemoveUserFromGroup", group).catch(err => console.error(err.toString()));
});

Наконец, мы обновим наш файл Program.cs проекта DotnetClient. Мы заменим содержимое оператора while следующим:

var message = string.Empty;
var groupName = string.Empty;
Console.WriteLine("Please specify the action:");
Console.WriteLine("0 - broadcast to all");
Console.WriteLine("1 - send to others");
Console.WriteLine("2 - send to self");
Console.WriteLine("3 - send to individual");
Console.WriteLine("4 - send to a group");
Console.WriteLine("5 - add user to a group");
Console.WriteLine("6 - remove user from a group");
Console.WriteLine("exit - Exit the program");
var action = Console.ReadLine();
if (action != "5" && action != "6")
{
    Console.WriteLine("Please specify the message:");
    message = Console.ReadLine();
}
if (action == "4" || action == "5" || action == "6")
{
    Console.WriteLine("Please specify the group name:");
    groupName = Console.ReadLine();
}
switch (action)
{
    case "0":
        await hubConnection.SendAsync("BroadcastMessage", message);
        break;
    case "1":
        await hubConnection.SendAsync("SendToOthers", message);
        break;
    case "2":
        await hubConnection.SendAsync("SendToCaller", message);
        break;
    case "3":
        Console.WriteLine("Please specify the connection id:");
        var connectionId = Console.ReadLine();
        await hubConnection.SendAsync("SendToIndividual", connectionId, message);
        break;
    case "4":
        hubConnection.SendAsync("SendToGroup", groupName, message).Wait();
        break;
    case "5":
        hubConnection.SendAsync("AddUserToGroup", groupName).Wait();
        break;
    case "6":
        hubConnection.SendAsync("RemoveUserFromGroup", groupName).Wait();
        break;
    case "exit":
        running = false;
        break;
    default:
        Console.WriteLine("Invalid action specified");
        break;
}

И это все. Наш код готов. Теперь, если мы запустим оба приложения SignalRServer и DotnetClient, вы сможете играть с группами. Как показано на этом снимке экрана, мы можем добавлять клиентов в группы, удалять их из групп и отправлять сообщения в определенные группы:

На этом статья об отправке сообщений SignalR конкретным клиентам заканчивается. Теперь подведем итог тому, что мы узнали.

Краткое содержание

В этой статье вы узнали, что при выборе широковещательной рассылки сообщения всем клиентам в концентраторе SignalR есть удобный параметр, позволяющий исключить отправителя сообщения из списка получателей.

Вы также узнали, что существует несколько способов отправки сообщений из концентратора SignalR отдельным пользователям. Вы можете отправить сообщение обратно отправителю. Или вы можете отправлять сообщения конкретному клиенту, указав его идентификатор подключения. Или вы можете указать несколько идентификаторов подключения и отправить сообщение более чем одному клиенту одновременно.

Но самый простой способ отправить сообщения нескольким клиентам одновременно — это явно добавить клиентов в группы. В отличие от идентификаторов соединения, имена групп легко читаются. И вы можете легко добавлять клиентов или удалять клиентов из групп по желанию.

До сих пор мы рассмотрели только то, как отправлять отдельные сообщения при каждом вызове. В следующей статье вы узнаете, как использовать потоковую передачу в SignalR, то есть способ отправки нескольких сообщений через открытый канал.

дальнейшее чтение

Управление пользователями и группами в SignalR: https://docs.microsoft.com/en-us/aspnet/core/signalr/groups

Координация кластера IoT с SignalR: https://scientificprogrammer.net/2020/10/30/coordinating-iot-cluster-with-signalr

P.S. Эта статья представляет собой главу из книги SignalR на .NET 6 — полное руководство.

Первоначально опубликовано на https://scientificprogrammer.net 24 сентября 2022 г.