Mvc, SignalR ve Redis ile Microservice Mantığında Kullanıcı Bilgilendirme
Selamlar,
Bu makalede Visual Studio 2017 ile bir Mvc proje yaratılıp, Session Timeout süresi 30sn’nin altında kalan tüm clientlar bir popup penceresi ile uyarılacaklardır. Bu işlem için, SignalR Hub sınıfına connect olan her bir client’ın, connectionID’si, key ve session başlangıç zamanı value olarak, Redis’e kaydedilecektir. (Redis hakkında detaylı bilgiyi, buradaki makalemden ulaşabilirsiniz.) Ayrıca signalR hakkında detaylı bilgiye de bu makalemden erişebilirsiniz.
Daha sonra bu sistemden bağımsız, arkada çalışan bir microservice uygulama redis içerisindeki tüm elemanları gezerek, SessionTimeOut süresinin bitmesine 30 sn kalan tüm clientları, signalR Hub methodunu tetikleyerek websocket ile uyaracak ve işi biten herbir elemanı redisden çıkaracaktır.
Projenin Genel Tablosu:
Örnek Çalışma Ekranı:
Öncelikle aşağıdaki paketler Nuget’den indirilir:
Bu makalede Redis için ServiceStack kütüphanesi kullanılmıştır.
Socket teknolojisi için de SignalR kullanılmıştır.
Ayrıca Local Windows 10 bir makinada redis server ve client uygulamaların çalıştırılması için aşağıdaki dosyaların indirilmesi gerekmektedir. Ben aşağıdaki dosyaları bu adresten , şu anki son sürüm olan (3.2.100)’i indirdim.
Asp.Net’de Session’ın TimeOut süresi default olarak 20 dakikadır. Aşağıdaki webconfig ayarı ile bu süre test amaçlı 1 dakikaya düşürülmüştür.
1 2 3 4 5 |
<system.web> <compilation debug="true" targetFramework="4.6.1"/> <httpRuntime targetFramework="4.6.1"/> <sessionState mode="InProc" cookieless="true" timeout="1"/> </system.web> |
Not: “İlk aşamada aşağıda yazılan kodların çoğu ihtiyaca göre tekrardan değiştirilecektir. Tüm code’un bir seferde yazılmamasının nedeni, sizin hangi ihtiyaçlardan ne yazılması gerektiğini daha iyi anlamanızı sağlamaktır.”
HomeController.cs(Index): Aşağıda görüldüğü gibi client sayfaya ilk geldiği zaman test amaçlı Session‘a “UserName” atanmıştır.
1 2 3 4 5 6 7 8 9 |
public class HomeController : Controller { // GET: Home public ActionResult Index() { Session.Add("UserName", "Bora Kaşmer"); return View(); } } |
Index.cshtml: Aşağıda görüldüğü gibi ilgili “<Head>” section’ında scriptler=> signalR ve jquery yolları tanımlanmıştır. Ayrıca magic script denilen “signalR/hubs” tanımlaması unutulmamalıdır. “$.connection.session” ile Session:Hub sınıfına connect olunur. Ayrıca “hubProxy.client.notifySession()” function’ı ile server sidedaki “Session:Hub” sınıfından gönderilen=> client’ın connectionID‘si ve yine aynı client’a ait Session TimeOut zamanı mesaj olarak console’a basılır.
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 |
@{ Layout = null; } <!DOCTYPE html> <html> <head> <script src="~/scripts/jquery-3.1.1.min.js"></script> <script src="~/scripts/jquery.signalR-2.2.1.min.js"></script> <script src="~/signalr/hubs"></script> <link href="~/Content/bootstrap.min.css" rel="stylesheet" /> <meta name="viewport" content="width=device-width" /> <title>Index</title> <script> var hubProxy = $.connection.session; $.connection.hub.logging = true; $.connection.hub.start().done(function () { console.log("hub.start.done"); }).fail(function (error) { console.log(error); }); hubProxy.client.notifySession = function (message) { console.log(message); }; </script> </head> <body> <div class="container"> <div class="jumbotron"> <h2>SignalR Session Notify Page</h2> </div> </div> </body> </html> |
Class Session: Hub: Aşağıda görüldüğü gibi Client Sayfaya geldiği zaman signalR tarafında Session:Hub sınıfına connect olunur. İşte tam da bu sırada “OnConnected” methodu çağrılır.
- Öncelikle “RedisEndpoint” yani redis sunucuya bağlanmak için gerekli olan bağlantı ayarlar local makina olarak ayarlanır.
- “section” değişkenine yukarıda webconfig’de tanımlanan session TimeOut süresi “Configuration” ve “SessionStateSection” sınıfları ile çekilerek atanır. Burada amaç session’ın time out olucağı zamanı bir config dosyadan almaktır. Böylece bu süre uygulama çalışırken de kolayca config dosyasından değiştirilebilir.
- RedisClient’a bağlanıldıktan sonra connect olacak Client’ın connectionID’si “Context.ConnectionId” ile çekilir. Redis’e key olarak eklenecek bu ID’nin, daha önceden var olup olmadığı “ContainsKey()” methodu ile kontrol edilerek, işlem adımlarına devam edilir.
- “client.set()” methodu ile Redis’e bağlanan Client’ın connectionID‘si key olarak ve Session’ın timeout olucağı zaman da value olarak atanır.
- Son olarak connect olan client, “Clients.Caller” şeklinde yakalanarak client side tarafdaki “notifySession()” function’ı, ilgili client’ın connectionID’si ve Session’ın TimeOut olacağı zaman ile birlikte parametre olarak gönderilir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class Session : Hub { static RedisEndpoint conf = new RedisEndpoint() { Host = "127.0.0.1", Port = 6379 }; public override async Task OnConnected() { Configuration config = WebConfigurationManager.OpenWebConfiguration("~/Web.Config"); SessionStateSection section = (SessionStateSection)config.GetSection("system.web/sessionState"); using (IRedisClient client = new RedisClient(conf)) { if (!client.ContainsKey(Context.ConnectionId)) { client.Set(Context.ConnectionId, DateTime.Now.AddMinutes(section.Timeout.TotalMinutes)); } await Clients.Caller.notifySession(Context.ConnectionId + ":" + DateTime.Now.AddMinutes(section.Timeout.TotalMinutes)); } } } |
Şimdi sıra geldi client’ın session timeout olmasına 30 sn kala gösterilecek olan uyarı mesajının html’ine ve gösterilmesi için tetiklenecek olan javascript’in kodlanmasına:
Öncelikle aşağıdaki gibi bir css oluşturulur.
alert.css:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
.alert { padding: 20px; background-color: #f44336; color: white; } .closebtn { margin-left: 15px; color: white; font-weight: bold; float: right; font-size: 22px; line-height: 20px; cursor: pointer; transition: 0.3s; } .closebtn:hover { color: black; } |
Örnek Alert Görünümü:
Index.html: Aşağıda görüldüğü gibi görünümü “display:none” ile gizlenmiş uyarı mesajının verildiği bir “div“‘e konulmuştur. İçerisinde “<strong>” html tagleri ile yazılmış uyarı mesajı bulunmaktadır.
1 2 3 4 5 6 7 8 9 10 11 |
<body> <div class="container"> <div class="jumbotron"> <h2>SignalR Session Notify Page</h2> <div class="alert" style="display:none"> <span class="closebtn" onclick="this.parentElement.style.display='none';">×</span> <strong>Uyarı!</strong> Session Süreniz 30sn Sonra Dolucak!. </div> </div> </div> </body> |
Index.html/Script: Script tagları arasına aşağıdaki “hubclient.notifyUser()” function’ı eklenir. Amacı microservice’te süresi dolan bir client bulunduğunda signalR Hub sınıfı olan Session’da, yeni yazacağımız bir methodun, bu yazdığımız client tarafındaki function’ı tetikleyip, süresi dolan cilent’a ilgili uyarının gösterilmesidir.
1 2 3 4 5 6 |
<script> hubProxy.client.notifyUser = function () { $('.alert').css('display', 'inline-block'); }; </script> |
Şimdi sıra geldi redis’i her 30sn’de bir kontrol eden Microservice yazmaya. Yukarıda görüldüğü gibi “CheckSession” adında yeni bir Console Application yaratılır.
CheckSession/Program.cs (Full): Bu proje için de yine Service.Stack.Redis kütüphanesi indirilmiştir. Ayrıca session süresi 30’sn kalan clientları uyarmak için aşağıda görüldüğü gibi SignalR.Client indirilmiştir.
- Öncelikle static olarak connect olunacak remote’daki Redis sunucusunun erişim bilgileri ip ve portu ile birlikte tanımlanır. “new RedisEndpoint() { Host = “127.0.0.1”, Port = 6379 }“
- Redis’deki kayıtların önceden belirlenen 10sn de bir zaman aralığında taranabilmesi için, yeniden static bir Timer nesnesi yaratılır. “static Timer _Timer = new Timer(10000)“
- Main() methodunda “_Timer” nesnesinin her 10sn bir tetiklenmesinde, çağrılacak “CheckRedis()” methodu bağlanır. Ve _Timer nesnesinin sayma işlemi “Start()” methodu ile başlanır.
CheckRedis():
- En başta yapılacak işlemin uzun sürebilmesinden dolayı ilgili “_Timer” nesnesi “Stop()” methodu ile durdurulur.
- using(){} ile ilgili “IRedisClient” create edilir.
- Redisdeki tüm keyler “client.SearchKeys(“*”)” methodu ile çekilir. Yani tüm clientlara ait “ConnectionID“‘ler.
- Tüm keyler “foreach()” ile gezilerek, value değer olan session’ın bitiş zamanı, şimdiki zamandan çıkarılarak “seconds” değişkenine kaç sn kaldığı atanır. “var seconds = sessionTime.Subtract(DateTime.Now).TotalSeconds” Böylece kalan zaman saniye cinsinden bulunmuş olunur.
- Eğer kalan süre 30sn’den az ise yukarıda yazılan Session Hub class’ına bağlanılır. Burada bir de parametre olarak, bağlanılan kaynağın console olduğunu gösteren “console=1” değişkeni atanmıştır. “var connection = new HubConnection(“http://localhost:2533/”, “console=1”)“
Önemli Not: “console=1” şeklinde url’de paramtere gönderilme nedeni, SignalR sınıfının “OnConnected()” methodunda gelen kaynağa göre redise yeni bir connectionID’li datanın, atılmasının engellenmesidir. Makelenin devamında “Session : Hub” sınıfının codeları değişecektir.
- İlgili “Session:Hub” sınıfına Connection işlemi yapılıp beklenildikten sonra, “connection.Start().Wait()” notifyClient() methodu gönderilecek client’ın connectionID’si (key) ve gönderilecek text mesaj ile birlikte parametre geçilerek tetiklenir(Invoke). “myHub.Invoke(“notifyClient”, new object[]{ key, “Session Süreniz 30sn Sonra Dolucak!.”}).Wait()“
- Uyarı gönderilen client’a ait kayıt (connectionID) keyine göre, redisden çıkarılır. “client.Remove(key)“
- Garbage Collector’un temizleme işlemine zorlanmasından sonra, “_Timer” tekrardan başlatılır. “_Timer.Start()“
Program.cs(Full):
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 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Timers; using System.Threading.Tasks; using ServiceStack.Redis; using Microsoft.AspNet.SignalR; using Microsoft.AspNet.SignalR.Client; namespace CheckSession { class Program { static RedisEndpoint conf = new RedisEndpoint() { Host = "127.0.0.1", Port = 6379 }; static Timer _Timer = new Timer(10000); static void Main(string[] args) { _Timer.Elapsed += CheckRedis; _Timer.Start(); Console.ReadLine(); } private static void CheckRedis(object sender, ElapsedEventArgs e) { _Timer.Stop(); using (IRedisClient client = new RedisClient(conf)) { var keys = client.SearchKeys("*"); Console.Clear(); foreach (string key in keys) { DateTime sessionTime = client.Get<DateTime>(key); var seconds = sessionTime.Subtract(DateTime.Now).TotalSeconds; Console.WriteLine("key: "+key+" Sn: "+seconds); if (seconds <= 30) { var connection = new HubConnection("http://localhost:2533/", "console=1"); //Make proxy to hub based on hub name on server var myHub = connection.CreateHubProxy("Session"); connection.Start().Wait(); myHub.Invoke("notifyClient", new object[]{ key, "Session Süreniz 30sn Sonra Dolucak!."}).Wait(); client.Remove(key); } } //Console.WriteLine("In TimerCallback: " + DateTime.Now); GC.Collect(); } _Timer.Start(); } } } |
Şimdi gelin önce SignalR Session: Hub sınıfına ait yukarıda Invoke edilen notifyClient() methodunun yazımına.
Clientları uyaran örnek ekran çıktıları.
notifyClient(): Aşağıda görüldüğü gibi “notifyClient()” methodunda, session dolma süresi 30sn’nin altında olan clientın “notifyUser()” methodu, connectionID ve gösterilecek message text parametreleri ile birlikte tetiklenir. Yukarıdaki resimde görüldüğü gibi 2 farklı client connect durumundadır. Soldaki client, microservisde de görüldüğü gibi “27.sn”‘de olduğundan, kendisine uyarı mesajı gösterilmiştir. Sağdaki client henüz “47.9.sn”‘de olduğundan kendisine hiç bir uyarı mesajı çıkarılmamıştır. Her ikisinin de ConnectionID’si browser console’da gözükmektedir. Herbir client, ilgili ConnectionID’lerine göre redis tarafında saklanmaktadır.
1 2 3 4 |
public async Task notifyClient(string connectionID,string message) { await Clients.Client(connectionID).notifyUser(message); } |
Şimdi sıra geldi SignalR Session:Hub sınıfına ait OnConnected() methodunun değiştirilmesine:
OnConnected(): Aşağıda görüldüğü gibi değişikliğin esas sebebi, “OnConnected()” methodunun, yukarıda yazılan microservice tarafından da çağrılmasından dolayı kayanağın belirlenmesi ihtiyacıdır. Çünkü console yani microservice tarafından yapılan connection kale alınmayacaktır. Bu ayrılıma gidilebilmesi için, Url bazında “console” parametresi gönderilmiştir. Yukarıda anlatılan maddelerin dışında :
- “isFormConsole” değişkeni isteğin Microservice’den mi yoksa Web sayfasından mı geldiğini belirler. Ayrıca “sessionID” her client’a özel unique bir ID’dir.
- Eğer console’dan gelinmiyor ise tüm işlere devam edilir ==> “if(isFromConsole == “0”)” koşulu eklenmiştir.
- Not: PersonList aşağıda detaylıca gösterilen ayrı bir sınıftır. Kullanım Amacı: Web sayfasında refresh yapılması durumunda, client’ın var olan connectionID’si değişmektedir. Bu durumda eğer önceden redis tarafında ilgili session’a ait tanımlanmış connectionID var ise, bu listeden(PersonList) ilgili sessionID’ye ait eski connectionID’nin kaldırılıp, yeni connectionID’li kayıdın konarak, redis tarafında güncelleme yapılması gerekmektedir. Böylece, süre dolduğu zaman microservice, ilgili güncellenmiş coonectionID ile doğru client’a erişip notify yapabilecektir.
- PersonList sınıfının “Add()” methodu ile sessionID ve ona ait signalR connectionID değerleri, eğer ilgili Dictionary Listede yok ise atanır. “if (!PersonList.HasSession(sessionID))“
- Session’ın timeout olacağı zaman hesaplanarak “sessionExpireDate” değerine atanır. “DateTime sessionExpireDate = DateTime.Now.AddMinutes(section.Timeout.TotalMinutes)“
- PersonList dictionary listesine “sessionID” key olarak, “ClientData()” sınıfı value olarak atanır. ClientID sınıfı, connect olan client’a ait “ConnectionID” ve “SessionExpireDate” değerlerini alır. “var _clientData = new ClientData() { ClientConnectionID = Context.ConnectionId, ClientSessionTime = sessionExpireDate }“
- * Eğer PersonList’de ilgili sessionID’ye ait bir kayıt var ise, bu durumda client ya ilgili sayfayı yeni bir pencerede açmıştır ya da sayfayı refresh yapmıştır. “if(seconds>30)” session time out süresi 30sn fazla ise, ilgili kayıt Redis’e eklenir. Değil ise kullanıcıya kalan süreye göre uyarı mesajı, signalR socket ile direk gönderilir. “await Clients.Caller.notifyUser(message)“
- “PersonList.ClearExpiredPersonList()” methodu ile session süresi tamamıyla dolmuş kayıtlar, PersonList Dictionary listesinden temizlenir.
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 |
public override async Task OnConnected() { var isFromConsole = Context.QueryString["console"]; var sessionID = Context.QueryString["sessionID"]; Configuration config = WebConfigurationManager.OpenWebConfiguration("~/Web.Config"); SessionStateSection section = (SessionStateSection)config.GetSection("system.web/sessionState"); try { using (IRedisClient client = new RedisClient(conf)) { if (isFromConsole == "0" && !client.ContainsKey(Context.ConnectionId)) // client(Redis)'de ilgili connection yok.. { //Eğer Önceden eklenmiş Listede Yok ise //Liste yapmada amaç önceden bu kayıt var ve Sayfayı Refresh yapmış ise, önceki connectionID'sini de Redisde yenileyebilmektir. if (!PersonList.HasSession(sessionID)) { DateTime sessionExpireDate = DateTime.Now.AddMinutes(section.Timeout.TotalMinutes); client.Set(Context.ConnectionId, sessionExpireDate); var _clientData = new ClientData() { ClientConnectionID = Context.ConnectionId, ClientSessionTime = sessionExpireDate }; PersonList.Add(sessionID, _clientData); } else { //Eğer Önceden eklenmiş Listede Var İse ama redis'de yok ise kısa bir süre önce sayfadan ayrılınmış demektir. //PersonList'e eklenen süre, session'ın süresini geçmiş ise redis'e atılmadan yani client'a gönderilmeden ilgili client function tetiklenir. DateTime sessionTime = PersonList.GetSession(sessionID).ClientSessionTime; var seconds = sessionTime.Subtract(DateTime.Now).TotalSeconds; if(seconds>30) client.Set(Context.ConnectionId, PersonList.GetSession(sessionID).ClientSessionTime); else { string message = seconds <= 0 ? "Session Süreniz Dolmuştur" : "Session Sürenizin Dolmasına çok az kalmıştır"; await Clients.Caller.notifyUser(message); } } PersonList.ClearExpiredPersonList(); //Süresi dolanlar listeden kaldırılır. } await Clients.Caller.notifySession(Context.ConnectionId + ":" + DateTime.Now.AddMinutes(section.Timeout.TotalMinutes)); } } catch (Exception ex) { int i = 0; } } |
*PersonList: PersonList sınıfı, dictionary nesnesinde herbir client’ın sessionID’sini key, bu key’e ait value değerini de ClientData sınıfı şeklinde tutmaktır. ClientData sınıfında client’ın signalR connectionID’si ve session’ın expire olacağı zaman datetime cinsinden saklanmaktadır. En önemli durum olarak, client bulunduğu sayfayı yeniler ise, signalR connectionID’si değişecektir. Bu durumda öncelikle client, disconnect olacağı için ilgili sessionID’ye ait kayıt(connectionID) Redis’den çıkartılır. Daha sonra client tekrardan connect olur ve PersonList dictionary nesnesinden unique sessionID key’i ile ilgili client’a ait sayfaya ilk geldiği session timeout süresi bulunur. Daha sonra Redis’e yeni connectionID’si ve en baştaki session timeout süresi ile tekrardan kaydedilir. Böylece eski session timeout süresi yeni connectionID ile redis tarafında atanmış 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 |
public static class PersonList { static private Dictionary<string, ClientData> _personList = new Dictionary<string, ClientData>(); private static Dictionary<string, ClientData> _PersonList { get { return _personList; } } public static void Add(string SessionID, ClientData _clientData) { if (!_PersonList.ContainsValue(_clientData)) { _PersonList[SessionID] = _clientData; } } public static void Remove(string SessionID) { _PersonList.Remove(SessionID); } public static bool HasSession(string SessionID) { return _PersonList.Any(cl => cl.Key.ToUpper() == SessionID.ToUpper()); } public static string GetSessionByConnectionID(string ConnectionID) { return _PersonList.FirstOrDefault(cl => cl.Value.ClientConnectionID == ConnectionID).Key; } public static ClientData GetSession(string SessionID) { return _PersonList[SessionID]; } public static void ClearExpiredPersonList() { _PersonList.Where(per => per.Value.ClientSessionTime.Subtract(DateTime.Now).TotalSeconds < 0).ToList() .ForEach(key => _PersonList.Remove(key.Key)); } } |
SignalR ‘da Connection’ın Yaşam Döngüsü:
ClientData: PersonList dictionary’de tutulan value tipidir.
1 2 3 4 5 |
public class ClientData { public DateTime ClientSessionTime { get; set; } public string ClientConnectionID { get; set; } } |
HomeController/Index(): Sayafa ilk gelinildiğinde Session değeri “ViewBag.SessionID” ile index.cshtml sayfasına aktarılır.
1 2 3 4 5 6 |
public ActionResult Index() { Session.Add("UserName", "Bora Kaşmer"); ViewBag.SessionID = Session.SessionID; return View(); } |
Index.cshtml(Full): “$.connection.hub.qs = “console=0&sessionID=@ViewBag.SessionID”” ile “Session:Hub” sınıfının OnConnected() methoduna client’ın sessionID değeri, parametre olarak gönderilir. Böylece hangi platformdan gelindiği anlaşılır. Böylece gelen client’a ait kayıtlar SessionList ve Redis’e atılır.
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 |
@{ Layout = null; } <!DOCTYPE html> <html> <head> <script src="~/scripts/jquery-3.1.1.min.js"></script> <script src="~/scripts/jquery.signalR-2.2.1.min.js"></script> <script src="~/signalr/hubs"></script> <link href="~/Content/bootstrap.min.css" rel="stylesheet" /> <meta name="viewport" content="width=device-width" /> <link href="~/Content/alert.css" rel="stylesheet" /> <title>Index</title> <script> var hubProxy = $.connection.session; $.connection.hub.logging = true; $.connection.hub.qs = "console=0&sessionID=@ViewBag.SessionID"; $.connection.hub.start().done(function () { console.log("hub.start.done"); }).fail(function (error) { console.log(error); }); hubProxy.client.notifySession = function (message) { $('#hdnConnectionID').val(message.split(":")[0]); console.log(message); }; hubProxy.client.notifyUser = function (message) { $('#messageSpan').text(message); $('.alert').css('display', 'inline-block'); }; </script> </head> <body> <div class="container"> <div class="jumbotron"> <h2>SignalR Session Notify Page</h2> <div class="alert" style="display:none"> <span class="closebtn" onclick="this.parentElement.style.display='none';">×</span> <strong>Uyarı!</strong><span id="messageSpan"></span> </div> </div> <input type="hidden" id="hdnConnectionID" /> <input type="hidden" id="hdnSessionID" value="@ViewBag.SessionID"> </div> </body> </html> |
OnDisconnected(): Client disconnect olunca:
- Disconnection işleminin Web tarafından olması url parameter’dan “console“‘a bakılarak araştırılır.
- Disconnet işlemi webden yapılmış ise ilgili connectionID’ye ait kayıt redis’den kaldırılır.
1 2 3 4 5 6 7 8 9 10 11 12 |
public override Task OnDisconnected(bool stopCalled) { var isFromConsole = Context.QueryString["console"]; using (IRedisClient client = new RedisClient(conf)) { if (isFromConsole == "0") { client.Remove(Context.ConnectionId); } return base.OnDisconnected(stopCalled); } } |
Basit Yol: Şimdi bu makaleyi okuyan bazı arkadaşların aklına, aşağıdaki gibi daha kısa bir yol gelebilir. Görüldüğü gibi client taraflı bir timer “Session.Timeout” değerini server side tarafından alıp toplam kalan “sn” değeri “popupTime” değişkenine atanıp geri sayılabilir. Süre dolunca da ilgili uyarı div’i gösterilebilir. Tabi client’ın ilk girdiği zaman yine bir yerde saklanıp kalan süre “sn” cinsinden Session’a atılmalıdır. Videoda da açıkladığım gibi bir sorunun bir çok çözümü vardır. Bu makalede amaç vizyonunuzu açmak ve karşınıza çıkabilecek çok daha komplike durumlar için size fikir vermekdir. Örneğin herkes için bitme zamanının aynı olduğu geri sayan bir online sınavda, herbir client’ın sınav sırasındaki durumunu redis’de tutup, sürenin bitmesine 20 dakika kala hala soruların yarısını tamamlamayanların acele etmeleri için push notification atmak istenir ise, bu sistem gayet başarılı çalışacaktır. İstenildiği kadar farklı bussines rulelar microservis olarak adlandırdığımız console applicationlara dağtılabilir.Herbir bussines logic’e yeni bir microservis yazılabilir. Kişilerin sınav sırasındaki anlık durumları redis’de tutulabilir. Son olarak signalR kullanılarak belirlenen kurallar içerisinde kalan clientlara, yine belirlenen rulelara göre özel mesajlar atılabilir. Böyle bir seneryoda kısa bir yol söz konusu değildir:)
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<script type="text/javascript"> var popupTime = (<%=Session.Timeout %> * 60000) - 30000; //30 saniye kala alert verilecek function countTime() { var timer = setTimeout("countTime()", 1000); if (popupTime > 0) { popupTime -= 1000; } else { clearTimeout(timer); $('.alert').css('display', 'inline-block'); } } </script> |
Benim bütün amacım sizin hayal dünyanızı, modern teknolojiler ile süsleyip neler yapabileceğinizi bir nebze olsun göstermektir. Unutmayın yazılımda hiçbirşey imkansız değil, sadece halen çözülmeyi bekleyen bir bulmacadır. İstenen codelar yazılsa dahi, her zaman daha iyi ve daha performanslı bir yolu vardır. Bu da sonsuz bir yolculuktur.
Yeni bir makalede görüşmek üzere hoşçakalın.
Source: https://github.com/ServiceStack/ServiceStack.Redis, https://docs.microsoft.com/tr-tr/aspnet/signalr/overview/guide-to-the-api/hubs-api-guide-net-client
Source Code: https://github.com/borakasmer/SignalR-RedisUserNotify
HomeController.cs(Full):
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 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
using Microsoft.AspNet.SignalR; using ServiceStack.Redis; using System; using System.Collections.Generic; using System.Configuration; using System.Linq; using System.Threading.Tasks; using System.Web.Configuration; using System.Web.Mvc; namespace SignalRUserNotify.Controllers { public class HomeController : Controller { // GET: Home public ActionResult Index() { Session.Add("UserName", "Bora Kaşmer"); ViewBag.SessionID = Session.SessionID; return View(); } } public class Session : Hub { static RedisEndpoint conf = new RedisEndpoint() { Host = "127.0.0.1", Port = 6379 }; public override async Task OnConnected() { var isFromConsole = Context.QueryString["console"]; var sessionID = Context.QueryString["sessionID"]; Configuration config = WebConfigurationManager.OpenWebConfiguration("~/Web.Config"); SessionStateSection section = (SessionStateSection)config.GetSection("system.web/sessionState"); try { using (IRedisClient client = new RedisClient(conf)) { if (isFromConsole == "0" && !client.ContainsKey(Context.ConnectionId)) // client(Redis)'de ilgili connection yok.. { //Eğer Önceden eklenmiş Listede Yok ise //Liste yapmadan amaç önceden bu kayıt var ve Sayfayı Refresh yapmış ise önceki connectionID'sini Redisden yenileyebilmek. if (!PersonList.HasSession(sessionID)) { DateTime sessionExpireDate = DateTime.Now.AddMinutes(section.Timeout.TotalMinutes); client.Set(Context.ConnectionId, sessionExpireDate); var _clientData = new ClientData() { ClientConnectionID = Context.ConnectionId, ClientSessionTime = sessionExpireDate }; PersonList.Add(sessionID, _clientData); } else { //Eğer Önceden eklenmiş Listede Var İse ama redis'de yok ise kısa bir süre önce sayfadan ayrılınmış demektir. //PersonList'e eklenen süre, session'ın süresini geçmiş ise redis'e atılmadan yani client'a gönderilmeden ilgili client function tetiklenir. DateTime sessionTime = PersonList.GetSession(sessionID).ClientSessionTime; var seconds = sessionTime.Subtract(DateTime.Now).TotalSeconds; if(seconds>30) client.Set(Context.ConnectionId, PersonList.GetSession(sessionID).ClientSessionTime); else { string message = seconds <= 0 ? "Session Süreniz Dolmuştur" : "Session Sürenizin Dolmasına çok az kalmıştır"; await Clients.Caller.notifyUser(message); } } PersonList.ClearExpiredPersonList(); //Süresi dolanlar listeden kaldırılır. } await Clients.Caller.notifySession(Context.ConnectionId + ":" + DateTime.Now.AddMinutes(section.Timeout.TotalMinutes)); } } catch (Exception ex) { int i = 0; } } public override Task OnDisconnected(bool stopCalled) { var isFromConsole = Context.QueryString["console"]; using (IRedisClient client = new RedisClient(conf)) { if (isFromConsole == "0") { var sessionID = Context.QueryString["sessionID"]; client.Remove(Context.ConnectionId); } return base.OnDisconnected(stopCalled); } } public async Task notifyClient(string connectionID,string message) { await Clients.Client(connectionID).notifyUser(message); } } public static class PersonList { static private Dictionary<string, ClientData> _personList = new Dictionary<string, ClientData>(); private static Dictionary<string, ClientData> _PersonList { get { return _personList; } } public static void Add(string SessionID, ClientData _clientData) { if (!_PersonList.ContainsValue(_clientData)) { _PersonList[SessionID] = _clientData; } } public static void Remove(string SessionID) { _PersonList.Remove(SessionID); } public static bool HasSession(string SessionID) { return _PersonList.Any(cl => cl.Key.ToUpper() == SessionID.ToUpper()); } public static string GetSessionByConnectionID(string ConnectionID) { return _PersonList.FirstOrDefault(cl => cl.Value.ClientConnectionID == ConnectionID).Key; } public static ClientData GetSession(string SessionID) { return _PersonList[SessionID]; } public static void ClearExpiredPersonList() { _PersonList.Where(per => per.Value.ClientSessionTime.Subtract(DateTime.Now).TotalSeconds < 0).ToList() .ForEach(key => _PersonList.Remove(key.Key)); } } public class ClientData { public DateTime ClientSessionTime { get; set; } public string ClientConnectionID { get; set; } } } |
Gerçekten kaliteli içerik üretiyorsun hocam başarılarının devamını dilerim.
Ben teşekkür ederim Tolgahan.