能依瀏覽器支援能力自動尋找最適合的通訊方式,是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
{
    public static class Startup
    {
        public static void ConfigureSignalR(IAppBuilder app)
        {
            app.MapSignalR();
        }
    }
 
    [HubName("marathron")]
    public class MarathronHub : Hub
    {
        //模擬資料物件
        public class Runner {
            public string Id { get; set; }
            public string Name { get; set; }
            public DateTime Birthday { get; set; }
            public int 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)
                    });
                }
            }
 
        }
 
        public void 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自動決定傳輸方式,以便觀察不同傳輸模式的行為。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <script src="../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 [ <span id="spnTransType"></span> ]</h3>
    <div>
        <input type="button" value="Test" />
    </div>
    <div id="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有更進一步的認識後,開發應用時心裡就更踏實囉~


Comments

# by 路人甲

請問Signalr,可以傳遞image嗎??

# by Jeffrey

to 路人甲,SignalR與前端溝通需透過JavaScript函式,故圖檔要變成函式參數才能傳遞,有兩種做法:1)Server將圖檔內容Cache起來,傳遞URL到前端,前端再修改<img> src屬性連到Server取出Cache中的圖檔 2)Server將byte[]轉成Base64編碼字串當成參數傳至前端,前端使用DataURI方式直接顯示(參考:http://blog.darkthread.net/post-2010-11-05-data-uri.aspx)。若不用管IE678死活,我會選擇後者。

# by 路人甲

您好,其實我需求是client端有可能是wpf,web不一定,但他們之間可以透過signal r互傳圖片給其他方看到,本身是有做出wpf to wpf的,就單純建tcp socket再互傳Stream這種做法,因為看到Signal r這個技術,在思考Signal r傳圖的可能性,所以才詢問您,感謝您提供的Solution,我會先自行再深入研究消化一下,謝謝您的回答。

# by james

SignalR on the Wire – an informal description of the SignalR protocol http://blog.3d-logic.com/2015/03/29/signalr-on-the-wire-an-informal-description-of-the-signalr-protocol/ 謝謝你提供這樣的詳細的資料,上面的網頁有寫signal r的參數說明,希望透過這個網站提供給其他人知道,謝謝。

# by Jeffrey

to james,非常詳細的資料,簡直是SignalR協定大解密,謝謝分享。

# by 路人乙

黑暗大: 請問一下,上方路人甲的需求,有幾點想請教您的意見 1.Server將byte[]轉成Base64編碼字串再傳,當連續傳2張圖片,且檔案較大時,我的理解是其實他到底層socket,還是把它轉成byte[]一個一個送出去,所以當連續傳圖,Client端接收就可能導致不知讀到那裏才算一個段落轉成圖片,又或者是當網路狀況忽然不好時,Client端也無法知道何時才算接收完一張圖片,這部分是否有比較好的作法可解決??? 2.我看您http://blog.darkthread.net/post-2010-11-05-data-uri.aspx 這篇所寫的第二點,基於TCP傳輸特性,傳送一個大檔案會比連續傳送多個拆解的小檔更快速有效率,我以為不管是大或小,到最後底層都是用byte[]一個一個送出去,為什麼傳大檔會較有效率??

# by Jeffrey

to 路人乙, 答1:HTTP的傳輸協定在傳送資料時,會先送Header,再送資料本身,Header包含要傳送資料的長度,故接收端可確認資料是否收齊,資料必須完整接收才會抛給程式做後續處理,不至於錯亂。 答2:而每次傳送一個檔案都要附一個Header,當一個大檔拆成十個小檔,就要多傳九個Header,可以想像成「有些HTTP Request處理邏輯是以次計費的」,如果目的地相同,同樣重量的東西寄一大箱比拆成十小箱送宅急便來得省。

# by 路人丙

請問如果要透過SignalR將前端上傳的圖片轉成base64再傳到後端是否會有參數過長的問題。因為實作之後只要傳遞base64,websocket就會自動斷開

# by Jeffrey

to 路人丙,查到用 WebSocket 傳送資料長度受限的說法:We don't allow large messages to be passed from Client to server when using websockets due to the way thing share buffered in memory. https://github.com/SignalR/SignalR/issues/2774#issuecomment-32461440 你參考看看。

# by Akira

請問一下SignalR 1.2.2 Client for Windows Form,如何在Connection上加入Cross Domain的參數? persistentConnection = new Connection("http://localhost:6685/realtime/echo");

Post a comment