SignalR Kullanıcılarını Bağlantılarla Eşleme

yazan: Tom FitzMacken

Uyarı

Bu belgeler SignalR'nin en son sürümüne yönelik değildir. SignalR ASP.NET Core göz atın.

Bu konuda, kullanıcılar ve bağlantıları hakkındaki bilgilerin nasıl tutulacakları gösterilmektedir.

Patrick Fletcher bu konunun yazılmasına yardımcı oldu.

Bu konuda kullanılan yazılım sürümleri

Bu konunun önceki sürümleri

SignalR'nin önceki sürümleri hakkında bilgi için bkz. SignalR Eski Sürümleri.

Sorular ve yorumlar

Lütfen bu öğreticiyi nasıl beğendiğiniz ve sayfanın altındaki yorumlarda neleri geliştirebileceğimiz hakkında geri bildirim bırakın. Öğreticiyle doğrudan ilgili olmayan sorularınız varsa bunları ASP.NET SignalR forumunu veya StackOverflow.com gönderebilirsiniz.

Giriş

Hub'a bağlanan her istemci benzersiz bir bağlantı kimliği geçirir. Bu değeri hub bağlamının Context.ConnectionId özelliğinde alabilirsiniz. Uygulamanızın bir kullanıcıyı bağlantı kimliğine eşlemesi ve bu eşlemeyi devam ettirmesi gerekiyorsa, aşağıdakilerden birini kullanabilirsiniz:

Bu uygulamaların her biri bu konuda gösterilmiştir. Kullanıcı bağlantı durumunu izlemek için sınıfının , OnDisconnectedve OnReconnected yöntemlerini Hub kullanırsınızOnConnected.

Uygulamanız için en iyi yaklaşım aşağıdakilere bağlıdır:

  • Uygulamanızı barındıran web sunucularının sayısı.
  • Bağlı olan kullanıcıların listesini almanız gerekip gerekmediği.
  • Uygulama veya sunucu yeniden başlatıldığında grup ve kullanıcı bilgilerini kalıcı hale getirmek isteyip istemediğiniz.
  • Dış sunucuyu çağırma gecikmesinin sorun oluşturup oluşturmadığı.

Aşağıdaki tabloda bu konular için hangi yaklaşımın işe yaradığı gösterilmektedir.

Değerlendirme Birden fazla sunucu Şu anda bağlı olan kullanıcıların listesini alma Yeniden başlatmalardan sonra bilgileri kalıcı hale En iyi performans
UserID Sağlayıcısı
Bellek içi
Tek kullanıcılı gruplar
Kalıcı, dış

IUserID sağlayıcısı

Bu özellik, kullanıcıların yeni bir IUserIdProvider arabirimi aracılığıyla bir IRequest'i temel alan userId değerini belirtmesine olanak tanır.

The IUserIdProvider

public interface IUserIdProvider
{
    string GetUserId(IRequest request);
}

Varsayılan olarak, kullanıcı adı olarak kullanıcının IPrincipal.Identity.Name adını kullanan bir uygulama olacaktır. Bunu değiştirmek için uygulamanız başlatıldığında uygulamanızı IUserIdProvider genel ana bilgisayara kaydedin:

GlobalHost.DependencyResolver.Register(typeof(IUserIdProvider), () => new MyIdProvider());

Bir hub'ın içinden aşağıdaki API aracılığıyla bu kullanıcılara ileti gönderebileceksiniz:

Belirli bir kullanıcıya ileti gönderme

public class MyHub : Hub
{
    public void Send(string userId, string message)
    {
        Clients.User(userId).send(message);
    }
}

Bellek içi depolama

Aşağıdaki örnekler, bellekte depolanan bir sözlükte bağlantı ve kullanıcı bilgilerinin nasıl tutulduğunu gösterir. Sözlük, bağlantı kimliğini depolamak için bir HashSet kullanır. Herhangi bir anda kullanıcının SignalR uygulamasıyla birden fazla bağlantısı olabilir. Örneğin, birden çok cihaz veya birden fazla tarayıcı sekmesi aracılığıyla bağlanan bir kullanıcının birden fazla bağlantı kimliği olabilir.

