[SignalR][ASP.NET] 使用 SignalR 來達成電腦網頁與手機網頁即時互動

Standard

之前寫過一篇 [Node.js] 使用 Node.js 來達成電腦網頁與手機網頁即時互動,當時是用 Node.js,最近看到了 .NET 也提供了相似的解決方案 ── SignalR。

官方的介紹是:Async signaling library for .NET to help build real-time, multi-user interactive web applications.
SignalR 一樣可以做即時跟多使用者的互動網路應用程式。
在製作案件時,有時候是無法在客戶的主機上動手腳的,為了不要被技術跟環境侷限,多會一種解決方案也不賴呀。
而且寫的時候覺得 SignalR 寫起來更簡單,很多事都在背後偷偷幫你做完了(.NET 似乎都這樣封裝得好好的,但還是要搞懂其中原理比較好呀)。

廢話不多說,來看一下怎麼實作此次的需求囉。

此次開發使用:

  • Visual Studio 2010 Professional
  • ASP.NET 4.0 (C#)

如何透過 NuGet 安裝 SignalR

噢,之前好像沒寫過 NuGet,那就先簡單介紹一下如何透過 NuGet 安裝最新的 SignalR 吧。
1. 開啟 VS2010,建立新專案,選擇 Visual C# 的 ASP.NET 空白 Web 應用程式。

  1. 建立好之後,在「參考」上面點右鍵,選擇「管理 NuGet 套件…」,如圖。(我的 NuGet 已升級至 2.x 版本)
  1. 接著搜尋線上的套件:SignalR,記得把左上角的「僅限穩定」改為選擇「包括發行前版本」。
    選擇安裝「Microsoft ASP.NET SignalR」,上面那幾個是相依套件,也會跟著一起安裝。
  2. 安裝完畢後,「參考」裡面會自動加入需要的 SignalR 參考,然後專案也會自動產生「Scripts」目錄,裡面自動加入 jQuery 以及 jQuery SignalR 的 .js 檔。

實戰

這次我們要做的也是讓手機掃描網頁上的 QRCode,進而用手機控制網頁的動作。
本次實作選擇使用 Hub 模式(另一種是 PersistentConnection)。

  1. 首先,新建一個新的類別 Core.cs,定義伺服器端的動作。
  • Client 類別定義了 Client 端的屬性,有 UniqueKey(唯一 Key,用來讓電腦跟手機產生關連。在網頁端可以用 js 或透過 Flash ActionScript 生成。)跟 ConnectionId(連線後,SignalR 會自動分配給 Client 端的 GUID)
  • Core 類別實作 Hub。
    • RegisterClient:每次產生連線後,就會呼叫 RegisterClient 方法,也將 Client 端產生的 key 值註冊進去。
      註冊到一個 Client 的集合,供接下來的動作做比對。
      註冊完之後,就呼叫 Client 端的 registerComplete 事件。
    • MobileOpenWebpage:主要動作之一。會在手機掃描完 QRCode 後打開手機版網頁,連線成功後呼叫這個事件,檢查相應的電腦端 Client,呼叫它的 playMovie 事件。
    • ChangeBackground:也是主要動作之一。本例手機網頁上有個 Button,點擊後會呼叫這個事件,這個事件檢查相應的電腦端後,呼叫它的 changeBackground 事件。
    • RetrieveClient:上面兩個方法都有用到的方法。用來檢查相應的手機端跟電腦端,取得各自的 ConnectionId,供上面的動作使用。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Microsoft.AspNet.SignalR.Hubs;

namespace SignalRQRCode
{
    public class Client
    {
        // 唯一 Key, 用來讓電腦跟手機產生關連
        public string UniqueKey { get; set; }

        // 連線 guid
        public string ConnectionId { get; set; }
    }

    public class Core : Hub
    {
        // Client 集合
        public static List<Client> _clients = new List<Client>();
        private object _syncRoot = new object();

        public string MobileConnectionId { get; set; }
        public string ComputerConnectionId { get; set; }

        /// <summary>
        /// 註冊目前使用者
        /// </summary>
        /// <param name="key"></param>
        public void RegisterClient(string key)
        {
            lock (_syncRoot)
            {
                // 在集合中找出目前使用者
                var client = _clients
                    .FirstOrDefault(x => x.ConnectionId == Context.ConnectionId);

                // 如果找不到,就新增到集合中(配上 javascript 生成的 key 值)
                if (client == null)
                {
                    client = new Client { ConnectionId = Context.ConnectionId, UniqueKey = key };
                    _clients.Add(client);
                }
            }

            Clients.Client(Context.ConnectionId).registerComplete();

        }

        /// <summary>
        /// 取得 Client 端
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        private bool RetrieveClient(string key)
        {
            // 找出手機端 (目前)
            var mobile = _clients.FirstOrDefault(x => x.ConnectionId == Context.ConnectionId);

            // 若找不到目前手機端,應該是出了什麼錯 ... 不要繼續往下做
            if (mobile == null) return false;

            // 電腦端使用者
            var computer = _clients
                .FirstOrDefault(x => x.ConnectionId != Context.ConnectionId &&
                    x.UniqueKey == key);

            // 找不到另一方裝置
            if (computer == null)
            {
                Clients.Client(Context.ConnectionId).noOpponent();
                return false;
            }

            // 紀錄手機端的 ConnectionId
            MobileConnectionId = mobile.ConnectionId;
            // 紀錄電腦端的 ConnectionId
            ComputerConnectionId = computer.ConnectionId;

            return true;
        }

        /// <summary>
        /// 手機打開網頁事件
        /// </summary>
        /// <param name="key"></param>
        public void MobileOpenWebpage(string key)
        {
            if (!RetrieveClient(key))
                return;

            if (MobileConnectionId == "" || ComputerConnectionId == "")
                return;

            // 叫電腦端做事
            Clients.Client(ComputerConnectionId).playMovie();
        }

        /// <summary>
        /// 變換電腦端背景顏色
        /// </summary>
        /// <param name="key"></param>
        public void ChangeBackground(string key)
        {
            if (!RetrieveClient(key))
                return;

            if (MobileConnectionId == "" || ComputerConnectionId == "")
                return;

            // 叫電腦端做事
            Clients.Client(ComputerConnectionId).changeBackground();
        }

    }
}
  1. 然後看看 Client 端的,先看電腦端(還有手機端)。這張網頁上可視的 element 只有一個 QRcode 的 div。
  • head 內要引入三個 script:
    1. jQuery
    2. jQuery SignalR
    3. signalr/hubs。上面兩個東西,透過 NuGet 安裝後就會在 Scripts 目錄了,拖過來就有,但這行比較特別,你找不到這個目錄,但執行的時候就會出現了(真神奇!)。注意是寫這個應用程式的相對路徑。
  • 主要 script 的部分:
    • 建立連線成功後,將手機網頁 + key 的網址透過 Google Chart 產生 QRCode,顯示於 qrcode 這個 div 中。
    • 呼叫伺服器端事件 RegisterClient,並將 key 帶進去註冊成新的 Client。(記得呼叫伺服器端事件,首字是小寫喔!)
    • 然後定義三個 Client 端的事件:registerComplete、playMovie 與 changeBackground。
    • registerComplete:將 Client 註冊完成後,伺服器端會呼叫這個 Client 端事件,這邊很簡單地只用 alert 顯示訊息。
    • playMovie:手機端打開網頁後,檢查有相應的電腦端後,會透過伺服器端呼叫電腦端這個事件。原本是拿來控制 Flash 動畫的,所以叫 playMovie 😛
    • changeBackground:手機端的網頁上有顆按鈕,點擊後會透過伺服器端呼叫電腦端網頁的這個事件。效果是讓背景顏色隨機變換。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta name="author" content="patw, Patrick Wang" />
    <title>SignalR 電腦端網頁</title>
    <script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script>
    <script src="Scripts/jquery.signalR-1.0.0-alpha2.min.js" type="text/javascript"></script>
    <script src="signalr/hubs"></script>
</head>
<body>

    <script type="text/javascript">

        $(document).ready(function () {

            // 透過 $.connection.core 建立對應服務器端的類別 Core(名稱要一樣)
            var core = $.connection.core;
            var key = NewGuid();

            // Start the connection
            $.connection.hub.start().done(function () {

                // 手機網頁的網址
                var mobileurl = "http://手機網頁的網址/";

                // 顯示給手機照的 qrcode
                $("#qrcode").append("<img src='http://chart.apis.google.com/chart?chs=300x300&cht=qr&chl=" + mobileurl + "?key=" + key + "&choe=UTF-8' />");

                // 將此 key 註冊到 server 端(伺服器端事件首字都小寫喔)
                core.server.registerClient(key);

            });

            // Client 註冊完成事件
            core.client.registerComplete = function () {
                alert("Key: " + key + " 正在等待手機開啟中");
            };

            // 手機網頁開啟事件
            core.client.playMovie = function () {
                alert("手機開啟網頁了!");
            };

            // 更換背景顏色
            core.client.changeBackground = function () {
                var str = "0123456789abcdef", t = "";
                for (j = 0; j < 6; j++) {
                    t = t + str.charAt(Math.random() * str.length);
                }

                $("body").attr("style", "background-color:#" + t);
            };
        });

        // 用來產生類似 GUID 的字串
        function S4() {
            return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
        }

        function NewGuid() {
            return (S4() + S4());
        }

    </script>

    <div id="qrcode"></div>

</body>
</html>
  1. 手機端的部分:
  • 畫面上唯一可視的東西是一顆 Button,用來控制電腦端網頁的背景顏色。
  • 跟電腦端一樣,head 內要引入三個 script(本例我是將手機網頁放在 mobile/ 子目錄中,因此會用 ../ 來設相對路徑):
    1. jQuery
    2. jQuery SignalR
    3. signalr/hubs
  • getParameterByName:不想寫到後端程式,所以取得 Querystring Parameter 值的部分就用 JavaScript 處理了。
  • 主要 script 的部分:
    • 建立連線成功後,呼叫伺服器端事件 RegisterClient(一樣注意在這邊首字是小寫),將 key 註冊到 Client 集合中。
    • 註冊成功後,再呼叫伺服器端事件 MobileOpenWebpage。這是手機端用來通知伺服器端「我已經將網頁打開囉,請再發個通知給電腦端吧!」的事件。
    • 手機端唯一的 Client 端的事件是 noOpponent。伺服器端會在找不到相應 key 的電腦端時呼叫此事件,用來通知使用者:電腦端遺失,他可能得重掃。
    • registerComplete:將 Client 註冊完成後,伺服器端會呼叫這個 Client 端事件,這邊很簡單地只用 alert 顯示訊息。
    • #btnChangeBg 是畫面上唯一的 Button,這邊綁定它的 click 事件,點擊後會呼叫伺服器端的 changeBackground 事件,再轉知相符 key 電腦端的 Client 事件,用來改變網頁背景顏色。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<meta name="author" content="patw, Patrick Wang" />
    <title>SignalR 手機端網頁</title>
    <style type="text/css">
        #btnChangeBg
        {
            width: 80%;
            height: 300px;
            font-size: 11pt;
        }
    </style>
    <script src="../Scripts/jquery-1.6.4.min.js" type="text/javascript"></script>
    <script src="../Scripts/jquery.signalR-1.0.0-alpha2.min.js" type="text/javascript"></script>
    <script src="../signalr/hubs"></script>
    <script type="text/javascript">
        // 用 JavaScript 方式取得 GET 值
        function getParameterByName(name) {
            name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
            var regexS = "[\\?&]" + name + "=([^&#]*)";
            var regex = new RegExp(regexS);
            var results = regex.exec(window.location.search);
            if (results == null)
                return "";
            else
                return decodeURIComponent(results[1].replace(/\+/g, " "));
        }
    </script>
</head>
<body>

        <script type="text/javascript">

            $(document).ready(function () {

                // 透過 $.connection.core 建立對應服務器端的類別 Core(名稱要一樣)
                var core = $.connection.core;
                var key = getParameterByName("key");

                if (key == "") {
                    alert("Hey.. key is empty! please scan QRCode to get this page!");
                }

                // Start the connection
                $.connection.hub.start().done(function () {

                    // 將此 key 註冊到 server 端(伺服器端事件首字都小寫喔)
                    core.server.registerClient(key).done(function () {
                        // 註冊完,呼叫手機打開網頁事件,去找電腦端
                        core.server.mobileOpenWebpage(key);
                    });

                });

                // 找不到電腦端事件
                core.client.noOpponent = function () {
                    alert("No opponent!\r\nPlease refresh your webpage on computer, and try rescan QRCode by your mobile!");
                };

                $("#btnChangeBg").bind("click", function () {

                    // 呼叫 Server 端的變換背景顏色事件
                    core.server.changeBackground(key);

                });

            });

        </script>

        <input type="button" id="btnChangeBg" value="用手機變換電腦端的網頁背景顏色" />

</body>
</html>

範例全部的程式碼如上,下面也附上範例檔:
本範例的 VS2010 專案檔下載

第一次寫 SignalR 的東西,若有錯誤的地方請偷偷告訴我啦 😛
下次再試試看用 PersistentConnection 模式來做些有趣的東西囉~

6 thoughts on “[SignalR][ASP.NET] 使用 SignalR 來達成電腦網頁與手機網頁即時互動

  1. kim

    大哥你好
    我用了vs2017是可以執行
    補充文件有三點:
    1.OWIN啟動類別:Web>一般>OWIN啟動類別,命名為Startup.cs
    目的: 服务器需要知道要截获并将定向到 SignalR 的 URL
    不然執行會失敗
    程式內只要加入一行指令即可
    (中間略)
    public void Configuration(IAppBuilder app)
    {
    // 如需如何設定應用程式的詳細資訊,請瀏覽 https://go.microsoft.com/fwlink/?LinkID=316888
    加入下段
    app.MapSignalR();//這一段程式主要的作用就是跟 MVC Router 一樣讓別人知道 SignalR 的位置。
    }
    (略)

    2.另外我是掛載到IIS7,需要安裝NET Frame4.5(https://www.microsoft.com/zh-TW/download/confirmation.aspx?id=30653)

    Web.config加入二行

    <system.webServer>

    </system.webServer>
    這樣就不會被IIS誤判可能是虛擬目錄…

    3.index.aspx其中一句

    /signalr/hubs
    這段在Debug模式下正常,但是IIS卻不能執行
    必須改成
    http://../(專案名稱)%20/signalr/hubs
    例如: http://../WebApplication5/signalr/hubs

    參考解決方法http://no2don.blogspot.com/2012/10/c-signalrhubs.html

  2. kim

    前面發表文章過長,沒有被顯示,故再留言
    剛才試vs2017,是成功的

    注意有三點
    1.OWIN啟動類別
    選擇Web>一般>OWIN啟動類別,命名為Startup.cs
    目的: 服务器需要知道要截获并将定向到 SignalR 的 URL。
    否則無法執行

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *