Базовая статья находится здесь.
SignalR предназначен для двухстороннего обмена (полнодуплексный режим) информацией между браузером и web-сервером. В качестве транспорта могут быть использованы технологии: WebSockets, Server-Sent events, или http (Long Polling). Передаваемые сообщения могут быть закодированы различными способами, включая JSON, или MessagePackProtocol (передача информации со сжатием).
Server-Sent events похож на WebSockets, но реализует одностороннее постоянное соединение, в котором сервер может отправлять сообщения клиентам.
По сравнению с WebSockets, SignalR предоставляет следующие возможности:
- Автоматическое восстановление соединения
- Альтернативные транспорты
- API для создания server-to-client remote procedure calls (RPC)
- Возможность параллельной отправки сообщений всем клиентам, либо группе клиентов
- Hubs. A SignalR Hub is a high-level pipeline that enables connected servers and clients to invoke methods on each other
- Интеграция с механизмами аутентификации пользователей, а также использование Dependency Injection
Слабые стороны SignalR:
- Слабый QoS
- Доставка событий не гарантируется. Требуется доработка функционала
- Количество SDK относительно не большое: C#, Java (Android), Python и JavaScript
- Масштабировать использование SignalR достаточно сложно
В банковской сфере SignalR не любят по следующим причинам:
- WebSockets: на старых маршрутизаторах может на хватать entries для хранения всех установленных на данный момент соединений. По этой причине, часть установленных соединений разрывается маршрутизаторами и это приводит к потере части сообщений. В системах мониторинга это выглядит как систематический, не контролируемый разрыв соединений по всей сети
- Long-polling: балансировщик нагрузки должен уметь переадрессовывать запрос на тот же сервер (Sticky Load Balancing), который ранее обрабатывал запросы того же SignalR-клиента. Далеко не все балансировщики умеют это делать.
SignalR работает поверж http и https, используя дополнительный заголовок запроса Upgrade, чтобы переключиться с HTTP на WebSockets.
В ASP.NET Core входят серверные библиотеки SignalR, но клиентские библиотеки необходимо добавлять в проект.
Для того, чтобы добавить клиентские (браузерные) библиотеки SignalR следует указать курсором мыши в "Solution Explorer" на папку "wwwroot" и выбрать в контекстном меню команду "Add -> Client-Side Library". В появившемся диалоге следует выбрать "unpkg" в поле "Provider". В поле "Library" следует указать @microsoft/signalr@latest
. Также необходимо выбрать папку, в которую будут помещены исходные тексты приложения - Target Location.
Полученные из репозитария js-файлы (папка в "dist") необходимо включить на соответствующей HTML-странице.
В случае использования Visual Studio Code, рекомендуется сначала установить пакет Microsoft.Web.LibraryManager.Cli
dotnet tool install -g Microsoft.Web.LibraryManager.Cli
А уже затем установить необходимые компоненты, посредством libman:
libman install @microsoft/signalr@latest -p unpkg -d wwwroot/js/signalr --files dist/browser/signalr.js --files dist/browser/signalr.min.js
В лекции Microsoft указывается на то, что в проект должны быть добавлены следующие зависимости:
- Microsoft.AspNetCore.SignalR.Client
- Microsoft.AspNetCore.SignalR.Protocols.MessagePack
Protocol.MassagePack добавляет сжатие передаваемой информации - Microsoft рекомендует добавлять его в проекты с SignalR.
В приложении ASP.NET Core 8, сгенерированном по шаблону в Visual Studio 2022, поддержка SignalR была встроена изначало.
В дополнительных главах книги "C# 10 and .NET 6. Modern Cross-Platform Development" by Mark J. Price, есть пример приложения ASP.NET Core, в котором используется технология SignalR.
Hub (концентратор) - это точка привязки (Endpoint), в которую серверное приложение передаёт информацию о запросе клиента. Hub - строго-типизированный, т.е. он не может быть использован отдельно от класса, который реализует обработку таких запросов. В статье Tutorial: Get started with ASP.NET Core SignalR приведён следующий пример реализации:
using Microsoft.AspNetCore.SignalR;
namespace SignalRChat.Hubs
{
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
}
Метод SendMessage() может быть вызван из JavaScript-кода в браузере, который выглядит следующим образом:
document.getElementById("sendButton").addEventListener("click", function (event) {
var user = document.getElementById("userInput").value;
var message = document.getElementById("messageInput").value;
connection.invoke("SendMessage", user, message).catch(function (err) {
return console.error(err.toString());
});
event.preventDefault();
});
В реализации SendMessage() мы отправляем полученное сообщение всем клиентам, используя Clients.All.SendAsync().
Если мы хотим отправлять сообщения SignalR из контроллера web-сервера, следует использовать механизм Dependency Injection. Например:
public class EventModel : PageModel
{
private static IHubContext<CinnaHub>? _hubContext = null;
public EventModel(IHubContext<CinnaHub> hub)
{
_hubContext = hub;
}
public async Task<ActionResult> OnPostChangeStateAsync([FromBody] StorageStateDTO val)
{
try
{
await _hubContext.Clients.All.SendAsync("onStatusChange", val.storageId, val.state);
// Плохой пример: список параметров разрастается, код становится хуже управляемым
public void SendMessage(string to, string body);
// Лучший пример: создаём отдельный класс Message, который удобно расширять
// и это слабо влияет на управляемость программного кода
public class Message
{
public string To { get; set; }
public string Body { get; set; }
}
public void SendMessage(Message message);
В файл "Program.cs" следует внести следующие изменения:
builder.Services.AddRazorPages();
builder.Services.AddSignalR(); // <-- Добавляем базовый сервис SignalR
// ...
var app = builder.Build();
// ...
app.MapHub<CinnaHub>("/monitoringHub"); // <-- Указываем Endpoint для концентратора
app.Run();
Если от клиента будет получен запрос на подлючение к "/monitoringHub", то этот запрос будет передаваться экземпляру класса CinnaHub. Отсюда следует, что в приложении может быть реализовано несколько разных концентраторов SignalR.
app.MapHub<AnotherSignalRHub>("/anotherHub");
При получении сообщения о подключении нового пользователя мы можем узнать уникальный номер подключённого (ConnectionId) и даже отправить сообщение этому потребителю:
public override async Task OnConnectedAsync()
{
string connectionIdSafe = (Context?.ConnectionId ?? "Unknown");
try
{
Debug.WriteLine($"New connection: {connectionIdSafe}");
// Отправляем сообщение клиенту при подключении клиента к серверу
await Clients.Client(connectionIdSafe).SendAsync("onInit");
}
catch (Exception ex)
{
Console.WriteLine($"{connectionIdSafe} failed to connect: {ex.Message}");
throw; // Пробрасываем исключение выше по коду, не изменяя Stack information
}
finally
{
await base.OnConnectedAsync();
}
}
В клиентском JavaScript-коде следует добавить следующие команды:
document.addEventListener('DOMContentLoaded', function (event) {
// Взаимодействие посредством SignalR
var connection = new signalR.HubConnectionBuilder().withUrl("/monitoringHub").build();
connection.on("onStatusChange", function (storageId, status) {
console.log(`Received new status: ${storageId} = ${status}`);
});
connection.start().then(function () {
// Здесь можно отправить стартовое сообщение. Например:
connection.invoke("SendMessage", "Petrov", "Hello, SignalR!").catch(function (err) {
return console.error(err.toString());
});
}).catch(function (err) {
return console.error(err.toString());
});
});
Мы можем настроить условия работы SignalR (options), при добавлении сервиса, например:
services.AddSignalR(options => {
// Настройка параметров концентратора SignalR
options.EnableDetailedErrors = true;
})
.WithAutomaticReconnect() // <-- Автоматическое восстановление соединения
.AddMessagePackProtocol(); // <-- Активация режима сжатия сообщений
В приведённом выше примере, мы активируем использование MessagePackProtocol, автоматическую переустановку соединения при потере связи, а также добавляем детализованную информацию об ошибках в работе механизма.
Следует заметить, что если мы вызываем WithAutomaticReconnect() без параметров, то клиент настраивает ожидание 0, 2, 10 и 30 секунд соответственно, прежде чем пытаться выполнить каждую попытку повторного подключения, остановившись после четырех неудачных попыток. Т.е. в этом варианте, соединение может не быть автоматически восстановлено, если связь отсутствовала больше 45 секунд.
Обработка восстановления соединения может выглядеть следующим образом (директивы логирования опущены для уменьшения объёма кода):
connection.Closed += async (error) =>
{
await Task.Delay(Random.Shared.Next(2000, 5000));
// Запускаем метод мультиплексора, которой многократно пытается подключиться
// к адаптеру, пока не преуспеет в этом
await Start();
};
connection.Reconnecting += (error) =>
{
return Task.CompletedTask;
};
connection.Reconnected += (message) =>
{
return Task.CompletedTask;
};
Microsoft рекомендует определить интерфейс с методами, на которые будут подписываться клиенты:
namespace BlazingPizza.Server.Hubs;
public interface IOrderStatusHub
{
/// <summary>
/// This event name should match <see cref="OrderStatusHubConsts.EventNames.OrderStatusChanged"/>,
/// which is shared with clients for discoverability.
/// </summary>
Task OrderStatusChanged(OrderWithStatus order);
}
В реализации интерфейса Microsoft предлагает определить два метода, которые позволят пользователям подписываться и отписываться на получение событий:
namespace BlazingPizza.Server.Hubs;
[Authorize]
public class OrderStatusHub : Hub<IOrderStatusHub>
{
/// <summary>
/// Adds the current connection to the order's unique group identifier, where
/// order status changes will be notified in real-time.
/// This method name should match <see cref="OrderStatusHubConsts.MethodNames.StartTrackingOrder"/>,
/// which is shared with clients for discoverability.
/// </summary>
public Task StartTrackingOrder(Order order) =>
Groups.AddToGroupAsync(
Context.ConnectionId, order.ToOrderTrackingGroupId());
/// <summary>
/// Removes the current connection from the order's unique group identifier,
/// ending real-time change updates for this order.
/// This method name should match <see cref="OrderStatusHubConsts.MethodNames.StopTrackingOrder"/>,
/// which is shared with clients for discoverability.
/// </summary>
public Task StopTrackingOrder(Order order) =>
Groups.RemoveFromGroupAsync(
Context.ConnectionId, order.ToOrderTrackingGroupId());
}
Т.е. когда мы получаем событие в Hub, у нас есть идентификатор клиентской сессии, доступ к которой осуществляется через Context.ConnectionId. Также мы можем включать, или исключать соединение из группы, используя методы AddToGroupAsync() и RemoveFromGroupAsync(). Первый параметр вызовов - идентификатор соединения, а второй - имя группы (string).
Соответственно, мы можем отправить некоторое сообщение всем соединениям, которые входят в группу. Например:
await Clients.Group(groupname).SendAsync("Notify", $"{username} вошел в чат");
Мы можем использовать Context.ConnectionId для того, чтобы создать группу подключения для конкретного пользователя:
public override Task OnConnectedAsync()
{
string userId = _userManager.GetUserId(this.Context.User);
if (null != userId) {
// При создании соединения, связываем конкретное соединение
// с GUID-пользователя, который подключается к SignalR
await Groups.AddToGroupAsync(Context.ConnectionId, userId);
Отправить сообщение такому пользователю можно было бы выбрав группу с его/её именем:
await _hubContext.Clients.Group(u).SendAsync("Send", message);
Однако, чтобы список групп не разрастался, можно систематически находить пустые группы (в которых нет реальных соединений) и удалять их.
В соответствии с рекомендациями Microsoft не нужно вручную удалять пользователей из групп. В примерах, Microsoft приводит вот такой код:
public class ContosoChatHub : Hub
{
public Task JoinRoom(string roomName)
{
return Groups.Add(Context.ConnectionId, roomName);
}
public Task LeaveRoom(string roomName)
{
return Groups.Remove(Context.ConnectionId, roomName);
}
}
Вот что написано в старой документации по SignalR: "SignalR connection refers to a logical relationship between a client and a server URL, maintained by the SignalR API and uniquely identified by a connection ID. The data about this relationship is maintained by SignalR and is used to establish a transport connection. The relationship ends and SignalR disposes of the data when the client calls the Stop method or a timeout limit is reached while SignalR is attempting to re-establish a lost transport connection". Т.е. как только SignalR понимает, что соединение с клиентом завершилось, он автоматически удаляет всю информацию об этом соединении.
В конструкторе GarmrHub включены зависимости: UserManager<ApplicationUser> userManager, ILogger<GarmrHub> logger
Реализованы статические методы, которые могут быть вызваны компонентами web-сервера (например, планировщиком CmdActionTask).
GarmrHub.onUpdateRunningTask(actionTask);
В реализации каждого глобального статического метода выполняется обязательная проверка на наличие Hub-а:
if (null != _hubContext)
{
// ...некоторый код
}
Довольно много смешанного кода (синхронный/асинхронный). Для того, чтобы дождаться выполнения асинхронного вызова, используется явное завершение ожидания .GetAwaiter().GetResult()
:
static public void onUpdateRunningTask(CmdActionTask obj)
{
// ... какой-то код
_hubContext.Clients.Group("TechAdministrators").SendAsync("UpdateTask", sb.ToString()).GetAwaiter().GetResult();
}
В проекте очень активно используются группы для адресной рассылки сообщений.
Callback-функция OnConnectedAsync() используется для добавления соединения в группу конкретного пользователя, если этот пользователь был успешно аутентифицирован:
/// <summary>
/// Callback-метод, вызывается при получении запроса на соединение
/// от браузера. Часть функционала, связанного с библиотекой SignalR
/// </summary>
public override Task OnConnectedAsync()
{
string userId = _userManager.GetUserId(this.Context.User);
if (null != userId) {
// При создании соединения, связываем конкретное соединение
// с GUID-пользователя, который подключается к SignalR
Groups.AddToGroupAsync(Context.ConnectionId, userId);
}
return base.OnConnectedAsync();
}
Таким образом обеспечивается отправка уведомлений, имеющих отношение только к конкретному пользователю со страниц, на которых этот пользователь аутентифицирован.
Регистрация SignalR выполняется также, как и в ASP.NET Core 8:
services.AddSignalR();
Однако, дополнительно выполняется конфигурирование SignalR с целью настройки режима подключения:
services.Configure<SignalRConfiguration>(Configuration.GetSection("SignalRConfiguration"));
Настройка выглядит следующим образом:
"SignalRConfiguration": {
"Protocol": "LongPolling" //WebSockets
},
Endpoint для SignalR указывается чуть другим способом:
app.UseEndpoints(endpoints => {
endpoints.MapHub<GarmrHub>("/garmr"); // Добавляем Routing для SignalR
endpoints.MapRazorPages();
});
В CinnaHub заработал вот такой код:
using Microsoft.AspNetCore.SignalR;
namespace CinnaPages.Hubs
{
public class CinnaHub : Hub
{
private static IHubContext<CinnaHub>? _hubContext;
public CinnaHub(IHubContext<CinnaHub> hubContext)
{
_hubContext = hubContext;
}
// Метод используется для отправки сообщения об изменении состояния ХЦК
static public async Task SendStatusChange(int storageId, int status)
{
if (_hubContext != null)
{
await _hubContext.Clients.All.SendAsync("onStatusChange", storageId, status);
}
}
}
}
Крайне важно было использовать модификаторы static для IHubContext<> и для асинхронного метода.
После этого можно из любого места вызывать этот метод:
public class EventModel : PageModel
{
public async Task<ActionResult> OnPostChangeStateAsync([FromBody] StorageStateDTO val)
{
try
{
await CinnaHub.SendStatusChange(val.storageId, val.state);
Общая логика работы может быть такая - клиент подписывается на получение информации в конкретной группе и сервер посылает клиенту сообщение только через группу.
Оформления подписки клиентом может выглядеть следующим образом:
connection.start().then(function () {
const _thisStorageId = $("#StorageID").text();
connection.invoke("StartObservation", _thisStorageId).catch(function (err) {
return console.error(err.toString());
});
}).catch(function (err) {
return console.error(err.toString());
});
Соответствующий метод на сервере может выглядеть так:
public class CinnaHub : Hub
{
public async Task StartObservation(string storage)
{
if (_hubContext != null)
{
await _hubContext.Groups.AddToGroupAsync(this.Context.ConnectionId, storage);
}
}
}
Для отправки сообщения клиенту, может быть использован вот такой код:
public class CinnaHub : Hub
{
static public async Task SendModuleStatusChange(int storageId, int moduleId, int status)
{
if (_hubContext != null)
{
await _hubContext.Clients.Groups(storageId.ToString()).SendAsync("onModuleStatusChange", moduleId, status);
}
}
}
В приложении ASP.NET Core 8 можно создать класс-Singleton, который создаст поток исполнения и будет циклически выполнять некоторое действие. Для этого необходимо определить класс, в котором есть метод, запускающий отдельный поток и вызывать этот метод из конструктора:
public interface ITaskSingleton
{
public void Loop();
}
public class TaskSingleton : ITaskSingleton
{
public void Loop()
{
Task TimerTask = Task.Run(async () => {
try
{
for (;;)
{
// ... <-- Здесь выполняем какую-то полезную нагрузку
await Task.Delay(10 * 1000);
}
}
catch
{
await Task.Delay(300 * 1000);
// В случае возникновения исключения, мы выйдем из цикла for(;;)
Loop();
}
});
}
}
Затем, необходимо в "Program.cs" зарегистрировать Singleton:
builder.Services.AddSingleton<ITaskSingleton, TaskSingleton>();
После этого, нужно создать экземпляр singleton-а, и вызвать у него метод Loop:
var service = app.Services.GetService<ITaskSingleton>();
service?.Loop();
Время жизни singleton-а соответствует времени жизни приложения. Цикл внутри задачи будет выполняться до завершения приложения. Стоит заметить, что Singleton будет создан при первом его запросе посредством GetService(). Вызывать конкретный метод singleton-а не обязательно.
Ключевая статья
Для подключения клиента SignalR к SignalR-концентратору необходимо установить зависимость:
Microsoft.AspNetCore.SignalR.Client
Нам следует создать Singleton, в котором создаётся экземпляр HubConnection, например:
public class TaskSingleton : ITaskSingleton
{
HubConnection? connection = null;
public TaskSingleton()
{
connection = new HubConnectionBuilder()
.WithUrl("wss://localhost:8080/adapterHub")
.Build();
connection.Closed += async (error) =>
{
await Task.Delay(Random.Shared.Next(2000, 5000));
await connection.StartAsync();
};
}
В конструкторе мы формируем соединение, указывая IP-адрес и порт компьютера, к которому мы подключаемся, протокол подключения (wss) и Endpoint (/adapterHub). Следует обратить особое внимание на протокол подключения - легитимными вариантами являются ws (WebSockets) и wss (WebSockets Secure). И сервер, и клиент должны использовать один и тот же протокол.
ВНИМАНИЕ! Достаточно частая ошибка комбинирования имени хоста и Endpoint-а состоит в том, что и в конце имени хоста, и в начале Endpoint-а используется косая черта. В этом случае может получится, например, вот такой адрес:
wss://localhost:8080//adapterHub
. Адрес с двумя следующими подряд косыми чертами не корректный и это приводит к ошибке подключения. На практике проблема возникает, если оператор копирует адрес сервера, к которому подключается из браузера и меняет протокол. Например, копируетhttps://localhost:8080/
и заменяет наwss://localhost:8080/
, не удалив завершающую косую черту. Устранять подобные ошибки достаточно сложно.
Мы можем добавить логирование работы движка SignalR, используя chaining methods:
var connection = new HubConnectionBuilder()
.ConfigureLogging(logging =>
{
// Для поиска причин сбоя в подключении по WebSockets добавляем
// максимальную трассировку в консоль
logging.SetMinimumLevel(LogLevel.Trace);
logging.AddConsole();
})
.WithUrl(url)
.Build();
Далее, нам следует определить метод, который откроет соединение и подпишется на некоторые сообщения от SignalR-концентратора:
public interface ITaskSingleton
{
public Task Start();
}
public async Task Start()
{
if (connection != null)
{
if (connection.State != HubConnectionState.Connected)
{
connection.On<string, string>("ReceiveMessage", (user, message) =>
{
Console.WriteLine($"I has got a message ({message}) from {user}");
});
await connection.StartAsync();
}
}
}
Ключевым является вызов connection.StartAsync()
, который устанавливает соединение с SignalR-концентратором. После того, как соединение будет установлено, можно будет отправлять сообщения, или получить их по подписке:
public async Task SendMessage()
{
if (connection != null)
{
try
{
await connection.InvokeAsync("SendMessage", "Monitoring", "Here is the monitoring!");
}
catch (Exception)
{
}
}
}
Вызов метод StartAsync() может завершиться по тайм-ауту без установки соединения. Проверить, что соединение не удалось установить можно проверкой текущего состояния соединения connection.State
значению HubConnectionState.Disconnected
. Для того, чтобы в подобной ситуации установить соединение, следует периодически вызывать метод StartAsync(). Пример реализации:
public static async Task<bool> ConnectWithRetryAsync(HubConnection connection, CancellationToken token)
{
// Keep trying to until we can start or the token is canceled.
while (true)
{
try
{
await connection.StartAsync(token);
Debug.Assert(connection.State == HubConnectionState.Connected);
return true;
}
catch when (token.IsCancellationRequested)
{
return false;
}
catch
{
// Failed to connect, trying again in 5000 ms.
Debug.Assert(connection.State == HubConnectionState.Disconnected);
await Task.Delay(5000);
}
}
}
Этот пример взят из статьи ASP.NET Core SignalR .NET Client by Microsoft.
Стоит отметить, что получить CancellationToken можно используя экземпляр класса CancellationTokenSource. Класс CancellationTokenSource - позволяет остановить асинхронную операцию посредством вызова метода Cancel(). Эксземпляр класса CancellationToken используется внутри Task-а для того, чтобы определить, было ли запрошено прерывания задачи (task-а) внешним кодом.
Для SignalR не создаётся какого-то особенного порта, он использует тот же самый порт, который устанавливается для http/https Endpoint-ов web-сервера. Если мы запускаем приложение из под Visual Studio, то этот инструмент выделяет некоторый порт "по умолчанию". Чтобы установить конкретный, фиксированный номер порта, следует создать профиль запуска. Для это следует перейти в свойства Solution-а, найти раздел "Debug -> General" и нажать на ссылку "Open debug launch profiles UI". Откроется форма, в которой можно настроить параметры профилей http, https, "IIS Express". Если мы, например, запускаем приложение по https, то следует выбрать профиль https, найти в нём параметр "App URL" и установить нужный нам порт, например: "https://localhost:8080;http://localhost:5086"
При изменении параметров Visual Studio создаст файл "\Properties\launchSettings.json", в котором будет сохранена соответствующая настройка, например:
{
"profiles": {
"https": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "https://localhost:8080;http://localhost:5086"
}
}
}
ВНИМАНИЕ! В приведённом выше примере отражается только часть файла "\Properties\launchSettings.json".
Разметка для отправки сообщения концентратору SignalR:
<div class="row p-1">
<div class="col-1">User</div>
<div class="col-5"><input type="text" id="userInput" /></div>
</div>
<div class="row p-1">
<div class="col-1">Message</div>
<div class="col-5"><input type="text" class="w-100" id="messageInput" /></div>
</div>
<div class="row p-1">
<div class="col-6 text-end">
<input type="button" id="sendButton" value="Send Message" />
</div>
</div>
JavaScript-код для отправки введённого текста через концентратор SignalR:
document.getElementById("sendButton").addEventListener("click", function (event) {
var user = document.getElementById("userInput").value;
var message = document.getElementById("messageInput").value;
connection.invoke("SendMessage", user, message).catch(function (err) {
return console.error(err.toString());
});
event.preventDefault();
});
Используя Developer Console, можно увидеть много интересных вещей:
- В обе стороны ходят сообщения типа 6 (ping)
- В сообщениях есть поле "invocationId", который может быть использован для чего?
- Аргументы вызова функции передаются в массиве
Сообщение от js-клиента серверу:
const obj = {
user: "Petrov",
message: "Hello, SignalR!"
};
connection.invoke("SendMessage", obj)
.then(function () {
console.log("AcceptedProcessed");
})
.catch(function (err) {
return console.error(err.toString());
});
{
"arguments": [{
"user": "Petrov",
"message": "Hello, SignalR!"
}],
"invocationId": "2",
"target": "SendMessage",
"type": 1
}
Ответ сервера клиенту:
{
"type": 3,
"invocationId": "2",
"result": null
}
Как мы видим, сервер возвращает клиенту invocationId, что позволяет предположить, что SignalR использует механизм подтверждения обработки сообщения, т.е. постарается гарантированно доставить сообщение серверу.
Сообщение сервера клиенту:
var answer = new MessageWrapper { user = $"{wrapper.user} -> Server", message = wrapper.message };
await Clients.All.SendAsync("ReceiveMessage", answer);
{
"type": 1,
"target": "ReceiveMessage",
"arguments": [{
"user": "Petrov -> Server",
"message": "Hello, SignalR!"
}]
}
Клиентская JavaScript-библиотека SignalR позволяет обработать успешную доставку конкретного сообщения используя метод then() из Promise, который возвращает метод connection.invoke()
:
const obj = {
user: "Petrov",
message: "Hello, SignalR!"
}
connection.invoke("SendMessage", obj)
.then(function () {
console.log("AcceptedProcessed");
})
.catch(function (err) {
console.error(err.toString());
});