能依瀏覽器支援能力自動尋找最適合的通訊方式,是SignalR最迷人之處。SignalR 2.0共支援Forever Frame、Long Polling、Server Sent Event、WebSocket四種通訊方式,在Introduction to SignalR的Transports and fallbacks一節有詳細說明,但對茶包射手來說,沒有追究到每一個動作所對應的封包,就不算徹底解開謎團,午夜夢迴之際總要平添幾許遺憾... (謎之聲: 有那麼嚴重嗎?)
於是,為賦新詞強說愁打破砂鍋追根究底的CSI等級鑑識展開了。
第一步是先建立應用SignalR傳輸的測試網頁,寫了一個簡單的MarathonHub,宣告Runner型別當成資料物件,Server端提供OneShotTest()方法,執行時則呼叫Client端的addRunner(),將Runner資料送至前端。
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using Newtonsoft.Json;
using Owin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
namespace Darkthread.SignalR.Test
{
publicstaticclass Startup
{
publicstaticvoid ConfigureSignalR(IAppBuilder app)
{
app.MapSignalR();
}
}
[HubName("marathron")]
publicclass MarathronHub : Hub
{
//模擬資料物件
publicclass Runner {
publicstring Id { get; set; }
publicstring Name { get; set; }
public DateTime Birthday { get; set; }
publicint PersonalBest { get; set; }
[JsonIgnore]
public AutoResetEvent RoundTripSync = new AutoResetEvent(false);
}
static Dictionary<string, Runner> dataStore = null;
public MarathronHub()
{
//產生10000筆模擬資料
if (dataStore == null)
{
dataStore = new Dictionary<string,Runner>();
Random rnd = new Random();
int COUNT = 100;
for (var i = 0; i < COUNT; i++)
{
string id = Guid.NewGuid().ToString();
dataStore.Add(id, new Runner()
{
Id = id,
Name = i == COUNT - 1 ? "Last" : "Runner" + i,
Birthday = DateTime.Today
.AddDays(-6000 - rnd.Next(6000)).ToUniversalTime(),
PersonalBest = 7600 + rnd.Next(7600)
});
}
}
}
publicvoid OneShotTest()
{
Clients.Caller.addRunner(dataStore.Values.First());
}
}
}
網頁部分也很平常,addRunner()在接收到Runner資料後放入陣列,並在網頁顯示其JSON內容,Test鈕則負責觸發Server端的OneShotTest()。特別之處是有一段程式碼由URL的?trans=xxx取得foreverFrame、longPolling、serverSentEvents或webSockets四個字串,透過$.connection.hub.start({ transport: ... })強制指定傳輸方式,防止SignalR自動決定傳輸方式,以便觀察不同傳輸模式的行為。
<!DOCTYPEhtml>
<htmlxmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
<scriptsrc="../Scripts/jquery-1.10.2.js"></script>
<script src="../Scripts/jquery.signalR-2.0.0.js"></script>
<script src="../signalr/hubs"></script>
<script>
$(function () {
var marathron = $.connection.marathron;
runners = [];
marathron.client.addRunner = function (runner) {
$("#dvDisp").text(JSON.stringify(runner));
runners.push(runner);
};
var re = /[?]trans=(.+)/;Choose image...
var m = re.exec(location.href);
var transType = m && m.length > 1 ? m[1] : null;
$("#spnTransType").text(transType);
// Start the connection
$.connection.hub.start(transType ? { transport: transType } : undefined)
.done(function () {
$(":button").click(function () {
runners = [];
marathron.server.oneShotTest();
});
});
});
</script>
</head>
<body>
<h3>Transport Type [ <spanid="spnTransType"></span> ]</h3>
<div>
<inputtype="button"value="Test"/>
</div>
<divid="dvDisp"style="width: 500px; font-size: 9pt;">
</div>
</body>
</html>
為讓觀察結果單純化,測試過程只有兩個步驟: 載入網頁、按下測試鈕,看到JSON結果就結束。觀察工具主要採用Fiddler,至於Fiddler無法涵蓋之處(如: Server Sent Event及WebSocket的傳輸內容)再動用小型核武—Microsoft Network Monitor。
【Forever Frame】
好了,第一個測試是Forever Frame,這是IE獨有選項:
如上圖所示,SignalR偷偷在網頁嵌入一個IFrame,連向SignalR提供的內容,巧妙之處在於其中一段一段的<script>並非一次載入,而是分次送至前端後馬上執行,藉此實現持續傳送內容並控制前端動作的效果。而透過Fiddler可觀察到按鈕呼叫Server端OneShotTest()時,網頁會送出一個/signalr/send Request(如下圖所示),POST Form內容註明Hub為marathron、Method為OneShotTest,無傳入參數,SignalR收到後便會觸發MarathronHub的OneShotTest()方法。
【Long Polling】
接著來看Long Polling:
Long Polling的特色在於網頁會以XHR送出一個/signalr/poll Request(上圖第8個Request,藍底),但Server端先不送回結果讓Client痴等,直到有資料要傳至Client;按下測試鈕時Client送出第9個Request(/signalr/send),第8個Request立即得到Server端回應並結束(即上圖最下方的JSON,包含了參數A為Runner物件,H指定Hub為"marathron",M註明Method為addRunner),接著Client會馬上再送出一個/signalr/poll Request(上圖第10個Request)。以上觀察實證了Long Polling的運作原理,送出一個Request,Server端先Hold住,直到有東西要送至Client端時才傳回結果並結束,一旦Request結束,Client端要馬上另起一個相同Request維持連繫。
【Server Sent Event】
Server Sent Event也是HTML5新增的機制,透過特殊的Header供瀏覽器識別,讓Request持續保持開啟狀態,Server端便可持續透過這個Request不斷送資料到前端。IE不支援Server Sent Event,我們改用Chrome來測試。
Server Sent Event的表現跟Long Polling有點像,如上圖所示,第7個Request(/signalr/connect?transport=serverSentEvents)的Result為"-"、Body為-1,代表Request一直沒有結束。但差異在於Long Polling的Request在Server送回結果後會結束再另建新Request;但在上圖我們連發8、9兩個/signalr/send,Server Sent Event的7號Request仍持續開啟。由於Fiddler無法觀察未結束的Request內容,無法觀察Server送回資料,此時招喚Microsoft Network Monitor上場~
在上圖的第37 Frame,Client端(192.168.1.105)送出GET /SignalR/signalr/connect?transport=serverSentEventS;Frame 38 Server回應OK,Server Sent Event管道建立完成,之後的Frame可看到Server持續透過此管道送封包給Client。
在Frame 61,我們找到Server將Runner JSON內容送至Client端的證據。
【WebSocket】
最後輪到本檔壓軸 -- WebSocket上場。
WebSocket最大的特色在於支援雙向傳輸,因此你不會看到先前一再出現的(/signalr/send) Request。要要怎麼檢視傳輸內容? 又得靠Network Monitor囉!
在上圖中,Frame 140送出GET /signalr/connect?transport=webSockets,Frame 141 Server給了一個特別的回應,Status = HTTP 101 Switching protocols,之後這條連線就換變成雙向的WebSocket傳輸。
Frame 144為按下測試鈕時Client送到Server端的封包(內容似經編碼,無法直接解讀)。Frame 144 Client送出資料後,Server有了一連串回應(幾乎同時,Time of Frame均為1.7886144),於Frame 149,抓到了,Server透過WebSocket傳回Runner JSON的證據。
【結論】
實驗完畢,驗證SignalR能透過四種不同傳輸管道完成相同動作,並且也觀察到Forever Frame、Long Polling、Server Sent Event及WebSocket的運作細節,對SignalR有更進一步的認識後,開發應用時心裡就更踏實囉~