Сопоставление пользователей SignalR с подключениями

; автор — Том ФитцМакен (Tom FitzMacken)

Предупреждение

Эта документация не для последней версии SignalR. Взгляните на ASP.NET Core SignalR.

В этом разделе показано, как сохранить сведения о пользователях и их подключениях.

Патрик Флетчер помог написать эту тему.

Версии программного обеспечения, используемые в этом разделе

Предыдущие версии этого раздела

Сведения о более ранних версиях SignalR см. в разделе Старые версии SignalR.

Вопросы и комментарии

Оставьте отзыв о том, как вам понравилось это руководство и что мы могли бы улучшить в комментариях в нижней части страницы. Если у вас есть вопросы, которые не связаны напрямую с руководством, вы можете опубликовать их на форуме ASP.NET SignalR или StackOverflow.com.

Введение

Каждый клиент, подключающийся к концентратору, передает уникальный идентификатор подключения. Это значение можно получить в свойстве Context.ConnectionId контекста концентратора. Если приложению необходимо сопоставить пользователя с идентификатором подключения и сохранить это сопоставление, можно использовать одно из следующих средств:

Каждая из этих реализаций показана в этом разделе. Для отслеживания OnConnectedсостояния подключения пользователя используются методы Hub , OnDisconnectedи OnReconnected класса .

Оптимальный подход для приложения зависит от следующих факторов:

  • Количество веб-серверов, на которых размещено приложение.
  • Требуется ли получить список подключенных пользователей.
  • Необходимо ли сохранять сведения о группе и пользователе при перезапуске приложения или сервера.
  • Является ли задержка вызова внешнего сервера проблемой.

В следующей таблице показано, какой подход подходит для этих рекомендаций.

Оценка Несколько серверов Получение списка подключенных пользователей Сохранение сведений после перезапуска Оптимальная производительность
Поставщик UserID
In-memory
Группы с одним пользователем
Постоянный, внешний

Поставщик 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");
        }
    }
}