一、理解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 示例代码如下:
服务端:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
//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);
}
}
}
|
1
2
3
4
5
6
7
8
|
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客户端:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
|
<!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相同,无需改变
客户端:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
|
<! 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 示例代码如下:
服务端:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
|
//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客户端:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
|
<!-- 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使用方式整理):