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了。

全部的代码路径:https://github.com/tianweimol/SignalRSLN.git

posted @ 2017-09-18 16:54  飞荷扬菊  阅读(188)  评论(0)    收藏  举报