欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

ASP.NET Core 聊天室实现(SignalR)

程序员文章站 2023-12-28 17:32:16
...

ASP.NET Core 聊天室实现(SingalR)


本篇文章的开发环境:

  • VS2017
  • .NET Core 2.2
  • js客户端 @aspnet/signalr 1.1.4

SignalR

SignalR是一个开源的库,一个构建实时应用的框架。实时应用可以在服务器中将内容实时推送到客户端中,特别适合聊天应用,股票交易,新闻推送,游戏等等非常多的应用场景。

机制

SignalR的机制其实很像远程过程调用,即RPC,因为SignalR区分服务端和客户端,服务端和客户端分别都要注册相应的函数,然后服务端调用在客户端注册的函数从而实现可以从服务端向客户端推送消息。

中心

SignalR的中心可以理解为是一组通讯接口,在服务端中心上定义的方法将可以被客户端调用,而在客户端中心上的定义的方法将可以被服务端调用。

组和用户

SignalR中组的概念可以理解为微信群或者QQ群,如果我们把不同的连接id添加到一个组里,那么我们可以往这个组里推送信息,那么就只有组里的人能收到推送。

SignalR中的用户又是什么?假设你有多台设备,手机,笔记本,平板等。每台设备都连接到了SignalR的应用当中,那么当SignalR以用户标识为参数来推送那么你所有连接到应用并且有你的标识的设备就都会收到推送。

传输方式

SignalR支持以下的通讯协议:

  1. WebSockets
  2. 服务器发送的事件
  3. 长轮询

SignalR会自动选择在客户端和服务端之间最好的连接方式。最差的情况就是采用长轮询了。

所以该框架本身会帮我们把具体的连接实现隐藏起来,从而提供给使用者完全一致的使用体验。

下面废话不多说,直接上代码。


SignalR代码示例

添加中间件

首先我们需要在Setup.csConfigureServices方法中添加SignalR的中间件:

注意:asp.net core 中已经包含了SignalR的服务端的库,但是js之类的客户端库需要自己通过npm进行下载。

services.AddSignalR();

然后在Configure方法中设置SignalR的路由:

app.UseSignalR(configure =>
    {
        configure.MapHub<ChatHub>("/api/chat");
    });

之后我们就可以通过/api/chat连接到SignalR服务端。

定义中心

下面我们来定义一个中心,这需要继承Hub来创建中心,并向中心添加方法,客户端可以调用标识符为public的方法:

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

客户端代码

由于大部分的场景都是使用的浏览器,所以我们这里就使用js的客户端(还有java,.net客户端),可以通过npm命令下载@aspnet/signalr这个包

npm install @aspnet/signalr

下载后我们需要把路径node_modules\@aspnet\signalr\dist\browser中的signalr.js文件复制到前端项目的输出目录。

然后就可以正常编写js代码了。

下面的代码就是在在客户端连接到服务端中心,并且定义一个客户端的中心方法:

//创建SignalR连接对象
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/api/chatHub")
    .build();

//定义方法
connection.on("ReceiveMessage", function (user, message) {
    //收到推送的具体实现,user,message就是服务器推送来的信息,可以更新显示到前端页面中。
});

//连接到服务器中心
connection.start().then(function(){
    //连接成功
}).catch(function (err) {
    //连接异常err
});

好了,那该做的工作都做好了,那么这个聊天室具体如何工作呢?

我们先来考虑一下前端页面发送信息如何处理,假设我们在前端页面输入一段信息,按发送按钮发送出去,前端代码如下:

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) {
        //出现异常
    });
});

我们可以看到这里给sendButton绑定了一个单击事件,事件内容就是获取输入的用户名和信息,然后调用我们在上一段代码中的connection对象,该对象有一个invoke方法,第一个参数就是服务器上的SignalR中心所定义的方法SendMessage,之后的参数对应的就是服务器中心上的参数列表。这里实际上就是调用了服务器上所定义的方法。

那我们看到服务器上面这个方法,就是这一段:

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

可以看到服务器上的SendMessage方法是对所有的已连接到服务器上SignalR的客户端都调用客户端的ReceiveMessage方法,并且传递两个参数,一个用户名一个信息。在客户端中心中定义的ReceiveMessage将会被执行,并且传入两个参数。

所以这就实现了在一个客户端点击发送按钮,所有的客户端都收到了推送。

SignalR进阶内容

点对点发送

