3、Hub基础:创建第一个Hub、定义服务端方法、客户端调用、连接生命周期
好,咱们正式开始写代码了。
这一章是整个SignalR课程的基石。说白了,你后面所有花哨的功能——群聊、实时看板、游戏对战——都离不开今天要讲的这几个核心概念。
我个人习惯,学任何新技术,第一件事就是先跑通一个最简单的例子。哪怕只是让服务器给客户端说一句"Hello",也比看十遍文档管用。
3.1 什么是Hub?
Hub,翻译过来叫"集线器"。你可以把它想象成一个电话总机。
客户端(浏览器、手机App)就是一个个电话机。它们不直接互相打电话,而是先拨到总机(Hub),由总机决定把消息转给谁。
在SignalR里,Hub就是服务端的一个类。它负责:
- 接收客户端的调用(客户端说:我要发消息)
- 向客户端推送消息(服务器说:有人发消息了,大家快看)
- 管理连接(谁上线了?谁掉线了?)
嗯,这里要注意:Hub本身是瞬时对象。每次客户端调用,都会创建一个新的Hub实例。所以千万别在Hub里存什么全局状态,我早期踩过这个坑。
3.2 创建你的第一个Hub
咱们用ASP.NET Core来演示。先建一个空的Web项目,然后添加一个类,继承Hub基类。
using Microsoft.AspNetCore.SignalR;
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
// 这里写你的业务逻辑
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
就这么简单?对,就这么简单。
SendMessage是服务端方法,客户端可以调用它。Clients.All.SendAsync是向所有连接的客户端广播消息。
我记得第一次写完这个类,心里还犯嘀咕:这也太轻量了吧?后来才发现,SignalR的精髓就在这——把复杂的实时通信抽象成简单的方法调用。
服务端方法 = 客户端可以调用的方法(通过Invoke或Send)
客户端方法 = 服务端可以调用的方法(通过Clients.xxx.SendAsync)
别搞反了,我见过不少新手在这上面栽跟头。
3.3 注册Hub和配置端点
光有类不行,还得告诉ASP.NET Core:嘿,我有个Hub,你得把它暴露出去。
在Program.cs里加上两行代码:
var builder = WebApplication.CreateBuilder(args);
// 注册SignalR服务
builder.Services.AddSignalR();
var app = builder.Build();
app.MapHub<ChatHub>("/chatHub"); // 映射Hub到路径
app.Run();
"/chatHub"这个路径,就是客户端要连接的URL。你可以随便起名,但团队里最好统一规范。我个人习惯用/hubs/功能名,比如/hubs/chat、/hubs/notification。
如果你用了CORS(跨域请求),记得把Hub的路径也加进去。我曾经排查了半天,结果发现是CORS把SignalR的协商请求给拦了。
3.4 客户端调用服务端方法
服务端写好了,客户端怎么调?咱们用JavaScript举例。
// 建立连接
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chatHub")
.build();
// 启动连接
connection.start().then(() => {
console.log("连接成功!");
// 调用服务端的 SendMessage 方法
connection.invoke("SendMessage", "张三", "大家好,我是张三")
.catch(err => console.error(err));
}).catch(err => console.error(err));
注意看connection.invoke("SendMessage", "张三", "大家好,我是张三")。
第一个参数是服务端方法名,后面跟着的是参数。参数个数、类型必须和服务端SendMessage(string user, string message)一一对应。
你想想看,如果服务端方法有3个参数,你只传了2个,SignalR会直接报错。嗯,这个错误信息还挺友好的,会告诉你"参数数量不匹配"。
3.5 服务端调用客户端方法
这是SignalR最核心的能力——服务器主动推消息。
在Hub里,通过Clients属性可以拿到所有连接的客户端引用。常用的有:
| 属性/方法 | 作用 |
|---|---|
Clients.All |
向所有连接的客户端发送 |
Clients.Caller |
只向调用当前方法的客户端发送 |
Clients.Others |
向除了调用者以外的所有客户端发送 |
Clients.Client("connectionId") |
向指定的单个客户端发送 |
Clients.Group("groupName") |
向指定组内的所有客户端发送 |
客户端这边,需要先注册一个方法来接收:
// 注册客户端方法,等待服务端调用
connection.on("ReceiveMessage", (user, message) => {
console.log(`${user} 说:${message}`);
// 这里可以更新UI,比如把消息显示在聊天框里
});
注意:connection.on必须在connection.start()之前注册。为什么?因为连接一旦建立,服务端随时可能发消息过来。如果你还没注册监听器,消息就丢了。
我曾经在项目里犯过一个低级错误——把connection.on放在了start()之后。结果呢?前几条消息永远收不到。排查了半天,最后发现是注册顺序的问题。所以记住:先注册,再启动。
3.6 连接生命周期
一个SignalR连接,从生到死,会经历几个阶段。理解这个,对调试和异常处理特别有帮助。
3.6.1 连接状态
连接对象有一个state属性,取值如下:
| 状态 | 说明 |
|---|---|
Disconnected |
未连接或已断开 |
Connecting |
正在连接中 |
Connected |
已连接,可以通信 |
Reconnecting |
自动重连中(需要启用自动重连) |
3.6.2 生命周期事件
SignalR提供了几个事件钩子,让你可以在关键时刻做点事情:
connection.onclose(() => {
console.log("连接已关闭");
// 可以在这里提示用户,或者尝试手动重连
});
connection.onreconnecting(() => {
console.log("正在重连...");
// 可以显示一个"网络不稳定"的提示
});
connection.onreconnected((connectionId) => {
console.log("重连成功,新ID:" + connectionId);
// 可以重新订阅一些必要的数据
});
我个人强烈建议,生产环境一定要启用自动重连。网络抖动是常态,用户不可能每次都手动刷新页面。
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chatHub")
.withAutomaticReconnect() // 启用自动重连
.build();
默认的重连策略是:先等0秒,然后2秒,10秒,30秒……每次间隔翻倍。你也可以自定义重连间隔,这个后面章节会细讲。
3.6.3 Hub的生命周期方法
在服务端,Hub基类提供了两个虚方法,你可以重写它们:
public class ChatHub : Hub
{
public override async Task OnConnectedAsync()
{
// 有客户端连接时触发
Console.WriteLine($"客户端连接:{Context.ConnectionId}");
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
// 有客户端断开时触发
Console.WriteLine($"客户端断开:{Context.ConnectionId}");
if (exception != null)
{
Console.WriteLine($"断开原因:{exception.Message}");
}
await base.OnDisconnectedAsync(exception);
}
}
这两个方法特别适合做在线用户管理。比如,有人上线了,你把他加到在线列表;有人下线了,你把他移除。
在OnDisconnectedAsync里,记得清理这个用户加入的所有组。否则下次他重连时,组里会有重复记录。我早期没注意这个,结果用户退出了,组里还留着他的影子,导致广播时出现重复消息。
3.7 一个完整的例子
把上面所有知识点串起来,就是一个最简单的聊天室:
// 服务端 ChatHub.cs
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
string formattedMessage = $"[{DateTime.Now:HH:mm:ss}] {user}:{message}";
await Clients.All.SendAsync("ReceiveMessage", formattedMessage);
}
public override async Task OnConnectedAsync()
{
await Clients.Others.SendAsync("ReceiveMessage", "系统", $"用户 {Context.ConnectionId} 加入了聊天室");
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
await Clients.Others.SendAsync("ReceiveMessage", "系统", $"用户 {Context.ConnectionId} 离开了聊天室");
await base.OnDisconnectedAsync(exception);
}
}
// 客户端 JavaScript
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chatHub")
.withAutomaticReconnect()
.build();
connection.on("ReceiveMessage", (message) => {
const li = document.createElement("li");
li.textContent = message;
document.getElementById("messages").appendChild(li);
});
connection.start().then(() => {
document.getElementById("sendBtn").addEventListener("click", () => {
const user = document.getElementById("userInput").value;
const msg = document.getElementById("msgInput").value;
connection.invoke("SendMessage", user, msg).catch(err => console.error(err));
});
});
你看,核心代码也就三四十行。一个能实时聊天的功能就出来了。
这就是SignalR的魅力——把复杂的WebSocket、长轮询、Server-Sent Events统统封装掉,让你专注于业务逻辑。
3.8 本章小结
- Hub是SignalR的核心抽象,负责服务端和客户端的双向通信
- 服务端方法用
public async Task定义,客户端通过invoke调用 - 客户端方法用
on注册,服务端通过Clients.xxx.SendAsync调用 - 连接生命周期包括:连接、重连、断开,每个阶段都有对应的事件
- 生产环境务必启用自动重连,并处理好
OnConnectedAsync和OnDisconnectedAsync
下一章,咱们会深入聊聊强类型Hub。说白了,就是让方法调用更安全、更优雅。到时候见。