在 Asp.NET MVC 中使用 SignalR 實現推送功能
羅朝輝 ( http://www.cnblogs.com/kesalin/ )
一,簡介
Signal 是微軟支持的一個運行在 Dot NET 平台上的 html websocket 框架。它出現的主要目的是實現服務器主動推送(Push)消息到客戶端頁面,這樣客戶端就不必重新發送請求或使用輪詢技術來獲取消息。
可訪問其官方網站:https://github.com/SignalR/ 獲取更多資訊。
二,實現機制
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 示例
五,引用
SignalR:https://github.com/SignalR/
利用SignalR實現遠端程式遙控功能:
http://blog.darkthread.net/post-2012-07-10-signalr-remote-controller.aspx
一個很酷的同步操作表格的示例(使用 jTable ):
http://www.codeproject.com/Articles/315938/Real-time-Asynchronous-Web-Pages-using-jTable-Sign
組通知示例:
http://www.codeproject.com/Articles/404662/SignalR-Group-Notifications