1. SignalR

1.1. SignalR 简介

ASP.NET SignalR 是 ASP.NET 开发人员的库,可简化将实时 web 功能添加到应用程序的过程。

实时 web 功能使服务器代码能够在可用时立即将内容推送到连接的客户端,而不是让服务器等待客户端请求新的数据。

SignalR 可用于将任何种类的 "实时" web 功能添加到 ASP.NET 应用程序。

通常以【聊天通讯】作为示例,其实可以做更多操作,比如监视应用程序、协作应用程序(如同步编辑文档)、作业进度更新和实时窗体

SignalR 提供了一个简单的 API,用于创建从服务器端 .NET 代码调用客户端浏览器中的JS函数。

SignalR 还包括用于连接管理的 API (例如,连接和断开连接事件)以及对连接进行分组。

SignalR 自动处理连接管理,让你可同时向所有连接的客户端广播消息,就像聊天室一样。

SignalR 支持 "服务器推送" 功能,在此功能中,服务器代码可以使用远程过程调用(RPC),而不是 web 上常见的请求-响应模型,在浏览器中调用客户端代码。

1.2. SignalR和WebSocket

1.2.1. WebSocket

WebSocket是一种在单个TCP连接上进行全双工通信的协议。

WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。

在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

1.2.2. 对比

SignalR 使用新的 WebSocket 传输(如果可用),并在必要时回退到较旧的传输(前端ajax轮询)。

当然,可以直接使用 WebSocket 编写应用,但使用 SignalR 意味着你需要实现的很多额外功能都已完成。

最重要的是,这意味着您可以对应用程序进行编码,以便利用 WebSocket,而不必担心为较旧的客户端创建单独的代码。

1.3. 通信模型

SignalR 的实现机制与.NET WCF 或 Remoting 是相似的,都是使用远程代理来实现。

在具体使用上,有两种不同通信模型:PersistentConnection 和 Hubs

通信模型 说明
Persistent Connections Persistent Connections表示一个发送单个,编组,广播信息的简单终结点。开发人员通过使用持久性连接Api,直接访问SignalR公开的底层通信协议。
Hubs Hubs是基于连接Api的更高级别的通信管道,它允许客户端和服务器上彼此直接调用方法,SignalR能够很神奇地处理跨机器的调度,使得客户端和服务器端能够轻松调用在对方端上的方法。使用Hub还允许开发人员将强类型的参数传递给方法并且绑定模型

SignalR 将整个连接,信息交换过程封装得非常漂亮,客户端与服务器端全部使用 JSON 来交换数据。

1.3.1. Connection连接

创建MVC项目

Connection连接的方式和Hub集线器非常类似,创建过程如下:

持久连接类

添加连接类

修改默认生成的【ChatConnection.cs】如下:

public class ChatConnection : PersistentConnection
{
    /// <summary>
    /// 当前连接用户数
    /// </summary>
    static int _connectionCount = 0;

    protected override Task OnConnected(IRequest request, string connectionId)
    {
        /*
        Interlocked 为多个线程共享的变量提供原子操作,保障多线程操作时同步
        Increment() 用于递增,同样的也有 Decrement() 方法
        https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.interlocked?view=netframework-4.8
        */
        Interlocked.Increment(ref _connectionCount);
        // 广播消息
        Connection.Broadcast($"新的连接加入,连接ID:{connectionId},已有连接数:{_connectionCount}");
        return Connection.Send(connectionId, $"双向连接成功,连接ID:{connectionId}");
    }

    protected override Task OnDisconnected(IRequest request, string connectionId, bool stopCalled)
    {
        Interlocked.Decrement(ref _connectionCount);
        return Connection.Broadcast($"{connectionId} 退出连接,也有连接数:{_connectionCount}");
    }

    protected override Task OnReceived(IRequest request, string connectionId, string data)
    {
        var message = $"{connectionId} 发送内容>> {data}";

        return Connection.Broadcast(data);
    }
}

启动路由注册

新增【Owin Startup】类,进行路由注册

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.MapSignalR<ChatConnection>("/Connections/ChatConnection");
    }
}

组件更新

由于默认添加的SignalR版本并不是最新的,在升级jQuery版本时,也需要一并升级SignalR。

打开项目的 "Nuget程序包" 管理器,对【jQuery】和【Microsoft.AspNet.SignalR】进行更新

界面处理

新增Home控制器,在默认Index视图中修改代码如下:

