.NET8使用SignalR实现实时通信的完整指南

 更新时间:2025年12月31日 09:19:42   作者:幌才_loong  
SignalR 是一个强大的实时通信库,能够在服务器和客户端之间建立双向通信,本文将详细介绍如何在 .NET 8 中使用 SignalR并深入探讨 SignalR 在使用通信技术的顺序

SignalR 是一个强大的实时通信库,能够在服务器和客户端之间建立双向通信。本文将详细介绍如何在 .NET 8 中使用 SignalR,包括服务端配置、CORS 设置、前端调用以及 WinForms 客户端实现,并深入探讨 SignalR 在使用通信技术的顺序:WebSocket -> Server-Sent Events (SSE)。

一、服务端实现

1. 安装 SignalR 包

在项目中安装 SignalR 核心包:

dotnet add package Microsoft.AspNetCore.SignalR

2. 配置 SignalR 与 CORS

由于 SignalR 可能涉及跨域请求,需要在 Program.cs 中配置 CORS:

var builder = WebApplication.CreateBuilder(args);

// 添加 SignalR 服务
builder.Services.AddSignalR();

// 配置 CORS
builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowAll", policy =>
    {
        policy.WithOrigins("https://localhost:7100") // 允许的前端地址
              .AllowAnyHeader()
              .AllowAnyMethod()
              .AllowCredentials(); // 允许携带认证信息(SignalR 必需)
    });
});

var app = builder.Build();

// 使用 CORS
app.UseCors("AllowAll");

// 映射 SignalR Hub
app.MapHub<ChatHub>("/chatHub");

app.Run();

3. 创建 Hub 类

创建一个继承自 Hub 的类来处理客户端连接和消息:

using Microsoft.AspNetCore.SignalR;
using System.Collections.Concurrent;

namespace SignalRDemo;

public class ChatHub : Hub
{
    // 存储用户与连接ID的映射
    private static readonly ConcurrentDictionary<string, string> _userConnections = new();
    // 存储群组与连接ID的映射
    private static readonly ConcurrentDictionary<string, HashSet<string>> _groupConnections = new();

    /// <summary>
    /// 客户端连接时触发
    /// </summary>
    public override async Task OnConnectedAsync()
    {
        Console.WriteLine($"客户端 {Context.ConnectionId} 已连接");
        await base.OnConnectedAsync();
    }

    /// <summary>
    /// 客户端断开连接时触发
    /// </summary>
    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        Console.WriteLine($"客户端 {Context.ConnectionId} 已断开连接");

        // 从用户映射中移除
        var user = _userConnections.FirstOrDefault(kv => kv.Value == Context.ConnectionId).Key;
        if (!string.IsNullOrEmpty(user))
        {
            _userConnections.TryRemove(user, out _);
        }

        // 从所有群组中移除
        foreach (var group in _groupConnections.Keys)
        {
            _groupConnections[group].Remove(Context.ConnectionId);
        }

        await base.OnDisconnectedAsync(exception);
    }

    /// <summary>
    /// 用户登录(绑定用户名和连接ID)
    /// </summary>
    public async Task Login(string username)
    {
        _userConnections[username] = Context.ConnectionId;
        await Clients.Caller.SendAsync("LoginSuccess", $"你已登录为 {username}");
    }

    /// <summary>
    /// 加入群组
    /// </summary>
    public async Task JoinGroup(string groupName)
    {
        if (!_groupConnections.ContainsKey(groupName))
        {
            _groupConnections[groupName] = new HashSet<string>();
        }
        _groupConnections[groupName].Add(Context.ConnectionId);

        await Clients.Caller.SendAsync("JoinGroupSuccess", $"你已加入群组 {groupName}");
    }

    /// <summary>
    /// 发送消息给所有人
    /// </summary>
    public async Task SendMessageToAll(string user, string message)
    {
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }

    /// <summary>
    /// 发送消息给单个人
    /// </summary>
    public async Task SendMessageToUser(string targetUser, string user, string message)
    {
        if (_userConnections.TryGetValue(targetUser, out var connectionId))
        {
            await Clients.Client(connectionId).SendAsync("ReceiveMessage", user, message);
        }
        else
        {
            await Clients.Caller.SendAsync("Error", $"用户 {targetUser} 不在线");
        }
    }

    /// <summary>
    /// 发送消息给多个人
    /// </summary>
    public async Task SendMessageToUsers(IReadOnlyList<string> targetUsers, string user, string message)
    {
        var connectionIds = new List<string>();
        foreach (var targetUser in targetUsers)
        {
            if (_userConnections.TryGetValue(targetUser, out var connectionId))
            {
                connectionIds.Add(connectionId);
            }
        }

        if (connectionIds.Any())
        {
            await Clients.Clients(connectionIds).SendAsync("ReceiveMessage", user, message);
        }
        else
        {
            await Clients.Caller.SendAsync("Error", "没有目标用户在线");
        }
    }

    /// <summary>
    /// 发送消息给群组
    /// </summary>
    public async Task SendMessageToGroup(string groupName, string user, string message)
    {
        if (_groupConnections.TryGetValue(groupName, out var connections))
        {
            if (connections.Any())
            {
                await Clients.Clients(connections).SendAsync("ReceiveMessage", user, message);
            }
            else
            {
                await Clients.Caller.SendAsync("Error", $"群组 {groupName} 没有成员");
            }
        }
        else
        {
            await Clients.Caller.SendAsync("Error", $"群组 {groupName} 不存在");
        }
    }
}

二、前端调用(浏览器)

1. 安装 SignalR 客户端

在前端项目中安装 SignalR 客户端库:

npm install @microsoft/signalr

2. 前端代码示例

创建一个简单的 HTML 页面,使用 JavaScript 连接 SignalR:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>SignalR Chat</title>
</head>
<body>
    <input type="text" id="username" placeholder="请输入用户名" />
    <button onclick="login()">登录</button>
    <br />
    <input type="text" id="message" placeholder="请输入消息" />
    <button onclick="sendToAll()">发送给所有人</button>
    <button onclick="sendToUser()">发送给单个人</button>
    <button onclick="sendToGroup()">发送给群组</button>
    <ul id="messages"></ul>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/7.0.0/signalr.min.js"></script>
    <script>
        let connection;
        let username;

        // 连接 SignalR
        function connect() {
            connection = new signalR.HubConnectionBuilder()
                .withUrl("https://localhost:7138/chatHub") // 后端地址
                .build();

            // 接收消息
            connection.on("ReceiveMessage", (user, message) => {
                const li = document.createElement("li");
                li.textContent = `${user}: ${message}`;
                document.getElementById("messages").appendChild(li);
            });

            // 连接成功事件
            connection.start().then(() => {
                console.log("已连接到 SignalR 服务端");
            }).catch(err => {
                console.error(err.toString());
            });
        }

        // 登录
        function login() {
            username = document.getElementById("username").value;
            connection.invoke("Login", username).catch(err => {
                console.error(err.toString());
            });
        }

        // 发送给所有人
        function sendToAll() {
            const message = document.getElementById("message").value;
            connection.invoke("SendMessageToAll", username, message).catch(err => {
                console.error(err.toString());
            });
        }

        // 发送给单个人
        function sendToUser() {
            const targetUser = prompt("请输入目标用户名:");
            const message = document.getElementById("message").value;
            connection.invoke("SendMessageToUser", targetUser, username, message).catch(err => {
                console.error(err.toString());
            });
        }

        // 发送给群组
        function sendToGroup() {
            const groupName = prompt("请输入群组名称:");
            const message = document.getElementById("message").value;
            connection.invoke("SendMessageToGroup", groupName, username, message).catch(err => {
                console.error(err.toString());
            });
        }

        // 页面加载时连接
        connect();
    </script>
</body>
</html>

三、WinForms 客户端调用

1. 安装 SignalR 客户端包

dotnet add package Microsoft.AspNetCore.SignalR.Client

2. WinForms 客户端代码

在窗体中添加以下控件:

  • TextBox:用于输入用户名
  • TextBox:用于输入消息
  • Button:登录按钮
  • Button:发送给所有人按钮
  • ListBox:显示接收的消息

然后编写代码:

using Microsoft.AspNetCore.SignalR.Client;
using System;
using System.Windows.Forms;

namespace SignalRWinFormsClient;

public partial class Form1 : Form
{
    private HubConnection _connection;
    private string _username;

    public Form1()
    {
        InitializeComponent();
    }

