一、理解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 OnLineUsers = new ConcurrentDictionary();
[HubMethodName("send")]
public void Send(string message)
{
string clientName = OnLineUsers[Context.ConnectionId];
message = HttpUtility.HtmlEncode(message).Replace("\r\n", "
").Replace("\n", "
");
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", "
").Replace("\n", "
");
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客户端:
聊天室
大众聊天室
聊天名称:
@Html.TextBox("clientname", ViewBag.ClientName as string, new { @readonly = "readonly", style = "width:300px;" })
聊天对象:
@Html.DropDownList("users", ViewBag.OnLineUsers as IEnumerable)
@Html.TextArea("message", new { rows = 5, style = "width:500px;" })
服务端与客户端代码都比较简单,网上相关的说明也有,这里就不再解说了,只说一下这种方式JS端调用服务端方法采用:chat.invoke,而被服务端回调的方法则采用:chat.on (这里的chat是createHubProxy创建得来的)
第二种:采用集线器类(Hub)+自动生成代理模式
DEMO - 2 示例代码如下:
服务端与DEMO 1相同,无需改变
客户端:
聊天室
大众聊天室
聊天名称:
@Html.TextBox("clientname", ViewBag.ClientName as string, new { @readonly = "readonly", style = "width:300px;" })
聊天对象:
@Html.DropDownList("users", ViewBag.OnLineUsers as IEnumerable)
@Html.TextArea("message", new { rows = 5, style = "width:500px;" })
上述代码中特别需要注意的是,需要引用一个“不存在的JS目录”:,为什么要打引号,是因为我们在写代码的时候是不存在的,而当运行后就会自动生成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类:
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 monitoringIdList = new List();
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
以下是实时监控到进入EnterPage页面的用户情况:(服务状况:)
用户进入消息
授 权
EnterPage
该页面浏览受限,已自动将您的浏览请求发给管理员,请稍候。。。
上述代码可以看出与采用Hub(集线器类)的不同之处,一是:Startup.Configuration中是需要指定app.MapSignalR
运行效果如下截图示:
SignalR支持额外附加:QueryString、Cookie、State,具体的客户端设置与服务端接收请见上面的代码,同时也可以参见如下其它博主总结的表格(SignalR的Javascript客户端API使用方式整理):