@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <title>SingnalR持久连接类 Demo</title>
</head>
<body>
    <h1>SignalR永久连接类 Demo</h1>
    <div>
        <input type="text" id="message" placeholder="请输入消息内容..." />
        <button id="btnSend">Send</button>
        <ul id="discussion"></ul>
    </div>

    <script src="~/Scripts/jquery-3.4.1.min.js"></script>
    <script src="~/Scripts/jquery.signalR-2.4.1.min.js"></script>
    <script>
        const conn = $.connection('/Connections/ChatConnection');
        conn.logging = true;

        // 客户端接收消息处理
        conn.received(function (data) {
            $('#discussion').append(`<li>${data}</li>`);
        });

        // 连接错误处理
        conn.error(function (err) {
            console.error(`与服务器连接异常:${err}`);
        });

        conn.start().done(function () {
            $('#btnSend').click(function () {
                // 向服务端发送消息
                conn.send($('#message').val());
            });
        });

    </script>
</body>
</html>

建立持久连接

通过多个浏览器进行测试,关闭浏览器标签会同步显示对应connection退出连接。

需要注意的是,如果是刷新页面,需要一定的时间才能响应退出连接。//TODO

1.3.2. Hub聊天应用

应用程序

在 Visual Studio 中,创建一个 ASP.NET Web 应用程序,选择空模板,如下所示:

Hub集线器

在解决方案资源管理器中,右键单击项目,然后选择 "添加 > 新项",选择" SignalR Hub 类(v2) "

添加SignalR类后,vs会自动将相关程序集引用至该项目,可以从引用中查看新增了SignalR相关的程序集。

将新的ChatHub.cs类文件中的代码替换为以下代码:

using System;
using System.Web;
using Microsoft.AspNet.SignalR;
namespace SignalRChat
{
    public class ChatHub : Hub
    {
        public void Send(string name, string message)
        {
            // Call the broadcastMessage method to update clients.
            Clients.All.broadcastMessage(name, message);
        }
    }
}

Send 方法演示了几个中心概念:

  • 在中心声明公共方法,使客户端可以调用它们。
  • 使用 Microsoft.AspNet.SignalR.Hub.Clients 动态属性与连接到此集线器的所有客户端通信。
  • 在客户端上调用一个函数(如 broadcastMessage 函数)以更新客户端。

注册SignalR

在 "添加新项"-SignalRChat选择 "安装 > Visual C# > Web ",然后选择 " OWIN Startup 类",如下图所示:

将Startup类【Configuration】方法中添加SignalR的注册代码app.MapSignalR();,类如下所示:

[assembly: OwinStartup(typeof(SignalRChat.Startup))]
namespace SignalRChat
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            // 注册 SignalR 中间件
            app.MapSignalR();
        }
    }
}

组件更新

由于默认添加的SignalR版本并不是最新的,在升级jQuery版本时,也需要一并升级SignalR。

打开项目的 "Nuget程序包" 管理器,对【jQuery】和【Microsoft.AspNet.SignalR】进行更新

Hub前端调用

在项目中新增html页面【chat.html】:

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>聊天室</title>
    <meta charset="utf-8" />
</head>
<body>
    <div class="container">
        <input type="text" id="message" />
        <input type="button" id="sendmessage" value="Send" />
        <input type="hidden" id="displayname" />
        <ul id="discussion"></ul>
    </div>

    <script src="Scripts/jquery-3.4.1.min.js"></script>
    <script src="Scripts/jquery.signalR-2.4.1.min.js"></script>
    <script src="signalr/hubs"></script>

    <script>
        $(function () {
            // 创建服务端hub引用的代理
            var chat = $.connection.chatHub;
            // 创建客户端的回调方法,broadcastMessage方法由服务端远程调用,将消息广播至每个客户端
            chat.client.broadcastMessage = (name, message) => {
                $('#discussion').append(`
                    <li>
                    <strong>${name}</strong>:
                    <em>${message}</em>
                    </li>
                    `);
            };

            // 设置隐藏域的名称,通过prompt弹框输入
            $('#displayname').val(prompt('Enter your name:'));
            $('#message').focus();

            // 打开连接后进行发送事件的注册
            $.connection.hub.start().done(function () {
                $('#sendmessage').click(() => {
                    // 远程调用hub上的Send方法
                    chat.server.send($('#displayname').val(), $('#message').val());
                    $('#message').val('').focus();
                });

            });
        })
    </script>
