SignalR初学
SignalR_Share_Mol
@(我的分享)[SignalR]
按照惯例,扯闲篇
我们都知道,http协议是无状态的,所谓无状态,我习惯把它称作是“金鱼模式”。
我以李老板去看金鱼这个场景来说明“无状态”。金鱼的记忆时间是非常短的(据说只有7秒,但我非鱼,安知鱼之记忆时间)。李老板趴在鱼缸旁边看金鱼。
金鱼:我操,这谁啊,这么丑。。。
7秒后,金鱼:我操,这谁啊,这么丑。。。
7秒后,金鱼:我操,这谁啊,这么丑。。。
七七49秒后,鱼吐白沫:这谁啊,这么丑。。。
这就是所谓的无状态。我们把金鱼看成服务端,李老板看成客户端。每次李老板看鱼的时候,就相当于是客户端向服务端发起了一次请求。服务端针对当前的请求返回“我操,这谁啊,这么丑”。
下一次,李老板再看鱼的时候,鱼早忘了这孙子是谁了,又返回“我操,这谁啊,这么丑!”。
服务端不会记住客户端的状态,这就是所谓的无状态。
上一次我分享的websocket技术,就是在这种无状态的协议的大前提下诞生的。但是很多人都觉得websocket有各种各样的问题。比如只能针对当前会话实现“全双工”链接。更有一些有语言之见的程序员觉得websocket这根本就不是自己的“母语”嘛。
OK,下面就该SignalR出场了!
SignalR是什么
关于SignalR的定义,有两个关键词
.net
websocket
SignalR是.net框架下的一个类库,几乎所有的教程中都不会说,SignalR现在只支持.net Framework(至少在我打字的时候,还是这样)。如果你在.net core下面需要使用SignalR,那就只能使用第三方扩展,或者自己基于 websocket进行封装。不过,这么牛X的技术,我相信微软在不久的将来会封装到.net core中的。
SignalR是基于websocket这个API来实现的。当然,前提是你的浏览要支持websocket。如果你的浏览器不支持websocket,那么SingalR会基于轮循来实现(见websocket分享中的说明)。这就好像jquery的ajax一样。我们用js来写ajax的时候,通常会判断一下你的浏览器是否支持ActiveXObject,如果不支持,那么改用xmlHttpRequest,看起来像是这样:
function createxmlHttpRequest() {
if (window.ActiveXObject) {
return new ActiveXObject("Microsoft.XMLHTTP");
} else if (window.XMLHttpRequest) {
return new XMLHttpRequest();
}
}
在SignalR中也是一样的。只是它的源码太长了,我不想贴出来了。有兴趣,可以自己拿ilspy看一下。
通过Demo说明
我写了一下小demo,这个demo是一个在线聊天室。
这个聊天室有两个页面,分别是管理员页面和客户页面。
管理员可以看到所有已连接的客户端,可以向所有的客户端发消息;可以向某个分组的用户发消息;可以向某个特定的用户发消息。
用户只能向所有人发消息。
功能需求不太合理,不过我只想SignalR怎么用。
搭架子
- 增加StartUp入口类。这个类要写在你的界面层的最外面(和web.config放在同一层)。
- 增加SignalR引用。建议使用nuget来引用,这样可以保证你的引用一定是最新、最稳定的。
- 在StartUp类中增加Configuration方法。并为StartUp增加启动属性,看起来像是这样:
using Microsoft.Owin;
using Owin;
[assembly: OwinStartup(typeof(SignalR.Server.MVC.Startup))]
namespace SignalR.Server.MVC
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.MapSignalR();
}
}
}
- 增加“集线器”类,我程序中叫“MolHub”,看起来像是这样:
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNet.SignalR;
using SignalR.Server.MVC.Common;
using SignalR.Server.MVC.Models;
using Newtonsoft.Json;
namespace SignalR.Server.MVC
{
public class MolHub : Hub
{
public void SendMessage(string name, string msg)
{
Clients.All.onMessage($"{name}和大家说:{msg}");
}
}
MolHub这个类必须要实现Hub,用来表示当前类是一个集线器类。
所谓集线器类,就是说,所以有的客户端都是要连接到这个类上的(包括管理员页面)。
集线器类会记录客户端的连接属性。
- 写页面,这里我使用asp.net来写页面,各位亲们用webform也是一样的。
控制器代码如下:
// Controller
public ActionResult Index()
{
return View();
}
页面代码如下:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Index</title>
<script src="~/Scripts/jquery-1.6.4.min.js"></script>
<script src="~/Scripts/jquery.signalR-2.2.2.min.js"></script>
<script src="~/signalr/hubs" type="text/javascript"></script>
</head>
<body>
<div>
<label>昵称:</label><input type="text" id="txtName" value="@Model.UserName" />
<br />
<label>消息:</label><input type="text" id="txtMsg" value="这是一条来自火星的消息,hello,地球人!" />
<hr />
<button id="sendtoserver">发送</button>
</div>
<div>
<label>收到的消息:</label>
<ul id="ulMsg">
</ul>
</div>
</body>
</html>
<script>
--JS代码在下面,为了语法高亮,我分开写了--
</script>
$(function () {
// 定义一个事件,这个事件由服务端来调用
$.connection.molHub.client.onMessage = function (msg) {
addMsga(msg);
}
// 所有的事件都定义完成以后,才可以写下面的代码来发起连接
$.connection.hub.start().done(function (data) {
// 连接成功执行这里
addMsga("与服务端连接成功!");
}).fail(function () {
// 连接失败执行这里
addMsga("与服务端连接失败!");
});
// 点击发送消息按钮的事件
$('button#sendtoserver').click(function () {
// 告诉服务器我发了一条消息,相当于调用服务端的SendMessage方法.
$.connection.molHub.server.sendMessage($('input#txtName').val(), $('input#txtMsg').val());
});
});
// 页面展示相关的方法。addMsga是用来把服务器发来的消息展示在界面上
function addMsga(msg)
{
$('<li>' + msg + '</li>').appendTo($('ul#ulMsg'));
}
- 上面的代码有两个地方需要注意,第一是
<script src="~/signalr/hubs" type="text/javascript"></script>
这个引用你只需要乖乖地这样写就行了,SignalR会把这句话解析成对应的js引用。
第二点是所有的事件、参数的定义,一定要写在$.connection.hub.start()之前。
第一句JS
$.connection.molHub.client.onMessage
表示:我在客户端定义一个事件,这个事件叫onMessage,这个事件可以在服务端被获取到,并且可以被调用。对应到服务端,
Clients.All.onMessage($"{name}和大家说:{msg}");这解码器话就是用来调用客户端的onMessage方法的。
同样的,我在服务端定义了一个SendMessage方法,在客户端也可以调用,比如上面的按钮点击事件中,我就调用了服务端的sendMessage方法:
$.connection.molHub.server.sendMessage($('input#txtName').val(), $('input#txtMsg').val());
这样,架子就搭起来了。当然现在的功能只有发消息和收消息。你可以试着多开几个页面,互相聊天。
客户端分组
我们在觉的聊天室中,一定会看见“房间”的概念,当然,这里所说的房间并不是老徐在如家开的房间。而是规定一个范围,有一部分用户在这个范围中。在我的Demo中,用“分组”来替代房间的概念。我定义一个类来描述用户对象,这个类看起来像是这样:
/// <summary>
/// 用户对象
/// </summary>
[Serializable]
public class UserModel
{
/// <summary>
/// 性别,默认为男
/// </summary>
public string Gender { get; set; } = "男";
/// <summary>
/// 组名
/// </summary>
public string GroupName { get; set; }
/// <summary>
/// 姓名
/// </summary>
public string UserName { get; set; }
/// <summary>
/// 唯一标识用户连接的ID。每个连接都会对应一个唯一的ConnectionId,这个connectionId看起来和GUID很像
/// </summary>
public string ConnectionId { get; set; }
public override string ToString()
{
return JsonConvert.SerializeObject(this);
}
}
这样一来,每个用户的页面就可以对应一个用户对象了。我的用户界面要改造成这样:

对应的前端代码如下:
@model SignalR.Server.MVC.Models.UserModel
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Index</title>
<script src="~/Scripts/jquery-1.6.4.min.js"></script>
<script src="~/Scripts/jquery.signalR-2.2.2.min.js"></script>
<script src="~/signalr/hubs" type="text/javascript"></script>
</head>
<body>
<fieldset>
<legend>个人信息</legend>
<div>
<ul>
<li>姓名:@Model.UserName</li>
<li>性别:@Model.Gender</li>
<li>分组:@Model.GroupName</li>
</ul>
</div>
</fieldset>
<div>
<label>昵称:</label><input type="text" id="txtName" value="@Model.UserName" />
<br />
<label>消息:</label><input type="text" id="txtMsg" value="这是一条来自火星的消息,hello,地球人!" />
<hr />
<button id="sendtoserver">发送</button>
</div>
<div>
<label>收到的消息:</label>
<ul id="ulMsg">
</ul>
</div>
</body>
</html>
<script>
$(function () {
$.connection.molHub.client.onMessage = function (msg) {
addMsga(msg);
}
$.connection.hub.qs = { gender: "@Model.Gender", userName: "@Model.UserName", groupName: "@Model.GroupName" }
$.connection.hub.start().done(function (data) {
addMsga("与服务端连接成功!");
}).fail(function () {
addMsga("与服务端连接失败!");
});
$('button#sendtoserver').click(function () {
$.connection.molHub.server.sendMessage($('input#txtName').val(), $('input#txtMsg').val());
});
});
function addMsga(msg)
{
$('<li>' + msg + '</li>').appendTo($('ul#ulMsg'));
}
</script>
无非就是增加了用户信息的展示,并没有其它的新功能。
管理员页面
接下来我要完成管理员页面,它看起来像是这样:

这个功能就有点复杂了,除了要发送给所有人(在“搭架子”步骤中已实现),还要实现分组发送、针对特定用户发送。
分组发送
分组发送和向所有人发送的区别就是,分组是针对一小撮特定的人来发送消息的,那么,必然有一个标识来描述这一小撮特定的人,我们在UserModer 中定义了GroupName就是干这个用处的。
思路:我可以在服务端定义一个方法,这个方法只给特定的分组发送消息。
有思路以后,我们会遇到一个问题,我应该如何获取到某个特定的组?
SignalR已经封装了一个名为Group的方法,你只需要告诉它组名,就可以得到这个分组了。这个方法看起来像是这样:
public void SendToClientByGroup(string group, string msg)
{
Clients.Group(group).onMessage($"管理员对组{group}说:{msg}");
}
因为所有的客户端都是连接到“集线器”的,所以所有的交互方法都要写到MolHub中,而不是控制器中。
而前端要做的事,只是调用这个方法就可以了。当然,前端还要把分组名称传给集线器。前端代码如下:
// 按组发送消息
$('button#sendByGroup').click(function () {
$.connection.molHub.server.sendToClientByGroup($('select#selectGroup').val(), $('input#txtMsg').val());
});
ps:在我的Demo中,是通过下拉框的形式来展示分组的。
按用户发送消息
分组发送的功能搞定以后,向特定用户发消息的功能就可以猜一下喽。SingalR是不是也封装了一个获取特定用户的方法?
那简直是一定的嘛。不废话了,直接上代码:
// 集线器
public async Task SendToClientByNameAsync(string Connectionid, string msg)
{
await Task.Run(()=> Clients.Client(Connectionid).onMessage($"管理员对你说:{msg}") );
}
// 前端
// 按客户端发送消息
$('button#sendByClient').click(function () {
$.connection.molHub.server.sendToClientByNameAsync($('select#selectClient').val(), $('input#txtMsg').val());
});
重看集线器
我相信你一定有一个疑问,就是这些Connectionid或groupName是从哪里来的。
groupName当然好说了,用户打开页面的时候,我在后台给它一个userModel对象就OK了,代码如下:
public async Task<ActionResult> Index()
{
return await Task.Run(() =>
{
currentGender = currentGender.Equals("男") ? "女" : "男";
if (currentGroup < 3)
{
currentGroup++;
}
else
{
currentGroup = 0;
}
UserModel model = new UserModel()
{
GroupName = $"组{currentGroup}",
Gender = currentGender,
UserName = $"姓名{nameFlag++}"
};
return View(model);
});
}
connectionid是从哪里来的呢?connectionid是唯一标识当前会话的一个字符串,这个字符串长得非常像一个GUID。也就是说,只有当客户端连接上来以后,我才能知道connectionid和userModel的对应关系。然后我可以把这个对应关系存起来(你可以存到数据库或内存,我选择存入redis)。
这样,我就可以很方便地查找到connectionid或groupname了。

浙公网安备 33010602011771号