Nine MVP's Blog

27/10/2012

TEST POST

Filed under: Uncategorized — Nine MVP @ 11:58 pm

TEST PUBLISH FROM MICROSOFT WORD 2013 PROFESSIONAL PLUS

 

24/10/2012

ASP.NET MVC Series : Web Push Technology II.2

Filed under: ASP.NET, ASP.NET MVC, C#, html5, javascript, knockout, WEB, websockets — Tags: , , , , , , , — Nine MVP @ 4:10 pm

จาก ตอนที่ I ผมได้แนะนำการทำงานพื้นฐานของคำสั่งทั้ง javascript และ protocol กันไปแล้ว และเมื่อที่ผ่านมา ตอนที่ II.1 ได้พูดถึงการทำ Push Mail Service ด้วย SSE

สำหรับตอนนี้เราจะมาทดสอบตัว websockets โดยใช้โจทย์เดิมกัน  ซึ่งสำหรับตอนนี้จะเป็นการเปลี่ยนการทำงานจากการใช้ SSE มาเป็น WebSockets Engine ในการสื่อสารกับ Push Mail Service    ซึ่งจะมีการปรับเปลี่ยนทั้ง client javascript  และ Web API Service มาดูกัน

การทำงานและ output จะเหมือนกับโปรเจ็ค SSE ก่อนนี้ทุกอย่าง ดังนั้นผมจะอธิบายเพียงส่วนที่แตกต่างกันครับ

 

แนะนำ MvcPushWS Project

ส่วนทีต้องติดตั้งเพิ่มเติมสำหรับ MvcPushWS Project ก็คือ

Microsoft.WebSockets โดยหาได้จาก nuget windows ตามภาพด้านล่าง

image

 

สำหรับ Flow โปรแกรมมีจำนวน step มี 9 จุดที่น่าสนใจ

ลำดับที่ 0.-3. ผมจะข้ามไปโดยหากสนใจจะอ่านให้กลับไปดู ตอนที่ II.1 ครับ

image

 

4. เมื่อ user มี email รีเทิร์นค่ากลับมาก็ให้ทำการ connect to Push Service

ใน javascript function InitialWS(); จะเป็นการสร้างและกำหนดค่าการทำงานของ WebSockets  ให้ชี้ไปยัง EmailServiceController (WebAPI) ซึ่งเป็น push service ที่เราสร้างขึ้นมาสำหรับส่ง email ใหม่ที่ส่งเข้ามาจากผู้ใช้อื่นๆ โดยจะมีการ add event ไว้ 1 คือ onmessage

   1: //WS Setup

   2:     function InitialWS() {

   3:         if ("WebSocket" in window) {

   4:             var uri = 'ws://' + window.location.host + '/api/EmailService/?email=' + viewModel.email();

   5:             socket = new WebSocket(uri);

   6:             socket.onmessage = function(e) {

   7:                 console.log(e.data); // log 

   8:                 var mails = JSON.parse(e.data);

   9:                 //alert from email

  10:                 showNewMessageAlert(mails[0].From);

  11:                 //binding array object to viewmodel(template binding)

  12:                 viewModel.messages.valueWillMutate();

  13:                 ko.utils.arrayPushAll(viewModel.messages(), mails);

  14:                 viewModel.messages.valueHasMutated();

  15:                 //sort inbox item

  16:                 SortMessageDesc();

  17:             };

  18:         } else {

  19:             // the browser doesn't support WebSockets

  20:             alert("WebSockets NOT supported here!\r\n\r\nBrowser: " + navigator.userAgent + "\r\n\r\n");

  21:         }

  22:     }

  1. line3: ทำการตรวจสอบว่า user browser รองรับการทำงานของ websockets หรือไม่
  2. line4: สร้าง url ปลายทางไปยัง push mail service และส่ง email pameter ไปด้วย
  3. line5: สร้างตัวแปร WebSocket และส่ง url ให้เพื่อทำการติดต่อกับ service
  4. line6-8: ทำการกำหนดการทำงานให้กับ onmessage event โดยให้เอาข้อมูลที่ส่งมาจากทางฝั่ง service ทำการ log และแปลงค่าเป็น json object
  5. line10-16: ทำการแสดงผล popup, โยนเข้า knockout model แล้วจัดลำดับ email ใน inbox ให้ DESC ReceivedDate

เมื่อมีการทำการ new WebSocket(url) ตรงนี้จะทำงานทันที โดยติดต่อไปยัง EmailServiceController.Get() Action โดยมี request header หน้าตาแบบนี้

           WebSockets Request Header

image

มีพารามิเตอร์ที่เกี่ยวข้องกับ websocket ดังนี้

  • Upgrade: websocket
  • Connection: xxx, Upgrade

เป็นค่าเพื่อบอก push email service ว่านี่เป็นการติดต่อมาจาก websocket  ให้ทำการสลับการทำงานจาก HTTP Protocol ไปเป็น WebSocket Protocol

  • Sec-WebSocket-Key: kmDqIkNBFU0iWMWb5fNiPQ==

