Hub类这个名字起的十分贴切,SignalR中,Hub类抽象了服务端中收发消息的组件。
我们可以使用OnConnectedAsync()
和OnDisconnectedAsync()
这两个生命周期函数执行连接建立时和断开时的服务端逻辑。
using Microsoft.AspNetCore.SignalR;
namespace DemoSignalR.Hubs;
public class ChatHub : Hub
{
private readonly ILogger<ChatHub> _logger;
public ChatHub(ILogger<ChatHub> logger)
{
_logger = logger;
}
public override async Task OnConnectedAsync()
{
_logger.LogInformation("连接已建立");
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
_logger.LogInformation("连接已断开");
await base.OnDisconnectedAsync(exception);
}
public async Task SendMessage(string message)
{
string connectionId = Context.ConnectionId;
await Clients.All.SendAsync("ReceiveMessage", $"{connectionId}: {message}");
}
}
代码中,我们重写了父类的OnConnectedAsync()
和OnDisconnectedAsync()
方法,分别在连接建立和断开时打印一条信息。
SignalR封装了群组的实现,每个连接可以被加入或移除到一个群组,消息能以群组粒度筛选广播。下面例子中,我们在连接建立时,从Session中取出Username
字段并以此为依据分配群组,我们有3个群组:Group1
、Group2
、GroupPublic
,其中Tom
和Jerry
加入群组1和公共群组,Spike
和Tyke
加入群组2和公共群组,其余人只加入公共群组。
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Caching.Distributed;
using System.Text.Json;
namespace DemoSignalR.Hubs;
public class ChatHub : Hub
{
private readonly ILogger<ChatHub> _logger;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IDistributedCache _distributedCache;
public ChatHub(ILogger<ChatHub> logger, IHttpContextAccessor httpContextAccessor, IDistributedCache distributedCache)
{
_logger = logger;
_httpContextAccessor = httpContextAccessor;
_distributedCache = distributedCache;
}
public override async Task OnConnectedAsync()
{
List<string> currentGroups = new();
string? name = _httpContextAccessor.HttpContext?.Session.GetString("Username");
_logger.LogInformation("{name}已连接", name);
if (name == "Tom" || name == "Jerry")
{
// 汤姆和杰瑞加入群组Group1
currentGroups.Add("Group1");
_logger.LogInformation("{name}加入群组Group1", name);
}
else if (name == "Spike" || name == "Tyke")
{
// 史派克和泰克加入群组Group2
currentGroups.Add("Group2");
_logger.LogInformation("{name}加入群组Group2", name);
}
// 所有人都加入公开群组
currentGroups.Add("GroupPublic");
_logger.LogInformation("{name}加入群组GroupPublic", name);
// 相关信息写入缓存
await _distributedCache.SetStringAsync($"MSG_INFO_NAME_{Context.ConnectionId}", JsonSerializer.Serialize(name));
await _distributedCache.SetStringAsync($"MSG_INFO_GROUPS_{Context.ConnectionId}", JsonSerializer.Serialize(currentGroups));
// 执行加入群组任务
IEnumerable<Task> tasks = currentGroups.Select(g => Groups.AddToGroupAsync(Context.ConnectionId, g));
await Task.WhenAll(tasks);
await base.OnConnectedAsync();
}
public async Task SendMessage(string group, string message)
{
string nameStr = await _distributedCache.GetStringAsync($"MSG_INFO_NAME_{Context.ConnectionId}");
string? name = JsonSerializer.Deserialize<string>(nameStr);
string currentGroupsStr = await _distributedCache.GetStringAsync($"MSG_INFO_GROUPS_{Context.ConnectionId}");
List<string>? currentGroups = JsonSerializer.Deserialize<List<string>>(currentGroupsStr);
if (currentGroups != null && currentGroups.Contains(group))
{
await Clients.Group(group).SendAsync("ReceiveMessage", $"[{group}] {name}: {message}");
}
}
}
这段代码比较长,但逻辑很简单,建立连接时我们通过IHttpContextAccessor
读取了建立连接时HTTP上下文对应的Session信息Username
,随后我们基于这个字段对连接归属的群组进行了分配,这里我们只是硬编码的用户和群组归属关系,实际开发中,我们可能还需要查询数据库来获取用户属于哪些群组。发送消息写的比较简单,我们直接把消息发给了参数传入的群组,此外我们还额外检查了参数传入的群组名是否在当前连接加入的群组中。
除了按群组筛选,SignalR也能很方便的实现向客户端单独发消息或是向特定连接发送消息。最后,我们总结一些常用的筛选消息发送方法。