之前在玩树莓派的时候看到一篇文章,Real Time Baby Monitoring from Raspberry PI using SignalR Streaming and Cognitive Vision Service, 主要内容是一位奶爸,用树莓派的默认的摄像头,监控摇篮中的Baby,通过SingalR,连接Azure的认知服务,如果检测到小孩哭,则提醒,然后可以查看监控。这里面有几个要点,首先是如何从树莓派的摄像头获取图形帧以及如何将树莓派上的图像帧展现出来。这里面最主要的就是涉及到了SignalR的内容,于是我学习了一下,以下是简单的学习笔记,都是参考官方文档的。如果你可以看英文,建议直接看 Introduction to ASP.NET Core SignalR MSDN上的相关文档,我这里只是做个笔记。

什么是SingalR


   ASP.NET Core SignalR类库,他能够够简化开发一些对实时性要求比较高的应用,比如能够将服务端的数据,即时的推送到所有连接的客户端。这让我想到了电子邮件,以前,客户端根本不知道有没有新邮件,你只有主动去刷新,或者定时刷新,这样比较耗资源,而且会对服务端造成压力,如果能在服务端收到邮件后能主动推送到客户端这样体验就更好,当时好像Gmail就是这样做的,这种场景下,就比较适合使用SignalR,如果有如下需求,可以考虑使用SignalR。

  • 需要高频更新的场景,比如游戏,比分看板、投票、地图、GPS应用
  • 一些仪表板和监控引用,比如监控仪表板、即时销售更新信息
  • 需要协作的应用,比如一些共享办公程序,会议等
  • 需要通知提醒的应用,比如社交网络、电子邮件、聊天,游戏中的警报和通知等

    SignalR提供了API能够用来提供服务端到客户端的远程过程调用(RPC)。RPC能用从服务端的.NET Core中调用客户端的JavaScript函数。SignalR通过对之前的技术比如WebSocket,长轮询,服务端事件等进行了封装,并且会自动从中选择最佳的方法来实现传输。这有点像WCF,他对之前的一些RPC方法比如Remoting进行了封装,使得用起来很方便,不需要关注复杂的细节。

SignalR中的关键对象Hub


  SignalR使用对像Hubs来实现客户端和服务端之间的双向数据传输。Hub翻译过来就是集线器,一种网络数据交换设备,用在这里很合适。Hub是一种高级别的管道,可以运行客户端和服务端相互调用的方法。SignalR 自动处理跨计算机边界的调度,使客户端能够调用服务器上的方法,反之亦然。 通过将强类型参数传递给方法,进而使用模型绑定。 SignalR 提供了两个内置的Hub协议:基于 JSON 的文本协议和基于 MessagePack的二进制协议。 与 JSON 相比,MessagePack 通常会创建较小的消息。 较早的浏览器必须支持 XHR 级别 2 ,才能提供 MessagePack 协议支持。
   Hub通过发送包含客户端方法的名称和参数的消息来调用客户端代码。 作为方法参数发送的对象将使用配置的协议进行反序列化。 客户端尝试将名称与客户端代码中的方法匹配, 当客户端找到匹配项时,它将调用方法并向其传递反序列化的参数数据。

一个SignalR聊天程序


    这个例子来自MSDN官方,官方的例子很简单明了,这里简单说一下。首先要建一个ASP.NET Core Web程序,主要作为服务端承载SignalR,当然也可以作为客户端,例子使用的是VS2019和ASP.NET Core 3.1,

  • Step1:新建工程文件:

 

  • Step2:添加客户端SingalR库

  • Step3:编写服务端Hub代码。

    新建Hubs文件夹,在里面添加一个ChatHub类,继承自Hub类,内容如下:

public class ChatHub : Hub
{
    public async Task SendMessage(string user, string message)
    {
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}

    这个就是一个简单聊天程序的群发的关键类了。这个是服务端的逻辑。客户端通过JavaScript(还有其他客户端比如C#,Java等)调用服务端的这个SendMessage发送消息,该方法包含user和message两个参数。方法里面通过Hub基类的Clients对象,All表示所有连接到Hub上的用户,给所有用户的客户端发送信息。该方法第一个参数为JavaScript的表示,相当于客户端需要注册ReceiveMessage消息,这样服务端发送这个方法时,客户端能被调用。上面代码完成之后,还要在Startup里注册相关方法:

public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddSignalR();
}

       以及,定义url地址。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ... 
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
        endpoints.MapHub<ChatHub>("/ChatHub");
    });
}
  • 编写客户端界面和逻辑。

修改Page\Index.cshtml 内容如下:

@page
<div class="container">
    <div class="row">&nbsp;</div>
    <div class="row">
        <div class="col-2">User</div>
        <div class="col-4"><input type="text" id="userInput" /></div>
    </div>
    <div class="row">
        <div class="col-2">Message</div>
        <div class="col-4"><input type="text" id="messageInput" /></div>
    </div>
    <div class="row">&nbsp;</div>
    <div class="row">
        <div class="col-6">
            <input type="button" id="sendButton" value="Send Message" />
        </div>
    </div>
</div>
<div class="row">
    <div class="col-12">
        <hr />
    </div>
</div>
<div class="row">
    <div class="col-6">
        <ul id="messagesList"></ul>
    </div>
</div>
<script src="~/lib/microsoft-signalr/signalr.min.js"></script>
<script src="~/js/chat.js"></script>

  这是个普通的html页面,里面定义了两个输入框,一个按钮,以及用来接收消息的列表,在最先面引入了两个js,一个是signalr.js,这个是官方客户端调用的js类,另外一个是chat.js,这个也是上面按钮对应的逻辑,以及我们调用Hub服务端代码和接收服务端回调的逻辑。在wwwroot\js下面创建chat.js文件,内容如下:

"use strict";

var connection = new signalR.HubConnectionBuilder().withUrl("/ChatHub").build();

connection.on("ReceiveMessage", function (user, message) {
    var msg = message.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
    var encodingMsg = user + " says: " + msg;
    var li = document.createElement("li");
    li.textContent = encodingMsg;
    document.getElementById("messagesList").appendChild(li);
});

connection.start().then(function () {
    document.getElementById("sendButton").disabled = false;
}).catch(function (err) {
    return console.error(err.toString());
});

document.getElementById("sendButton").addEventListener("click", function (event) {
    var user = document.getElementById("userInput").value;
    var message = document.getElementById("messageInput").value;
    connection.invoke("SendMessage", user, message).catch(function (err) {
        return console.error(err.toString());
    });
    event.preventDefault();
});
  • 第一句“use strict;”表示JavaScript的严谨模式。
  • 第二句是建立对服务端Hub的连接。
  • 第三句是注册服务端Hub里面SendAsync方法的回调,connect.on的第一个参数,必须跟SendAsync的第一个方法相同,这样,当Hub里面调用SendAsync方法时,JavaScript里的connection.on后面的function就会执行。这里可以看到,当服务端Hub发送消息的时候,这里接收到user和message,并将其追加到ul列表元素中。
  • 第四句connection.start()是开启对Hub的连接。如果连接成功,则让页面上的“发送”按钮可用。
  • 第五句是注册页面上的“发送”按钮的事件,当按下时,首先获取用户输入的用户名和内容,然后通过connection.invoke调用服务端hub里定义的“SendMessage“方法,这里的第一个参数名字必须跟Hub里定义的方法相同。通过调用服务端的方法,这样就把消息发给所有用户了。

    以上就完成了一个简单的聊天程序。上面这个是JavaScript客户端,现在添加一个.NET的WinForm客户端:

.NET 客户端


   除了可以使用JavaScript作为SignalR客户端来跟服务端Hub相互调用之外,还可以使用C#客户端,和Java客户端,Java不是很会,这里就演示C#客户端。

   首先新建一个Winform应用程序,然后添加对“Microsoft.AspNetCore.SignalR.Client”包的引用。不一定要是.NET Core的,一般的.NET Framework就可以。目前.NET Core还不支持Winform,界面如下:

   

    在ChatForm.cs代码中,逻辑如下:

HubConnection connection;
public Form1()
{
    InitializeComponent();

    connection = new HubConnectionBuilder().WithUrl("https://localhost:44330/ChatHub").Build();
    connection.Closed += async (error) => {

        await Task.Delay(new Random().Next(0, 5) * 1000);
        await connection.StartAsync();
    };

    connection.On<string, string>("ReceiveMessage", (user, message) => {
        this.BeginInvoke((MethodInvoker)delegate {
            var newMessage = $"{user}: {message}";
            messagesList.Items.Add(newMessage);
        });
    });
}

    在构造函数中定义HubConnection客户端对象,并且注册断线重连和服务端回调方法。

    在Connect按钮 和 Send按钮的Click事件,内容如下:

private async void btnConnect_Click(object sender, EventArgs e)
{
    try
    {
        await connection.StartAsync();
        messagesList.Items.Add("Connection Start");
        btnConnect.Enabled = false;
        btnSend.Enabled = true;
    }
    catch (Exception ex)
    {
        messagesList.Items.Add(ex.Message);
    }
}

private async void btnSend_Click(object sender, EventArgs e)
{
    try
    {
        await connection.InvokeAsync("SendMessage", txtUserId.Text, txtUserName.Text);
    }
    catch (Exception ex)
    {
        messagesList.Items.Add(ex.Message);
    }
}

   首先需要调用StartAsync来连上服务端Hub,连上之后才能进行后续的操作。在发送消息中,使用connection.InvokeAsync方法,来远程调用服务端里的Hub方法。这个跟js很类似。

   现在,服务端和两种类型客户端的代码都写好了,是不是看起来很简单😃

运行


   先将服务端承载Hub的ASP.NET Core应用程序运行起来,他既包含了服务端也包含了客户端,我这里开了两个,一个Chrome一个Edge,然后再把WinForm也允许起来,然后再各自的页面输入内容,可以看到,都能收到所有人发送的消息。