จะอยู่ในรูปของ base64 encode  ทาง push email service จะเอาค่านี้ไปใช้งานเพื่อสร้าง handshake สำหรับคุยกันระหว่าง websocket client/server

ส่วนของ origin url จะถูกสร้างโดย browser

  • Sec-WebSocket-Version: 13

เป็นการบอก websocket client ว่าทางฝั่ง Client Browser นั้นใช้ protocol version ไหน ซึ่ง version 13 นั้นเป็น final spec ที่ทาง IEFT กำหนดให้เป็น standard

 

5. Create PushEmailHandler for new subscriber (EmailServiceController)

เมื่อ websocket มีการทำ connect มาที่ EmailService มีการสั่งยิงมาด้วย GET (WebSocket ทำงานโดยใช้ GET) มาเข้า HttpGet Action ซึ่งมีอยู่ตัวเดียวใน EmailServiceController ก็คือ

Get(HttpRequestMessage request)  ซึ่งจะมีการสร้าง WebSocketHandler class (มีใน Microsoft.Websockets จาก Nuget) ขี้นใหม่สำหรับ PushMailWSHandler เอาไว้ช่วยจัดการกับ ws request

   1: public class EmailServiceController : ApiController

   2: {

   3:     //Get

   4:     public HttpResponseMessage Get(HttpRequestMessage request)

   5:     {

   6:         //accept only websockets request

   7:         if (!HttpContext.Current.IsWebSocketRequest)

   8:             return request.CreateErrorResponse(HttpStatusCode.Forbidden, "the service support only websocket client");

   9:  

  10:         //get user's email

  11:         var email = request.QueryString("email");

  12:  

  13:         //user's email is required parameter

  14:         if (string.IsNullOrEmpty(email.Trim()))

  15:             return request.CreateErrorResponse(HttpStatusCode.BadRequest, "the service required users email parameter.");

  16:  

  17:         //register websockets handler to context

  18:         HttpContext.Current.AcceptWebSocketRequest(new PushMailWSHandler(email));

  19:         return Request.CreateResponse(HttpStatusCode.SwitchingProtocols);

  20:     }

  21: }

  1. line7-8: ตรวจสอบว่าเป็นการส่งค่าสั่งมาจาก websocket หรือไม่ถ้าไม่ก็ส่ง error response แบบ forbidden พร้อม error message กลับไป
  2. line11: อ่าน email parameter มาจาก QueryString
  3. line14-15: หากไม่พบ email parameter ก็ส่ง error response แบบ badrequest พร้อม error message กลับไป
  4. line18-19: กำหนดให้ HttpContext ทำการรับ websocket handler ตัวใหม่สำหรับ pushmail ที่สร้างไว้ (เล่าตอนหลัง) และสร้าง response ตาม handler ที่เพิ่งกำหนดเข้าไปใหม่ ก็คือ switch to websocket protocol

  อธิบายตัว PushMailHandler class

