Поделиться через


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

Патрик Флетчер (Patrick Fletcher), Том ФитцМаккен (Tom FitzMacken)

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

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

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

Введение

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

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

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

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

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

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

Хранилище в памяти

В следующих примерах показано, как сохранить сведения о подключении и пользователях в словаре, который хранится в памяти. Словарь использует для 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()
        {
            string name = Context.User.Identity.Name;

            _connections.Remove(name, Context.ConnectionId);

            return base.OnDisconnected();
        }

        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()
        {
            using (var db = new UserContext())
            {
                var connection = db.Connections.Find(Context.ConnectionId);
                connection.Connected = false;
                db.SaveChanges();
            }
            return base.OnDisconnected();
        }
    }
}

Хранилище таблиц 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()
        {
            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();
        }

        private CloudTable GetConnectionTable()
        {
            var storageAccount =
                CloudStorageAccount.Parse(
                CloudConfigurationManager.GetSetting("StorageConnectionString"));
            var tableClient = storageAccount.CreateCloudTableClient();
            return tableClient.GetTableReference("connection");
        }
    }
}