SignalR

SignalR

SignalR 是一个开源库,最初由微软开发,用于简化在 ASP.NET 应用程序中添加实时 Web 功能的过程。它使得服务器端代码能够将内容推送到连接的客户端浏览器或其他设备上,而不需要客户端显式地请求新数据。这种能力被称为“服务器推送”,与传统的基于请求-响应模式的 HTTP 交互形成对比。

SignalR 提供了一个简单的 API 来创建从服务器端 .NET 代码调用客户端(例如浏览器中的 JavaScript 函数)上的远程过程调用 (RPC)。此外,SignalR 还包括了用于管理连接(如连接和断开事件)、分组连接以及广播消息到所有或特定客户端的功能。

SignalR 的特点之一是它会根据运行时环境自动选择最合适的通信协议来实现高效的双向通信。它支持多种传输方式,包括:

  • WebSocket:这是 SignalR 首选的传输方式,因为它提供了全双工通信通道,并且具有较低的延迟。
  • Server-Sent Events (SSE):一种允许服务器向浏览器推送更新的技术。
  • Long Polling:客户端发出一个 HTTP 请求并保持连接打开,直到服务器有数据返回。
  • Forever Frame:主要用于 Internet Explorer 浏览器,通过隐藏的 iframe 实现长连接。

当 WebSocket 可用时,SignalR 会优先使用它;如果 WebSocket 不被支持或不可用,则会降级到其他传输方式以确保兼容性。


使用方式

对于 WebSocket 来说,在现代浏览器中,WebSocket API 是内置的,因此不需要额外导入任何库或依赖项。你可以直接使用 new WebSocket(url) 创建一个 WebSocket 实例,并利用其事件监听器来处理连接、消息接收和发送等操作。

然而,对于 SignalR,情况有所不同。SignalR 提供了一套更高级别的抽象和功能,它不是一个原生的浏览器 API,而是由微软开发的一个库。因此,为了在客户端使用 SignalR,你需要引入 SignalR 客户端库。


配置方式

SignalR可以部署在WebAPI中,也可以单独部署一个独立的即时通讯服务器

Program.cs
// 添加signalR服务
builder.Services.AddSignalR();

// 添加SignalR路由
app.MapHub<MsgHub>("/ChatHub"); // 配置管道终结点
MyHub
using Microsoft.AspNetCore.SignalR;

namespace SignalR;

public class MsgHub:Hub
{
}

触发方式

其实总结下来,触发SignalR由服务端向客户端推送消息的方式有两种

服务端调用

通过依赖注入的形式将hub上下文对象注入到控制器(其实所有层都可以的)

通过上下文对象调用Clients对象,调用其中方法就可以实现向客户端推送数据

这个消息的发起人可以为任意的一个人(其实可以看作是服务端直接发起的消息)

[ApiController]
[Route("[controller]")]
public class SignalRController : ControllerBase
{
    private readonly IHubContext<MsgHub> _hubContext;

    public SignalRController(IHubContext<MsgHub> hubContext)
    {
        _hubContext = hubContext;
    }

    [HttpGet]
    public async Task Send([FromQuery] string msg)
    {
        await _hubContext.Clients.All.SendAsync("ReceiveMessage", msg);
    }
}

从客户端调用

在Client中,调用服务端的Send方法,方法内部逻辑再向Client推送信息

这个消息的发起人为一个Client(可以看作是客户端发起的消息)

public class MsgHub:Hub
{
    // 这里是被客户端调用的服务端方法
    public async Task Send(string message)
    {
        // 这里是被服务端调用的客户端方法
        // 所有人收到
        await Clients.All.SendAsync("ReceiveMessage", message);
        
        // 除调用者外其他人能收到
        await Clients.Others.SendAsync("ReceiveMessage", message);
    }
}

分组

用一个简单的聊天室项目来实现分组功能

  • Context对象:SignalR 提供的一个特定上下文对象
    • ConnectionId:每个连接到 SignalR Hub 的客户端都会被分配一个唯一的标识符,这是用来区分不同客户端的关键信息。
    • UserIdentifier:类似于HttpContext.User.Identity.Name,但它更灵活,默认会尝试使用 ClaimTypes.NameIdentifier 或 ClaimTypes.Name。如果这些声明不存在,它可能会选择其他可用的声明。
  • Groups:Hub类提供的一个静态属性,它提供了对组管理功能的访问。你可以使用它来添加或移除客户端连接到组

在这里,以聊天室名字作为组名,以Context.ConnectionId作客户端的唯一标识符加入组中,在发送消息时,调用Clients.Group对象中方法来实现组内消息发送

public class ChatHub : Hub
{
    /// <summary>
    /// 用户加入聊天室的方法。
    /// </summary>
    /// <param name="roomName">要加入的聊天室名称。</param>
    /// <returns>一个异步任务,表示操作完成。</returns>
    public async Task JoinRoom(string roomName)
    {
        // 将当前连接的客户端添加到指定的组(即聊天室)
        await Groups.AddToGroupAsync(Context.ConnectionId, roomName);

        // 向该组内的所有客户端发送消息,通知某用户已加入房间
        await Clients.Group(roomName).SendAsync("ReceiveMessage", $"{Context.UserIdentifier} has joined the room {roomName}");
    }

    /// <summary>
    /// 用户离开聊天室的方法。
    /// </summary>
    /// <param name="roomName">要离开的聊天室名称。</param>
    /// <returns>一个异步任务,表示操作完成。</returns>
    public async Task LeaveRoom(string roomName)
    {
        // 从指定的组中移除当前连接的客户端
        await Groups.RemoveFromGroupAsync(Context.ConnectionId, roomName);

        // 向该组内的所有客户端发送消息,通知某用户已离开房间
        await Clients.Group(roomName).SendAsync("ReceiveMessage", $"{Context.UserIdentifier} has left the room {roomName}");
    }

    /// <summary>
    /// 向特定聊天室发送消息的方法。
    /// </summary>
    /// <param name="roomName">目标聊天室名称。</param>
    /// <param name="message">要发送的消息内容。</param>
    /// <returns>一个异步任务,表示操作完成。</returns>
    public async Task SendMessageToRoom(string roomName, string message)
    {
        // 向指定组内的所有客户端发送消息
        await Clients.Group(roomName).SendAsync("ReceiveMessage", $"{Context.UserIdentifier}: {message}");
    }
}

客户端代码

<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/6.0.0/signalr.min.js"></script>
<script>
    const connection = new signalR.HubConnectionBuilder()
        .withUrl("/chathub")
        .build();
    
	// 客户端方法ReceiveMessage,由服务端调用
    connection.on("ReceiveMessage", function (message) {
        console.log(message); // 显示收到的消息
    });

    async function start() {
        try {
            await connection.start();
            console.log("Connected to SignalR hub.");
        } catch (err) {
            console.error(err.toString());
        }
    }

    // 加入房间
    async function joinRoom(roomName) {
        await connection.invoke("JoinRoom", roomName);
    }

    // 离开房间
    async function leaveRoom(roomName) {
        await connection.invoke("LeaveRoom", roomName);
    }

    // 发送消息到房间
    async function sendMessageToRoom(roomName, message) {
        await connection.invoke("SendMessageToRoom", roomName, message);
    }

    // 初始化连接
    start().catch(console.error);

    // 示例:假设你想在页面加载时自动加入“general”聊天室
    window.onload = async () => {
        await joinRoom("general");
    };
</script>