Nine MVP's Blog

25/05/2015

WEB API: Quota

Filed under: Article, ASP.NET, WEB APIs — Tags: , — Nine MVP @ 2:46 am

แนวคิดเกี่ยวกับการทำ Quota การใช้งานของ API

WEB API: Quota.

05/04/2014

Secure Server Side Information

Filed under: ASP.NET, ASP.NET MVC, Core System, WEB, Web Security — Nine MVP @ 4:24 pm

เนื่องจากเคยกล่าวไว้ไปหลายรอบในกลุ่ม Facebook เรื่องการปิด Server side information เพื่อความปลอดภัย ลดภาวะเสี่ยงที่จะถูกโจมตีจากช่องโหว่ของ software และ OS เอง

ตัวอย่าง  หลังจากที่ผมได้ยิง request ไปที่ website แห่งนึง ผมลองดู HTTP Header ที่ส่งกลับมาจะพบรายละเอียดดังภาพ

4-5-2014 3-48-28 PM

มาบทความนี้จะช่วยระบุวิธีการปิด HTTP Response Header ที่ส่งออกมาจากฝั่ง Server ไปยัง Client

 

3 ขั้นตอนลับลวงพราง สู่ความปลอดภัยระดับพื้นฐาน

 

1. ลบหรือลวงค่า header X-Powered-By : ASP.NET

ให้ไปแก้ไขใน IIS เปิด website ที่ต้องการมาแก้ไข กดเลือกที่ website ตามรูปด้านล่าง

4-5-2014 3-33-13 PM

4-5-2014 3-35-49 PM

เป็นอันสำเร็จ

 

2. ลบหรือลวงค่า header Server : Microsoft-IIS/8.0 และ X-AspNet-Version : 4.0.xxxx

สร้าง class ขึ้นมาตัวนึง แล้ว inherit IHttpModule Interface เพื่อลบหรือแก้ไขค่าของ Server header

public class SecureInfoModule : IHttpModule
{
    public void Init(HttpApplication context)
    {
        context.PreSendRequestHeaders += OnPreSendRequestHeaders;
    }
 
    public void Dispose() { }
 
    void OnPreSendRequestHeaders(object sender, EventArgs e)
    {
        HttpContext.Current.Response.Headers.Set("Server", "I don't know ..."); //ใส่ข้อความถึง hacker
    }
}

ต่อจากนั้นนำ module ที่สร้างไว้มา register ลงใน webserver tag
ให้เปิด Web.Config มาแก้ไขกำหนดสั่งปิดการแสดงผล version ของ http runtime

<system.web>
  <httpRuntime enableVersionHeader="false"/>
 ....
 </systen.web>
 นำ class module ที่สร้างไว้ มาใส่ใน webServer tag
 <system.webServer>
 <modules>
 <add name="SecureInfoModule" type="ใส่  namespace ให้ตรง.SecureInfoModule" />
 .........
 </modules>
 </system.webServer>

 

3. ลบหรือลวงค่า X-AspNetMvc-Version : 4.0

เปิดโปรเจ็คใน Visual Studio มองหา Global.asax.cs เปิดมาใส่โค้ด

protected void Application_Start()
{
    MvcHandler.DisableMvcResponseHeader = true;
    .....
}

 

หวังว่าจะเป็นประโยชน์สำหรับเหล่าสาวก .NET  😀

03/11/2013

ASP.NET MVC Series : Photo Upload Tool (PLUpload, Bootstrap)

Filed under: ASP.NET, ASP.NET MVC, C#, html5, javascript — Tags: , , , , — Nine MVP @ 7:55 pm

ตอนนี้จะเป็นบทความสุุดท้ายที่จะโพสใน wordpress นี้ซึ่งผมจะย้ายไป blog ส่วนตัวซึ่งจะจัดการรายละเอียดต่างๆได้สะดวกกว่านี้

ตอนนี้จะมาแนะนำการสร้างเครื่องมือในการทำ Photo Upload สำหรับ MVC และเช่นเคยไม่เกริ่นมากมาย ก็เข้าเรื่องกันเลย

มาดู library ที่จะใช้งาน

plupload  library ตัวนี้มีดีที่รองรับ multi browser สามารถสลับ engine ไปใช้ html4, html5, flash, siverlight, etc. ตามที่ browser จะรองรับหรือมีติดตั้งไว้ และสามารถทำงานแบบ multiple file selection, file filter (GPLv2 License) ซึ่งใครไม่เข้าใจ plupload ให้อ่านที่นี่ก่อน เพราะผมจะไม่อธิบายการทำงานในบทความนี้
http://www.plupload.com

ตอนนี้จะแนะนำการใช้งานโดยสร้าง UI ขึ้นมาใช้งานเองโดยเพิ่มเติมความสามารถเรื่อง thumbnail, image dimension และตรวจสอบว่า browser รองรับการทำงานของฟังชั่นที่เราเพิ่มเติมเข้าไปหรือไม่

หน้าตาและตัวอย่างการทำงาน

สำหรับ Browser ที่รองรับ HTML5

สำหรับ browser ที่ไม่รองรับ HTML5

ตัวอย่าง code ใน Razor View

10-31-2013 3-02-37 PM

Line 3: เป็นการเรียกใช้ _layout.cshtml ซึ่งจะมี javascript พื้นฐานพวก jquery, css ต่างๆไว้ใช้งาน
Line 9: เรียกใช้ css สำหรับ xPhotoUpload
Line 11-17: ประกาศ tag สำหรับ เรียกใช้ตัว xphotoupload และกำหนดค่าต่าง ๆ ผ่าน data-* property

  • data-multiple-upload = true/ false ต้องการใช้งานโหมด single/multiple upload
  • data-upload-url = action for upload url
  • data-flash-url = กำหนด url path สำหรับ flash engine ของ plupload
  • data-filter-extension =  นามสกุลไฟล์ที่ต้องการมองเห็น
  • data-size-limit = ขนาดของไฟล์ที่อนุญาตให้ upload
  • data-chunk-size = ขนาดชิ้นงานที่จะทำการอัพโหลดต่อครั้ง
  • data-show-console = แสดงคอนโซลการทำงานของ upload tool

โค้ดส่วน Controller

10-31-2013 6-42-22 PM

Line 16-19: เป็นการนำวิว Index.cshtml ส่งกลับไปให้ client เพื่อแสดงผล
Line 22: รับเข้าเป็น HttpPostedFileBase parameter เพื่อง่ายในการเรียกใช้  หรือเปลี่ยนไปอ่านจาก Request.File
Line 25-30: เป็นการสร้าง Upload Directory ตามวันที่
Line 33: สั่งให้บันทึก File ที่ upload มาลงตาม path ดังกล่าว
Line 35: ส่งค่ากลับ

ส่วนของ script/style bundle

11-1-2013 2-24-17 PM

Line 28: kendo core เอาไว้ใช้ทำ template
Line 29: plupload script ใช้เป็น engine สำหรัย upload
Line 30: XPhotoUpload script ที่เราเขียนไว้เพิ่มเติมใช้ในงานนี้
Line 33-35: เป็น css ที่ใช้งานทั้งหมด

โครงสร้างไฟล์ในโปรเจคที่เดโม

ใน Content จะมีสองส่วนคือ plupload folder กับ xphotoupload.css ส่วน bootstrap ถือว่าต้องใช้เป็นพื้นในงานนี้อยู่แล้ว
11-3-2013 4-16-56 PM

ต่อมา Scripts จะมี plupload, kendo.core.min.2013.319.js, XPhotoUpload.js ซึ่งใช้ในการทำงานส่วน javascript
11-3-2013 4-17-15 PM

แนะนำ code ใน XPhotoUpload.js บางบรรทัดที่น่าสนใจ

การเข้าถึง File จาก browser

image
การเข้าถึง file upload โดยใช้ HTML5 API ทุก browser จะมี object สองตัวนี้ให้เรียกใช้งานซึ่งจะมี standard function ในการใช้ทำงานกับ file ได้

หารายละเอียดของรูปที่จะอัพโหลดเพื่อตรวจสอบ dimension

image
Line 98: สร้าง image object ขึ้น
Line 108: เรียกใช้ _URL object โดยให้สร้าง url address ของไฟล์ที่เลือกเขามาใน plupload และส่งให้ img object
Line 99-102: เป็นการผูก function ให้กับ img เพื่อให้ทำงานเมื่อมีการโหลดภาพเข้ามา ซึ่งจะเกิดทันทีที่กำหนดค่าให้ img.src ที่บรรทัด 108

การทำ progress bar

image
เป็น event ที่เกิดขึ้นโดย plupload และแสดงผลโดยใช้ css class ของ bootstrap  โดยการขยายความกว้างไปพร้อมๆ กำหนด % ของไฟล์ที่ plupload ส่งค่ามาให้ใน event นี้

การใช้ Kendo Template ในการทำ html template binding

image
Line 19: table template สำหรับแสดงผลตารางเพื่อสร้างกรอบการทำงานของทูลอัพโหลด
Line 20: table row แบบไม่แสดงผล thumbnail ในกรณีที่ browser ไม่รองรับการทำงานของ HTML5
Line 21: table row แบบแสดงผล thumbnail หาก browser รองรับการทำงาน

ใน table row ทั้งสองแบบจะมี tag ลักษณะ  #= property name # เพื่อบอกให้ kendo มองหา property ของ object นั้นมาแทนค่าลงไปโดยใช้ code ชุดข้างล่างนี้ซึ่งมี 2 แบบ
แบบสั่ง bind โดยไม่มี object
image
และแบบมี object
image

ส่งท้าย

หากเรามีความรู้ในส่วนของ client script อย่าง javascript ให้ลึกพอ เราก็จะสามารถสร้างสรรค์งานขึ้นตามที่เราต้องการได้ ซึ่งก็เป็นเรื่องที่สมควรจะต้องมีสำหรับ web developer ทุกท่าน Smile

สำหรับ project demo สามารถ download ได้ที่ https://bitbucket.org/NineMvp/mvc-upload #Git
สำหรับ zip file https://bitbucket.org/NineMvp/mvc-upload/get/b68e85296709.zip

02/02/2013

ASP.NET MVC Series: High Performance JSON Parser for ASP.NET MVC

Filed under: ASP.NET, ASP.NET MVC, C#, javascript — Nine MVP @ 9:17 pm

กลับมาอีกครั้งกับบทความตอนใหม่ จากที่ผมได้พูดเปรยไว้ในโพสนึงในกลุ่ม ASP.NET & MVC Developers Thailand Group ว่าอย่าทนใช้ Serializer ของเดิมๆ จึงขอขยายความด้วยบทความตอนนี้

ในปัจจุบัน JSON ได้ถูกใช้งานกันอย่างกว้างขวางจากหลาย application ซึ่งเป็นที่นิยมบน HTTP ใช้ในการส่งข้อมูลกันไปมา Request/Response

โดยปกติใครที่เขียน MVC/JQuery บ่อยๆก็มักจะได้ใช้งาน JSON และแม้ว่าปกติก็ทำงานได้อยู่แล้ว #ทำงานได้ไม่ได้หมายความว่าทำได้ดีที่สุด#

ดังนั้นสำหรับตอนนี้เราจะมาแนะนำให้รู้จักเจ้า JavaScriptSerializer Class ที่มีอยุ่ใน .NET Framework และก็ถูกเรียกใช้เยอะหลายจุดเลยทีเดียวใน MVC โดยบทความตอนนี้จะนำ Serializer ที่ดีกว่าจะหาของที่ดีกว่ามาแทนที่ของเดิมโดยทำงานเร็วกว่าหลายเท่าตัว

Performance Issue:

ก่อนอื่นทำความรุ้จักกับ JavaScriptSerializer Class เป็นคลาสที่เอาไว้ช่วยทำการแปลงจาก .NET Object ไปเป็น JSON (Serialization) และใช้แปลง JSON ไปเป็น .NET Object (Deserialization) นั่นเอง

การใช้งานก็เรียกใช้แบบนี้

string jsonIn = "{ \"Name\" : \"Nine\", \"Salary\" : 500.00 }";
var serializer = new System.Web.Script.Serialization.JavaScriptSerializer();

//Call Deserialize to .NET object
Person person = serializer.Deserialize<Person>(jsonIn);

//modify object value
person.Name = "ASP.NET MVC";
person.Salary = 10000;

//Call Serialize to JSON string
string jsonOut = serializer.Serialize(person);

ซึ่งหากไม่คิดอะไรมากก็ใช้งานได้แล้วครับ เพียงแต่อยากให้ดูผลการทดสอบของเว็บไซต์ที่ผมนำมาอ้างอิงกันก่อน J

The results (in milliseconds) as well as the average payload size (for each of the 100K objects serialized) are as follows.

Graphically this is how they look:



ref: http://theburningmonk.com/2012/08/performance-test-json-serializers-part-iii/

จากกราฟด้านบนจะเห็นได้ว่า JavaScripSerializer นั้นทำงานได้ช้ามาก (แต่ JayRock แย่สุด) เมื่อเทียบกับ library อื่นๆแล้ว แต่นี่ก็ยังไม่ใช่ผลการทดสอบของเรา

ผมจึงหยิบเอาตัวที่ใกล้ชิดกับเรามากที่สุดมาทำการทดสอบคือ JavaScriptSerializer, JSON.NET (มีอยู่ใน MVC project) และ ServiceStack.Text (ผลการทดสอบดีที่สุด

และจะนำมาใช้งานในบทความนี้)

JSON.NET : http://json.codeplex.com/

ServiceStack.Text : https://github.com/ServiceStack/ServiceStack.Text

 

TEST CASE:

การทดสอบจะใช้ Stopwatch ในการจับเวลาการทำงานโดย for loop 1,000,000 ครั้ง

โดยทดสอบทั้งการ Serialize และ Deserialize ดังนี้

int times = 1000000;
string output = "";


//JavaScriptSerializer Class
var startMSJson = Stopwatch.StartNew();
var msjs = new System.Web.Script.Serialization.JavaScriptSerializer();
for (int i = 0; i < times; i++)
{
var testObject = new { Name = "Nine" + i.ToString(), Salary = 500.00 };
var temp = msjs.Serialize(testObject);
}
startMSJson.Stop();
output += "Javascript Serializer : " + startMSJson.ElapsedMilliseconds.ToString() + " ms" +
Environment.NewLine;

startMSJson = Stopwatch.StartNew();
for (int i = 0; i < times; i++)
{
var jsonIn = "{ \"Name\" : \"Nine\", \"Salary\" : 500.00 }";
var temp = msjs.Deserialize<Person>(jsonIn);
}
startMSJson.Stop();
output += "Javascript Deserializer : " + startMSJson.ElapsedMilliseconds.ToString() + " ms" +
Environment.NewLine;



//JSON.NET
var startJSONNET = Stopwatch.StartNew();
for (int i = 0; i < times; i++)
{
var testObject = new { Name = "Nine" + i.ToString(), Salary = 500.00 };
var temp = Newtonsoft.Json.JsonConvert.SerializeObject(testObject);
}
startJSONNET.Stop();
output += "JSON.NET Serializer : " + startJSONNET.ElapsedMilliseconds.ToString() + " ms" +
Environment.NewLine;

startJSONNET = Stopwatch.StartNew();
for (int i = 0; i < times; i++)
{
var jsonIn = "{ \"Name\" : \"Nine\", \"Salary\" : 500.00 }";
var temp = Newtonsoft.Json.JsonConvert.DeserializeObject<Person>(jsonIn);
}
startJSONNET.Stop();
output += "JSON.NET Deserializer : " + startJSONNET.ElapsedMilliseconds.ToString() + " ms" +
Environment.NewLine;



//ServiceStack.Text
var startSSTJson = Stopwatch.StartNew();
for (int i = 0; i < times; i++)
{
var testObject = new { Name = "Nine" + i.ToString(), Salary = 500.00 };
var temp = ServiceStack.Text.JsonSerializer.SerializeToString(testObject);
}
startSSTJson.Stop();
output += "ServiceStack.Text Serializer : " + startSSTJson.ElapsedMilliseconds.ToString() + " ms" +
Environment.NewLine;

startSSTJson = Stopwatch.StartNew();
for (int i = 0; i < times; i++)
{
var jsonIn = "{ \"Name\" : \"Nine\", \"Salary\" : 500.00 }";
var temp = ServiceStack.Text.JsonSerializer.DeserializeFromString<Person>(jsonIn);
}
startSSTJson.Stop();
output += "ServiceStack.Text Deserializer : " + startSSTJson.ElapsedMilliseconds.ToString() + " ms" +
Environment.NewLine;

 

Output

  • Javascript Serializer : 11589 ms
  • Javascript Deserializer : 7769 ms
  • JSON.NET Serializer : 3783 ms
  • JSON.NET Deserializer : 6641 ms
  • ServiceStack.Text Serializer : 2069 ms
  • ServiceStack.Text Deserializer : 1211 ms

หลังทดสอบพบว่าผลการทำงานของ JavaScriptSerializer นั้นแย่มากจริงๆ รองลงมา JSON.NET ก็ทำได้ดี แต่ ServiceStack.Text ทำได้ดีที่สุด ซึ่งวัตถุประสงค์หลักของบทความนี้ต้องการจะบอกว่าใน ASP.NET MVC มีการใช้งาน Serializer ตัวที่แย่ที่สุดนั้นเอง

และเราจะนำตัว ServiceStack.Text มาใช้งานทดแทนเจ้า JavaScriptSerializer กัน แต่ว่าถูกใช้งานอยู่ที่ไหนบ้างหละ?

 

Where is JavaScriptSerializer used in ASP.NET MVC?

ต่อไปจะพากันไปหาจุดที่มีการเรียกใช้เจ้า JavaScriptSerializer ซึ่งมีอยู่หลายจุด แต่ขอนำจุดที่สำคัญของระบบจริงๆ มาแก้ไขกันในบทความนี้ ซึ่งจะเน้นไปที่ Input และ Output ซึ่งมีอยู่ 2 จุดคือ

Model Binder (INPUT)

จุดแรก model binder จะทำหน้าที่จับเอา request โดยตรวจดู content type ว่าเป็นชนิด json จากนั้นก็นำค่าไปทำการ deserialize ไปเป็น .net object ตามคลาสที่ประกาศไว้ใน Action นั้นๆ เช่น

public JsonResult NewPerson(Person person)
{
return Json(new Person());
}

จากตัวอย่างด้านบน Person จะเป็นคลาสที่กำหนดไว้ใน NewPerson Action ของ Controller เมื่อมีการ request กลับมาที่ Controller/NewPerson ด้วยการ json post ตัวอย่างด้านล่างนี้

"{ \"Name\" : \"Nine\", \"Salary\" : 500.00 }"

ก่อนที่จะเข้ามาใน NewPerson Action จะมีการสร้าง Person object จากส่งกลับเข้ามาโดยไปเรียก ModelBinder (หากไม่มี custom binder จะไปเรียก DefaultModelBinder มาทำงาน) โดยดักจับ content-type ว่าเป็น request ประเภทใด

ในกรณีนี้ส่งมาเป็น JSON ก็จะไปเรียกตัว JsonValueProviderFactory (ValueProviderFactory เป็น base class) ซึ่งเจ้านี่เองที่มีการใช้งาน JavaScriptSerializer Class ในการแปลง JSON มาเป็น object เพื่อส่งกลับมาใน Action

แกะดู code เจ้า JsonValueProviderFactory กัน

public sealed class JsonValueProviderFactory : ValueProviderFactory 

{

private static void AddToBackingStore(JsonValueProviderFactory.EntryLimitedDictionary backingStore, string prefix, object value){ … }

private static object GetDeserializedObject(ControllerContext controllerContext)

{

if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))

return (object) null;


string input = new StreamReader(controllerContext.HttpContext.Request.InputStream).ReadToEnd();



if (string.IsNullOrEmpty(input))

return (object) null;


else

return new JavaScriptSerializer().DeserializeObject(input);

}



public override IValueProvider GetValueProvider(ControllerContext controllerContext){ }


private static string MakeArrayKey(string prefix, int index) { }


private static string MakePropertyKey(string prefix, string propertyName) { }


private class EntryLimitedDictionary


{


………

private static int GetMaximumDepth()


{


NameValueCollection appSettings = ConfigurationManager.AppSettings;


if (appSettings != null)


{


string[] values = appSettings.GetValues("aspnet:MaxJsonDeserializerMembers");


int result;

if (values != null && values.Length > 0 && int.TryParse(values[0], out result))


return result;


}


return 1000;


}


}


}

จะพบว่าถูกเรียกใน GetDesirializedObject() (ซึ่งถูก GetValueProvider() เรียกอีกที) นั่นเอง ทีนี้เราจะมาสร้างของเราใช้งานเองครับ J

ผมสร้าง Class ขึ้นใหม่โดย Inherit เจ้า ValueProviderFactory และใช้เจ้า ServiceStack.Text ใน GetValueProvider() ตรงๆแบบนี้ โดยใช้ AsExpandoObject() Extension Method แทนการทำงานอื่นๆของ JsonValueProviderFactory

public sealed class JsonServiceStackValueProviderFactory : ValueProviderFactory
{
public override IValueProvider GetValueProvider(ControllerContext controllerContext)
{
if (controllerContext == null)
throw new ArgumentNullException("controllerContext");

if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
return null;

var reader = new StreamReader(controllerContext.HttpContext.Request.InputStream).BaseStream;

return new DictionaryValueProvider<object>(
ServiceStack.Text.JsonSerializer.DeserializeFromStream<Dictionary<string, object>>(reader).AsExpandoObject(),
CultureInfo.CurrentCulture);
}
}

มาดู Extension Method ตัวที่ว่ากันครับ

public static class CollectionExtensions
{
public static ExpandoObject AsExpandoObject(this IDictionary<string, object> dictionary)
{
var epo = new ExpandoObject();
var epoDic = epo as IDictionary<string, object>;

foreach (var item in dictionary)
{
bool processed = false;

if (item.Value is IDictionary<string, object>)
{
epoDic.Add(item.Key, AsExpandoObject((IDictionary<string, object>)item.Value));
processed = true;
}
else if (item.Value is ICollection)
{
var itemList = new List<object>();
foreach (var item2 in (ICollection)item.Value)
if (item2 is IDictionary<string, object>)
itemList.Add(AsExpandoObject((IDictionary<string, object>)item2));
else
itemList.Add(AsExpandoObject(new Dictionary<string, object> { { "Unknown", item2 } }));

if (itemList.Count > 0)
{
epoDic.Add(item.Key, itemList);
processed = true;
}
}

if (!processed)
epoDic.Add(item);
}

return epo;
}
}

การนำไปใช้งานก็ไปที่ Global.asax.cs วางโค้ดลงไปตาม

protected void Application_Start() 
{
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
AuthConfig.RegisterAuth();

ValueProviderFactories.Factories.Remove(ValueProviderFactories.Factories.OfType<JsonValueProviderFactory>().FirstOrDefault());
ValueProviderFactories.Factories.Add(new JsonServiceStackValueProviderFactory());


}

การทำงานคือเราจะไป remove เจ้า JsonValueProviderFactory ออกจาก ValueProviderFactories Class และเพิ่ม JsonServiceStackValueProviderFactory อันใหม่ของเราเข้าไป

JsonResult (Output)

จุดที่สองจะเป็นการ render JSON string กลับออกไปยัง Client เรามาดูโค้ดของ JsonResult Class ที่ ASP.NET MVC ใช้อยู่ในปัจจุบัน

public class JsonResult : ActionResult
{

public override void ExecuteResult(ControllerContext context)
{

if (context == null)

throw new ArgumentNullException("context");

if (this.JsonRequestBehavior == JsonRequestBehavior.DenyGet && String.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))

throw new InvalidOperationException(MvcResources.JsonRequest_GetNotAllowed);

HttpResponseBase response = context.HttpContext.Response;

response.ContentType = string.IsNullOrEmpty(this.ContentType) ? "application/json" : this.ContentType;

if (this.ContentEncoding != null) response.ContentEncoding = this.ContentEncoding;

if (this.Data == null) return;

JavaScriptSerializer scriptSerializer = new JavaScriptSerializer();

if (this.MaxJsonLength.HasValue)


scriptSerializer.MaxJsonLength = this.MaxJsonLength.Value;

if (this.RecursionLimit.HasValue)


scriptSerializer.RecursionLimit = this.RecursionLimit.Value;


response.Write(scriptSerializer.Serialize(this.Data));

}

}

ต่อไปเราจะสร้าง JsonResult ของเราเอง โดยสร้างคลาสใหม่ขึ้นมา แล้ว Inherit เจ้า JsonResult ให้กับคลาส

แล้วก็ทำการ override ExecuteResult() ใหม่ตามนี้


public class JsonServiceStackResult : JsonResult
{

public override void ExecuteResult(ControllerContext context)
{


HttpResponseBase response = context.HttpContext.Response;


response.ContentType = !String.IsNullOrEmpty(ContentType) ? ContentType : "application/json";



if (ContentEncoding != null)
{


response.ContentEncoding = ContentEncoding;


}

if (Data != null)
{


response.Write(ServiceStack.Text.JsonSerializer.SerializeToString(Data));


}


}


}

ตั้งชื่อซะยาวเรียกใช้คงลำบาก งั้นเราก็ไปสร้าง Base Controller ของเราไว้ใช้งานแทนเจ้า return Json(); แบบนี้


public abstract class MyBaseController : Controller
{


protected new JsonResult Json(object data, JsonRequestBehavior behavior)


{ return Json(data, "application/json", Encoding.UTF8, behavior); }


protected new JsonResult Json(object data)
{

return Json(data, "application/json", Encoding.UTF8, JsonRequestBehavior.DenyGet);


}



protected override JsonResult Json(object data, string contentType, Encoding contentEncoding, JsonRequestBehavior behavior)
{

return new JsonServiceStackResult
{

Data = data,


ContentType = contentType,


ContentEncoding = contentEncoding


};


}


}

ตอนจะใช้งานก็แค่นำ MyBaseController ไป inherit แทนที่ Controller Class แล้วค่อยเรียก Json() เหมือนปกติเดิม J

public class HomeController : MyBaseController
{


public JsonResult NewPerson(Person person)
{

return Json(new Person());

}

}

 
 
 

Conclusion

ในบทความนี้หวังว่าจะมีประโยชน์สำหรับผู้ที่ต้องการเพิ่มประสิทธิภาพให้กับตัว MVC App โดยบางท่านอาจจะยังไม่ทราบส่วนนี้ จนกว่าจะต้องทำ Performance tuning กันเพื่อเพิ่มประสิทธิภาพในการทำงานในจุดที่ช้าและทำงานได้ไม่ดี

และจากบทความตอนนี้ท่านสามารถนำสิ่งเหล่านี้ไปใช้งานได้ทั้ง ASP.NET MVC และ ASP.NET WEB API (ตัวนี้เหมาะที่จะเปลี่ยนใหม่เพราะทำงานกับ Data จำนวนมาก)

สำหรับตอนนี้ขอจบไว้เพียงเท่านี้ เจอกันใหม่ตอนหน้าครับ

 


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

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

30/09/2012

ASP.NET MVC Series: Web Push Technology I

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

สำหรับบทความตอนนี้จะขอพูดถึงเทคโนโลยี่ของ HTML5 ตัวนึงที่ค่อนข้างมาแรงคือ Web Push Technology  ซึ่งเป็นเทคโนโลยีที่ช่วยให้เราลดปริมาณการส่งคำขอ request จาก Client browser ไปยัง server เพื่อให้ทำการประมวลผลตามคำสั่งตามคำขอนั้นอยู่ตลอดเวลา ตัวอย่างเช่น โปรแกรม chat ที่ต้องการดูคู่สนทนาพิมพ์โต้ตอบ หรือโปรแกรมstock ที่ต้องการดูความเปลี่ยนแปลงของค่าหุ้นเป็นต้น   

ซึ่งก่อนนี้เราจะต้องเขียนโปรแกรมให้ตั้งเวลาเพื่อสร้างคำขอไปยังserver ให้ทำการดึงข้อมูลที่เปลี่ยนแปลงมาอัพเดทยัง client browser ลักษณะการทำงานแบบนี้เราเรียกว่า Pull Model

Poll/Pull Technique มี 2 แบบคือ

1. Traditional Polling

คือการใช้ javascript : setInterval() และ setTimout() ในการทำงาน

setInterval(function(){
      $.ajax({ url: “/api/StockQuoteAPI”,
                   success: function(data){
                                   //Update your dashboard gauge
                                  stockQuoteGraph.setValue(data.value);
                                  },
                   dataType: “json”});
   }, 30000);
(function poll(){
        setTimeout(function(){
               $.ajax({ url: “/api/StockQuoteAPI”,
                            success: function(data){
                                           //Update your dashboard gauge 
                                           stockQuoteGraph.setValue(data.value);
                                           //Setup the next poll recursively
                                           poll();
                                          },
                            dataType: “json”});
          }, 30000);
})();

*ทั้ง setInterval(), setTimeout() ต่างก็จะทำงานเมื่อถึงเวลาที่กำหนดไว้ใน interval  สำหรับ setInterval() อาจจะทำงานซ้ำซ้อนกันหากความถี่ตั้งไว้น้อยและ server ตอบกลับช้า ข้อมูลที่ตอบกลับมาอาจจะไม่เรียงลำดับ

 

ปัญหาเรื่อง performance ของระบบ

image

ตามภาพด้านบนจะเห็นว่าทางซ้ายคือ stock exchange application ซึงทางฝั่งผู้ใช้ได้โหลดไปเพื่อดูข้อมูล แต่ทุก 10 วินาทีโปรแกรม JavaScript จะทำการสร้าง request และส่งคำสั่งไปยังเซอเวอร์เพื่อขอข้อมูลที่เปลี่ยนแปลง ซึ่งข้อมูลอาจจะไม่มีการเปลี่ยนแปลงเลยก็ได้ ซึ่งหากคำนึงถึงจุดนี้จะเห็นว่าอาจจะเป็นการเพิ่มการทำงานให้ตัว server ตามภาพด้านล่าง

 