</body>
</html>

代码示例显示了如何使用 SignalR jQuery 库与 SignalR 中心通信。

运用代理的思想,chat对象用于代理服务端的chatHub集线器。

var chat = $.connection.chatHub;

在 JavaScript 中,对服务器类及其成员的引用必须是 camelCase。

此代码示例引用C#中的ChatHub类作为 chatHub,同样的【ChatHub.cs】类中Send()方法在js中对应chat.server.send()

多浏览器测试

设置【chat.html】为起始页面,开启多浏览器进行聊天测试:

MVC中Hub应用

在MVC项目中Hub应用类似,在MVC模板中添加Owin启动类【Startup】,注册集线器的连接。

具体可以参考微软案例:

教程: SignalR 2 和 MVC 5 的实时聊天

1.4. 通信案例

1.4.1. 一对一通信

新建一个空的ASP.NET Mvc项目,取名为:SignalROneToOne。

添加Hub集线器,命名为:【OneToOneHub.cs】。

关于SignalR组件更新过程省略。

以下给出关键步骤

用户实体类

在Model文件夹中创建用户实体类【User】:

/// <summary>
/// 用户实体类
/// </summary>
public class User
{
    /// <summary>
    /// 连接ID
    /// </summary>
    public string ConnectionID { get; set; }

    public string Name { get; set; }

    public User(string name, string connectionId)
    {
        Name = name;
        ConnectionID = connectionId;
    }
}

OneToOne集线器

整体实现思路:

  1. 先实现在线用户列表的展示
  2. 实现聊天窗口的添加
  3. 聊天消息的发送和接收,接收方默认自动打开聊天窗口

【OneToOneHub】集线器代码如下:

/// <summary>
/// 点对点聊天
/// </summary>
[HubName("chat")]
public class OneToOneHub : Hub
{
    /// <summary>
    /// 在线用户列表
    /// </summary>
    public static List<User> userList = new List<User>();

    /// <summary>
    /// 重写连接事件
    /// </summary>
    /// <returns></returns>
    public override Task OnConnected()
    {
        // 根据connectionId判断用户是否已在用户列表中
        var user = userList.FirstOrDefault(t => t.ConnectionId == Context.ConnectionId);
        if (null == user)
        {
            // 首次建立连接时无姓名信息,需要后面再设置姓名
            user = new User("", Context.ConnectionId);
            userList.Add(user);
        }

        return base.OnConnected();
    }

    /// <summary>
    /// 重写断开连接事件
    /// </summary>
    /// <param name="stopCalled"></param>
    /// <returns></returns>
    public override Task OnDisconnected(bool stopCalled)
    {
        // 断开连接后从用户列表移除
        var user = userList.FirstOrDefault(t => t.ConnectionId == Context.ConnectionId);
        if (null != user)
        {
            userList.Remove(user);
        }

        UpdateOnlineUser();

        return base.OnDisconnected(stopCalled);
    }

    /// <summary>
    /// 更新在线用户列表中用户姓名
    /// </summary>
    /// <param name="name"></param>
    public void UpdateUserName(string name)
    {
        var currentUser = userList.FirstOrDefault(t => t.ConnectionId == Context.ConnectionId);
        if (null != currentUser)
        {
            // 更新姓名
            currentUser.Name = name;
        }
        UpdateOnlineUser();
    }

    /// <summary>
    /// 通知所有客户端更新 在线用户列表
    /// </summary>
    private void UpdateOnlineUser()
    {
        var list = userList.Select(t => new { t.Name, t.ConnectionId }).ToList();

        string jsonUsers = JsonConvert.SerializeObject(list);
        Clients.All.updateOnlineUser(jsonUsers);
    }

    /// <summary>
    /// 发送消息
    /// </summary>
    /// <param name="targetConnId">对方ConnectionId</param>
    /// <param name="message">消息内容</param>
    public void SendMessage(string targetConnId, string message)
    {
        var targetUser = userList.FirstOrDefault(t => t.ConnectionId == targetConnId);
        var currentUser = userList.FirstOrDefault(t => t.ConnectionId == Context.ConnectionId);

        if (null != targetUser && null != currentUser)
        {
            // 发给接收方
            Clients.Client(targetConnId).addMessage(message, currentUser.Name, currentUser.ConnectionId);

            // 给自己发送,表示消息已送达
            Clients.Client(Context.ConnectionId).addMessage(message, targetUser.Name, targetUser.ConnectionId);
        }
    }

}