上面的例子中是客户端点击发送按钮后就推送到所有的客户端去了。我们知道推送到单个客户端也是可能能够实现的,那么如何实现呢?

我们定义在服务器上的中心是需要继承Hub,而Hub中包含了三个属性,分别是

类型 属性名称 作用
IHubCallerClients Clients 客户端选择
HubCallerContext Context 连接上下文,连接id等
IGroupManager Groups 组管理

所以也就是我们上面看到的我们能够访问Clients的原因,要实现点对点发送,我们就可以用

Clients.Client(connectionId).SendAsync("ReceiveMessage", user, message);

来实现指向特定连接id来进行发送。

注意:连接id是不能够在客户端代码中获取的,微软也不建议暴露连接id,所以一般我们可以在用户登陆时在服务端获取连接id,手动把用户名或者其他用户标识关联该连接id。连接id可以在Hub类中的Context属性中获取。

其他客户端选择方式

上面的是点对点的客户端选择,如果我们看Hub类里其实还有很多的选择方式:

属性或方法名称 作用
Caller 调用者
Others 除了调用者以外
OthersInGroup 除了指定组以外
All 所有连接的客户端
AllExcept 除了一组指定的连接id以外
Client 指定某一连接id
Clients 指定多个连接id
Group 指定某一组
GroupExcept 指定某一组,并且除开指定的多个连接id
Groups 指定多个组
User 指定某一用户
Users 指定多个用户

服务端推送

上面的例子我们是聊天室所以是由客户端触发,并且发送到客户端的。那么如果想实现服务端自动推送该如何实现呢?

我们可以通过构造函数注入的方式来获取一个IHubContext<ChatHub>实例,如下:

public class SomeController : Controller
{
    private readonly IHubContext<ChatHub> _chatHub;

    public SomeController(IHubContext<ChatHub> chatHub)
    {
        _chatHub = chatHub;
    }
}

然后可以在控制器中调用_chatHub

public async Task<IActionResult> Index()
{
    await _hubContext.Clients.All.SendAsync("ReceiveMessage", "user", "someMessage");
    return View();
}

强类型中心

我们之前所定义的中心是继承Hub这个类,我们调用客户端上注册的方法是通过SendAsync("ReceiveMessage", user, message);方法指定一个客户端上方法的名称来进行调用的,这样有可能会因为方法拼写错误而造成方法调用失败。

所以为了代替Hub,我们可以引入强类型中心Hub<T>来实现,把客户端的方法放到一个接口中:

public interface IChatClient
{
    ReceiveMessage(user,message);
}

然后再继承Hub<IChatClient>

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

这样经过泛型来启用编译时来对名称进行检查可以防止因为拼写错误而导致的很多问题。

如何创建组?我们留意到Hub类中有一个IGroupManager类型的名为Groups的属性,通过该属性可以添加某个连接id到组,或者从组里移除某连接id。

添加某连接id到组:

await Groups.AddToGroupAsync(ConnectionId, groupName);

移除某连接id:

await Groups.RemoveFromGroupAsync(ConnectionId, groupName);

对某个组推送信息,可以按照前面的客户端选择来选择组然后发送。

注意:重新建立连接后不会保留组成员的身份(可能因为连接id不一样),所以重新连接后需要重新加入组。

自定义类型参数

微软推荐采用自定义类型的参数作为服务端和客户端之间数据的交换。我们之前定义的中心方法是采用的两个形参,如果以后服务端增加一个参数,三个形参,那么客户端如果还没修改过来继续传递两个实参过来,那么就会报错。

而如果用自定义类型如下:

public class ChatParam
{
    User{get;set;}
    Message{get;set;}

    Other{get;set;}
}

就算后面服务端继续添加了一个参数,如果客户端继续发送两个参数过来,那么第三个参数也只是null,而不会报错,保证了系统的向后兼容性。

跨域

如果请求存在跨域,那么需要在Setup.cs里开启允许跨域请求客户端才能够连接上SignalR

总结

上面就是SignalR这个实时框架的一些使用入门了,总的来说该框架隐藏了很多的细节,而且学习曲线很平滑,挺轻量的一个库。性能方面我没有做过多的测试所以性能方面的表现就不太清楚了。


喜欢这篇文章的话关注我的公众号吧,有空就分享一下技术文章,哈哈哈。
ASP.NET Core 聊天室实现(SignalR)

上一篇:

下一篇: