一、理解SignalR
ASP .NET SignalR 是一個ASP .NET 下的類庫,可以在ASP .NET 的Web專案中實現實時通訊(即:客戶端(Web頁面)和伺服器端可以互相實時的通知訊息及呼叫方法),SignalR有三種傳輸模式:LongLooping(長輪詢)、WebSocket(HTML5的WEB套接字)、Forever Frame(隱藏框架的長請求連線),可以在WEB客戶端顯式指定一種或幾種,也可以採取預設(推薦),若採取預設,SignalR會根據瀏覽器的環境自動選擇合適的傳輸方式。
二、SignalR的三種實現方式
第一種:採用集線器類(Hub)+非自動生成代理模式:服務端與客戶端分別定義的相對應的方法,客戶端通過代理物件呼叫服務端的方法,服務端通過IHubConnectionContext回撥客戶端的方法,客戶端通過回撥方法接收結果。
之前我寫過一篇文章《分享一個基於長連線+長輪詢+原生的JS及AJAX實現的多人線上即時交流聊天室》,是通過長輪詢+長連線的方式來實現的線上多人聊天室功能,從程式碼量來看就知道實現起來並不簡單,而如今有了SignalR,會簡單很多,我這裡使用SignalR再來寫一個簡單的線上多人聊天室示例,以便大家快速掌握SignalR。
DEMO - 1 示例程式碼如下:
服務端:
//Startup類檔案
using System;
using System.Threading.Tasks;
using Microsoft.Owin;
using Owin;
using Microsoft.AspNet.SignalR;
[assembly: OwinStartup(typeof(TestWebApp.Models.Startup))]
namespace TestWebApp.Models
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.MapSignalR();
}
}
}
//ChatHub類檔案
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace TestWebApp.Models
{
[HubName("chat")]
public class ChatHub : Hub
{
public static ConcurrentDictionary<string, string> OnLineUsers = new ConcurrentDictionary<string, string>();
[HubMethodName("send")]
public void Send(string message)
{
string clientName = OnLineUsers[Context.ConnectionId];
message = HttpUtility.HtmlEncode(message).Replace("\r\n", "<br/>").Replace("\n", "<br/>");
Clients.All.receiveMessage(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), clientName, message);
}
[HubMethodName("sendOne")]
public void Send(string toUserId, string message)
{
string clientName = OnLineUsers[Context.ConnectionId];
message = HttpUtility.HtmlEncode(message).Replace("\r\n", "<br/>").Replace("\n", "<br/>");
Clients.Caller.receiveMessage(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), string.Format("您對 {1}", clientName, OnLineUsers[toUserId]), message);
Clients.Client(toUserId).receiveMessage(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), string.Format("{0} 對您", clientName), message);
}
public override System.Threading.Tasks.Task OnConnected()
{
string clientName = Context.QueryString["clientName"].ToString();
OnLineUsers.AddOrUpdate(Context.ConnectionId, clientName, (key, value) => clientName);
Clients.All.userChange(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), string.Format("{0} 加入了。", clientName), OnLineUsers.ToArray());
return base.OnConnected();
}
public override System.Threading.Tasks.Task OnDisconnected(bool stopCalled)
{
string clientName = Context.QueryString["clientName"].ToString();
Clients.All.userChange(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), string.Format("{0} 離開了。", clientName), OnLineUsers.ToArray());
OnLineUsers.TryRemove(Context.ConnectionId, out clientName);
return base.OnDisconnected(stopCalled);
}
}
}
public ActionResult Index()
{
ViewBag.ClientName = "聊客-" + Guid.NewGuid().ToString("N");
var onLineUserList = ChatHub.OnLineUsers.Select(u => new SelectListItem() { Text = u.Value, Value = u.Key }).ToList();
onLineUserList.Insert(0, new SelectListItem() { Text = "-所有人-", Value = "" });
ViewBag.OnLineUsers = onLineUserList;
return View();
}
WEB客戶端:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta charset="utf-8" />
<title>聊天室</title>
<script src="~/Scripts/jquery-1.6.4.min.js" type="text/javascript"></script>
<script src="~/Scripts/jquery.signalR-2.2.0.min.js" type="text/javascript"></script>
<style type="text/css">
#chatbox {
width: 100%;
height: 500px;
border: 2px solid blue;
padding: 5px;
margin: 5px 0px;
overflow-x: hidden;
overflow-y: auto;
}
.linfo {
}
.rinfo {
text-align: right;
}
</style>
<script type="text/javascript">
$(function () {
var clientName = $("#clientname").val();
var eChatBox = $("#chatbox");
var eUsers = $("#users");
var conn = $.hubConnection();
conn.qs = { "clientName": clientName };
conn.start().done(function () {
$("#btnSend").click(function () {
var toUserId = eUsers.val();
if (toUserId != "") {
chat.invoke("sendOne", toUserId, $("#message").val())
.done(function () {
//alert("傳送成功!");
$("#message").val("").focus();
})
.fail(function (e) {
alert(e);
$("#message").focus();
});
}
else {
chat.invoke("send", $("#message").val())
.done(function () {
//alert("傳送成功!");
$("#message").val("").focus();
})
.fail(function (e) {
alert(e);
$("#message").focus();
});
}
});
});
var chat = conn.createHubProxy("chat");
chat.on("receiveMessage", function (dt, cn, msg) {
var clsName = "linfo";
if (cn == clientName || cn.indexOf("您對") >= 0) clsName = "rinfo";
eChatBox.append("<p class='" + clsName + "'>" + dt + " <strong>" + cn + "</strong> 說:<br/>" + msg + "</p>");
eChatBox.scrollTop(eChatBox[0].scrollHeight);
});
chat.on("userChange", function (dt, msg, users) {
eChatBox.append("<p>" + dt + " " + msg + "</p>");
eUsers.find("option[value!='']").remove();
for (var i = 0; i < users.length; i++) {
if (users[i].Value == clientName) continue;
eUsers.append("<option value='" + users[i].Key + "'>" + users[i].Value + "</option>")
}
});
});
</script>
</head>
<body>
<h3>大眾聊天室</h3>
<div id="chatbox">
</div>
<div>
<span>聊天名稱:</span>
@Html.TextBox("clientname", ViewBag.ClientName as string, new { @readonly = "readonly", style = "width:300px;" })
<span>聊天物件:</span>
@Html.DropDownList("users", ViewBag.OnLineUsers as IEnumerable<SelectListItem>)
</div>
<div>
@Html.TextArea("message", new { rows = 5, style = "width:500px;" })
<input type="button" value="傳送訊息" id="btnSend" />
</div>
</body>
</html>
服務端與客戶端程式碼都比較簡單,網上相關的說明也有,這裡就不再解說了,只說一下這種方式JS端呼叫服務端方法採用:chat.invoke,而被服務端回撥的方法則採用:chat.on (這裡的chat是createHubProxy建立得來的)
第二種:採用集線器類(Hub)+自動生成代理模式
DEMO - 2 示例程式碼如下:
服務端與DEMO 1相同,無需改變
客戶端:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta charset="utf-8" />
<title>聊天室</title>
<script src="~/Scripts/jquery-1.6.4.min.js" type="text/javascript"></script>
<script src="~/Scripts/jquery.signalR-2.2.0.min.js" type="text/javascript"></script>
<script src="~/signalr/hubs" type="text/javascript"></script>
<style type="text/css">
#chatbox {
width: 100%;
height: 500px;
border: 2px solid blue;
padding: 5px;
margin: 5px 0px;
overflow-x: hidden;
overflow-y: auto;
}
.linfo {
}
.rinfo {
text-align: right;
}
</style>
<script type="text/javascript">
$(function () {
var clientName = $("#clientname").val();
var eChatBox = $("#chatbox");
var eUsers = $("#users");
var chat = $.connection.chat;
$.connection.hub.qs = { "clientName": clientName };
chat.state.test = "test";
chat.client.receiveMessage = function (dt, cn, msg) {
var clsName = "linfo";
if (cn == clientName || cn.indexOf("您對")>=0) clsName = "rinfo";
eChatBox.append("<p class='" + clsName + "'>" + dt + " <strong>" + cn + "</strong> 說:<br/>" + msg + "</p>");
eChatBox.scrollTop(eChatBox[0].scrollHeight);
}
chat.client.userChange = function (dt, msg, users) {
eChatBox.append("<p>" + dt + " " + msg + "</p>");
eUsers.find("option[value!='']").remove();
for (var i = 0; i < users.length; i++) {
if (users[i].Value == clientName) continue;
eUsers.append("<option value='" + users[i].Key + "'>" + users[i].Value + "</option>")
}
}
$.connection.hub.start().done(function () {
$("#btnSend").click(function () {
var toUserId = eUsers.val();
if (toUserId != "") {
chat.server.sendOne(toUserId, $("#message").val())
.done(function () {
//alert("傳送成功!");
$("#message").val("").focus();
})
.fail(function (e) {
alert(e);
$("#message").focus();
});
}
else {
chat.server.send($("#message").val())
.done(function () {
//alert("傳送成功!");
$("#message").val("").focus();
})
.fail(function (e) {
alert(e);
$("#message").focus();
});
}
});
});
});
</script>
</head>
<body>
<h3>大眾聊天室</h3>
<div id="chatbox">
</div>
<div>
<span>聊天名稱:</span>
@Html.TextBox("clientname", ViewBag.ClientName as string, new { @readonly = "readonly", style = "width:300px;" })
<span>聊天物件:</span>
@Html.DropDownList("users", ViewBag.OnLineUsers as IEnumerable<SelectListItem>)
</div>
<div>
@Html.TextArea("message", new { rows = 5, style = "width:500px;" })
<input type="button" value="傳送訊息" id="btnSend" />
</div>
</body>
</html>
上述程式碼中特別需要注意的是,需要引用一個“不存在的JS目錄”:<script src="~/signalr/hubs" type="text/javascript"></script>,為什麼要打引號,是因為我們在寫程式碼的時候是不存在的,而當執行後就會自動生成signalr的代理指令碼,這就是與非自動生成代理指令碼最根本的區別,也正是因為這個自動生成的指令碼,我們可以在JS中更加方便的呼叫服務端方法及定義回撥方法,呼叫服務端方法採用:chat.server.XXX,而被服務端回撥的客戶端方法則採用:chat.client.XXX
看一下上述兩種的執行效果截圖吧:
第三種:採用持久化連線類(PersistentConnection)
DEMO - 3 示例程式碼如下:
服務端:
//Startup類:
using System;
using System.Threading.Tasks;
using Microsoft.Owin;
using Owin;
using Microsoft.AspNet.SignalR;
[assembly: OwinStartup(typeof(TestWebApp.Models.Startup))]
namespace TestWebApp.Models
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.MapSignalR<MyConnection>("/MyConnection");
}
}
}
//MyConnection類:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using Microsoft.AspNet.SignalR;
namespace TestWebApp.Models
{
public class MyConnection : PersistentConnection
{
private static List<string> monitoringIdList = new List<string>();
protected override Task OnConnected(IRequest request, string connectionId)
{
bool IsMonitoring = (request.QueryString["Monitoring"] ?? "").ToString() == "Y";
if (IsMonitoring)
{
if (!monitoringIdList.Contains(connectionId))
{
monitoringIdList.Add(connectionId);
}
return Connection.Send(connectionId, "ready");
}
else
{
if (monitoringIdList.Count > 0)
{
return Connection.Send(monitoringIdList, "in_" + connectionId);
}
else
{
return Connection.Send(connectionId, "nobody");
}
}
}
protected override Task OnReceived(IRequest request, string connectionId, string data)
{
if (monitoringIdList.Contains(connectionId))
{
return Connection.Send(data, "pass");
}
return null;
}
protected override Task OnDisconnected(IRequest request, string connectionId, bool stopCalled)
{
if (!monitoringIdList.Contains(connectionId))
{
return Connection.Send(monitoringIdList, "out_" + connectionId);
}
return null;
}
}
}
WEB客戶端:
<!-- MonitoringPage.cshtml 監控管理頁面-->
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>MonitoringPage</title>
<script src="~/Scripts/jquery-1.6.4.min.js" type="text/javascript"></script>
<script src="~/Scripts/jquery.signalR-2.2.0.min.js" type="text/javascript"></script>
<style type="text/css">
table {
border:1px solid #808080;
width:600px;
}
td {
border:1px solid #808080;
padding:3px;
}
.odd{ background-color: #bbf;}
.even{ background-color:#ffc; }
.non-temptr {
display:none;
}
</style>
<script type="text/javascript">
$(function () {
$("#userstable tbody tr:odd").addClass("odd");
$("#userstable tbody tr:even").addClass("even");
var conn = $.connection("/MyConnection", {"Monitoring":"Y"});
conn.start().done(function () {
$("#userstable").delegate("button.pass", "click", function () {
var rid = $(this).parent("td").prev().attr("data-rid");
conn.send(rid);
var tr = $(this).parents("tr");
tr.remove();
});
}).fail(function (msg) {
alert(msg);
});
conn.received(function (msg) {
if (msg == "ready")
{
$("#spstatus").html("監控服務已就緒");
return;
}
else if (msg.indexOf("in_") == 0) {
var tr = $(".non-temptr").clone(true);
tr.removeClass("non-temptr");
var td = tr.children().first();
var rid = msg.toString().substr("in_".length);
td.html(rid + "進入被監控頁面,是否允許?");
td.attr("data-rid", rid);
$("#userstable tbody").append(tr);
}
else
{
var rid = msg.toString().substr("out_".length);
$("td[data-rid=" + rid + "]").parent("tr").remove();
}
});
});
</script>
</head>
<body>
<div>
以下是實時監控到進入EnterPage頁面的使用者情況:(服務狀況:<strong><span id="spstatus"></span></strong>)
</div>
<table id="userstable">
<tr>
<td>使用者進入訊息</td>
<td>授 權</td>
</tr>
<tr class="non-temptr">
<td></td>
<td style="width:100px"><button class="pass">允許</button></td>
</tr>
</table>
</body>
</html>
<!-- EnterPage.cshtml 監控受限頁面-->
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>EnterPage</title>
<script src="~/Scripts/jquery-1.6.4.min.js" type="text/javascript"></script>
<script src="~/Scripts/jquery.signalR-2.2.0.min.js" type="text/javascript"></script>
</head>
<body>
<script type="text/javascript">
$(function () {
var conn = $.connection("/MyConnection");
conn.start().fail(function (msg) {
alert(msg);
});
conn.received(function (data) {
if (data == "pass") {
$("#msg").html("管理員已稽核通過,可以進入瀏覽詳情。");
setTimeout(function () {
self.location = "http://www.zuowenjun.cn";
}, 3000);
}
else
{
$("#msg").html("無管理員線上,請稍候再重新進入該頁面。");
}
});
});
</script>
<div id="msg">
該頁面瀏覽受限,已自動將您的瀏覽請求發給管理員,請稍候。。。
</div>
</body>
</html>
上述程式碼可以看出與採用Hub(集線器類)的不同之處,一是:Startup.Configuration中是需要指定app.MapSignalR<MyConnection>("/MyConnection"),二是需實現繼承自PersistentConnection類的自定義的持久化連線類,在這個連線中可以重寫:OnConnected、OnDisconnected、OnReceived、OnReconnected、ProcessRequest方法,同時有幾個重要的屬性成員Connection、Groups,服務端發訊息給客戶端採用:Connection.Broadcast(廣播,所有客戶端都可以收到訊息),Connection.Send(傳送給指定的客戶端)
執行效果如下截圖示:
SignalR支援額外附加:QueryString、Cookie、State,具體的客戶端設定與服務端接收請見上面的程式碼,同時也可以參見如下其它博主總結的表格(SignalR的Javascript客戶端API使用方式整理):