Uygulama kapatılırsa, tüm bilgiler kaybolur, ancak kullanıcılar bağlantılarını yeniden kurdukçe yeniden doldurulur. Ortamınız birden fazla web sunucusu içeriyorsa bellek içi depolama çalışmaz çünkü her sunucunun ayrı bir bağlantı koleksiyonu olur.

İlk örnekte, kullanıcıların bağlantılarla eşlemesini yöneten bir sınıf gösterilmektedir. HashSet anahtarı kullanıcının adı olacaktır.

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

Sonraki örnekte bir hub'dan bağlantı eşleme sınıfının nasıl kullanılacağı gösterilmektedir. sınıfının örneği değişken adında _connectionsdepolanır.

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

Tek kullanıcılı gruplar

Her kullanıcı için bir grup oluşturabilir ve sonra yalnızca o kullanıcıya ulaşmak istediğinizde bu gruba bir ileti gönderebilirsiniz. Her grubun adı, kullanıcının adıdır. Kullanıcının birden fazla bağlantısı varsa, her bağlantı kimliği kullanıcının grubuna eklenir.

Kullanıcı bağlantısı kesildiğinde kullanıcıyı gruptan el ile kaldırmamalısınız. Bu eylem SignalR çerçevesi tarafından otomatik olarak gerçekleştirilir.

Aşağıdaki örnekte tek kullanıcılı grupların nasıl uygulandığı gösterilmektedir.

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

Kalıcı, harici depolama

Bu konuda, bağlantı bilgilerini depolamak için bir veritabanının veya Azure tablo depolamanın nasıl kullanılacağı gösterilmektedir. Her web sunucusu aynı veri deposuyla etkileşime geçebildiğinden, birden çok web sunucunuz olduğunda bu yaklaşım çalışır. Web sunucularınız çalışmayı durdurursa veya uygulama yeniden başlatılırsa yöntemi OnDisconnected çağrılmıyordur. Bu nedenle, veri deponuzun artık geçerli olmayan bağlantı kimlikleri için kayıtları olması mümkündür. Bu yalnız bırakılmış kayıtları temizlemek için, uygulamanızla ilgili bir zaman çerçevesi dışında oluşturulan tüm bağlantıları geçersiz kılmanız gerekebilir. Bu bölümdeki örnekler, bağlantı oluşturulduğunda izleme için bir değer içerir, ancak bunu arka plan işlemi olarak yapmak isteyebileceğiniz için eski kayıtların nasıl temizlendiğini göstermez.

Veritabanı

Aşağıdaki örneklerde, veritabanında bağlantı ve kullanıcı bilgilerinin nasıl tutulacakları gösterilir. Herhangi bir veri erişim teknolojisini kullanabilirsiniz; ancak aşağıdaki örnekte Entity Framework kullanarak modellerin nasıl tanımlanacağı gösterilmektedir. Bu varlık modelleri veritabanı tablolarına ve alanlarına karşılık gelir. Veri yapınız, uygulamanızın gereksinimlerine bağlı olarak önemli ölçüde farklılık gösterebilir.

İlk örnekte, birçok bağlantı varlığıyla ilişkilendirilebilen bir kullanıcı varlığının nasıl tanımlanacağı gösterilmektedir.

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

Ardından hub'dan her bağlantının durumunu aşağıda gösterilen kodla izleyebilirsiniz.

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 tablo depolama

Aşağıdaki Azure tablo depolama örneği, veritabanı örneğine benzer. Azure Tablo Depolama Hizmeti'ni kullanmaya başlamak için ihtiyacınız olan tüm bilgileri içermez. Bilgi için bkz. .NET'ten Tablo depolamayı kullanma.

Aşağıdaki örnekte bağlantı bilgilerini depolamaya yönelik bir tablo varlığı gösterilmektedir. Verileri kullanıcı adına göre bölümlere ayırır ve her varlığı bağlantı kimliğine göre tanımlar, böylece kullanıcının istediği zaman birden çok bağlantısı olabilir.

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

Hub'da her kullanıcının bağlantısının durumunu izlersiniz.

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