image

ตามภาพด้านบนจะพบว่าหากมีผู้ใช้งานจำนวนเป็นหมื่นคน และต้องทำการรีเฟรชข้อมูลหุ้นอยู่ตลอดเวลาทั้งที่ข้อมูลอาจจะยังไม่มีการเปลียนแปลงใดๆ แต่ server ก็ต้องโปรเซสงานตามคำขอแม้ผลที่ได้คือไม่พบการเปลี่ยนแปลงของหุ้น ซึ่งอาจจะลงไปคิวรี่ในดาต้าเบสเพื่อหาความเปลี่ยนแปลงเป็นต้น

การทำงานที่ยิงตรงไป server เพื่อขอข้อมูลที่เปลี่ยนแปลงนั้นก็ยังนับว่าเป็นการทำงานที่ไม่ค่อยจะคุ้มค่าเท่าไหร่นัก หากทำงานแล้วพบว่ายังไม่มีข้อมูลเปลี่ยนแปลงและยังต้องทำงานวนเวียนอยู่แบบนั้น  ไม่เป็นการดีแน่หากจำนวนผู้ใช้งานเพิ่มมากขึ้นซึ่งแน่นอนว่าความสามารถในการรองรับจำนวนผู้ใช้งานนั้นมีอยู่จำกัด อาจจะทำให้เกิด timeout จนถึง server ล่มอยู่บ่อยๆ  ก็อาจจะต้องลงทุนอัพเกรดเครื่องหรือไม่ก็ซื้อเครื่องเพิ่มทำ load balance เพื่อช่วยให้รองรับการทำงานได้มากขึ้น

 

2. Long Polling

คือการใช้เทคนิคของ traditional polling เข้ามาประยุกต์โดยตั้งเวลาให้สร้างรีเควสแรกไปยังเซอเวอร์และหากยังไม่มีข้อมูลเปลี่ยนแปลงก็ให้ค้างรอจนกว่าจะมีข้อมูลถึงจะส่งกลับมายัง client และมีกำหนด timeout ของการ poll แต่ละครั้ง ซึ่งเป็นเทคนิคเดียวกับที่ comet มีใช้งานคล้ายกัน  วิธีการนี้เป็นการทำงานได้ผลคล้ายกับการใช้ push เพียงแต่ต้องไปสร้าง logic ที่ฝั่ง server เพื่อจัดการรีเควสนั้นจนจบเงื่อนไขต่างๆ

image

จาก Long Polling ทำให้เกิดแนวคิดมากมายทั้งถูกนำไปสร้างเป็น library อย่างเช่น COMET(รู้จักกันอีกชื่อว่า AJAX Push) มีคนนำไปพัฒนาเป็น Application ต่างๆมากมายไม่ว่าจะ meebo, gmail chat เป็นต้น ซึ่งช่วยให้ใช้งานง่ายขึ้น หลายเบราเซอร์ก็รองรับการทำงานเพราะใช้งาน javascript/xmlhttprequest ที่มีในเกือบทุกเบราเซอร์ กระทั่งมีแนวคิดพัฒนาต่อยอดเพื่อให้ทำงานเป็น Push จริงๆ จนกลายเป็นอีกความสามารถนึงที่มีมากับ HTML5 ในช่วงต่อไป

 

แล้วก็มาถึงยุค HTML5 แจ้งเกิด Web Push Technology

มีมาตรฐาน 2 ตัวจะเกี่ยวกับ Push Technology ที่มาพร้อมกับ HTML5 คือ

1. Server Sent Event(SSE)

เป็นคุณสมบัติที่ช่วยให้เราทำ server push message ได้ แต่ยังคงใช้เทคนิคของ long polling เป็นการทำงานพื้นฐาน และยังเพิ่มความสามารถในเรื่อง auto reconnect ได้เมื่อรีเควสได้ถูก closed ลงหรือเกิด timeout ขึ้น และสามารถกำหนด event ต่างๆ ส่งกลับมาบอกทาง client ได้ว่าเป็นข้อมูลที่เกิดจาก event อะไร

การตรวจสอบและเริ่มใช้งาน SSE สามารถใช้ script นี้ในการตรวจสอบได้

if (!!window.EventSource){
    var source = new EventSource(‘/api/StockQuoteAPI/’);
} else {
    // browser not support SSE, use xhr
}

รูปแบบข้อมูลของ SSE ที่ส่งรับกลับมาจะเป็น content type แบบ text/event-stream  จะเป็นลักษณะโครงสร้างดังนี้

1. event : เป็น field ที่เอาไว้บอกว่าเป็นเหตุการณ์ชื่ออะไรที่เกิดขึ้นที่ server  สามารถเพิ่มเข้าไปใน eventsource ได้ด้วยการสั่ง addEventListener() และหากไม่ได้กำหนดก็ดักด้วย onmessage handler ได้เลย

2. data : เป็น field ที่มีไว้ใช้ส่งข้อมูลจากฝั่ง server มายัง client สามารถส่งแบบ multiline ได้ รวมทั้งสามารถส่งข้อมูลแบบ json ได้ด้วย

กรณีส่งข้อมูลกลับแบบบรรทัดเดียว จะมี data: message และปิดท้ายด้วย \n\n ดังตัวอย่าง *ปกติเราจะมองไม่เห็น (\n) ในข้อความ

event: QuoteChange\n

data: Stock Quote Data Start!\n\n

กรณีที่มีการส่งข้อมูลแบบหลายบรรทัด แต่บรรทัดสุดท้ายจะมี \n\n ปิด ดังตัวอย่าง

event: QuoteChange\n

data: Stock Quote Data Start!\n

data: PTT 35.09\n

data: KTB 30.42\n\n

3. id : เป็นการจัดเรียงลำดับข้อมูลมาจาก server กรณีที่เกิด connection close ไปโดยสาเหตุใดๆก็ตาม ทำให้ client สามารถ sort data ได้

id: 1\n

event: QuoteChange\n

data: Hello world\n\n

4. retry : เป็นค่าเวลาในการสัง่รอให้เชื่อมต่อกลับไปยัง source link เมื่อ เกิด timeout ขึ้น ปกติค่าจะอยู่ที่ 3 seconds แต่เราก็สามารถแก้ไข interval สำหรับ reconnect ได้เอง

และหากต้องการเปิดการเชื่อมต่อกับฝั่ง server สามารถใช้คำสั่ง

source.close();

 

2. Web Sockets (ws)

ต้องร้องว้าวกันเลยสำหรับ feature นี้  เพราะว่าผมเองก็ไม่คิดว่าจะมีการนำ socket ที่เปิด tcp วิ่งไปมา มาใช้งานบนเว็บ   เนื่องจากอาจจะไม่สามารถทำงานผ่าน firewall ได้นั้นเอง  แต่ทว่าเขาสามารถทำให้ใช้งานผ่าน firewall ได้ด้วยอเมซิ่งเลยทีเดียว โดยถ้าคุณใช้งาน web ที่ port 80/443 ได้ปกติ คุณก็จะสามารถใช้งาน ws ได้ปกติเช่นกัน (ไว้อธิบายกันรอบหน้า)

ตัว ws นี้สามารถทำงานแบบ 2-way communications แบบซึ่งส่งข้อมูลได้ทั้งแบบ upstream/downstream ภายใน connection เดียวกัน   ซึ่งต่างกับแบบ comet, SSE ที่ต้องใช้ 2 connection ซึ่งและจะทำให้มีประสิทธิภาพในการทำงานที่ดีกว่ามาก

แต่ ws จะไม่สามารถทำงานแบบ P2P หรือ server remote to client ได้ เพราะสร้างไว้ส่งข้อมูล request เพียงอย่างเดียวกับการทำงานในแบบโมเดลแบบอื่นๆ

ส่วน protocol ที่รองรับนั้นจะมี plain text, soap, xmpp มีเท่านี้ ณ ตอนนี้

การใช้งานตัว ws

สามารถเริ่มจาก script นี้