用户列表及聊天窗口

前端页面代码如下:

@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <title>一对一聊天</title>
    <style>
        div.chat-window {
            margin: 5px 0;
            width: 700px;
            border: 1px solid red;
        }
    </style>
</head>
<body>
    <h2>开始寻找好友聊天吧</h2>

    <div>
        <div>用户名称:【<strong id="userName"></strong></div>
        <div>当前连接id:【<label id="connId"></label></div>

        <div style="width:600px;border:1px solid #007acc">
            <div>在线用户列表:</div>
            <ul id="onlineUser"></ul>
        </div>

        <div id="discussion">
        </div>
    </div>

    <script src="~/Scripts/jquery-1.10.2.min.js"></script>
    <script src="~/Scripts/jquery.signalR-2.1.2.min.js"></script>
    <script src="~/signalr/hubs"></script>
    <script>
        // 当前用户
        var currentUser = {
            name: '',
            connectionId: ''
        };
        var chat;

        $(function () {
            chat = $.connection.chat;
            registerClientWork();

            // done() 建立连接成功回调
            $.connection.hub.start().done(function () {
                // 连接成功就有 connectionid,和服务端 Context.ConnectionId 一致
                currentUser.connectionId = $.connection.hub.id;
                currentUser.name = prompt('enter your name:');

                $('#userName').text(currentUser.name);
                $('#connId').text(currentUser.connectionId);

                // 通知服务端更新当前用户名
                chat.server.updateUserName(currentUser.name);
            });
        })

        // 注册客户端方法
        function registerClientWork() {
            // 当有用户接入(更新名称时)或者断开时需要更新列表
            chat.client.updateOnlineUser = function (data) {
                let userArray = JSON.parse(data);
                if (userArray.length > 0) {
                    $('#onlineUser').empty();
                    for(let uu of userArray) {
                        let chatButton = '';
                        if (uu.ConnectionId != currentUser.connectionId) {
                            // 当前用户不显示聊天按钮
                            chatButton = `<button class="chat-button"
                                data-connid="${uu.ConnectionId}"
                                data-name="${uu.Name}" >聊天</button>`;
                        }
                        $('#onlineUser').append(`<li>用户名:【${uu.Name}${chatButton}</li>`);
                    }
                }
            }

            // 收到服务端消息,插入到聊天窗口中显示
            chat.client.addMessage = function (message, targetName, targetConnId) {
                //debugger;
                // 在聊天窗口中根据 targetConnId 寻找聊天窗口,找不到则创建
                if ($(`#${targetConnId}`).length == 0) {
                    showChatWindow(targetConnId, targetName);
                }
                $(`#${targetConnId}`).find('ul.chat-content').append(`<li>${message}</li>`);
            }

            bindChat();
            bindMessageSend();
        }

        // 委托方式绑定聊天事件
        function bindChat() {
            $('#onlineUser').on('click', 'button.chat-button', function (e) {
                // debugger;
                let targetConnId = $(e.target).attr('data-connid');
                let targetName = $(e.target).attr('data-name');
                console.log('targetConnId:' + targetConnId);
                showChatWindow(targetConnId, targetName);
            });
        }

        // 新增聊天窗口
        function showChatWindow(connid, uname) {
            // 避免重复创建聊天窗口
            if ($(`#${connid}`).length > 0) {
                return;
            }
            let html = `
                <div class="chat-window" id="${connid}">
                <div><span>正在与【${uname}】聊天中...</span></div>
                <ul class="chat-content"></ul>
                <div>
                    <input type="text" />
                    <button data-connid="${connid}" class="btn-send">send</button>
                </div>
                </div>
                `;
            $('#discussion').append(html);
        }

        // 绑定消息发送事件
        function bindMessageSend() {
            $('#discussion').on('click', 'button.btn-send', function (e) {
                let targetConnId = $(e.target).attr('data-connid');
                let message = $(e.target).prev().val();
                message = `${currentUser.name}${message}`;
                console.log(`targetConnId:${targetConnId},msg:${message}`);
                // 调用服务端方法,指定对方connid发送消息
                chat.server.sendMessage(targetConnId, message);
                // 清空输入框
                $(e.target).prev().val('');
            });
        }

    </script>
</body>
</html>

聊天演示

results matching ""

    No results matching ""