在 Asp.NET MVC 中使用 SignalR 實現推送功能

韓天偉發表於2013-11-13

一,簡介
Signal 是微軟支援的一個執行在 Dot NET 平臺上的 html websocket 框架。它出現的主要目的是實現伺服器主動推送(Push)訊息到客戶端頁面,這樣客戶端就不必重新傳送請求或使用輪詢技術來獲取訊息。

 
二,實現機制
SignalR 的實現機制與 .NET WCF 或 Remoting 是相似的,都是使用遠端代理來實現。在具體使用上,有兩種不同目的的介面:PersistentConnection 和 Hubs,其中 PersistentConnection 是實現了長時間的 Javascript 輪詢(類似於 Comet),Hub 是用來解決實時資訊交換問題,它是利用 Javascript 動態載入執行方法實現的。SignalR 將整個連線,資訊交換過程封裝得非常漂亮,客戶端與伺服器端全部使用 JSON 來交換資料。

 

下面就 Hubs 介面的使用來講講整個流程:

1,在伺服器端定義對應的 hub class;

2,在客戶端定義 hub class 所對應的 proxy 類;

3,在客戶端與伺服器端建立連線(connection);

4,然後客戶端就可以呼叫 proxy 物件的方法來呼叫伺服器端的方法,也就是傳送 request 給伺服器端;

5,伺服器端接收到 request 之後,可以針對某個/組客戶端或所有客戶端(廣播)傳送訊息。

 

三,Hub 示例教程
1,工具準備
SignalR 執行在 .NET 4.5 平臺上,所以需要安裝 .NET 4.5。為了方便演示,本示例使用 ASP.NET MVC 在 Win 7 系統來實現。這需要安裝 ASP.NET MVC 3 或 ASP.NET MVC 4。

 

2,建立工程
開啟 VS2010/VS2012 新建名為 SignalRTutorial 的 ASP.NET MVC 3 Web Application 工程,選擇 Internet Application 模板, Razor 檢視引擎以及勾選 Use HTMl 5 標籤。


 
3,安裝 SignalR
開啟 NuGet 的 package manager console(Tools->Library package manager),輸入:install-package SignalR.Sample,回車安裝。如圖所示:


4,實現 Hub 伺服器端程式碼
向工程中新建 SignalR 目錄,在其中新增 ChatHub.cs 檔案,內容如下:

複製程式碼
namespace SignalTutorial.SignalR
{
    [HubName("chat")]
    public class Chat : Hub
    {
        public void Send(string clientName, string message)
        {
            //var toSelfinfo = "You had sent message " + message;
            //Caller.addSomeMessage(clientName, toSelfinfo);

            // Call the addMessage method on all clients
            Clients.addSomeMessage(clientName, message);
            //Clients[Context.ConnectionId].addSomeMessage(clientName, data);
        }
    }
}
複製程式碼

 

在上面的程式碼中:

1),HubName 這個特性是為了讓客戶端知道如何建立與伺服器端對應服務的代理物件,如果沒有設定該屬性,則以伺服器端的服務類名字作為 HubName 的預設值;

2),Chat 繼承自 Hub,從下面 Hub 的介面圖可以看出:Hub 支援向發起請求者(Caller),所有客戶端(Clients),特定組(Group) 推送訊息。


3),public void Send(string clientName, string message) 這個介面是被客戶端通過代理物件呼叫的;

4),Clients 是 Hub 的屬性,表示所有連結的客戶端頁面,它和 Caller 一樣是 dynamic,因為要直接對應到 Javascript 物件;

5),Clients.addSomeMessage(clientName, message); 表示伺服器端呼叫客戶端的 addSomeMessage 方法,這是一個 Javascript 方法,從而給客戶端推送訊息。

6),總結:這裡實現的服務很簡單,就是當一個客戶端呼叫 Send 方法向伺服器傳送 message 後,伺服器端負責將該 message 廣播給所有的客戶端(也可以給特定組或特定客戶端,見遮蔽程式碼),以實現聊天室的功能。

 

5,實現 Hub 客戶端程式碼
1),引用 SignalR Javascript

為了簡化引用 SignalR 指令碼操作,我直接在 View/Shared/_Layout.cshtml 中引入 SignalR 及其他指令碼:

複製程式碼
<head>
        <meta charset="utf-8" />
        <title>@ViewBag.Title</title>
        <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
        <script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
        <script src="@Url.Content("~/Scripts/jquery-1.6.4.js")" type="text/javascript"></script>
        <script src="@Url.Content("~/Scripts/jquery-ui-1.8.24.js")" type="text/javascript"></script>
        <script src="@Url.Content("~/Scripts/jquery.signalR-0.5.3.js")" type="text/javascript"></script>
        <script src="@Url.Content("~/Scripts/modernizr-1.7.min.js")" type="text/javascript"></script>
        <script src="@Url.Content("~/signalr/hubs")" type="text/javascript"></script>
    </head>
