Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Предупреждение
Эта документация не подходит для последней версии SignalR. Взгляните на ASP.NET Core SignalR.
В этом разделе показано, как хранить сведения о пользователях и их подключениях.
Патрик Флетчер помог написать эту тему.
Версии программного обеспечения, используемые в этом разделе
- Visual Studio 2013
- .NET 4.5
- SignalR версии 2
Предыдущие версии этого раздела
Сведения о более ранних версиях SignalR см. в разделе SignalR более ранних версий.
Вопросы и комментарии
Оставьте отзыв о том, как вам понравилось это руководство и что, по вашему мнению, мы можем улучшить в комментариях в нижней части страницы. Если у вас есть вопросы, которые не связаны напрямую с руководством, их можно опубликовать на форуме ASP.NET SignalR или StackOverflow.com.
Введение
Каждый клиент, подключающийся к концентратору, передает уникальный идентификатор подключения. Это значение можно получить в Context.ConnectionId свойстве контекста концентратора. Если приложению необходимо сопоставить пользователя с идентификатором подключения и сохранить это сопоставление, можно использовать одно из следующих действий:
- Поставщик идентификаторов пользователя (SignalR 2)
- Хранилище в памяти, например словарь
- Группа SignalR для каждого пользователя
- Постоянное, внешнее хранилище, например таблица базы данных или хранилище таблиц Azure
Каждая из этих реализаций показана в этом разделе. Вы используете методы OnConnected, OnDisconnected и OnReconnected класса Hub для отслеживания состояния подключения пользователя.
Оптимальный подход для вашего приложения зависит от следующих факторов:
- Количество веб-серверов, на которых размещено приложение.
- Нужно ли получить список подключенных пользователей.
- Необходимо ли сохранять сведения о группе и пользователе при перезапуске приложения или сервера.
- Является ли задержка вызова внешнего сервера проблемой.
В следующей таблице показано, какой подход подходит для этих рекомендаций.
| Рассмотрение | Несколько серверов | Получение списка подключенных пользователей | Сохранение информации после перезапуска | Оптимальная производительность |
|---|---|---|---|---|
| Поставщик идентификаторов пользователей |
|
|
||
| В памяти |
|
|
||
| Группы с одним пользователем |
|
|
||
| Постоянный, внешний |
|
|
|
Поставщик IUserID
Эта функция позволяет пользователям указывать идентификатор пользователя на основе IRequest через новый интерфейс IUserIdProvider.
The IUserIdProvider
public interface IUserIdProvider
{
string GetUserId(IRequest request);
}
По умолчанию будет реализована функция, которая использует IPrincipal.Identity.Name в качестве имени пользователя. Чтобы изменить это, зарегистрируйте реализацию IUserIdProvider в глобальном узле при запуске приложения:
GlobalHost.DependencyResolver.Register(typeof(IUserIdProvider), () => new MyIdProvider());
В центре вы сможете отправлять сообщения этим пользователям с помощью следующего API:
Отправка сообщения конкретному пользователю
public class MyHub : Hub
{
public void Send(string userId, string message)
{
Clients.User(userId).send(message);
}
}
Хранилище в памяти
В следующих примерах показано, как сохранить сведения о подключении и пользователе в словаре, который хранится в памяти. Словарь использует HashSet для хранения идентификатора подключения. В любой момент времени пользователь может иметь несколько подключений к приложению SignalR. Например, пользователь, подключенный через несколько устройств или несколько вкладок браузера, будет иметь несколько идентификаторов подключения.
Если приложение завершит работу, все сведения будут потеряны, но они будут повторно заполнены, так как пользователи повторно устанавливают свои подключения. Хранилище в памяти не работает, если среда включает несколько веб-серверов, так как каждый сервер будет иметь отдельную коллекцию подключений.
В первом примере показан класс, который управляет сопоставлением пользователей с подключениями. Ключом для HashSet будет имя пользователя.
using System.Collections.Generic;
using System.Linq;
namespace BasicChat
{
public class ConnectionMapping<T>
{
private readonly Dictionary<T, HashSet<string>> _connections =
new Dictionary<T, HashSet<string>>();
public int Count
{
get
{
return _connections.Count;
}
}
public void Add(T key, string connectionId)
{
lock (_connections)
{
HashSet<string> connections;
if (!_connections.TryGetValue(key, out connections))
{
connections = new HashSet<string>();
_connections.Add(key, connections);
}
lock (connections)
{
connections.Add(connectionId);
}
}
}
public IEnumerable<string> GetConnections(T key)
{
HashSet<string> connections;
if (_connections.TryGetValue(key, out connections))
{
return connections;
}
return Enumerable.Empty<string>();
}
public void Remove(T key, string connectionId)
{
lock (_connections)
{
HashSet<string> connections;
if (!_connections.TryGetValue(key, out connections))
{
return;
}
lock (connections)
{
connections.Remove(connectionId);
if (connections.Count == 0)
{
_connections.Remove(key);
}
}
}
}
}
}
В следующем примере показано, как использовать класс сопоставления подключений из концентратора. Экземпляр класса хранится в имени _connectionsпеременной.
using System.Threading.Tasks;
using Microsoft.AspNet.SignalR;
namespace BasicChat
{
[Authorize]
public class ChatHub : Hub
{
private readonly static ConnectionMapping<string> _connections =
new ConnectionMapping<string>();
public void SendChatMessage(string who, string message)
{
string name = Context.User.Identity.Name;
foreach (var connectionId in _connections.GetConnections(who))
{
Clients.Client(connectionId).addChatMessage(name + ": " + message);
}
}
public override Task OnConnected()
{
string name = Context.User.Identity.Name;
_connections.Add(name, Context.ConnectionId);
return base.OnConnected();
}
public override Task OnDisconnected(bool stopCalled)
{
string name = Context.User.Identity.Name;
_connections.Remove(name, Context.ConnectionId);
return base.OnDisconnected(stopCalled);
}
public override Task OnReconnected()
{
string name = Context.User.Identity.Name;
if (!_connections.GetConnections(name).Contains(Context.ConnectionId))
{
_connections.Add(name, Context.ConnectionId);
}
return base.OnReconnected();
}
}
}
Группы с одним пользователем
Вы можете создать группу для каждого пользователя, а затем отправить сообщение в эту группу, если вы хотите связаться только с этим пользователем. Имя каждой группы — это имя пользователя. Если у пользователя несколько подключений, каждый идентификатор подключения добавляется в группу пользователя.
При отключении пользователя вручную не следует удалять пользователя из группы. Это действие автоматически выполняется платформой SignalR.
В следующем примере показано, как реализовать группы с одним пользователем.
using Microsoft.AspNet.SignalR;
using System;
using System.Threading.Tasks;
namespace BasicChat
{
[Authorize]
public class ChatHub : Hub
{
public void SendChatMessage(string who, string message)
{
string name = Context.User.Identity.Name;
Clients.Group(who).addChatMessage(name + ": " + message);
}
public override Task OnConnected()
{
string name = Context.User.Identity.Name;
Groups.Add(Context.ConnectionId, name);
return base.OnConnected();
}
}
}
Постоянное, внешнее хранилище
В этом разделе показано, как использовать базу данных или хранилище таблиц Azure для хранения сведений о подключении. Этот подход работает при наличии нескольких веб-серверов, так как каждый веб-сервер может взаимодействовать с тем же репозиторием данных. Если веб-серверы перестают работать или приложение перезапускается, OnDisconnected метод не вызывается. Поэтому возможно, что ваше хранилище данных будет содержать записи для идентификаторов подключений, которые больше недействительны. Для очистки этих потерянных записей может потребоваться сделать недействительным любое подключение, созданное за пределами периода времени, соответствующего вашему приложению. Примеры в этом разделе содержат значение для отслеживания при создании подключения, но не показывают, как очистить старые записи, так как это может потребоваться сделать в качестве фонового процесса.
База данных
В следующих примерах показано, как хранить сведения о подключении и пользователе в базе данных. Вы можете использовать любую технологию доступа к данным; Однако в приведенном ниже примере показано, как определить модели с помощью Entity Framework. Эти модели сущностей соответствуют таблицам и полям базы данных. Структура данных может значительно отличаться в зависимости от требований приложения.
В первом примере показано, как определить сущность пользователя, которая может быть связана с множеством сущностей подключения.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;
namespace MapUsersSample
{
public class UserContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Connection> Connections { get; set; }
}
public class User
{
[Key]
public string UserName { get; set; }
public ICollection<Connection> Connections { get; set; }
}
public class Connection
{
public string ConnectionID { get; set; }
public string UserAgent { get; set; }
public bool Connected { get; set; }
}
}
Затем из концентратора можно отслеживать состояние каждого соединения с кодом, показанным ниже.
using System;
using System.Data.Entity;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Concurrent;
using Microsoft.AspNet.SignalR;
namespace MapUsersSample
{
[Authorize]
public class ChatHub : Hub
{
public void SendChatMessage(string who, string message)
{
var name = Context.User.Identity.Name;
using (var db = new UserContext())
{
var user = db.Users.Find(who);
if (user == null)
{
Clients.Caller.showErrorMessage("Could not find that user.");
}
else
{
db.Entry(user)
.Collection(u => u.Connections)
.Query()
.Where(c => c.Connected == true)
.Load();
if (user.Connections == null)
{
Clients.Caller.showErrorMessage("The user is no longer connected.");
}
else
{
foreach (var connection in user.Connections)
{
Clients.Client(connection.ConnectionID)
.addChatMessage(name + ": " + message);
}
}
}
}
}
public override Task OnConnected()
{
var name = Context.User.Identity.Name;
using (var db = new UserContext())
{
var user = db.Users
.Include(u => u.Connections)
.SingleOrDefault(u => u.UserName == name);
if (user == null)
{
user = new User
{
UserName = name,
Connections = new List<Connection>()
};
db.Users.Add(user);
}
user.Connections.Add(new Connection
{
ConnectionID = Context.ConnectionId,
UserAgent = Context.Request.Headers["User-Agent"],
Connected = true
});
db.SaveChanges();
}
return base.OnConnected();
}
public override Task OnDisconnected(bool stopCalled)
{
using (var db = new UserContext())
{
var connection = db.Connections.Find(Context.ConnectionId);
connection.Connected = false;
db.SaveChanges();
}
return base.OnDisconnected(stopCalled);
}
}
}
Табличное хранилище Azure
Следующий пример хранилища таблиц Azure аналогичен примеру базы данных. Она не включает всю информацию, которая вам понадобится, чтобы начать работу со службой хранилища таблиц Azure. Дополнительные сведения см. в разделе "Использование хранилища таблиц из .NET".
В следующем примере показана сущность таблицы для хранения сведений о подключении. Он секционирует данные по имени пользователя и определяет каждую сущность по идентификатору подключения, поэтому пользователь может иметь несколько подключений в любое время.
using Microsoft.WindowsAzure.Storage.Table;
using System;
namespace MapUsersSample
{
public class ConnectionEntity : TableEntity
{
public ConnectionEntity() { }
public ConnectionEntity(string userName, string connectionID)
{
this.PartitionKey = userName;
this.RowKey = connectionID;
}
}
}
В центре вы отслеживаете состояние подключения каждого пользователя.
using Microsoft.AspNet.SignalR;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Table;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace MapUsersSample
{
public class ChatHub : Hub
{
public void SendChatMessage(string who, string message)
{
var name = Context.User.Identity.Name;
var table = GetConnectionTable();
var query = new TableQuery<ConnectionEntity>()
.Where(TableQuery.GenerateFilterCondition(
"PartitionKey",
QueryComparisons.Equal,
who));
var queryResult = table.ExecuteQuery(query).ToList();
if (queryResult.Count == 0)
{
Clients.Caller.showErrorMessage("The user is no longer connected.");
}
else
{
foreach (var entity in queryResult)
{
Clients.Client(entity.RowKey).addChatMessage(name + ": " + message);
}
}
}
public override Task OnConnected()
{
var name = Context.User.Identity.Name;
var table = GetConnectionTable();
table.CreateIfNotExists();
var entity = new ConnectionEntity(
name.ToLower(),
Context.ConnectionId);
var insertOperation = TableOperation.InsertOrReplace(entity);
table.Execute(insertOperation);
return base.OnConnected();
}
public override Task OnDisconnected(bool stopCalled)
{
var name = Context.User.Identity.Name;
var table = GetConnectionTable();
var deleteOperation = TableOperation.Delete(
new ConnectionEntity(name, Context.ConnectionId) { ETag = "*" });
table.Execute(deleteOperation);
return base.OnDisconnected(stopCalled);
}
private CloudTable GetConnectionTable()
{
var storageAccount =
CloudStorageAccount.Parse(
CloudConfigurationManager.GetSetting("StorageConnectionString"));
var tableClient = storageAccount.CreateCloudTableClient();
return tableClient.GetTableReference("connection");
}
}
}