ได้ทำการสืบทอดจาก WebSocketHandler Class ที่มีมากับ Microsoft.WebSockets โดยเมื่อมีการเรียก constructor ให้ทำงานโดย remove old pushmail handler ตัวเก่าออกไปก่อน แล้วค่อย add ตัวใหม่ที่เพิ่งสร้างเข้าไป (ทำตามแนวคิด *มาทีหลังดังกว่า)

   1: using MvcPushWS.Models;

   2: using Newtonsoft.Json;

   3: using Microsoft.Web.WebSockets;

   4:  

   5: namespace MvcPushWS.Controllers

   6: {

   7:     public class PushMailWSHandler : WebSocketHandler

   8:     {

   9:         //EFcontext

  10:         private MailBoxDBContext mbctx = new MailBoxDBContext();

  11:         //message pool

  12:         private static List<MailMessageView> _newMails = new List<MailMessageView>();

  13:         //subscriber pools

  14:         private static ConcurrentDictionary<string, SubscriberPushMail> _subscribers = 

  15:             new ConcurrentDictionary<string, SubscriberPushMail>();

  16:  

  17:         //constructor

  18:         public PushMailWSHandler(string email)

  19:         {

  20:             // add subscriber to pool

  21:             if (!string.IsNullOrEmpty(email.Trim()))

  22:             {

  23:                 if (_subscribers.ContainsKey(email))

  24:                 {

  25:                     SubscriberPushMail dead;

  26:                     if (_subscribers.TryRemove(email, out dead))

  27:                     {

  28:                         dead.client.Close();

  29:                         dead = null;

  30:                     }

  31:                 }

  32:                 _subscribers.TryAdd(email, new SubscriberPushMail

  33:                 {

  34:                     Email = email,

  35:                     client = this

  36:                 });

  37:  

  38:             }

  39:         }

  40:  

  41: ...............

*ไม่ขออธิบาย code เพราะการทำงานจะคล้ายกับตอนที่แล้ว

 

6. เมื่อตอบกลับการทำงานไปยัง WebSocket Client

ตัว WebSocketHandler จะเพิ่มตัวแปะลงใน Response Header เพื่อตอบกลับการทำงานไปยัง client

          WebSocket Response Header

image

  • Upgrade: Websocket
  • Connection: Upgrade
  • Sec-WebSocket-Accept: teTd3/I5X/xGOf2RHcLCvf49Jb4=

เมื่อมี response ตอบกลับมาตัว HTTP Code เราจะมองเห็นเป็น 101 Switching Protocols โดยการทำงานต่อจากนี้เราจะไม่สามารถดูได้จาก browser dev tool อีกเนื่องจากทำงานจะมีการส่งข้อมูลกันเป็น frame ไปมาตามรูปด้านล่างนี้

image

image

 

 

7. เมื่อ User2 ทำการส่งอีเมลไปหา User1

ต่อไปเมื่อมี user2 เปิด browser เข้ามา load mailbox ของตัวเอง และทำการส่งอีเมลถึง user1 หลังจากกดปุ่ม Send จะเข้ามาทำงานใน click ของ jquery ที่เราได้ hook event เอาไว้

โดยใช้ตัว websocket object โดยให้ส่ง viewModel.newEmail ไปด้วยในรูปแบบของ json string และให้ทำการล้างค่าของ newEmail หลังจากทำงานเสร็จแล้ว

   1: $(document).ready(function () {

   2:     

   3:     //binding mapping to viewModel

   4:     ko.applyBindings(viewModel);

   5:  

   6:     $("#sendMail").click(function () {

   7:         

   8:         if (socket.readyState != WebSocket.OPEN)

   9:             return;

  10:  

  11:         var data = JSON.stringify(ko.mapping.toJS(viewModel.newEmail));

  12:  

  13:         //use websocket send json string to pushmail service

  14:         socket.send(data);

  15:        

  16:         //clear value

  17:         viewModel.newEmail.To('');

  18:         viewModel.newEmail.Title('');

  19:         viewModel.newEmail.Message('');

  20:     });

  21:  

  22: });

  1. line14: เป็นการใช้ websocket ส่งข้อมูลกลับไปที่ push mail service ของเรา

หลังจากที่ websocket client ได้ส่ง json string เข้ามาจะไปทำงานที่ PushMailHandler ตัวที่เราได้เก็บเอาไว้ก่อนหน้านี้ โดยจะเกิด Event OnMessage(string data) ขึ้น

   1: public class PushMailWSHandler : WebSocketHandler

   2: {

   3:     .........................

   4:     .....................

   5:  

   6:     //User send a email message to service

   7:     public override void OnMessage(string message)

   8:     {

   9:         var dtomail = JsonConvert.DeserializeObject<MailMessageView>(message);

  10:         try

  11:         {

  12:             var user = mbctx.Users.SingleOrDefault(o => o.UserName == dtomail.Username);

  13:             if (user == null)

  14:                 this.Send(JsonConvert.SerializeObject(new {success = false, msg = "not found user"}));

  15:  

  16:             var dt = DateTime.Now;

  17:  

  18:             dtomail.ReceivedDate = dt.ToString("dd/MM/yyyy HH:mm:ss");

  19:             var mail = new MailMessage

  20:             {

  21:                 Id = dtomail.Id = Guid.NewGuid(),

  22:                 From = dtomail.From,

  23:                 To = dtomail.To,

  24:                 Title = dtomail.Title,

  25:                 Message = dtomail.Message,

  26:                 IsRead = false,

  27:                 ReceivedDate = dt,

  28:                 User_Id = user.Id

  29:             };

  30:             mbctx.MailMessages.Add(mail);

  31:             mbctx.SaveChanges();

  32:             //add alert email if the target subscriber is online

  33:             if (_subscribers.ContainsKey(mail.To))

  34:             {

  35:                 _newMails.Add(dtomail);

  36:                 //push email to subscriber

  37:                 PushMessageToClient(mail.To);

  38:             }

  39:         }

  40:         catch (Exception ex)

  41:         {

  42:             //this.Send(JsonConvert.SerializeObject(new { success = false, msg = ex.Message }));

  43:         }

  44:  

  45:     }

  46:  

  47:     //push email to target subscriber

  48:     private void PushMessageToClient(string toEmail)

  49:     {

  50:         var subscribers = _subscribers.Where(o => o.Key.ToLower() == toEmail.ToLower().Trim());

  51:         foreach (var subscriber in subscribers)

  52:         {

  53:             var mails = _newMails.Where(o => o.To.ToLower().Trim() == toEmail.Trim().ToLower()).ToList();

  54:             if (mails.Count > 0)

  55:             {

  56:                 subscriber.Value.client.Send(JsonConvert.SerializeObject(mails));

  57:                 _newMails.RemoveAll(o => o.To == toEmail);

  58:             }

  59:         }

  60:  

  61:  

  62:     }

  63: }

  1. line7: คือ method ที่เราได้ override ไว้เมื่อมี client ได้ส่งข้อมูลกลับมาที่ service
  2. line9-31: ทำการ convert json string กลับมาเป็น object เพื่อใช้บันทึกลง database
  3. line37: เรียก pushmessagetoClient ไปยัง client ตามอีเมลที่ส่งเข้ามา
  4. line56: เมื่อหา client ปลายทางเจอแล้วก็เรียกใช้ PushMailWSHandler ที่เราเก็บไว้ตอนแรกใน flow ที่ 5.  ทำการเรียก Send(json string); ส่งไป

 

ถ้าอยากเขียน WebSocket Server เองไม่ต้องการใช้ Microsoft.WebSocket ?

ก็เขียนแบบ TCP Server ปกติได้เลยครับ เพียงแต่ตอน response กลับก็เขียน header กลับไปให้ครบตามนี้

   1: var listener = new TcpListener(IPAddress.Loopback, 8181);

   2: listener.Start();

   3: using (var client = listener.AcceptTcpClient())

   4: using (var stream = client.GetStream())

   5: using (var reader = new StreamReader(stream))

   6: using (var writer = new StreamWriter(stream))

   7: {

   8:     writer.WriteLine("HTTP/1.1 101 Web Socket Protocol Handshake");

   9:     writer.WriteLine("Upgrade: WebSocket");

  10:     writer.WriteLine("Connection: Upgrade");

  11:     writer.WriteLine("WebSocket-Origin: http://localhost:8080");

  12:     writer.WriteLine("WebSocket-Location: ws://localhost:8181/websession");

  13:     writer.WriteLine("");

  14: }

  15: listener.Stop();

 

ทิ้งท้ายก่อนจบ

สำหรับตอนนี้เป็นการทดสอบการใช้งานตัว WebSocket และ WebSocket Server ขึ้นใช้งานเอง เพื่อสร้างความเข้าใจในการทำงาน  แต่ตัวอย่างข้างต้นที่ได้หยิบยกมายังไม่เหมาะจะนำไปใช้งานจริง ดังนั้นอาจจะต้องแก้ไขและปรับแต่งหรืออาจจะไปมองหา WebSocket Server ที่มีอยู่มากมายตอนนี้ ซึ่งน่าสนใจอยู่หลายตัวครับ SuperWebSocket.Net, XSockets.NET หรืออีกมากมายที่ http://en.wikipedia.org/wiki/Comparison_of_WebSocket_implementations 

ปล. ก่อนจะเอาโปรเจ็ค demo ไปรันทดสอบรบกวนตรวจสอบ browser ของคูณกันก่อนนะครับ Smile  http://websocketstest.com/

image

 

Download Demo Project

*มี database script อยู่ใน project folder เอาไปสร้าง database และอย่าลืมเปลี่ยน connectionstring ใหม่


SONY DSC

About Me:

Chalermpon Areepong : Nine (นาย)

Microsoft MVP Thailand ASP.NET

ASP.NET & MVC Developers Thailand Group :http://www.facebook.com/groups/MVCTHAIDEV/

Greatfriends.biz Community Leader : http://greatfriends.biz

Email : nine_biz-talk.net at hotmail dot com

Blog : https://nine69.wordpress.com

15/10/2012

ASP.NET MVC Series: Web Push Technology II.1

Filed under: ASP.NET, ASP.NET MVC, Design Pattern, W3C, WEB — Tags: , , , , — Nine MVP @ 8:46 pm

จากตอนที่แล้ว web push technology I ได้แนะนำความเป็นมาของการทำงานแบบ push และเทคโนโลยีต่างๆที่มีให้ใช้งานกันไปแล้ว ตอนนี้เราจะมาเขียนโปรแกม โดยขอเริ่มที่การใช้งานตัว Server Sent Event (SSE) กันก่อน และจะไปจบที่ WebSockets (ws) ฮีโร่มัยซินของเราในตอนสุดท้ายของเรื่องนี้

เตรียมเครื่องมือ Tool:

  1. Visual Studio 2010+ (บทความใช้ vs2012)
  2. ASP.NET MVC 4.0
  3. HTML5 Browser (Chrome v20+, FF v14+, Opera v11+)
  4. jQuery latest version
  5. knockout.js latest version

โจทย์การทดสอบนี้คือโปรแกรม Push Mail

โจทย์ตัวอย่าง

  • เมื่อ load inbox ในครั้งแรกที่เข้าสู่ระบบ
  • ระบบสามารถ push email ไปยังผู้รับปลายทาง โดยเมื่อมีอีเมลเข้ามาใหม่ก็ส่งเข้า inbox ของ online user ทันที 
  • สามารถสร้างและส่งอีเมลไปหา user คนอื่นในระบบได้

มาถึงช่วงของการละเลงโค้ดในตอนนี้ผมขอนำเสนอการใช้งาน

 

Server Sent Event (EventSource) Solution

key technologies สิ่งที่วางไว้สร้าง SSE Solution นี้ก็จะมีดังนี้

  • Server จะเป็น ASP.NET WEB API  ในการบริการงานด้าน push mail ซึ่งส่งข้อมูลอีเมลใหม่ไปยัง user ที่ online อยู่หากมีการส่งอีเมล์เข้ามา ณ ตอนที่ออนไลน์อยู่ 
  • Client จะใช้เป็น ASP.NET MVC, jQuery, Knockout.js ในการทำงาน เข้าระบบ, บันทึกส่งเมล และแสดงผล push mail จาก server

image

จากภาพด้านบนอธิบายว่า

  1. เมื่อ user1 เข้าระบบมาแล้วก็ให้ทำการ load inbox เฉพาะทีมีคนส่งมาหากลับมาให้หน้าแรก

  2. พร้อมทั้ง subscribed เข้าใน pool ของ user online เพื่อใช้ในการ push new email

  3. return inbox data กลับไป

อาจจะ 10 นาทีต่อมามี user2  ทำการเข้าระบบมาส่งอีเมลโดย

  4. user2 เลือกส่งเมลหา user1

  5. พบ user1 ออนไลน์อยู่และทำการ push email ไปให้

 

SSE Demo Output

จำลองผู้ใช้งานขึ้น 2 คนชื่อ user1 และ user2 ตามภาพการทำงานด้านบน จะได้ผลตามลำดับภาพดังนี้

image 

เมื่อ user1 เปิด browser เข้าระบบมาครั้งแรก

image 

เมื่อ user1 ทำการใส่ username แล้วกดปุ่ม load inbox หากเข้าระบบได้ก็จะมีรายการ inbox item และสามารถส่งอีเมลได้ ซึ่งถือว่า user1 online อยู่ในระบบของ push mail แล้ว

image 

เมื่อ user2 เปิด browser เข้าระบบมาครั้งแรก และทำการใส่ username แล้วกดปุ่ม load inbox หากเข้าระบบได้ก็จะมีรายการ inbox item และสามารถส่งอีเมลได้ ซึ่งถือว่า user2 online อยู่ในระบบของ push mail แล้ว  

image

user2 ทำการส่งอีเมลหา user1 ที่ user1@nine.com ตามรูป กด send

image 

อีเมลจาก user2 ได้ถูกส่งเข้าไปในระบบเรียบร้อย

image

ที่ browser ของ user1 จะมี new email ส่งเข้า inbox ไปแสดงผลตามภาพ และมีป๊ปบอัพโผล่บอกว่าใครส่งมา

 

อธิบาย MVCPushSSE Project

มาดู flow การทำงานของตัวโปรแกรมใน mvc application จากภาพด้านล่าง (เขียนโค้ดเรียบๆไม่เล่นกระบวนท่ามาก)

image

 

* การอธิบายโค้ดจะยึดตาม flow ด้านบนและชี้โค้ดให้เห็นเป็นส่วนๆ

 

0. user access to homepage

เมื่อ user เปิด browser แล้วเข้าเว็บมาครั้งแรก http://localhost:xxx/ จะไปเข้า /Email/Index (controller/action) ตามที่ได้ตั้งค่าไว้ใน routeconfig ทำให้มีการสร้าง viewmodel ของ knockout เกิดขึ้นตามนี้

   1: <script>

   2:  

   3:     //EventSource object

   4:     var source;

   5:     

   6:     //main view model

   7:     var viewModel = {

   8:         username: ko.observable(""),//map to username input

   9:         email: ko.observable(""), //map to email input

  10:         messages: ko.observableArray([]) //dynamic array inbox message (json) output

  11:     };

  12:  

  13:     //new email object viewmodel

  14:     viewModel.newEmail = {

  15:         Username: ko.computed(function () { return viewModel.username(); }), //get username value from viewModel.username

  16:         To: ko.observable(""), //map to To input

  17:         From: ko.computed(function () { return viewModel.email(); }), //get From value from viewModel.email

  18:         Title: ko.observable(""), //map to To input

  19:         Message: ko.observable("") //map to Body input

  20:     };

และเมื่อ html document โหลดเสร็จเรียบร้อบก็จะถึงการสั่งให้ เอา viewmodel ด้านบนมา map เข้าใน knockout engine ด้วยคำสั่งใน line4

   1: $(document).ready(function () {

   2:  

   3:     //binding mapping to viewModel

   4:     ko.applyBindings(viewModel);

   5:  

 

จากนั้นเมื่อมีการเรียกเข้าไปที่ EmailController –> Index Action จะมี code ทำงานดังนี้

line4: สร้างตัวแปร efcontext ไว้ใช้งาน
line7-10: สร้าง action สำหรับไว้ดึง view ไปแสดงผลครั้งแรกที่เข้ามาโดยจะ return Index.cshtml ออกไป

   1: public class EmailController : Controller

   2: {

   3:  

   4:     MailBoxDBContext mbctx = new MailBoxDBContext();

   5:  

   6:     // GET: /Email/

   7:     public ActionResult Index()

   8:     {

   9:         return View();

  10:     }

 

1. เมื่อ user กด load mailbox

เมื่อ user ได้ใส่ username และกดส่งกลับเข้ามา javascript ใน index.cshtml ส่วนนี้จะทำงานตามที่ได้ hook event เอาไว้ โดยสั่ง GET username ส่งไปที่ /Email/GetMyInbox  ซึ่งจะไปโหลด user inbox  กลับออกมา

   1: $('#loadMailBox').live("click", function (e) {

   2:     viewModel.email('');

   3:     var username = $('#username').val();

   4:     $.getJSON("/Email/GetMyInbox", { username: viewModel.username() },

   5:         function (data) {

   6:             if (!data.success) {

   7:                 if (source != null) source.close();

   8:                 alert(data.msg);

   9:             } else {

  10:                 viewModel.email(data.email);

  11:                 InitialSSE();

  12:                 $('#inbox').empty();

  13:                 viewModel.messages(data.mails);

  14:             }

  15:         }

  16:     );

  17: });

  1. line4: เป็นการสั่ง getJSON() มี url, json object โดยดึงเอา viewModel.username ไปใช้  และทำการยิงไป “/Email/GetMyInbox”
  2. line10: ก็จะเอา json ที่ได้ส่ง email เข้าไปใน ko เพื่อไปแสดงผลใน input email
  3. line11: จากนั้นสั่งให้ setup ตัว SSE object ด้วย InitialSSE(); //ไว้อธิบายต่อตอนหลัง
  4. line12: จากนั้นก็สั่ง clear ทุกอย่างใน inbox
  5. line13: และส่ง item ที่โหลดมาจากได้ inbox ของ user ส่งไป binding ผ่าน viewModel.messages

 

2. การทำงานของ GetMyInbox Action 

หลังจากที่ GET มาเข้าที่ EmailController –> GetMyInbox () Action ซึ่งเป็นการจำลองว่า user login เข้ามาก็จะได้ inbox ของตัวเองกลับไป

   1: [HttpGet]

   2: public JsonResult GetMyInbox(string username)

   3: {

   4:     try

   5:     {

   6:         var user = mbctx.Users.SingleOrDefault(o => o.UserName == username);

   7:         //not found user

   8:         if (user == null)

   9:             return Json(new { success = false, msg = string.Format("not found user {0}", username) },

  10:                         JsonRequestBehavior.AllowGet);

  11:  

  12:         //get mailbox send to this users email

  13:         var mails = mbctx.MailMessages.Where(o => o.To.ToLower().Trim() == user.Email.ToLower().Trim())

  14:             .OrderByDescending(o => o.ReceivedDate).ToList()

  15:             .Select(o => new MailMessageView

  16:                     {

  17:                         Id = o.Id,

  18:                         To = o.To,

  19:                         From = o.From,

  20:                         ReceivedDate = o.ReceivedDate.ToString("dd/MM/yyyy HH:mm:ss"),

  21:                         Title = o.Title

  22:                     }).ToList();

  23:  

  24:         //return json to client

  25:         return Json(new { success = true, email = user.Email, mails = mails }, JsonRequestBehavior.AllowGet);

  26:  

  27:     }

  28:     catch (Exception ex)

  29:     {

  30:         return Json(new { success = false, msg = ex.Message }, JsonRequestBehavior.AllowGet);

  31:     }

  32: }

  1. line15-45: ดึงข้อมูลอีเมลทั้งหมดที่ส่งมาถึง To username นี้กลับออกไปในรูปของ JSON ทั้งหมดกลับออกไป

 

3. เมื่อได้ json กลับมา ตัว Knockout จะเอา json ไปสร้างรายการอีเมลใน inbox

อ่านย้อนกลับไปที่ข้อ 1.2, 1.3, 1.4, 1.5

 

4. เมื่อ user มี email รีเทิร์นค่ากลับมาก็ให้ทำการ connect to Push Service

จากนั้นใน javascript function InitialSSE(); จะเป็นการกำหนดตัว EventSource object ให้ชี้ไปยัง EmailServiceController (WebAPI) ซึ่งเป็น push service ที่เราสร้างขึ้นมาสำหรับส่ง email ใหม่ที่ส่งเข้ามาจากผู้ใช้อื่นๆ โดยจะมีการ add event ไว้ 3 ตัวแต่ใช้งานจริงแค่  ‘message’ event

   1: //EventSource Setup

   2: function InitialSSE() {

   3:     if (!!window.EventSource) {

   4:         var root = "@Url.Content("~/")";

   5:         source = new EventSource(root + 'api/EmailService/?email=' + viewModel.email());

   6:  

   7:         //Add message event

   8:         source.addEventListener('message', function (e) {

   9:             console.log(e.data); // log 

  10:             var mails = JSON.parse(e.data);

  11:             //alert from email

  12:             showNewMessageAlert(mails[0].From);

  13:             //binding array object to viewmodel(template binding)

  14:             viewModel.messages.valueWillMutate();

  15:             ko.utils.arrayPushAll(viewModel.messages(), mails);

  16:             viewModel.messages.valueHasMutated();

  17:             //sort inbox item

  18:             SortMessageDesc();

  19:  

  20:         }, false);

  21:  

  22:         source.addEventListener('open', function (e) {

  23:             console.log("open!");

  24:         }, false);

  25:  

  26:         source.addEventListener('error', function (e) {

  27:             if (e.readyState == EventSource.CLOSED) {

  28:                 console.log("error!");

  29:             }

  30:         }, false);

  31:     } else {

  32:         // not supported!

  33:         alert('The EventSource is not available.');

  34:     }

  35: }

  1. line3: ตรวจสอบว่า browser ที่ทำงานอยู่ปัจจุบันสามารถใช้งาน SSE ได้หรือไม่
  2. line4-5: เป็นการสร้างตัวแปร EventSource และกำหนด url ของ push service พร้อมทั้งส่งค่าของ email ของ user คนนี้ไปด้วย
  3. line8-20: มีการ add event ‘message’ ให้ตัว eventsource และเขียนคุมการทำงานให้ทำการแสดงผลเมื่อมี message ส่งมาจาก push service โดยทำงานกับ knockout และ jquery ในการแสดงผลข้อมูลที่ Inbox
  4. line22-24: บันทึกลง log เมื่อมีการเชื่อมต่อกับ push service แล้ว

เมื่อตัว EventSource ได้ถูกสร้างขึ้นก็จะยิง GET ไปยัง /api/EmailService/ ก็จะไปเข้า API Controller ของเราข้างล่าง

 

5. create long polling for online user (EmailServiceController)

การทำงานในขั้นต่อมาคือ มีการสั่งยิงมาด้วย GET (EventSource ทำงานโดยใช้ GET) มาเข้า HttpGet Action ซึ่งมีอยู่ตัวเดียวใน EmailServiceController ก็คือ

Get(HttpRequestMessage request)

   1: //EFcontext

   2: private MailBoxDBContext mbctx = new MailBoxDBContext();

   3: //message pool

   4: private static List<MailMessageView> _newMails = new List<MailMessageView>();

   5: //online subscribe user

   6: private static ConcurrentDictionary<string, SubscriberPushMail> _subscribers = 

   7:      new ConcurrentDictionary<string, SubscriberPushMail>();

   8:  

   9: //Get

  10: public HttpResponseMessage Get(HttpRequestMessage request)

  11: {

  12:      HttpResponseMessage response = request.CreateResponse();

  13:      var email = request.QueryString("email");

  14:  

  15:      if (!string.IsNullOrEmpty(email.Trim()))

  16:      {

  17:            response.Content = new PushStreamContent(

  18:                (stream, content, tc) =>

  19:                   {

  20:                        StreamWriter streamwriter = new StreamWriter(stream);

  21:                        if(_subscribers.ContainsKey(email))

  22:                        {

  23:                           SubscriberPushMail dead;

  24:                           if(_subscribers.TryRemove(email, out dead))

  25:                           {

  26:                                dead.Stream.Dispose();

  27:                                dead = null;

  28:                           }

  29:                        }

  30:                        _subscribers.TryAdd(email, new SubscriberPushMail

  31:                           {   Email = email, Stream = streamwriter });

  32:                    }, "text/event-stream");

  33:      }

  34:      return response;

  35: }

  1. line2: สร้างตัวแปรสำหรับ efcontext,
  2. line4: _newMails สำหรับเก็บอีเมลที่มีการส่งเข้ามาใหม่ และจะถูกลบออกเมื่อส่งออกไปแล้ว
  3. line6-7: _subscribers จะเป็นผู้ใช้ที่มีการ logon ล่าสุดเข้ามาเพื่อเก็บ connection ไว้ส่งข้อมูลกลับ
  4. Line10-35: จะเป็น Get Action สำหรับ EventSource ทำการเชื่อมต่อเข้ามา พร้อมรับ email ของ user เข้ามาเพื่อเพิ่มลงไปใน _subscribers เก็บทั้ง Email และ StreamWriter ของ HttpResponse เพื่อทำการส่งข้อมูลกลับไปในตอนหลัง และมีการตรวจสอบหากมี email นี้ใน _subscribers แล้วก็ให้ลบออกไปจากระบบ *แก้บั๊ก

 

6. Connected to Push Service

เมื่อรับ response กลับมาจาก Get() Action ด้านบนก็จะเกิด ‘open’ event ซึ่งให้ย้อนกลับไปดูข้อ 4.4 ด้านบน

 

7. เมื่อ User2 ทำการส่งอีเมลไปหา User1

ต่อไปเมื่อมี user2 เปิด browser เข้ามา load mailbox ของตัวเอง และทำการส่งอีเมลถึง user1 หลังจากกดปุ่ม Send

   1: $("#sendMail").click(function () {

   2:     $.ajax({

   3:         url: "/api/EmailService/",

   4:         data: JSON.stringify(ko.mapping.toJS(viewModel.newEmail)),

   5:         cache: false,

   6:         type: 'POST',

   7:         dataType: "json",

   8:         contentType: 'application/json; charset=utf-8',

   9:         success: function (data) {

  10:             var json = JSON.parse(data);

  11:             if (!json.success) {

  12:                 alert(data.msg);

  13:             }

  14:         }

  15:     });

  16:     viewModel.newEmail.To('');

  17:     viewModel.newEmail.Title('');

  18:     viewModel.newEmail.Message('');

  19: });

จะเข้ามาทำงานใน click ของ jquery ที่เราได้ hook event เอาไว้ โดยให้ทำการส่งไปที่ “/api/EmailService/”  และกำหนดข้อมูลโดยให้ส่ง viewModel.newEmail ไปด้วยในรูปแบบของ POST   และให้ทำการล้างค่าของ newEmail หลังจากทำงานเสร็จแล้ว

       Save and Push

หลังจากที่ jquery ได้ยิง POST เข้ามาที่ EmailService Controller ก็จะมาเข้าที่ SendEmail() Action เนื่องจากเป็น POST action ตัวเดียวใน controller นี้

   1: //Post

   2: public string SendEmail(MailMessageView newEmail)

   3: {

   4:     try

   5:     {

   6:         var user = mbctx.Users.SingleOrDefault(o => o.UserName == newEmail.Username);

   7:         if (user == null)

   8:             return Json.Encode(new {success = false, msg = "not found user"});

   9:         var dt = DateTime.Now;

  10:         newEmail.ReceivedDate = dt.ToString("dd/MM/yyyy HH:mm:ss");

  11:         var mail = new MailMessage

  12:                        {

  13:                            Id = newEmail.Id = Guid.NewGuid(),

  14:                            From = newEmail.From,

  15:                            To = newEmail.To,

  16:                            Title = newEmail.Title,

  17:                            Message = newEmail.Message,

  18:                            IsRead = false,

  19:                            ReceivedDate = dt,

  20:                            User_Id = user.Id

  21:                        };

  22:         mbctx.MailMessages.Add(mail);

  23:         mbctx.SaveChanges();

  24:         //add alert email if the target subscriber is online

  25:         if (_subscribers.ContainsKey(mail.To))

  26:         {

  27:             _newMails.Add(newEmail);

  28:             //push email to subscriber

  29:             PushMessageToClient(mail.To);

  30:         }

  31:         return Json.Encode(new {success = true});

  32:     }

  33:     catch (Exception ex)

  34:     {

  35:         return Json.Encode(new { success = false, msg = ex.Message });

  36:     }

  37: }

  38:  

  39: [NonAction]

  40: private void PushMessageToClient(string toEmail)

  41: {

  42:     var subscribers = _subscribers.Where(o => o.Key.ToLower() == toEmail.ToLower().Trim());

  43:     foreach (var subscriber in subscribers)

  44:     {

  45:         var isNewMail = false;

  46:         var mails = _newMails.Where(o => o.To.ToLower().Trim() == toEmail.Trim().ToLower()).ToList();

  47:         if (mails.Count > 0)

  48:         {

  49:             subscriber.Value.Stream.WriteLine("data:" + JsonConvert.SerializeObject(mails) + "\n");

  50:             try { subscriber.Value.Stream.Flush(); }catch { }

  51:             _newMails.RemoveAll(o => o.To == toEmail);

  52:         }

  53:     }

  54:  

  55:  

  56: }

  1. line6-23: ใช้ในการบันทึก new email ลง database
  2. line25-31: เป็นการตรวจสอบ user ที่จะ push และทำการ add email ลง message collection จากนั้นเรียก PushMessageToClient เพื่อส่ง email ไปให้ user คนนั้น
  3. line40: จะเป็น method ที่ใช้ในการ push
  4. line42-43: เลือก user ที่เราแอดเก็บไว้ในตอนแรกจาก email ของ user และทำการ loop ตามจำนวนที่พบ
  5. line46-47: ค้นหา email ที่ถูกส่งเข้ามาหา user นี้ออกมาทั้งหมด เมื่อนับแล้วมีมากกว่า 0 message
  6. line49-51: ใช้ stream ของ user นั้นและทำการสร้าง  data: โดยใช้เอา email มาทำ serialize ให้อยู่ในรูปของ json string แล้วสั่ง write / flush ตามลำดับ เมื่อจบก็ให้ remove email เหล่านั้นออกจาก collection

 

ทิ้งท้าย

ตัวอย่างสำหรับ Web Push ในตอนที่ 2 นี้จะมีแค่ส่วนของ SSE ซึ่งผมพยายามหยิบยกตัวอย่างที่ไม่ใช่การ broadcast message ซึ่งน่าจะพอเป็นไอเดียนำไปประยุกต์ใช้ต่อได้  ในตอนต่อไปจะเป็นการนำ websockets มาใช้งาน

โปรดติดตามตอนต่อไป Smile

 

Download Demo Project *มี database script อยู่ใน project folder เอาไปสร้าง database และอย่าลืมเปลี่ยน connectionstring ใหม่


SONY DSC

About Me:

Chalermpon Areepong : Nine (นาย)

Microsoft MVP Thailand ASP.NET

ASP.NET & MVC Developers Thailand Group :http://www.facebook.com/groups/MVCTHAIDEV/

Greatfriends.biz Community Leader : http://greatfriends.biz

Email : nine_biz-talk.net at hotmail dot com

Blog : https://nine69.wordpress.com

Blog at WordPress.com.