複製程式碼

 

注意:signalR 依賴於 jquery,所以 signalR 必須放在 jquery 之後,而 hubs 又必須放在 signalR 之後。

然後在 body 部分加入 HubChat Tab:

<li>@Html.ActionLink("HubChat", "HubChat", "Home")</li>

 

2),生成訪問頁面

在 HomeController 中新增如下方法:

public ActionResult HubChat()
{
    ViewBag.ClientName = "使用者-" + Rnd.Next(10000, 99999);
    return View();
}

 

這裡由伺服器根據隨機數來設定客戶端的名字,不夠嚴謹,因為隨機數生成的名字不是唯一的的,在這裡僅為簡化演示,實際應用中應該使用 GUID 。

然後生成對應的 View:HubChat.cshtml

複製程式碼
@model dynamic

@{
    ViewBag.Title = "title";
}

<script src="@Url.Content("~/Scripts/hubDemo.js")" type="text/javascript"></script>
<script type="text/javascript">
    $(document).ready(function () {
    });
</script>

<h2>Hub Chat</h2>

<div>
    <input type="text" id="Placeholder" value="@ViewBag.ClientName" hidden="true"/>
    <input type="text" id="msg" />
    <input type="button" id="broadcast" value="廣播" />
   
    <br />
    <br />

    <h3>
        訊息記錄: (你是:<span id="MyClientName">@ViewBag.ClientName</span>):
    </h3>

    <ul id="messages">
    </ul>
</div>
複製程式碼

 

在上面的頁面程式碼中,我新增了名為 hubDemo.js 的指令碼,這將在下面介紹;此外還有一個id 為 Placeholder 的隱藏 input 控制元件,這是為了向 Javascript 中傳遞客戶端的名字。

 

3),編寫 Javascript

向 Scripts 目錄新增新的 Javescript 指令碼:hubDemo.js。其內容如下:

複製程式碼
$(function () {

    var myClientName = $('#Placeholder').val();

    // Proxy created on the fly
    var chat = $.connection.chat;

    // Declare a function on the chat hub so the server can invoke it
    chat.addSomeMessage = function (clientName, message) {
        writeEvent('<b>' + clientName + '</b> 對大家說: ' + message, 'event-message');
    };

    $("#broadcast").click(function () {
        // Call the chat method on the server
        chat.send(myClientName, $('#msg').val())
                            .done(function () {
                                console.log('Sent message success!');
                            })
                            .fail(function (e) {
                                console.warn(e);
                            });
    });

    // Start the connection
    $.connection.hub.start();

    //A function to write events to the page
    function writeEvent(eventLog, logClass) {
        var now = new Date();
        var nowStr = now.getHours() + ':' + now.getMinutes() + ':' + now.getSeconds();
        $('#messages').prepend('<li class=" + logClass + "><b>' + nowStr + '</b> ' + eventLog + '.</li>');
    }
});
複製程式碼

 

上面程式碼有詳細的註釋,下面再講講關鍵之處:

1,首先獲取客戶端頁面的名字;

2,然後通過 $.connection.chat 建立對應伺服器端 Hub 類的代理物件 chat;

3,定義客戶端的 Javascript 方法 addSomeMessage ,伺服器通過 dynamic 方式呼叫客戶端的該方法以實現推送功能。在這裡每當收到伺服器推送來的訊息,就在客戶端頁面的 messages 列表表頭插入該訊息。

4,當點選廣播按鈕時,客戶端通過代理物件呼叫伺服器端的 send 方法以實現向伺服器傳送訊息。

5,通過 $.connection.hub.start(); 語句開啟連結。

 

6),編譯執行 Hub 示例
在多個瀏覽器視窗開啟頁面,效果如下:


 

四,Persistent Connection 示例教程
1,實現伺服器端程式碼
1),編寫伺服器 PersistentConnection 程式碼
向工程中 SignalR 目錄中新增 PersistentConnection.cs 檔案,內容如下:

複製程式碼
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using SignalR;

namespace SignalTutorial.SignalR
{
    public class MyConnection : PersistentConnection
    {
        protected override Task OnConnectedAsync(IRequest request, string connectionId)
        {
            return Connection.Broadcast("Connection " + connectionId + " connected");
        }

        protected override Task OnReconnectedAsync(IRequest request, IEnumerable<string> groups, string clientId)
        {
            return Connection.Broadcast("Client " + clientId + " re-connected");
        }

        protected override Task OnReceivedAsync(IRequest request, string connectionId, string data)
        {
            var info = data + ". ConnectionId is [" + connectionId + "]";
            // return Connection.Send(connectionId, info);  

            // Broadcast data to all clients
            return Connection.Broadcast(info);  
        }

        protected override Task OnDisconnectAsync(string connectionId)
        {
            return Connection.Broadcast("Connection " + connectionId + " disconncted");
        }

