SignalR ile Canlı Kullanıcı Sayısını Bulma ve Redis ile Loglama Bölüm 1
Selamlar,
Bugün farklı sunuculardaki bir portalın kullanıcı sayılarını, sunucu bazında real time olarak bulup hem bir monitör ekranında göstericeğiz, hem de redis ‘in pub/sub özelliğini kullanıp MsSql bir db’ye logluyacağız.
Öncelikle online kullanıcı sayısının alınacağı sayfayı Mvc ile oluşturalım.
index.cshtml: Aşağıdaki görüldüğü gibi online kullanıcı sayısının bulanacağı borakasmer.com’un örnek bir demo sayfası hazırlanmıştır. Css için Nuget’den bootstarp indirilmiştir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
@{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <link href="~/Content/bootstrap.min.css" rel="stylesheet" /> <title>Index</title> </head> <body> <div class="container"> <div class="jumbotron glyphicon-align-center"> <img src="~/Images/BoraBlog.png" class="center-block" /> </div> </div> </body> </html> |
Örnek kullanıcı sayısının bulunacağı sayfa.
Öncelikle online kullanıcı sayısının sayılabilmesi için Nuget’den aşağıdaki SignalR paketi indirilir. Bu makale yazılırken ki en güncel stabil sürüm 2.2.0 dır.
SignalR “Product” hub sınıfı aşağıda görüldüğü gibi oluşturulur. Daha sonra bu sınıfa ait “OnConnected()” ve “OnDisconnected()” methodları override edilecektir. Method isimlerinden de anlaşılacağı gibi, client sayfaya ilk geldiğinde ve ayrılındığında sayım amaçlı birtakım işlemler yapılacaktır. SignalR hakkında detaylı bilgiliyi önceki makalemden okuyabilirsiniz.
1 2 3 4 5 6 7 8 9 10 |
public class Product : Hub { public override async Task OnConnected() { } public override async Task OnDisconnected(bool stopCalled) { } } |
Öncelikle client side tarafında signalR sınıfına nasıl connect olunuyor onu inceleyelim. Aşağıda görüldüğü gibi ilgili signalR.js ve magic script dediğimiz “~/signalr/hubs” aşağıdaki gibi sayfaya eklenmiştir. İlgili connection işlemi yapılıp console’a ilgili bildiri yazılmıştır. Burada önemli nokta aşağıda gördüğünüz gibi “sayHello()” function’ı dır. Normalde hiçbir işe yaramamaktadır. Yalnız “hubProxy“‘e herhangi bir function bağlanmadan, server side taraftaki “OnConnected()” methodu tetiklenmemektedir. Console’a ilgili mesaj yazılsa da server side tarafa, ilgili benzer function(“sayHello()“) yazılmadan düşülememektedir. Bu sanırım ileride düzeltilecek bir durum:)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
@{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>Index</title> <script src="~/scripts/jquery-2.2.2.min.js"></script> <script src="~/scripts/jquery.signalR-2.2.0.min.js"></script> <script src="~/signalr/hubs"></script> <link href="~/Content/bootstrap.min.css" rel="stylesheet" /> <script> var hubProxy = $.connection.product; $.connection.hub.logging = true; $.connection.hub.start().done(function () { console.log("hub.start.done"); }).fail(function (error) { console.log(error); }); hubProxy.client.sayHello = function () { alert("App Start"); } </script> </head> <body> <div class="container"> <div class="jumbotron glyphicon-align-center"> <img src="~/Images/BoraBlog.png" class="center-block" /> </div> </div> </body> </html> |
Sıra geldi ilgili canlı kullanıcı sayısını saymaya. Öncelikle aşağıda görüldüğü gibi “UserClass” adında bir static sınıfı oluşturulmuştur. “users” List : Uniq bir ID ile gelicek olan userları saklarken, Lock nesnesi olarak, “lockObject” kullanılacaktır.
1 2 3 4 5 |
public static class UserClass { public static List<string> users = new List<string>(); public static object lockObject = new Object(); } |
“OnConnected()” methodu’na aşağıdaki kodlar eklenir: Öncelikle, gelen client’ın “ConnectionId“‘si alınmıştır. Ilgili “clientID“‘ye göre böyle bir user’ın “users” Liste’de olup olmadığına bakılır. Eğer yok ise ilgili client ‘ın girdiği sayfaya bakılır. Örneğin aşağıda client’ın girdiği sayfanın “Admin” olup olmadığına bakılmıştır. Admin değil ise ve önceden bu user listede yok ise, ilgili user listeye eklenir. Aşağıda yorumlanan bir satır vardır. İstenir ise ilgili client bir guruba’da örneğin aşağıda “index” gurubuna da eklenebilirdi. Buradan çıkarılacak ders, amaca göre girilen sayfa guruplarına göre de kullanıcı sayası tutulabilinir. Bu örnekde “Admin” sayfasında ve server bazında toplam kullanıcı sayısı gösterileceği için sadece “index” sayfasına giren userların sayısı saydırılmıştır.
HomeController.cs/Product:Hub :
1 2 3 4 5 6 7 8 9 10 11 |
public override async Task OnConnected() { string clientId = Context.ConnectionId; if (UserClass.users.IndexOf(clientId) == -1) { bool isAdmin = Context.Request.Headers["Referer"].Contains("Admin"); //Groups.Add(clientId, "Index"); if (!isAdmin) UserClass.users.Add(clientId); } } |
Client çıkış yaptığı zaman aşağıdaki “OnDisconneted()” methodu tetiklenmektedir. Burada farklı olan durum, client’ın geldiği sayfanın “Headers” bilgisinin okunamamasıdır. Çünkü sayfa çoktan kapanmıştır:) Kapanmış bir sayfanın header’ının okunamaması dan dolayı, ilgili kodlar yorumlanmıştır. Eğer ilgili client, yani az önce sayfaydan çıkan user, listede var ise çıkarılmaktadır.
Not: Eğer birkaç tane guruba göre kullanıcı sayısı tutuluyor ise, o zaman client’ın her bir guruba göre tek tek bakılıp çıkarılmasına gerek yoktur. Zaten asenkron olarak yapılmaya çalışılan bu işlem hiçbir zaman tamamlanamayacaktır. Çünkü client ile signalR arasında, hiçbir connection kalmamıştır. .Net disconnect durumunda kendi Groups sınıfını optimize edecektir. Ayrıca bir işlem yapılmasına gerek yoktur.
1 2 3 4 5 6 7 8 9 10 11 |
public override async Task OnDisconnected(bool stopCalled) { string clientId = Context.ConnectionId; if (UserClass.users.IndexOf(clientId) > -1) { //bool isAdmin = Context.Request.Headers["Referer"].Contains("Admin"); //Disconnect'e Url bilgisi gelmiyor. Çünkü connection yok.Ayrıca yapılmasına gerek yoktur! //if (!isAdmin) UserClass.users.Remove(clientId); //Hicbir zaman admin olmadigi icin kontrole gerek yok. } } |
Şimdi sıra geldi ilgili kullanıcı sayısını bir de cache’de tutup, cache time out olduğu zaman server bazında MsSql bir DB’ye kaydetmeye. Ben bunun için Redis kullandım. Eğer redis hakkında daha detaylı bir bilgi edinmek isterseniz önceki makalemi inceleye bilirsiniz. Windows 10 ortamında redis’in çalışması için ilgili “redis-server.exe” ve “redis-cli.exe” dosyaları indirilir. Windows 10 ortamında redis server çalıştırılınca aşağıdaki gibi bir console ekranı ile karşılaşılır. Artık redis Windows ortamında ayağa kaldırılmıştır.
HomeController.cs/Product:Hub(Full): İlgili Product sınıfı aşağıdaki gibi değiştirilir. Öncelikle “RedisEndpoint()” default port olan:6379’dan local’e bağlanılır. Ayrıca web.config’den “ServerName”‘i alınır. Amaç hangi sunucudaki client sayısının toplanacağının belirlenmesidir.
Web.config: “ServerName”‘in tanımlandığı satır.
1 |
<add key="ServerName" value="BoraServer1"/> |
“using()” içerisinde ilgili “RedisCliemt()” tanımlanan EndPoint’e göre oluşturulur. Ve “OnConnected()” işleminin sonunda “CheckAndSetUserCount()” methodu çağrılır. İlgili method asenkron olarak çağrılmaktadır. Aynı işlemler “OnDisconnected()” methodu için de tekrarlanır.
“CheckAndSetUserCount()” methodu 4 parametre beklemektedir. İlk parametre RedisClient’dır. “Web.config”‘den alınan connection’a göre, bağlanılan redis server nesnesi parametre olarak verilir. 2. parametre “userCount” yani toplam kullanıcı sayısının tutulduğu static users adlı listedeki eleman sayısı toplamıdır. 3. Parametre client’ın connect olup olmadığının bilgisidir. Son yani 4. parametre client’ın bağlandığı sayfanın “Admin” sayfası olup olmadığına bakılır. “userCountWithServer” değişkeni adındanda anlaşılacağı gibi, “Web.config”‘den çekilen server ismi ve toplam online client sayısı birleştirilerek oluşturulan string bir değişkendir. Kısaca server’a ait toplam kullanıcı sayısını vermektedir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
public class Product : Hub { static RedisEndpoint conf = new RedisEndpoint() { Host = "127.0.0.1", Port = 6379 }; static string serverName = WebConfigurationManager.AppSettings["ServerName"]; public override async Task OnConnected() { string clientId = Context.ConnectionId; if (UserClass.users.IndexOf(clientId) == -1) { using (IRedisClient client = new RedisClient(conf)) { bool isAdmin = Context.Request.Headers["Referer"].Contains("Admin"); //Groups.Add(clientId, "Index"); if (!isAdmin) UserClass.users.Add(clientId); await CheckAndSetUserCount(client, UserClass.users.Count(), true, isAdmin); } } } public override async Task OnDisconnected(bool stopCalled) { string clientId = Context.ConnectionId; if (UserClass.users.IndexOf(clientId) > -1) { using (IRedisClient client = new RedisClient(conf)) { //bool isAdmin = Context.Request.Headers["Referer"].Contains("Admin"); //Disconnect'e Url bilgisi gelmiyor. //if (!isAdmin) UserClass.users.Remove(clientId); //Hicbir zaman admin olmadigi icin kontrole gerek yok. await CheckAndSetUserCount(client, UserClass.users.Count(), false, false); } } } public async Task CheckAndSetUserCount(IRedisClient client, int userCount, bool isConnect, bool isAdmin) { string userCountWithServer = serverName + "æ" + userCount.ToString(); if (isAdmin && isConnect) { await Groups.Add(Context.ConnectionId, "admin"); } else if (!client.ContainsKey("UserCount")) // && isConnect==true eklenirse reconnect durumunda Console'a deger yanlış yere gönderilmez. { lock (UserClass.lockObject) { if (!client.ContainsKey("UserCount")) { TimeSpan span = new TimeSpan(0, 0, 0, 20, 0); client.Set("UserCount", UserClass.users.Count, span); client.PublishMessage("UserCountService", userCountWithServer); } } } await Clients.Group("admin").refreshUserCount(userCountWithServer); } } |
Yukarıdaki örnekde görüldüğü gibi:
- Gelen client “Admin” ekranınden gelmiş ise “Groups.Add(Context.ConnectionId, “admin”)” ile geldiği connectionID ile birlikte “admin” gurubuna eklenir. Amaç online kullanıcı bilgisinin sadece bu guruba dahil clientlarda gösterilmesini sağlamaktır.
- Redis Cache’e ya ilkkez geliniyor ise ya da time out’a düşülmüş ise yandaki koşul sağlanmış olunur. “client.ContainsKey(“UserCount”)” Amaç redis cache’in timeout durumunda, toplam kullanıcı sayısının ayrıca yazılmış bir console application’a gönderilmesidir.(Pub/sub)
- Normalde Redis Cache’e deger atanırken lock işlemine gerek yoktur. Ama burada ayrıca “PublishMessage()” methodu da çağrıldığı için static “UserClass.lockObject” lock nesnesi kullanılmıştır. Aynı anda cache timeout’a düşen 1000 kullanıcının bu methodu topluca çağırmasını engellemek için, ilk gelen client’ın bu işlemi gerçekleştirirken diğer clientların bu işlemi beklemesi, ilgili static obje ile sağlanmaktadır.
- Redis Cache’e, “TimeSpan” ile 20sn lik expire süresi atanmıştır. TimeOut durumunda, var olan toplam yeni kullanıcı sayısı tekrardan ilgili cache’e atanacakdır.
- Redis Pub/Sub işleminde, yandaki method ile “client.PublishMessage(“UserCountService”, userCountWithServer)” oluşturulacak bir console application’a, kullanıcı sayısı ve server ismi ile birlikte gönderilir.
- Son olarak ilgili “admin” gurubunun tamamında client side’da “refreshUserCount” function’ı, ilgili kullanıcı sayısı ve server ismi ile birlikte tetiklenir. Böylece yeni kullanıcı girişinde ve çıkışında, değişen toplam kullanıcı sayısı monitoring ekranında yenilenmiş olunur.
Admin.cshtm: Aşağıda görüldüğü gibi öncelikle ilgili signalR Hub sınıfına bağlanılmış ve “refreshUserCount()” function’ı tanımlanmıştır. İlgili functionda gelen data split ile “server ismi” ve “toplam kullanıcı” sayısı olarak ayrılmıştır. Daha sonra gelen datadan server ismi ile “id” değeri aynı olan bir “<td>” içine online kullanıcı sayısı basılır. Böylece Admin ekranında kullanıcı sayısı SignalR ile real time olarak gösterilmiş olunur.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
@{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>Admin</title> <script src="~/scripts/jquery-2.2.2.min.js"></script> <script src="~/scripts/jquery.signalR-2.2.0.min.js"></script> <script src="~/signalr/hubs"></script> <link href="~/Content/bootstrap.min.css" rel="stylesheet" /> <script> var hubProxy = $.connection.product; $.connection.hub.logging = true; $.connection.hub.start().done(function () { console.log("hub.start.done"); }).fail(function (error) { console.log(error); }); hubProxy.client.refreshUserCount = function (data) { console.log(data); var serverName = data.split('æ')[0]; var userCount = data.split('æ')[1]; $("#Sunucu").html(serverName); $('#' + serverName).html(userCount); }; </script> </head> <body> <div class="container"> <div class="jumbotron"> <h1>Sunuculara Gore Online User Sayisi</h1> </div> <h2><font color="red"> Sunucu: <span id="Sunucu"></span></font></h2> </div> <table class="table"> <thead> <tr> <th>Sunucu Ismi</th> <th>Anlik User Sayisi</th> </tr> </thead> <tbody id="tbody"> <tr> <td>Bora Server 1</td> <td id="BoraServer1"></td> </tr> <tr> <td>Bora Server 2</td> <td id="BoraServer2">0</td> </tr> </tbody> </table> </body> </html> |
Geldik makelenin ilk bölümünün sonuna. Bu bölümde bir index sayfası için var olan kullanıcı sayısını real time olarak bulduk ve bunu Admin bir sayfada yine real time olarak SignalR socket kullanarak gösterdik. Ayrıca loglama amacı ile redis cache kullanıp, timeout zamanında bir sonraki bölümde yazılacak console application’a, kullanıcı sayısını sunucu bilgisi ile birlikte gönderdik. Bir sonraki bölümde bu bilgilerin gönderildiği consol application’ı yazıp MsSql server’a kaydedeceğiz. Ayrıca farklı bir sunucuda çalışan benzer bir Index sayfası yapıp, farklı sunuculardaki farklı online kullanıcı sayılarını ve toplamını gösteren bir Glabal Monitor sayfası oluşturacağız. Burada üzerinde düşünülmesi gereken ve maklenin esas amacı olan kısım, tüm sunuculardaki toplam kullanıcı sayısının nasıl gösterileceğidir. Herbir sunucuya ait signalR sınıfı ve kullanıcı sayısı farklı iken hepsini tek bir çatıda toplamak performans açısından çok hasas bir konudur.
Bir sonraki makalede görüşmek üzere hoşçakalın.
Source:
Bora abi bu mükemmel örnek için teşekkürler, yine muhteşem bir makale olmuş. Üyeleri ve ziyaretçileri birbirinden ayırmak için session ile oturum aşmış kullanıcıların sessionId değerlerini signalr ile nasıl alabiliriz? signalr ile o kullanıcı ile iletişime geçmek istiyorum. Mesela oturum aşmış üyelerden birine anlık mesaj göndermek için kullanmak isteyebilirim.
adam 2016 da yazmış inşallah cevabı bulmuştur banada cevap verir :D
Redis’in kullanımına örnek ve SignalR bilgisini gözden geçirmek açısından oldukça iyi bir makale.
Merhabalar, ben Akif Tuncel Meslek lisesini yeni bitirdim ve kendi çabamla yazılım sektöründe ilerlemek için çaba gösteriyorum.
Signal R teknolojisi üzerine deneyim sahibi değilim, sadece şunu danışmak istiyorum;
Tüm kullanıcıların değil de, sadece tek bir kullanıcının haberdar olmasını isteseydik
Bunun için tek bir connectionID yeterli olur muydu ?
Yani bir kullanıcının connectionID si var ise elimizde, bu ona ulaşmak için yeterli midir ?
Şöyle bir yol izleyebilir miyiz ?
Client.User(ConnectionId).send(message); bunu nasıl sağlarız ?
Teşekkür ederim.