    private async void Form1_Load(object sender, EventArgs e)
    {
        // 连接 SignalR
        _connection = new HubConnectionBuilder()
            .WithUrl("https://localhost:7138/chatHub")
            .Build();

        // 接收消息
        _connection.On<string, string>("ReceiveMessage", (user, message) =>
        {
            Invoke(new Action(() =>
            {
                listBoxMessages.Items.Add($"{user}: {message}");
            }));
        });

        try
        {
            await _connection.StartAsync();
            listBoxMessages.Items.Add("已连接到 SignalR 服务端");
        }
        catch (Exception ex)
        {
            listBoxMessages.Items.Add($"连接失败:{ex.Message}");
        }
    }

    private async void btnLogin_Click(object sender, EventArgs e)
    {
        _username = txtUsername.Text;
        try
        {
            await _connection.InvokeAsync("Login", _username);
            listBoxMessages.Items.Add($"你已登录为 {_username}");
        }
        catch (Exception ex)
        {
            listBoxMessages.Items.Add($"登录失败:{ex.Message}");
        }
    }

    private async void btnSendToAll_Click(object sender, EventArgs e)
    {
        var message = txtMessage.Text;
        try
        {
            await _connection.InvokeAsync("SendMessageToAll", _username, message);
            txtMessage.Clear();
        }
        catch (Exception ex)
        {
            listBoxMessages.Items.Add($"发送失败:{ex.Message}");
        }
    }
}

四、SignalR 通信技术顺序

SignalR 在建立连接时,会根据浏览器和服务器的支持情况,自动选择最佳的通信技术。其顺序如下:

1. WebSocket

  • 优先级最高,是 SignalR 的首选通信方式。
  • 全双工通信,性能最好,延迟最低。
  • 适用于大多数现代浏览器(Chrome、Firefox、Edge、Safari 等)。
  • 使用标准的 WebSocket 协议(ws://wss://)。

2. Server-Sent Events (SSE)

  • 如果 WebSocket 不可用(如浏览器不支持或被防火墙阻止),SignalR 会退而求其次使用 SSE。
  • 单向通信,服务器可以实时向客户端推送数据,但客户端无法向服务器推送数据。
  • 基于 HTTP 协议,使用 EventSource API。
  • 适用于需要实时更新但不需要双向通信的场景。

3. 长轮询 (Long Polling)

  • 如果 SSE 也不可用,SignalR 会使用长轮询作为最后的 fallback。
  • 客户端向服务器发送一个请求,服务器保持连接打开,直到有数据要发送或超时。
  • 超时后,客户端会立即发送一个新的请求。
  • 性能较差,但兼容性最好,适用于所有浏览器。

通信技术选择流程

客户端发送一个 HTTP 请求到服务器的 /chatHub/negotiate 端点,获取可用的通信技术列表。

客户端按照优先级顺序尝试使用这些技术:

  • 首先尝试 WebSocket。
  • 如果 WebSocket 失败,尝试 SSE。
  • 如果 SSE 也失败,使用长轮询。

一旦成功建立连接,就使用该技术进行实时通信。

技术对比

技术双向通信性能兼容性
WebSocket现代浏览器
SSE否(服务器到客户端)大多数现代浏览器
长轮询所有浏览器

五、注意事项

  • CORS 配置:必须在服务端正确配置 CORS,允许客户端来源并启用 AllowCredentials
  • 连接管理:使用 OnConnectedAsyncOnDisconnectedAsync 管理客户端连接状态。
  • 用户映射:维护用户与 ConnectionId 的映射,以便实现单人和多人消息发送。
  • 群组管理:使用 Groups 类或自定义映射管理群组。
  • 错误处理:客户端和服务端都需要处理连接中断和异常情况。
  • 扩展性:如果需要支持大规模部署,可以使用 Redis 作为 SignalR 的后端,实现多服务器之间的消息共享。

通过本文的指南,你可以快速在 .NET 8 中使用 SignalR 实现实时通信,并支持多种消息发送方式。无论是前端浏览器还是 WinForms 客户端,都能轻松接入 SignalR 服务。

以上就是.NET8使用SignalR实现实时通信的完整指南的详细内容,更多关于.NET8 SignalR实时通信的资料请关注脚本之家其它相关文章!

相关文章

最新评论