        protected override Task OnErrorAsync(Exception error)
        {
            return Connection.Broadcast("Error ocurred " + error);
        }
    }
}
複製程式碼

 

在上面的程式碼中:

1,MyConnection 繼承自 PersistentConnection,這樣我們就能在客戶端連線,重連線,斷開連線,傳送訊息以及連線出錯的情況下進行相關的處理。從下面的 PersistentConnection 介面中可以看到,PersistentConnection 同樣支援組進行推送。


 


2,推送訊息由 PersistentConnection 的屬性 Connection 來提供,它繼承自 IConnection 介面,該介面提供兩個函式來實現對特定客戶端的推送和廣播功能。

System.Threading.Tasks.Task Send(string signal, object value)
System.Threading.Tasks.Task Broadcast(object value)

 

2),配置訪問路由
為了支援客戶端訪問,需要在路由表中進行配置。開啟 Global.asax.cs ,修改 Application_Start() 函式如下:

複製程式碼
protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

    RouteTable.Routes.MapConnection<MyConnection>("echo", "echo/{*operation}");

    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);

    // Make connections wait 50s maximum for any response. After
    // 50s are up, trigger a timeout command and make the client reconnect.
    GlobalHost.Configuration.ConnectionTimeout = TimeSpan.FromSeconds(50);
    //DisconnectTimeout
    //HeartBeatInterval
    //KeepAlive
}
複製程式碼

 

在上面的程式碼中,我將 echo 及其子路徑的訪問對映到 MyConnection 上,並設定連線超時時間為 50 s。在這裡還可以設定其他的一些引數,如斷連超時時間,心跳間隔等。

 

2,實現客戶端程式碼
1),生成訪問頁面

在前面三 Hub 示例教程的基礎上,我們向該工程加入使用 Persistent Connection 的演示。和前面一樣,向 _Layout.cshtml 中加入 PersistentChat Tab:

<li>@Html.ActionLink("PersistentChat", "PersistentChat", "Home")</li>

 

然後在 HomeController 中新增如下方法:

public ActionResult PersistentChat()
{
    ViewBag.ClientName = "使用者-" + Rnd.Next(10000, 99999);
    return View();
}

 

這裡由伺服器根據隨機數來設定客戶端的名字,不夠嚴謹,因為隨機數生成的名字不是唯一的的,在這裡僅為簡化演示,實際應用中應該使用 GUID 。

然後生成對應的 頁面: PersistentChat.cshtml:

複製程式碼
@model dynamic

@{
    ViewBag.Title = "title";
}

<script src="@Url.Content("~/Scripts/persistent.js")" type="text/javascript"></script>

<h2>Persistent Chat</h2>

<div>
    <input type="text" id="Placeholder" value="@ViewBag.ClientName" hidden="true"/>
    <input type="text" id="msg" />
    <input type="button" id="broadcast" value="廣播" />

    <br />
    <br />
   
    <h3>
        訊息記錄: (你是:<span id="MyClientName">@ViewBag.ClientName</span>):
    </h3>

    <ul id="messages">
    </ul>

</div>
複製程式碼

 

在上面的頁面程式碼中,我新增了名為 persistent.js 的指令碼,這將在下面介紹;此外還有一個id 為 Placeholder 的隱藏 input 控制元件,這是為了向 Javascript 中傳遞客戶端的名字。

 

2),編寫 Javascript

向 Scripts 目錄新增新的 Javescript 指令碼:persistent.js。其內容如下:

複製程式碼
$(function () {

    var myClientName = $('#Placeholder').val();

    var connection = $.connection('/echo');

    connection.received(function (data) {
        var msg = new String(data);
        var index = msg.indexOf("#");
        var clientName = msg.substring(0, index);
        var content = msg.substring(index + 1);

        if (clientName == null || clientName == "") {
            writeEvent('<b>' + "系統訊息" + '</b>: ' + content, 'event-message');
        }
        else {
            writeEvent('<b>' + clientName + '</b> 對大家說: ' + content, 'event-message');
        }
    });

    connection.start();

    $("#broadcast").click(function () {
        var msg = myClientName + "#" + $('#msg').val();
        connection.send(msg);
    });

    //A function to write events to the page
    function writeEvent(eventLog, logClass) {
        var now = new Date();
        var nowStr = now.getHours() + ':' + now.getMinutes() + ':' + now.getSeconds();
        $('#messages').prepend('<li class=" + logClass + "><b>' + nowStr + '</b> ' + eventLog + '.</li>');
    }
});
複製程式碼

 

上面的程式碼基本與前面的 Hub 實現相同,在此就不再一一講述。有兩點值得說明:

1,建立連線時,指定路徑為 "/echo",該路徑在伺服器端的路由對映表被對映為 MyConnection,因而這個連線就被指向前面提供的 MyConnection。

2,將 clientName 資訊放入 message 中,並用 # 將 clientName 和訊息內容連線成一個 msg。

3,編譯執行 Persistent 示例

 

相關文章