کنترل ارتباط و کاربران در SignalR

جمعه 22 مرداد 1395

در این مقاله ما نحوه ی کنترل ارتباط کاربران بر روی Connectionها با استفاده از SignalR را یاد خواهیم گرفت ،مراحل انجام این کار به صورت گام به گام توضیح داده شده است.

کنترل ارتباط و کاربران در SignalR

معرفی:

به ازای هر اتصال کاربر به یک هاب ، یک Connection id انحصاری در نظر گرفته می شود.شما می توانید این مقدار را در Property به نام Context.ConnectionId که مربوط به هاب می باشد، بازیابی کنید.اگر برنامه ی شما نیاز دارد تا به کاربر Connection Id، بدهیم و این Id باقی بماند شما می توانید یکی از راه های زیر را استفاده کنید:

User ID Provider(SignalR)

ذخیره سازیIn-memory  مانند یک دیکشنری

گروهSignalR  برای هر کاربر

Permanent ،  external storage مانند یک جدول دیتابیس یا حافظه ی جدولی Azure

هر کدام از این پیاده سازی ها در این مقاله نشان داده می شود.شما از متد های OnConnected ، OnDisconnected و OnReconnected ،  برای پیگیری وضعیت اتصال کاربران که مربوط به کلاس Hub می باشد است .

بهترین روش برای برنامه شما به موارد زیر بستگی دارد:

* به تعداد وب سرورهایی که در حال میزبانی برنامه ی شما هستند .

* به تمایل و یا عدم تمایل شما برای داشتن لیست کاربران متصل شده به برنامه .

* به نیاز و یا عدم نیاز شما برای نگهداری گروه و اطلاعات کاربر زمانی که برنامه یا سرور راه اندازی مجدد می شود.

* به میزان مشکل ساز بودن  تاخیردر  فراخوانی یک سرور خارجی

جدول زیر نشان می دهد که کدام رویکرد برای موارد زیر مناسب است.

IUserID Provider

این ویژگی به کاربران اجازه می دهد تا نوع UserId خودشان را براساس IRequest و یک اینترفیس جدید به نام IUserIdProvider تشخیص دهند.

IUserProvider

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

به صورت پیش فرض ، یک بخش از برنامه وجوددارد که از Iprincipal.Identity.Name به عنوان نام کاربری استفاده می کند . برای تغییر آن، قبل از شروع برنامه پیاده سازی IUserIdProvider را با استفاده از global host خود انجام دهید:

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

ارسال پیغام به یک کاربر خاص:

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

 ذخیره سازی  In_memory :

در مثال های زیر نشان می دهد که چگونه Connection و اطلاعات کاربر در یک دیکشنری که در حافظه ذخیره می شود، نگهداری می شود. دیکشنری از یک HashSet برای ذخیره سازی Connection id استفاده می کند. در هر زمان یک کاربر می تواند بیش از یک Connection به نرم افزار Signal R  داشته باشدو برای مثال، کاربری که از طریق چندین Device یا بیش از یک تب مرورگر وصل شده است،بایدبیش از یک Connection id داشته باشد.

اگر برنامه خاموش شود ، همه ی اطلاعات از بین می رود، اما به محض آن که کاربر دوباره به برنامه متصل شود اطلاعات دوباره در برنامه جایگزین می شود.اگر شما در محیطی با بیش از یک وب سرور کار می کنید ذخیره سازی In_memory کار نخواهد کرد زیرا هر سرور به مجموعه ای از Connectionهای جداگانه نیاز دارد.

در اولین مثال یک کلاس را نشان می دهد که کنترل ارتباط کاربران بر روی Connectionها را مدیریت می کند.کلید برای 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);
                    }
                }
            }
        }
    }
}

در مثال بعدی نشان می دهد که چگونه از کلاس Connection mapping بوسیله ی یک هاب استفاده کنید.نمونه ای از کلاس در یک متغیر به نام 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();
        }
    }
}

گروه های Single_User

شما می توانید یک گروه برای هر کاربر ایجاد کنید و سپس زمانی که شما می خواهید یک پیام فقط به دست کاربر خاصی برسد این پیام را برای گروه بفرستید . نام هر گروه، نام کاربر می باشد. اگر یک کاربر بیش از یک Connection داشته باشد، هر Connection id  به گروه مربوط به کاربر اضافه می شود.

شما نباید زمانی که کاربر disconnect هست به صورت دستی کاربر را از گروه پاک کنید. این عمل به صورت خودکار بوسیله ی فریم ورک SignalR اجرا می شود.

در مثال زیر نشان می دهد که چگونه گروه های Single_user پیاده سازی می شود.

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 برای ذخیره سازی اطلاعات اتصال استفاده کنید. این روش زمانی کاربرد دارد که شما از چندین web server استفاده کنید زیرا هر Web server میتواند data repository مشابه ای تعامل داشته باشد. اگر Web server های شما از کار افتادند و یا application شما restart شود ، متد OnDisconnected فراخوانی نمی شود. بنابراین، این ممکن است که data repository شما connection هایی را در بر داشته باشد که دیگر معتبر نیستند.برای پاکسازی این رکورد های نامعتبر ، شما ممکن است بخواهید هر connection ای را که در خارج از یک بازه زمانی ساخته شده است و به Application شما مرتبط است را باطل کنید. مثال ها در این قسمت شامل یک مقدار برای ردیابی زمانی است که connection ایجاد شده است میباشند ولی پاکسازی رکورد های قدیمی را نشان نمیدهند به دلیل اینکه شما ممکن است بخواهید این فرایند را در پس زمینه انجام دهید.

دیتابیس

مثال های زیر نشان میدهند که چگونه باید اطلاعات connection و کاربر را در دیتابیس ذخیره کنیم. شما میتوانید از هر تکنولوژی ای برای دستیابی به داده ها استفاده کنید; اگرچه ، مثال زیر چگونگی تعریف مدل ها با استفاده از Entity Framework را به شما نشان می دهد. این مدل های entity همان جداول و فیلد های دیتابیس هستند. ساختار دیتابیس شما خیلی وابسته به احتیاجات نرم افزار شما است.

 مثال اول نشان میدهد که شما چگونه میتوانید یک کاربر بسازید و آن را به چندین connection انتساب دهید.

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

سپس از قسمت هاب ، با استفاده از کد زیر شما میتوانید وضعیت هر connection را ردیابی کنید.

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 نیاز دارید.

مثال زیر یک جدول را برای ذخیره سازی اطلاعات connection ها نشان میدهد. این جدول داده ها را با استفاده از user name تقسیم بندی ، و هر موجودیت را با Connection id متمایز میکند. بنابراین هر کاربر میتواند چندین connection مختلف داشته باشد.

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

در هاب شما میتوانید وضعیت هر Connection را ردیابی کنید.

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

آموزش asp.net mvc

برنامه نویسان

نویسنده 3355 مقاله در برنامه نویسان

کاربرانی که از نویسنده این مقاله تشکر کرده اند

در صورتی که در رابطه با این مقاله سوالی دارید، در تاپیک های انجمن مطرح کنید