#c# #signalr #signalr-hub #asp.net-core-signalr
#c# #signalr #signalr-концентратор #asp.net-core-signalr
Вопрос:
У меня есть следующий серверный SignalR
концентратор:
public class AlertHub : Hub
{
static ConcurrentDictionary<string, string> _users = new ConcurrentDictionary<string, string>();
public Task SendMessage(string user, string message)
{
return Clients.All.SendAsync("BroadcastMessage", user, message);
}
public Task SendMessageToUser(string userUid, string user, string message, string totalMessages)
{
var clientId = _users.FirstOrDefault(x => x.Value == userUid).Key;
return Clients.Client(clientId).SendAsync("BroadcastMessage", user, message, totalMessages);
}
public static void ClearState()
{
_users.Clear();
}
public override Task OnConnectedAsync()
{
_users.TryAdd(Context.ConnectionId, Context.ConnectionId);
return base.OnConnectedAsync();
}
public override Task OnDisconnectedAsync(Exception exception)
{
string userUid;
_users.TryRemove(Context.ConnectionId, out userUid);
return base.OnDisconnectedAsync(exception);
}
public void SetUserUid(string userUid)
{
_users[Context.ConnectionId] = userUid;
}
}
На моем клиенте непосредственно перед await _signalRConnection.StartAsync();
тем, как я установил userUid
в свой _users
словарь:
//Send user name for this client, so we won't need to send it with every message
await _signalRConnection.InvokeAsync("SetUserUid", this.State.UserUID);
И это работает идеально, без исключений.
Затем на моем клиенте, когда я пытаюсь отправить сообщение, я делаю следующее: (это обычно работает, исключение прерывистое)
await _signalRConnection.InvokeAsync("SendMessageToUser", model.UserUid.ToString().ToLower(), this.State.UserName, $"{model.UserName} has sent you a message.", countMessagesString);
Обычно это работает, но иногда он начал аварийно завершать работу, показывая на сервере следующую ошибку:
Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher.? [?] - MESSAGE: Failed to invoke hub method 'SendMessageToUser'.
System.ArgumentNullException: Value cannot be null. (Parameter 'connectionId')
Это связано с тем, что при попытке получить идентификатор клиента он возвращает здесь null:
var clientId = _users.FirstOrDefault(x => x.Value == userUid).Key; <-- HERE
return Clients.Client(clientId).SendAsync("BroadcastMessage", user, message, totalMessages);
Поэтому я не знаю, почему в _users
словаре иногда не будет ключа. Это довольно странно.
Есть какие-либо подсказки о том, как разобраться или решить эту проблему?
** ОБНОВИТЬ **
Это то, что я сделал:
public class AlertHubDictionary
{
static ConcurrentDictionary<string, string> _users;
public AlertHubDictionary()
{
_users = new ConcurrentDictionary<string, string>();
}
public ConcurrentDictionary<string, string> UsersDictionary { get { return _users; } }
public void ClearUsersDictionary()
{
_users.Clear();
}
}
Затем этот класс зарегистрирован как одноэлементный DI:
services.AddSingleton<AlertHubDictionary>();
И, наконец, выполните замены в классе Hub:
public class AlertHub : Hub
{
private readonly ILogger<AlertHub> _logger;
private AlertHubDictionary _hubDictionary;
static ConcurrentDictionary<string, string> _users = new ConcurrentDictionary<string, string>();
public AlertHub(ILogger<AlertHub> logger, AlertHubDictionary hubDictionary)
{
_logger = logger;
_hubDictionary = hubDictionary;
}
public Task SendMessage(string user, string message)
{
//Clients.Client("").SendAsync("BroadcastMessage", user, message);
return Clients.All.SendAsync("BroadcastMessage", user, message);
}
public Task SendMessageToUser(string userUid, string user, string message, string totalMessages)
{
//var clientId = _users.FirstOrDefault(x => x.Value == userUid).Key;
var clientId = _hubDictionary.UsersDictionary.FirstOrDefault(x => x.Value == userUid).Key;
using (var scope = _logger.BeginScope("SCOPED_VALUE"))
{
_logger.LogInformation($"Send message to userUid {userUid} - {user} with clientId {clientId}.");
}
if (clientId == null)
{
// the user is not connected
return Task.FromResult(0);
}
else
{
return Clients.Client(clientId).SendAsync("BroadcastMessage", user, message, totalMessages);
}
}
public void ClearState()
{
_hubDictionary.ClearUsersDictionary();
//_users.Clear();
using (var scope = _logger.BeginScope("SCOPED_VALUE"))
{
_logger.LogInformation("_users cleared");
}
}
public override Task OnConnectedAsync()
{
_hubDictionary.UsersDictionary.TryAdd(Context.ConnectionId, Context.ConnectionId);
//_users.TryAdd(Context.ConnectionId, Context.ConnectionId);
using (var scope = _logger.BeginScope("SCOPED_VALUE"))
{
_logger.LogInformation($"ConnectionId {Context.ConnectionId} connected.");
}
return base.OnConnectedAsync();
}
public override Task OnDisconnectedAsync(Exception exception)
{
string userUid;
_hubDictionary.UsersDictionary.TryRemove(Context.ConnectionId, out userUid);
using (var scope = _logger.BeginScope("SCOPED_VALUE"))
{
_logger.LogInformation($"UserUid {userUid} disconnected.");
}
return base.OnDisconnectedAsync(exception);
}
public void SetUserUid(string userUid)
{
_hubDictionary.UsersDictionary[Context.ConnectionId] = userUid;
using (var scope = _logger.BeginScope("SCOPED_VALUE"))
{
_logger.LogInformation($"Connection {Context.ConnectionId} linked to userUid {userUid}");
}
//ClientNameChanged?.Invoke(Context.ConnectionId, userName);
}
}
Комментарии:
1. Поместите запись в файл. Регистрируйте методы, в которых
_users
добавлено / удалено. Вероятно, вы время от времени теряете соединение = вот почему оно очищается.
Ответ №1:
Судя по тому, как вы фиксируете идентификаторы пользователей, похоже, что вы пытаетесь обработать концентратор SignalR как одноэлементный. Согласно документации Microsoft, концентратор является переходным классом:
https://learn.microsoft.com/en-us/aspnet/core/signalr/hubs?view=aspnetcore-3.1#create-and-use-hubs
Концентраторы являются временными:
Не сохраняйте состояние в свойстве класса hub. Каждый вызов метода концентратора выполняется в новом экземпляре концентратора.
Таким образом, если вы хотите сохранить общий экземпляр ваших зарегистрированных идентификаторов пользователей, вам, вероятно, следует управлять им как отдельным классом, зарегистрированным как одноэлементный и введенным в ваш класс hub, чтобы состояние можно было поддерживать независимо от hub.
Комментарии:
1. Потрясающе. Да, это имеет смысл. Любая ссылка, по которой я могу посмотреть, как ее реализовать?
2. @VAAA — Вы могли бы сделать это так же просто, как переместить этот словарь в его собственный класс, зарегистрировать его при запуске
service.AddSingleton<YourUserCacheClass>()
, а затем ввести его в конструктор концентратора, используя внедрение конструктора, как описано в learn.microsoft.com/en-us/aspnet/core/fundamentals /…3. Здесь есть очень похожий пример автономного класса от Microsoft, хотя и помеченный как внутренний, тогда как вас устроит public: github.com/dotnet/extensions/blob/release/3.1/src /…
4. Большое спасибо, я попробую
5. Я только что обновил внесенное мной изменение, я собираюсь попробовать.