发布于2026-07-23 阅读(0)
扫一扫,手机访问
SignalR 是实时通信领域的老牌选手了,它能在服务器和客户端之间搭建起双向通信的桥梁。这篇文章会带你在 .NET 8 里把 SignalR 跑起来,从服务端配置、CORS 设置,到前端浏览器调用、WinForms 客户端实现,全流程走一遍。顺便还会深入聊聊 SignalR 选择通信技术的那套逻辑——WebSocket → Server-Sent Events (SSE) 的优先级顺序,到底是怎么一回事。
先把核心包请进项目:
dotnet add package Microsoft.AspNetCore.SignalR
跨域问题在 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");
app.Run();
注意 AllowCredentials() 这一行——SignalR 默认要求带上认证信息,少了它连接会报错。
创建一个继承自 Hub 的类,用来处理客户端连接、消息收发这些核心逻辑。下面这个 ChatHub 实现了用户登录、群组管理、以及多种消息发送方式:
using Microsoft.AspNetCore.SignalR;
using System.Collections.Concurrent;
namespace SignalRDemo;
public class ChatHub : Hub
{
// 存储用户与连接ID的映射
private static readonly ConcurrentDictionary _userConnections = new();
// 存储群组与连接ID的映射
private static readonly ConcurrentDictionary> _groupConnections = new();
///
/// 客户端连接时触发
///
public override async Task OnConnectedAsync()
{
Console.WriteLine($"客户端 {Context.ConnectionId} 已连接");
await base.OnConnectedAsync();
}
///
/// 客户端断开连接时触发
///
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);
}
///
/// 用户登录(绑定用户名和连接ID)
///
public async Task Login(string username)
{
_userConnections[username] = Context.ConnectionId;
await Clients.Caller.SendAsync("LoginSuccess", $"你已登录为 {username}");
}
///
/// 加入群组
///
public async Task JoinGroup(string groupName)
{
if (!_groupConnections.ContainsKey(groupName))
{
_groupConnections[groupName] = new HashSet();
}
_groupConnections[groupName].Add(Context.ConnectionId);
await Clients.Caller.SendAsync("JoinGroupSuccess", $"你已加入群组 {groupName}");
}
///
/// 发送消息给所有人
///
public async Task SendMessageToAll(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
///
/// 发送消息给单个人
///
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} 不在线");
}
}
///
/// 发送消息给多个人
///
public async Task SendMessageToUsers(IReadOnlyList targetUsers, string user, string message)
{
var connectionIds = new List();
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", "没有目标用户在线");
}
}
///
/// 发送消息给群组
///
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} 不存在");
}
}
}
这里用 ConcurrentDictionary 来维护用户和连接ID的映射,以及群组和成员列表的映射。实际生产环境可能需要数据库或者 Redis 来持久化,但 demo 阶段这样够用。
在前端项目里装客户端库:
npm install @microsoft/signalr
下面是一个简单的 HTML 页面,通过 Ja vaScript 连接 SignalR 并实现登录、发送消息等功能:
SignalR Chat
注意这里的 withUrl 地址要和后端映射的 /chatHub 一致,并且端口别写错。
在 WinForms 项目里通过 NuGet 安装:
dotnet add package Microsoft.AspNetCore.SignalR.Client
在窗体上拖几个控件——用户名输入框、消息输入框、登录按钮、发送给所有人按钮,再加一个 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("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}");
}
}
}
WinForms 里要注意 UI 线程的调用——Invoke 把更新操作切回主线程,否则会抛异常。
SignalR 在建立连接时,会根据浏览器和服务器的支持情况自动选择最佳通信技术。这个选择顺序很有意思,也是很多开发者容易忽略的细节。它的优先级是:
ws:// 或 wss://)。EventSource API。客户端启动时,会先发一个 HTTP 请求到 /chatHub/negotiate 端点,拿回服务器支持的技术列表。然后客户端按照 WebSocket → SSE → 长轮询 的顺序尝试。一旦某个技术成功建立连接,后续通信就全部基于它。这个过程对开发者是完全透明的,但了解它有助于排查连接问题——比如为什么明明支持 WebSocket 却用了长轮询?多半是 CORS 或者中间件配置没到位。
| 技术 | 双向通信 | 性能 | 兼容性 |
|---|---|---|---|
| WebSocket | 是 | 高 | 现代浏览器 |
| SSE | 否(服务器到客户端) | 中 | 大多数现代浏览器 |
| 长轮询 | 是 | 低 | 所有浏览器 |
AllowCredentials,否则前端连接会报跨域错误。OnConnectedAsync 和 OnDisconnectedAsync,可以记录在线状态、清理资源。ConnectionId 的映射关系,是实现单播和组播的基础。Groups 类,也可以像上面那样自己维护字典,灵活度更高。通过这篇文章,你应该能快速在 .NET 8 里把 SignalR 跑起来,并且支持浏览器和 WinForms 两种客户端。实时通信的坑不少,但把核心链路打通了,后面的扩展就水到渠成了。
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8