Mapowanie użytkowników usługi SignalR na połączenia w usłudze SignalR 1.x

Autor: Patrick Fletcher, Tom FitzMacken

Ostrzeżenie

Ta dokumentacja nie jest przeznaczona dla najnowszej wersji usługi SignalR. Przyjrzyj się ASP.NET Core SignalR.

W tym temacie pokazano, jak zachować informacje o użytkownikach i ich połączeniach.

Wprowadzenie

Każdy klient łączący się z koncentratorem przekazuje unikatowy identyfikator połączenia. Tę wartość można pobrać we Context.ConnectionId właściwości kontekstu centrum. Jeśli aplikacja musi zamapować użytkownika na identyfikator połączenia i zachować to mapowanie, możesz użyć jednej z następujących opcji:

Każda z tych implementacji jest pokazana w tym temacie. Metody , OnDisconnectedi OnReconnectedHub klasy służą OnConnecteddo śledzenia stanu połączenia użytkownika.

Najlepsze podejście dla aplikacji zależy od:

  • Liczba serwerów internetowych hostowania aplikacji.
  • Niezależnie od tego, czy chcesz uzyskać listę aktualnie połączonych użytkowników.
  • Niezależnie od tego, czy musisz utrwalać informacje o grupie i użytkowniku podczas ponownego uruchamiania aplikacji lub serwera.
  • Czy opóźnienie wywoływania serwera zewnętrznego jest problemem.

W poniższej tabeli przedstawiono, które podejście działa w przypadku tych zagadnień.

Kwestie do rozważenia Więcej niż jeden serwer Pobieranie listy aktualnie połączonych użytkowników Utrwalanie informacji po ponownym uruchomieniu Optymalna wydajność
W pamięci
Grupy pojedynczego użytkownika
Trwałe, zewnętrzne

Magazyn w pamięci

W poniższych przykładach pokazano, jak zachować połączenie i informacje o użytkowniku w słowniku przechowywanym w pamięci. Słownik używa elementu do HashSet przechowywania identyfikatora połączenia. W dowolnym momencie użytkownik może mieć więcej niż jedno połączenie z aplikacją SignalR. Na przykład użytkownik połączony za pośrednictwem wielu urządzeń lub więcej niż jednej karty przeglądarki będzie miał więcej niż jeden identyfikator połączenia.

Jeśli aplikacja zostanie zamknięta, wszystkie informacje zostaną utracone, ale zostaną ponownie wypełnione, ponieważ użytkownicy ponownie ustanowią swoje połączenia. Magazyn w pamięci nie działa, jeśli środowisko zawiera więcej niż jeden serwer internetowy, ponieważ każdy serwer będzie miał oddzielną kolekcję połączeń.

W pierwszym przykładzie przedstawiono klasę zarządzaną mapowaniem użytkowników na połączenia. Kluczem zestawu skrótów będzie nazwa użytkownika.

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);
                    }
                }
            }
        }
    }
}

W następnym przykładzie pokazano, jak używać klasy mapowania połączeń z centrum. Wystąpienie klasy jest przechowywane w nazwie _connectionszmiennej .

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();
        }
    }
}

Grupy pojedynczego użytkownika

Możesz utworzyć grupę dla każdego użytkownika, a następnie wysłać wiadomość do tej grupy, gdy chcesz uzyskać dostęp tylko do tego użytkownika. Nazwa każdej grupy to nazwa użytkownika. Jeśli użytkownik ma więcej niż jedno połączenie, każdy identyfikator połączenia jest dodawany do grupy użytkownika.

Nie należy ręcznie usuwać użytkownika z grupy po rozłączeniu użytkownika. Ta akcja jest wykonywana automatycznie przez platformę SignalR.

W poniższym przykładzie pokazano, jak zaimplementować grupy pojedynczego użytkownika.

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();
        }
    }
}

Trwały, zewnętrzny magazyn

W tym temacie pokazano, jak używać bazy danych lub usługi Azure Table Storage do przechowywania informacji o połączeniu. Takie podejście działa, gdy masz wiele serwerów sieci Web, ponieważ każdy serwer internetowy może wchodzić w interakcje z tym samym repozytorium danych. Jeśli serwery internetowe przestaną działać lub aplikacja zostanie uruchomiona ponownie, OnDisconnected metoda nie zostanie wywołana. W związku z tym możliwe, że repozytorium danych będzie zawierać rekordy identyfikatorów połączeń, które nie są już prawidłowe. Aby wyczyścić te oddzielone rekordy, możesz unieważnić wszystkie połączenia utworzone poza przedziałem czasu, które są istotne dla aplikacji. Przykłady w tej sekcji zawierają wartość śledzenia podczas tworzenia połączenia, ale nie pokazują, jak wyczyścić stare rekordy, ponieważ możesz to zrobić jako proces w tle.

baza danych

W poniższych przykładach pokazano, jak zachować informacje o połączeniu i użytkowniku w bazie danych. Możesz użyć dowolnej technologii dostępu do danych; jednak w poniższym przykładzie pokazano, jak definiować modele przy użyciu programu Entity Framework. Te modele jednostek odpowiadają tabelom i polam bazy danych. Struktura danych może się znacznie różnić w zależności od wymagań aplikacji.

W pierwszym przykładzie pokazano, jak zdefiniować jednostkę użytkownika, która może być skojarzona z wieloma jednostkami połączenia.

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; }
    }
}

Następnie z centrum można śledzić stan każdego połączenia z kodem przedstawionym poniżej.

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 Table Storage

Poniższy przykład usługi Azure Table Storage jest podobny do przykładu bazy danych. Nie zawiera wszystkich informacji, które należy rozpocząć pracę z usługą Azure Table Storage. Aby uzyskać informacje, zobacz How to use Table Storage from .NET (Jak używać usługi Table Storage z platformy .NET).

W poniższym przykładzie przedstawiono jednostkę tabeli do przechowywania informacji o połączeniu. Partycjonuje dane według nazwy użytkownika i identyfikuje każdą jednostkę według identyfikatora połączenia, dzięki czemu użytkownik może mieć wiele połączeń w dowolnym momencie.

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;
        }
    }
}

W centrum śledzisz stan połączenia każdego użytkownika.

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");
        }
    }
}