if(“WebSocket” in window){ //if current browser is supported

   var myWs = new WebScoket(“ws://www.ninemvp.com/xws.svc”);

}

ตัว ws: จะเป็น protocol และตามหลังด้วย //endpoint address
แต่ถ้าทำงานบน SSL ก็กำหนดเป็น wss:

var myWs = new WebScoket(“wss://www.ninemvp.com/xws.svc”);

ตัว ws จะมี event ทั้งหมด 4 แบบคือ

  1. myWs.onopen เมื่อทำการเชื่อมต่อไปยัง ws server สำเร็จ
  2. myWs.onmessage เมื่อทาง server มีการส่ง message กลับมา
  3. myWs.onclose เมื่อทำการ close connection เสร็จแล้ว
  4. myWs.onerror เมื่อเกิด error ขึ้นในการทำงาน

ตัวอย่างการผูก event เพื่อดักการทำงาน

myWs.onopen = function() { alert(“Connection open …”); };

myWs.onmessage = function(msg) { alert( “Received Message: ” + msg.data); };

myWs.onclose = function() { alert(“Connection closed.”); };

myWs.onerror = function(msg) { alert( “Error Message: ” + msg.data); };

การส่งข้อมูลและรับข้อมูลกับทางฝั่ง server

การส่งข้อมูลใช้คำสั่ง

myWs.send(“test send data”);

myWs.close();

ส่วนการรับข้อมูลจะไปเข้า event onmessage ซึ่งเขียนดักการทำงานไว้ตามที่บอกไปข้างต้น

เราสามารถส่ง text ได้ทุกรูปแบบผ่านทาง ws ดังนั้นข้อมูลที่เป็น json ต่างๆก็ไม่ต้องกังวล

*ตัว ws จะใช้งานได้ก็ต่อเมื่อเรามี Web Socket Server มาใช้งานด้วยครับ ซึ่งก็มีหลากหลายให้ใช้งานเข้าไปดูกันได้ที่นี่ 

 

ทิ้งท้าย

ตอนนี้เป็นการแนะนำให้รู้จักว่าตั้งแต่อดีตจนถึงปัจจุุบัน เราสามารถส่งข้อมูลจาก server ไปยัง client ได้วิธีใดบ้าง ซึ่งมีหลายวิธีทั้ง push แท้และเทียมกันไป ในตอนหน้าเรามาเขียน mvc push application กันครับ

 


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

19/09/2012

HTTP?

Filed under: ASP.NET, ASP.NET MVC, W3C, WEB — Nine MVP @ 12:01 am

HTTP คืออะไร

เราเขียนโปรแกรมทำงานกับมันแต่ไม่ได้สนใจรายละเอียดกันเท่าไหร่ อาจะเพราะ browser, script, framework ต่างๆ ช่วยซ่อนความซับซ้อนให้ แต่ยังไงคนทำเว็บก็ควรรู้ไว้เพราะมีประโยชน์ครับ

HTTP ย่อมาจาก Hypertext Transport Protocol เป็นโปรโตคอลสื่อสารที่ทำงานอยู่บนระบบโปรโตคอล TCP HTTP ใช้ในระบบเครือข่าย www (World Wide Web])

ใช้ในการแลกเปลี่ยนข้อมูลกันระหว่าง Server และ Client ที่เรียกใช้โดย  www   เราอ้างถึงทรัพยากร html, image, script, css, file, etc. ที่ใช้ผ่าน url  (Uniform Resoure Locators)  และแลกเปลี่ยนข้อมูลในรูปแบบภาษา HTML (HyperText Markup Language) โดยทำการแลกเปลี่ยนข้อมูลกันบน www

HTTP จะทำงานที่พอร์ต 80 เป็น defaultการส่งข้อมูลของ HTTP จะอยู่ในรูปแบบ Clear text คือ เป็นตัวอักษรต่างๆทุกรูปแบบ ไม่มีการเข้ารหัสใดๆส่วนมาตรฐานของ HTTP ปัจจุบันคือ HTTP 1.1 ซึ่งใช้กันอยู่ในปัจจุบันนี้

การทำงานของ HTTP คือโดยปกติจะเป็นการยิงไปรับกลับ Request-Response

การส่งข้อมูลไปกลับเราเรียกว่าเป็นช่วงของการทำ HTTP Session ระหว่าง client/server

โครงสร้างของ HTTP Request จะประกอบด้วย

  • request-line
  • general-headers
  • request-headers
  • entity-headers
  • empty-line
  • message-body
  • message-trailers

ตัวอย่าง ส่งคำขอไปยัง  http://localhost:12345/home/index 

GET: /home/index HTTP/1.1   ( reqest line

Accept: text/html,application/xhtml+xml; ( request-headers

Host: localhost:53596 ( request-headers

User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64; rv:16.0) Gecko/20100101 Firefox/16.0  ( request-headers

ใน Request Line จะมี HTTP Request Method นำหน้าเพื่อบ่งบอกว่าเป็น verb อะไร ใน HTTP/1.1  มีให้ใช้รวมทั้งหมด 9 ตัว

HEAD, GET,  POST, PUT, DELETE, TRACE, OPTIONS, CONNECT, PATCH

แต่ส่วนใหญ่ใช้เขียนโปรแกรมบนเว็บแค่ GET, POST, PUT, DELETE ก็เพียงพอทำงาน ที่เหลือเอาไว้ใช้งานในระบบเซอวิสบริการอื่นๆ

ส่วน Accept จะบอกว่า ให้ web server ตอบกลับข้อมูลมาใน format ใด

Host ก็บอกว่าส่ง GET ไปที่ไหน

User-Agent บอกรายละเอียดของ client ว่า browser, plaform คืออะไรบ้าง

หลังจากที่ Web Server ประมวลผลแล้วก็จะตอบกลับมาเป็น HTTP Response มีโครงสร้างประมาณนี้

  • status-line
  • general-headers
  • response-headers
  • entity-headers
  • empty-line
  • message-body
  • message-trailers

ตัวอย่าง response ที่ได้จากการส่ง request ในข้างต้น

HTTP/1.1 200 OK ( status-line

Cache-Control: private ( response-headers

Content-Type: text/html; charset=utf-8 ( response-headers

Server: Microsoft-IIS/8.0 ( response-headers

X-AspNetMvc-Version: 4.0 ( response-headers

X-AspNet-Version: 4.0.30319 ( response-headers

X-SourceFiles: =?UTF-8?B?QzpcVXNlcnNcTmluZVxEb2N1bWVudHNcVmlzdWFsIFN0dWRpbyAyMDEyXFByb2plY3RzXE12Y0FwcGxpY2F0aW9uMVxNdmNBcHBsaWNhdGlvbjFcaG9tZVxpbmRleA==?=     ( response-headers

X-Powered-By: ASP.NET ( response-headers

Date: Tue, 18 Sep 2012 15:57:22 GMT ( general-headers

Content-Length: 395 ( response-headers

<!DOCTYPE html> ( message-body –> html document

<html>

<head>

<meta charset=”utf-8″ />

<meta name=”viewport” content=”width=device-width” />

<title>Index</title>

<link href=”/Content/site.css” rel=”stylesheet”/>

<script src=”/Scripts/modernizr-2.6.2.js”></script>

</head>

<body>

<h2>Index</h2>

<script src=”/Scripts/jquery-1.8.1.js”></script>

</body>

</html>

ตรง status-line จะบอกให้เรารู้ว่า web server ทำงานผลลัพธ์เป็นอย่างไรบ้าง ในตัวอย่างก็คือทำงานเป็นปกติ (STATUS 200) ซึ่งก็มีหลาย status number ให้เราเข้าใจว่าผลการทำงานผิดพลาดยังไง

ส่วนของ  response-header ที่เป็นรายละเอียดของ platform ค่อนข้างจะไม่ปลอดภัยเพราะมีแจ้งไว้หมดว่า

  • ใช้ web server อะไร
  • webapp พัฒนาด้วยอะไร
  • runtime version อะไร

หากเรารู้เราก็ควรจะปกปิดข้อมูลเหล่านี้ไว้ครับ ป้องกัน hacker เล่นงานในตอนหลัง

ยังมีข้อมูลอีกมากนะครับ อยากให้ทำความเข้าใจส่วนล่างสุดบ้าง ช่วยให้คุณทำงานได้ราบรื่นขึ้น รู้จริง รู้แจ้ง

ซึ่งช่วยแก้ไขปัญหาได้จริงๆซึ่งคุณไม่ควรมองข้าม เพราะกลุ่ม hacker มองหาช่องอยู่ตลอดเวลา 😉

ในอีกไม่นานนี้คาดว่าเราคงจะได้ใช้ HTTP 2.0 เรียกกันว่า HTTP Speed+Mobility ซึ่งจะถูกพัฒนาขึ้นโดย httpbis ซึ่งมีทั้ง google (SPDY) และ microsoft (HTTP S&M) ต่างช่วยทำ research ตั้งเป้าหมายที่จะทำ async connection multiplex, การบีบอัด header, มีท่อของ request-response, รองรับของ http1.1 ของเดิม  ขอบอกว่า Web socket คือคีย์หลักของงานนี้  เราก็ได้แต่ตั้งตารอคอยให้ถึงวันนั้นต่อไป

หวังว่าพอเป็นความรู้ได้บ้างครับ

Nine (นาย)

Microsoft MVP ASP.NET

16/05/2011

ASP.NET 4.0 : High Scalable ASP.NET Session and Cache with Windows Server AppFabric Caching vol.2

Filed under: ASP.NET, Core System — Tags: , — Nine MVP @ 1:58 am
ตอนที่ 1 (Part 1)
ตอนที่ 2 (Part 2)

 

Programming Level:

  • Intermediate

Computer Skills:

  1. ASP.NET 4.0
  2. C#

Development Tool and Library

  1. OS: Windows 7 or Windows Server 2008 or later
  2. Visual Studio 2010 or later
  3. Windows Server AppFabric Caching (download for x86, for x64)
  4. ASP.NET 4 Providers for Windows Server AppFabric Caching (download)

Agenda

  • Introduce
  • Basic of ASP.NET Session and Cache
  • Sample ASP.NET Web Application with InProc Mode
  • Case Study: Problem of ASP.NET Web Application with InProc Mode
  • Solution: Implement Cache Server
  • Create and Setting AppFabric Cache for ASP.NET Web Application
  • Using AppFabric Cache within ASP.NET Web Application
  • Test Run ASP.NET Web Application and Monitoring Cache
  • Conclusion

Introduce

จากตอนที่แล้วผมได้กล่าวถึง Session State ใน Mode ต่าง ๆ  โดยเราจะมาทำความเข้าใจเกี่ยวกับหน่วยความจำภายในของ ASP.NET กัน และดูตัวอย่างเว็บที่ใช้งาน Session และ Cache ของ ASP.NET ด้วยค่าเริ่มต้นของ web.config

ต่อด้วยทำความรู้จักกับปัญหาของ ASP.NET Web Application ที่ออกแบบไว้โดยสถาปัตยกรรมแบบง่ายๆ ที่มีข้อจำกัดในหลาย ๆ ด้าน รวมไปถึงผลกระทบอื่น ๆ ซึ่งเราจะมาแก้ปัญหาเหล่านี้ด้วยการใช้ AppFabric Caching มาช่วยในการแก้ปัญหา

 

ASP.NET Session and Cache

เรามาทำความรู้จักกับหน่วยความจำที่ของ ASP.NET คร่าว ๆ กันก่อนครับ

Session

System.Web.HttpContext.Current.Session หรือ Session เป็นหน่วยความจำที่สามารถโปรแกรมเมอร์สามารถเก็บข้อมูลของ user แต่ละคนโดยแยกจากกัน ไม่สามารถใช้ร่วมกันได้ ซึ่งเซสชั่นจะยังมีอายุคงอยู่จนกว่าจะ timeout หรือสั่งทำลายโดยโปรแกรม

Session[“SessionName”] = null;

Session.Remove(“session name”);

Session.Abandon();

Cache มี 3 ชนิดครับ

Data Caching เป็น Programmatically สามารถเรียกใช้โดยอ้างถึง HttpContext.Current.Cache หรือ Cache ซึ่งเป็นหน่วยความจำที่เก็บข้อมูลคล้ายกับเซสชั่น แต่แคชสามารถแชร์กันใช้งานกันภายใน Web Application โดยทุกเซสชั่นสามารถเข้าถึงข้อมูลของแคชได้ สามารถจัดเก็บได้โดยโปรแกรมเมอร์เอง และการสร้างแคชจะต้องกำหนดอายุุของแคชเสมอ

var expiredDT = DateTime.Now.AddMinutes(10);
Cache.Insert("AllCustomers",custs, null, expiredDT, TimeSpan.Zero, CacheItemPriority.Default, null);

Output Caching เป็น Automatic Cache ของ Page โปรแกรมเมอร์ไม่สามารถสั่งเก็บค่าหรือเรียกมาใช้งานได้ การเก็บแคชจะกระทำโดย ASP.NET โดยเก็บข้อมูลตามค่า Vary Option ที่ตั้งไว้ และจะทำลายแคชตามอายุที่กำหนดไว้ โดยสามารถกำหนด directive tag ใน aspx page ไว้ประมาณนี้

<%@ OutputCache Duration="30" Location="Any" VaryByParam="none" %>

Fragment Caching เป็น Automatic Cache ของ user control มีลักษณะเหมือนกับ Output Caching แต่จะจัดเก็บแคชของบางส่วนใน Page เช่น user control เป็นต้น การใช้งานก็สามารถเพิ่ม directive tag ในหน้า ascx ดังนี้

<%@ OutputCache Duration="30" Location="Any" VaryByParam="none" %>

ข้อมูลที่จะสามารถเก็บไว้ใน Session และ Cache ได้คือข้อมูลที่สามารถ Serializable ได้เท่านั้น เช่น Primitive data type, POCO, XML เป็นต้น

เราก็ได้รู้จักกับ Session, Cache กันไปแล้ว ต่อไปเราจะไปดูการเรียกใช้งานในเว็บโปรแกรมกันครับ

 

ตัวอย่าง: ASP.NET Web Application with InProc Mode (Default Mode)

Download Code Sample Here

InProc Mode จะเป็นการเก็บข้อมูลเซสชั่นและแคชทั้งหมดเอาที่หน่วยความจำของเครื่อง Web Server โดยตัวอย่างเว็บนี้จะมีหน้า default.aspx เป็นส่วนที่ใช้ทำการทดสอบครับ ซึ่งจะมีการเรียกใช้งานหน่วยความจำของ ASP.NET ทั้ง Session, Cache Data, OutputCache ตามปกติดังนี้

image

image

ในส่วนที่ 1 ไว้สำหรับทดสอบ OutputCache ของ Page ซึ่งผมได้วาง @ OutputCahce เอาไว้ในหน้า default.aspx

<%@ OutputCache Duration="1" Location="Any" VaryByParam="none" %>

ปล. เอา tag ออกเพื่อทดสอบกรณีที่ไม่ใช้ OutputCache หรือกำหนดค่า 1 จะใกล้เคียงกับไม่เก็บแคช

GetDate Button
  1. protected void btnGetDate_Click(object sender, EventArgs e)
  2. {
  3.     lblShowDate.Text =  DateTime.Now.ToString("dd/MM/yyyy hh:mm:ss");
  4. }

ทดสอบ 1 Duration=1 เมื่อกด Get Date Time button เพื่อดึงเวลาจะเห็นว่าเวลาจะเปลี่ยนแปลงตลอดทุกครั้งที่กดปุ่มเนื่องจาก กำหนดค่าไว้ต่ำสุดที่ 1 วินาที (แต่ถ้าไวพอ 1วิ กด2คลิ๊ก ก็ไม่เปลี่ยนจ้า)

ทดสอบ 2 Duration=15 ASP.NET จะเก็บแคชของหน้านั้นเอาไว้ 15 วินาทีโดยไม่ต้องกลับไปทำงานใน CodeBehind เพื่อโหลดข้อมูลกลับมาใหม่ สังเกตุได้ว่าเวลาจะไม่เปลี่ยนไปจนกว่าจะครบ 15 วินาที

ส่วนที่ 2 จะ load data มาจาก database และนำไปเก็บ ไว้ใน Session และ Cache

Load Data Button
  1. protected void btnLoadDataToSession_Click(object sender, EventArgs e)
  2. {
  3.     using( var db = new NorthwindEntities())
  4.     {
  5.         db.ContextOptions.LazyLoadingEnabled = false;
  6.         var custs = db.Customers.ToList();
  7.         var orders = db.Orders.ToList();
  8.        
  9.         // Stored to Session
  10.         // System.Web.HttpContext.Current.Session
  11.         Session["AllCustomers"] = custs;
  12.         Session["AllOrders"] = orders;
  13.  
  14.         // Stored to Cache
  15.         // System.Web.HttpContext.Current.Cache
  16.         var expiredDT = DateTime.Now.AddMinutes(10);
  17.         Cache.Insert("AllCustomers",custs, null,
  18.             expiredDT, TimeSpan.Zero, CacheItemPriority.Default, null);
  19.         Cache.Insert("AllOrders", orders, null,
  20.             expiredDT, TimeSpan.Zero, CacheItemPriority.Default, null);
  21.  
  22.         DropDownList1.Items.Clear();
  23.         DropDownList1.DataSource = Session.Keys;
  24.         DropDownList1.DataBind();
  25.         DropDownList1.Items.Insert(0, "");
  26.         DropDownList1.SelectedIndex = 0;
  27.  
  28.         DropDownList2.Items.Clear();
  29.         DropDownList2.DataSource = Cache.GetEnumerator().ForLinq().Select(pair => pair.Key);
  30.         DropDownList2.DataBind();
  31.         DropDownList2.Items.Insert(0, "");
  32.         DropDownList2.SelectedIndex = 0;
  33.  
  34.  
  35.     }
  36. }

line 3-7 ทำการดึงข้อมูลของ Customer, Order ขึ้นมาทั้งหมด

line 11-12 เก็บข้อมูล Customer, Order ลงใน Session ทั้งสองตัว

line 16-20 เก็บข้อมูล Customer, Order ลงใน Cache และกำหนดอายุของแคชไว้ที่ 10 นาที

line 22-32 ทำการผูกคีย์ของ Session และ Cache กับ Dropdownlist ทั้ง 2 เพื่อเอาไว้ใช้ในการดึงข้อมูล

และเราจะทำการดักจับไอเท็มของ Session และ Cache ไว้เพื่อทดสอบ scope การใช้งาน ว่าแตกต่างกันอย่างไร

Page Load Event
  1. protected void Page_Load(object sender, EventArgs e)
  2. {
  3.     if (!IsPostBack)
  4.     {
  5.         DropDownList1.Items.Clear();
  6.         DropDownList1.DataSource = Session.Keys;
  7.         DropDownList1.DataBind();
  8.         DropDownList1.Items.Insert(0, "");
  9.         DropDownList1.SelectedIndex = 0;
  10.  
  11.         DropDownList2.Items.Clear();
  12.         DropDownList2.DataSource = Cache.GetEnumerator().ForLinq().Select(pair => pair.Key);
  13.         DropDownList2.DataBind();
  14.         DropDownList2.Items.Insert(0, "");
  15.         DropDownList2.SelectedIndex = 0;
  16.     }
  17. }

ส่วนที่ 3 ใน dropdownlist ทั้งสองจะมี Key ที่ใช้ในการดึงข้อมูลออกมาจาก Session และ Cache เพื่อที่จะเลือกและผูกข้อมูลเข้ากับ DataGrid เพื่อแสดงผล เมื่อเลือก item ใน dropdownlist จะ postback กลับไปเพื่อดึงข้อมูลจาก session, cache ที่ต้องการ

Dropdown IndexChanged Code
  1. // Get Data From Session
  2. protected void DropDownList1_SelectedIndexChanged(object sender, EventArgs e)
  3. {
  4.     List<Order> orders = null;
  5.     List<Customer> custs = null;
  6.     if (!string.IsNullOrEmpty(DropDownList1.SelectedValue))
  7.     {
  8.         if (DropDownList1.SelectedValue == "AllCustomers")
  9.         {
  10.             custs = Session[DropDownList1.SelectedValue] as List<Customer>;
  11.             GridView1.DataSource = custs;
  12.             GridView1.DataBind();
  13.         }
  14.         else if (DropDownList1.SelectedValue == "AllOrders")
  15.         {
  16.             orders = Session[DropDownList1.SelectedValue] as List<Order>;
  17.             GridView1.DataSource = orders;
  18.             GridView1.DataBind();
  19.         }
  20.     }
  21. }
  22.  
  23. //Get Data From Cache
  24. protected void DropDownList2_SelectedIndexChanged(object sender, EventArgs e)
  25. {
  26.     List<Order> orders = null;
  27.     List<Customer> custs = null;
  28.     if (!string.IsNullOrEmpty(DropDownList2.SelectedValue))
  29.     {
  30.         if (DropDownList2.SelectedValue == "AllCustomers")
  31.         {
  32.             custs = Cache.Get(DropDownList2.SelectedValue) as List<Customer>;
  33.             GridView1.DataSource = custs;
  34.             GridView1.DataBind();
  35.         }
  36.         else if (DropDownList1.SelectedValue == "AllOrders")
  37.         {
  38.             orders = Cache.Get(DropDownList2.SelectedValue) as List<Order>;
  39.             GridView1.DataSource = orders;
  40.             GridView1.DataBind();
  41.         }
  42.     }
  43. }

line 8-18 เป็นการดึงค่าโดยใช้ key AllCustomers, AllOrders ไปดึงค่าที่เก็บอยู่ใน Session และผูกข้อมูลเข้ากับ Gridview เพื่อแสดงค่า

line 30-40 เป็นการดึงค่าโดยใช้ key AllCustomers, AllOrders ไปดึงค่าที่เก็บอยู่ใน Cache และผูกข้อมูลเข้ากับ Gridview เพื่อแสดงค่า

และสุุดท้ายก็จะเป็น web.config ที่เป็น ASP.NET4 ของเว็บนี้ครับ (โล่งๆ ไม่มีอะไรเลย)

web.config
  1. <?xml version="1.0"?>
  2. <configuration>
  3.   <connectionStrings>
  4.     <add name="NorthwindEntities" connectionString="metadata=res://*/NorthwindDB.csdl|res://*/NorthwindDB.ssdl|
  5.          res://*/NorthwindDB.msl;provider=System.Data.SqlClient;provider connection string=&quot;
  6.          data source=.;initial catalog=Northwind;integrated security=True;multipleactiveresultsets=True;App=EntityFramework&quot;"
  7.          providerName="System.Data.EntityClient" />
  8.   </connectionStrings>
  9.  
  10.   <system.web>
  11.     <compilation debug="true" targetFramework="4.0" />
  12.   </system.web>
  13.  
  14.   <system.webServer>
  15.      <modules runAllManagedModulesForAllRequests="true"/>
  16.   </system.webServer>
  17. </configuration>

 

Case Study : ตัวอย่างปัญหาจากตัวอย่างเว็บด้านบน (Problem of ASP.NET Session and Cache with InProc Mode)

ในกรณีตัวอย่างนี้ ตัว web application วิเคราะห์และทดสอบการจำเป็นในการใช้ Session, Cache, OutputPage  ในส่วนที่เหมาะสมของเว็บแล้ว แต่ว่ายังใช้หน่วยความจำของ Web Server ในการเก็บข้อมูลเหล่านั้นตรง ๆ ก็คือ Session = InProc Mode และ default Cache Provider ที่มีมาให้นั่นเอง

เมื่อเราพัฒนาเว็บแอ๊พพลิเคชั่นผ่านการทดสอบในส่วนของบิสซิเนสเรียบร้อยแล้ว ก็จะทำเทสในเรื่อง Load Test เพื่อหาค่าความสามารถในการรองรับ concurrent user ที่จะเข้ามาใช้งานระบบ ซึ่งในตัวอย่างได้สมมติเอาว่าผ่านการทำ load test มาแล้ว ซึ่งทั้งระบบ Web Server และ Database นั้น รองรับจำนวน user อยู่ที่ 200 concurrent โดยใช้งานปกติจะมีจำนวน concurrent ประมาณ 80-150 คน

image

เมื่อถึงเวลาหนึ่งทางกิจการได้เติบโตขึ้น ทำให้จำนวนผู้เข้าใช้งานเว็บมีจำนวนมากขึ้นเป็นลำดับ ซึ่งอาจจะเป็นช่วงเวลาเช่นช่วงเที่ยง เย็น และวันหยุด พบว่าหลายครั้งที่ Web Server Down เพราะต้องบริการข้อมูลยูสเซอร์ที่เข้าใช้งานพร้อมกันจำนวน 300 user concurrents ซึ่งเกินจากที่ระบบจะรองรับได้ ดังภาพด้านล่าง

image

ผลคือระบบล่ม ทุกอย่างจะเริ่มต้นได้ใหม่ก็จนกว่าจะ restart Web Site/Database กันใหม่ แบบนี้ทำให้ธุรกิจเสียหายแน่ ๆ ครับ

ปัญหาและสาเหตุมีดังนี้

Client : เนื่อง user แต่ละคนจะต้องสร้าง session ขึ้นเป็นของตัวเอง และการกดเพื่อสร้าง request กลับไปที่ web server ทุกครั้ง และจำเป็นจะต้องเข้าเว็บเซอร์เวอร์เดิมทุกรีเควส หากเว็บเซอร์เวอร์ตัวนั้นล่มไป เซสชั่นทุกอย่างก็จะหายไปหมดเช่นกัน

Web Server : มีการเก็บ Session, Cache แยกจากกันในแต่ละเว็บเซอร์เวอร์ ซึ่งไม่สามารถที่จะแชร์ข้ามเครื่องกันได้ ทำให้ข้อมูลบางตัวที่ควรจะแชร์กันได้ต้องถูกสร้างขึ้นใหม่ทุกครั้งที่มี user เข้ามาใช้งาน

Database : มีการใช้งาน Disk อย่างหนัก เพราะทุก request ที่ส่งเข้ามาเกือบทั้งหมดจะต้องเข้าถึงข้อมูลตลอดเวลา

ก่อนที่เราจะเลือกทางแก้ปัญหาโดยการลงทุนเพิ่ม Web Server, Upgrade Database Server เหล่านั้น หากพิจารณากันให้ดี  ปัญหาที่สำคัญก็คือ

1. เกิดข้อจำกัดของ Web Server ในส่วนของ Memory สำหรับเก็บข้อมูลพวก Session, Cache โดยแยกไปตามขนาดของ RAM ที่ติดตั้งในแต่ไว้ใน Server

2. เกิดข้อจำกัดของ User Process ในการทำงาน โดยจะทำงานได้เพียงบน Web Server ตัวเดียวเท่านั้น

3. มีการเข้าถึง Database ตลอดเวลาเพื่อดึงข้อมูลที่ซ้ำๆกันทุกครั้ง ที่ผู้ใช้ร้องขอบริการเดิม ๆ

4. Web Server ใน Farm ทุกตัวควรจะเห็น Session ของ User คนเดิม เมื่อเกิดปัญหากรณี Web Server ตัวใด ๆ ที่ User ใช้งานอยู่เกิดล่ม ก็สามารถที่จะยังทำงานต่อด้วย web server ตัวอื่นได้ทัทีโดยข้อมูลปัจจุบันยังคงอยู่

ซึ่งปัญหาเหล่านี้แก้ได้ครับ

 

Solution : Implement Cache Server (AppFabric Caching)

จากตอนที่ 1 นั้นผมได้แนะนำตัว AppFabric Cache Service ไปแล้ว ซึ่งเราจะนำเข้ามาใช้แก้ไขปัญหาที่เกิดขึ้นข้างต้น โดยก่อนที่จะเข้าถึง Database เราก็วาง Cache Server เอาไว้ให้เป็นตัวบริการเก็บข้อมูลทั้ง Session, Cache ตามรูปดังนี้

image

จากโมเดลด้านบนนี้ เราใช้ Cache Server มาวางโดยเอาไว้แก้ไขปัญหาของ Web Server ดังนี้

1. เพิ่มขนาดของ Cache บน Web Server  โดยขนาดจะขึ้นอยู่กับหน่วยความจำของ Cache Server

2. เอาไว้เก็บ User Session แทนการเก็บบน Web Server ป้องกันปัญหากรณี web server ที่ user ใช้งานอยู่ขณะนั้นเกิดปัญหา ทำให้ Web Server อีกตัวสามารถทำงานแทน web server ตัวที่ล่มไปได้ทันที

3. เป็นข้อมูลที่ถูกเรีกยใช้บ่อย และข้อมูลนั้น อาจจะเป็น Static Data หรือมีการเปลี่ยนแปลงของข้อมูลน้อย  ลดภาระของ Database ในการดึงข้อมูลส่งให้ Web Server บ่อย ๆ

ต่อไปเข้าสู่ขั้นตอนการสร้างและนำ AppFabric Cache ไปใช้ใน ASP.NET กันครับ

 

Create and Setting AppFabric Cache for ASP.NET Web Application

ต่อไปเราจะทำการสร้างแคชดาต้าเบส และตั้งค่าเอาไว้ใช้งานกับ asp.net web application ของเราครับ

โดยจะสร้างแคชเอาไว้ 2 ตัวโดยให้ชือ่ว่า ASPNETSessionCache เอาไว้เก็บข้อมูลที่เป็นเซสชั่นและ ASPNETOutputCache เอาไว้เก็บข้อมูลแคช ทำตามขึ้นตอนดังนี้

1. เรียกหน้าต่าง Caching Administration Windows Powershell ออกมาใช้งาน

2. พิมพ์

Use-CacheCluster

3. สั่งสตาร์ท service ด้วยคำสั่ง

Start-CacheCluster

4. เพิ่ม user สำหรับเข้าใช้งานแคช ให้ Add Current user สำหรับ Debug

เลือก 1 ชุดคำสั่งตาม windows ที่ใช้ครับ

– กรณีใช้ service account  (all windows edition)

Grant-CacheAllowedClientAccount –Account “ComputerName\UserName”

– กรณีใช้ Network Server Account (win03, win03r2, win08, vista)

grant-cacheallowedclientaccount -Account "NETWORK SERVICE"

– ใช้ ASPNET User account ใน Windows 7/WS08R2:

grant-cacheallowedclientaccount -Account "IIS AppPool\ASP.NET v4.0"

5. สร้างแคชสำหรับ asp.net ไว้เก็บเซสชั่นชื่อ ASPNETSessionCache และ ASPNETOutputCache

New-Cache –CacheName ASPNETSessionCache –Eviction None –TimeToLive 60

New-Cache –CacheName ASPNETOutputCache –Eviction None –TimeToLive 60

6. เรียกดู Cache DB ที่สร้างไว้ทั้งสองก้อนว่าสำเร็จหรือไม่ด้วยคำสั่ง

Get-Cache

image

7. ตรวจสอบดูว่าแคชทั้ง 2 ว่าตั้งค่าไว้อย่างไรบ้าง

Get-CacheConfig ASPNETSessionCache

Get-CacheConfig ASPNETOutputCache

image

7. ตรวจสอบค่า Staticstics ว่ามีสถานะปัจจุบันอย่างไรบ้าง

Get-CacheStatistics ASPNETSessionCache

Get-CacheStatistics ASPNETOutputCache

image

แค่นี้ AppFabric Cache ก็พร้อมใช้งานแล้วครับ ต่อไปเราจะไปดูการตั้งค่าและเรียกใช้งานภายใน asp.net กัน

 

Using AppFabric Cache within ASP.NET Web Application

Download Code Sample Here

ในขั้นตอนนี้เราจะย้าย Session และ Cache จากหน่วยความจำภายในเครื่องไปเก็บไว้ใน AppFabric Cache

การเรียกใช้ AppFabric Cache ภายใน Web Application นั้น ในตอนนี้ผมขอเลือกใช้ Library ตัวนึงซึ่งมีความสามารถเยอะกว่า Cache Client ที่ไมโครซอฟต์มีมาให้ตั้งแต่ติดตั้ง AppFabric

การนำไปใช้งานนั้นมี 2 วิธีคือ ทำผ่าน web.config และ เขียนโค้ด

ในขั้นตอนแรก  ดาวน์โหลด ASP.NET 4 Providers for AppFabric Caching จาก CodePlex มาใช้งาน ให้ unzip ออกและมองหา “Microsoft.Web.DistributedCache.dll” โดยตัวนี้จะเป็น Custom Provider ที่เรียกใช้ Microsoft.ApplicationServer.Caching.Client.dll ของ AppFabric อีกทีนึง

Setting Method 1: Edit web.config

เปิด Web Application ขึ้นมาแก้ไข web.config ดังนี้

– เพิ่มค่า configSection สำหรับ Cache Client ของ Microsoft เพื่อให้รู้จัก dataCachcClient Section

web.config: configSections
  1. <section name="dataCacheClient"
  2.     type="Microsoft.ApplicationServer.Caching.DataCacheClientSection,
  3.         Microsoft.ApplicationServer.Caching.Core, Version=1.0.0.0,
  4.         Culture=neutral, PublicKeyToken=31bf3856ad364e35"
  5.     allowLocation="true"
  6.     allowDefinition="Everywhere"/>

– เพิ่ม dataCacheClient Section เพื่อกำหนดที่อยู่ของ AppFabric Host และ Port

web.config: dataCacheClient
  1. <dataCacheClient>
  2.   <hosts>
  3.     <host name="Nine-NB" cachePort="22233" />
  4.   </hosts>
  5. </dataCacheClient>

– ตั้งค่าการใช้ Cache ของ ASP.NET ไปที่ AppFabric โดยกำหนดเพิ่มเติมใน <system.web>

web.config: <sessionState>
  1. <sessionState mode="Custom" customProvider="DistributedSessionAspSessionProvider">
  2.   <providers>
  3.     <!– specify the named cache for session data –>
  4.     <!–<add name="AppFabricCacheSessionStoreProvider"
  5.       type="Microsoft.ApplicationServer.Caching.DataCacheSessionStoreProvider"
  6.       cacheName="ASPNETSession"
  7.       sharedId="ASPNETSession"/>–>
  8.     <add name="DistributedSessionAspSessionProvider"
  9.          type="Microsoft.Web.DistributedCache.DistributedCacheSessionStateStoreProvider,
  10.          Microsoft.Web.DistributedCache"
  11.         cacheName="ASPNETSessionCache"
  12.           applicationName="ASPNETWeb"
  13.           useBlobMode="false" />
  14.   </providers>
  15. </sessionState>

line 8  ตั้งค่าชื่อของ provider ที่เราต้องการจะเพิ่มเติม

line 9 กำหนด type ของ dll ที่เรานำมาใช้

line11-13  เป็นการะบุ Cache โดยอ้างถึง ASPNETSession ที่สร้างไว้ใน AppFabric Cache, กำหนด ApplicationName ที่จะเข้าใช้แคชก้อนดังกล่าว และระบุไม่ใช่ Blob Mode

line – 1 กำหนด mode=”Custom” และเลือกใช้ customProivder=”DistributedSessionAspSessionProvider” ตามที่ได้ตั้งค่าไว้

– ตั้งค่าการใช้ Page Output Cache ของ ASP.NET ไปที่ AppFabric โดยกำหนดเพิ่มเติมใน <system.web>

web.config: <caching>
  1. <caching>
  2.   <outputCache defaultProvider="DistributedCache">
  3.     <providers>
  4.       <add name="DistributedCache"
  5.            type="Microsoft.Web.DistributedCache.DistributedCacheOutputCacheProvider,
  6.            Microsoft.Web.DistributedCache" cacheName="ASPNETOutputCache" applicationName ="ASPNETWeb" />
  7.     </providers>
  8.   </outputCache>
  9. </caching>

line 4 กำหนดชื่อของ Cache ที่เราใช้อ้างอิง

line 5 ใส่ type ของ dll ที่จะใช้ในการทำ page output cache

line 6 cacheName กำหนดให้ใช้แคชที่ชื่อ ASPNEToutputCache และกำหนดชื่อ applicationName เป็น ASPNETWeb

line 1 ที่ defaultProvider กำหนดให้ใช้ DistributedCache name ที่เราได้สร้างไว้

Setting Method 2: Cache with Programmatically

เพิ่มโค้ดใน Global.asax.cs ดังนี้

Code Snippet
  1. void Application_Start(object sender, EventArgs e)
  2. {
  3.     DistributedCacheSessionStateStoreProvider.CacheConnecting += DistributedCacheSessionStateStoreProvider_CacheConnecting;
  4.     DistributedCacheOutputCacheProvider.CacheConnecting += DistributedCacheOutputCacheProvider_CacheConnecting;
  5. }
  6.  
  7. void DistributedCacheOutputCacheProvider_CacheConnecting(object sender, CacheConnectingEventArgs e)
  8. {
  9.     e.CacheFactoryConfiguration = new DataCacheFactoryConfiguration()
  10.             {
  11.                 Servers = new DataCacheServerEndpoint[] {new DataCacheServerEndpoint("Nine-NB", 22233)}
  12.             };
  13.     e.CacheName = "ASPNETOutputCache";
  14. }
  15.  
  16. void DistributedCacheSessionStateStoreProvider_CacheConnecting(object sender, CacheConnectingEventArgs e)
  17. {
  18.     e.CacheFactoryConfiguration = new DataCacheFactoryConfiguration()
  19.         {
  20.             Servers = new DataCacheServerEndpoint[] { new DataCacheServerEndpoint("Nine-NB", 22233) }
  21.         };
  22.     e.CacheName = "ASPNETSessionCache";
  23. }

line 3-4 ทำการผูก event ให้ CacheSession และ OutputCache Provider เมื่อมีการร้องขอให้ทำการ connect ไปยัง Cache Server ก็ให้ไปทำ event Connecting ทั้งสอง

line 7-14 เป็น event ของ Output Cache โดยทำการ add host, port number และกำหนดชื่อของ Cache Name ที่จะใช้

line 16-23 เป็น event ของ Cache Session โดยทำการ add host, port number และกำหนดชื่อของ Cache Name ที่จะใช้

 

Test Run ASP.NET Web Application and Monitoring Cache

เริ่มแรกให้ทำการตรวจสอบว่า Cache ของ ASPNETSessionCache และ ASPNETOutputCache นั้นยังมีสถานะก่อนใช้งานเป็นดังนี้

image

ยังไม่มีการเก็บข้อมูลใด ๆ ลงใน Cache ทั้งสองก้อน    จากนั้นเราจะทำการ Run Web Site ด้วย Visual Studio 2010 ด้วยการกด F5 ได้ผลขึ้นดังนี้

image

เนื่องจากมี page output cache เกิดขึ้น ดงันั้นเราจะไปตรวจสอบที่ ASPNETOutputCache จะพบได้ว่ามีการเก็บ object ลงไปใน Cache เพิ่มดังนี้

image

ที่เห็นนี้จะเป็น Automatic Cache และ Session สำหรับ System ในการทำงานของ ASP.NET Web Application ซึ่งจะเกิดขึ้นทุกครั้งที่ User เข้ามาทำงานในหน้า Default.aspx

ต่อไปเราจะทำการ เก็บค่าลง Session และ Cache ด้วยการกด button ในหัวข้อที่ 2 Load Data

เมื่อ response กลับมา ลองไปคลิ๊กที่ downdown ทั้งสองจะพบว่ามี key ของทั้ง Session และ Cache ที่เราเก็บค่าไว้ใน AppFabric Caching นั่นเอง

ทีนี้เรากลับไปดูความเปลี่ยนแปลงที่เกิดขึ้นใน Cache ทั้งสองตัวกันครับ

image

ก็จะพบว่ามีจำนวนของ Item ที่เก็บเพิ่มขึ้นอันเนื่องมาจากการเก็บข้อมูลลงไปนั่นเอง

 

Conclusion

หวังว่าพอจะมองเห็นประโยชน์จากดีไซน์นี้ หากต้องการที่จะ scale ระบบ web server ให้ทำงานได้สมบูรณ์และมีประสิทธิภาพสูงสุดนั้น บทความนี้ก็เป็นอีกหนึ่งแนวทางในการนำไปสู่ผลสำเร็จนั้นครับ

ในตอนต่อไป ผมจะพูดถึงการนำ AppFabric Caching ไปใช้ใน MVC, EF, NHibernate เท่าที่จะพอนึกออกครับ และหากมีโอกาสผมจะทำการทดสอบในส่วนของการทำ Cache Cluster, Fault Torrelance Testing, Performance Tesing ต่อไปด้วยครับ


Download Code Sample Here

Chalermpon Areepong Nine (นาย)

Microsoft MVP Thailand ASP.NET

email : nine_biz-talk.net at hotmail dot com

20/04/2011

Add a Reference to the Microsoft.ApplicationServer.Caching.Client Assembly

Filed under: ASP.NET, Core System — Nine MVP @ 1:02 am

ขอ copy มาแปะไว้ ref กันลืมแหะๆ 

When writing code to leverage the Windows Server AppFabric caching capabilities you will need to set a reference to the caching client classes, which are located in Microsoft.ApplicationServer.Caching.Client.dll.  Within Visual Studio 2010 this process is a bit different than how you normally approach adding a .NET Framework library reference. Usually you click the project and select to add a reference.  However, due to the AppFabric installation process, the procedure for adding the caching client reference differs.

To add a reference to Microsoft.ApplicationServer.Caching.Client

  1. Right-click your project and select Add Reference.
  2. Click the Browse tab.
  3. Enter the following in the file name and then click Enter.
    • 64bit Windows: %windir%\Sysnative\AppFabric
    • 32bit Windows: %windir%\System32\AppFabric
  4. Locate and select both Microsoft.ApplicationServer.Caching.Client and Microsoft.ApplicationServer.Caching.Core
For more information see the official documentation in Preparing the Development Environment for Cache Client Development
Older Posts »

Create a free website or blog at WordPress.com.