SignalR
服务端
https://learn.microsoft.com/zh-cn/aspnet/core/signalr/hubs?view=aspnetcore-6.0
builder.Services.AddSignalR();
app.MapRazorPages();
app.MapHub<ChatHub>("/Chat"); // 定义 signalR 服务类
创建服务端类
// 继承于 Hub
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
=> await Clients.All.SendAsync("ReceiveMessage", user, message);
}
Hub 中 Context
属性 | 说明 |
---|---|
ConnectionId | 获取连接的唯一 ID(由 SignalR 分配)。 每个连接都有一个连接 ID。 |
UserIdentifier | 获取用户标识符。 默认情况下,SignalR 使用与连接关联的 ClaimsPrincipal 中的 ClaimTypes.NameIdentifier 作为用户标识符。 |
User | 获取与当前用户关联的 ClaimsPrincipal。 |
Items | 获取可用于在此连接范围内共享数据的键/值集合。 数据可以存储在此集合中,会在不同的中心方法调用间为连接持久保存。 |
Features | 获取连接上可用的功能的集合。 目前,在大多数情况下不需要此集合,因此未对其进行详细记录。 |
ConnectionAborted | 获取一个 CancellationToken,它会在连接中止时发出通知。 |
方法 | 说明 |
---|---|
GetHttpContext | 返回 HttpContext 连接,或者 null 如果连接未与 HTTP 请求关联。 对于 HTTP 连接,请使用此方法获取 HTTP 标头和查询字符串等信息。 |
Abort | 中止连接。 |
Hub 中 Client
属性 | 说明 |
---|---|
All | 对所有连接的客户端调用方法 |
Caller | 对调用了中心方法的客户端调用方法 |
Others | 对所有连接的客户端调用方法(调用了方法的客户端除外) |
方法 | 说明 |
---|---|
AllExcept | 对所有连接的客户端调用方法(指定连接除外) |
Client | 对连接的一个特定客户端调用方法 |
Clients | 对连接的多个特定客户端调用方法 |
Group | 对指定组中的所有连接调用方法 |
GroupExcept | 对指定组中的所有连接调用方法(指定连接除外) |
Groups | 对多个连接组调用方法 |
OthersInGroup | 对一个连接组调用方法(不包括调用了中心方法的客户端) |
User | 对与一个特定用户关联的所有连接调用方法 |
Users | 对与多个指定用户关联的所有连接调用方法 |
向客户端发消息
// 向所有连接者发消息
public async Task SendMessage(string user, string message)
=> await Clients.All.SendAsync("ReceiveMessage", user, message);
// 向调用者发消息
public async Task SendMessageToCaller(string user, string message)
=> await Clients.Caller.SendAsync("ReceiveMessage", user, message);
// 向指定组发消息
public async Task SendMessageToGroup(string user, string message)
=> await Clients.Group("SignalR Users").SendAsync("ReceiveMessage", user, message);
// 能客户端实现一个接口,可以不使用字符中来调用客户端消息
public class StronglyTypedChatHub : Hub<IChatClient>
{
public async Task SendMessageToCaller(string user, string message)
=> await Clients.Caller.ReceiveMessage(user, message); // 这是接口中定义的方法
}
// 重定义服务端方法名
[HubMethodName("SendMessageToUser")]
public async Task DirectMessage(string user, string message)
=> await Clients.User(user).SendAsync("ReceiveMessage", user, message);
连接与断开
## 连接事件
public override async Task OnConnectedAsync()
{
await Groups.AddToGroupAsync(Context.ConnectionId, "SignalR Users");
await base.OnConnectedAsync();
}
## 断开事件, 如果客户端主动断开 ``connection.stop()`` 则 exception = null
public override async Task OnDisconnectedAsync(Exception? exception)
{
await base.OnDisconnectedAsync(exception);
}
处理函数
- 异常将发送到调用该方法的客户端,应该只传输 message 部分,连接不会断开
- 如果要完整异常内容,抛出 HubException
其它类中注入
# 直接注入
IHubContext<ChatHub>
# 注入强类型
IHubContext<ChatHub, IChatClient> chatHubContext)
# 中间件中访问
context.RequestServices.GetRequiredService<IHubContext<ChatHub>>
# ihost 中访问
var host = CreateHostBuilder(args).Build();
host.Services.GetService(typeof(IHubContext<ChatHub>));
用户与组
# 给指定用户发消息
Clients.User(user).SendAsync("ReceiveMessage", message);
# 加入用户至组
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
# 从组中移除用户
await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
# 给全组发消息
await Clients.Group(groupName).SendAsync("Send", $"{Context.ConnectionId} has joined the group {groupName}.");
参数定义
一般建议参数使用类,这样扩展时增加字段就好了,这样函数可以保持向下兼容
筛选器
https://learn.microsoft.com/zh-cn/aspnet/core/signalr/hub-filters?view=aspnetcore-6.0
相当于中间件,可用户连接、断开、发消息时进行处理
客户端
.net 为例
https://learn.microsoft.com/zh-cn/aspnet/core/signalr/dotnet-client?view=aspnetcore-6.0&tabs=visual-studio
Install-Package Microsoft.AspNetCore.SignalR.Client
connection = new HubConnectionBuilder().WithUrl("http://localhost:53353/ChatHub") .Build(); // 创建连接,
// .WithAutomaticReconnect 设置连接完成后断开自动连接,默认重试 4 次,可自定义, 但是 StartAsync 失败,要手工进行重试
await connection.StartAsync(); // 开始连接
connection.Closed += async (error) =>{ /** 断开事件 */ };
connection.On<string, string>("ReceiveMessage", (user, message) => { /** 接收消息 */ });
await connection.InvokeAsync("SendMessage", userTextBox.Text, messageTextBox.Text); // 发送消息
connection.Reconnecting += error => { /** 断开重连接中 */ }
connection.Reconnected += connectionId => { /** 连接完成 */} // 配置为跳过协商 connectionId 为 null
connection.State // 连接状态
多 SignalR 服务端
https://learn.microsoft.com/zh-cn/aspnet/core/signalr/scale?view=aspnetcore-6.0
可以使用 Azure SignalR 服务,使用 Redis 通知服务,将通知同步至所有 SignalR 服务器上
配置
https://learn.microsoft.com/zh-cn/aspnet/core/signalr/configuration?view=aspnetcore-6.0&tabs=dotnet
# 对 json 参数进行配置不区分大小写
builder.Services.AddSignalR()
.AddJsonProtocol(options => {
options.PayloadSerializerOptions.PropertyNamingPolicy = null;
});
# 其它参数配置
builder.Services.AddSignalR(hubOptions =>
{
hubOptions.EnableDetailedErrors = true;
hubOptions.KeepAliveInterval = TimeSpan.FromMinutes(1);
});
# 高级设置这里配置 , 比如配置连接支持类型
app.MapHub<ChatHub>("/chathub", options =>
{
options.Transports = HttpTransportType.WebSockets |HttpTransportType.LongPolling;
}
);
# 客户端配置
var connection = new HubConnectionBuilder()
.WithUrl("https://example.com/chathub")
.ConfigureLogging(logging => {
logging.SetMinimumLevel(LogLevel.Information);
logging.AddConsole();
}).Build();
# 客户端配置连接支持类型
var connection = new HubConnectionBuilder()
.WithUrl("https://example.com/chathub", HttpTransportType.WebSockets | HttpTransportType.LongPolling)
.Build();
# 配置身份认证
var connection = new HubConnectionBuilder()
.WithUrl("https://example.com/chathub", options => {
options.AccessTokenProvider = async () => {
// Get and return the access token.
};
})
.Build();
身份验证和授权
https://learn.microsoft.com/zh-cn/aspnet/core/signalr/authn-and-authz?view=aspnetcore-6.0
从零搭建一个IdentityServer——目录(更新中...) - 7m鱼 - 博客园 (cnblogs.com)
# 客户端提供 token,必要时在这里更新 token
var connection = new HubConnectionBuilder()
.WithUrl("https://example.com/chathub", options =>
{
options.AccessTokenProvider = () => Task.FromResult(_myAccessToken);
})
.Build();
晚点再看
安全注意
https://learn.microsoft.com/zh-cn/aspnet/core/signalr/security?view=aspnetcore-6.0
- 跨域资源共享
- WebSocket 源限制
- ConnectionId
- 访问令牌日志记录
- 异常
- 缓冲区管理
什么是 MessagePack?
https://learn.microsoft.com/zh-cn/aspnet/core/signalr/messagepackhubprotocol?view=aspnetcore-6.0
MessagePack 是一种快速而紧凑的二进制序列化格式。 当性能和带宽是一个关注点时,它很有用,因为它会创建比 JSON 更小的消息。 查看网络跟踪和日志时,二进制消息不可读取,除非这些字节是通过 MessagePack 分析器传递的。 SignalR 为 MessagePack 格式提供内置支持,并提供 API 供客户端和服务器使用。
流式传输
服务端至客户端
使用 IAsyncEnumerable
服务端
// 定义返回 IAsyncEnumerable 函数供客户端调用
public async IAsyncEnumerable<int> DataCounter(
int count,
int delay,
[EnumeratorCancellation]
CancellationToken cancellationToken)
{
for (var i = 0; i < count; i++)
{
cancellationToken.ThrowIfCancellationRequested();
yield return i;
await Task.Delay(delay, cancellationToken);
}
}
客户端
// 调用 服务端函数返回枚举数据
var stream = this.hubConnection.StreamAsync<int>("DataCounter", 10, 25, CancellationToken.None);
await foreach (var item in stream)
{
LoginInfo($@"获得数据 {item}");
}
使用 ChannelReader
ChannelReader 应该是为异步传输定义的一个类
服务端
// 服务端定义返回数据函数
public ChannelReader<int> ChannelCounter(
int count,
int delay,
CancellationToken cancellationToken)
{
var channel = Channel.CreateUnbounded<int>();
// 不要等待,让客户端云等待
_ = WriteItemsAsync(channel.Writer, count, delay, cancellationToken);
return channel.Reader;
}
private async Task WriteItemsAsync(
ChannelWriter<int> writer,
int count,
int delay,
CancellationToken cancellationToken)
{
Exception? localException = null;
try
{
for (var i = 0; i < count; i++)
{
await writer.WriteAsync(i, cancellationToken);
await Task.Delay(delay, cancellationToken);
}
}
catch (Exception ex)
{
localException = ex;
}
finally
{
writer.Complete(localException);
}
}
客户端
var channel = await this.hubConnection.StreamAsChannelAsync<int>("ChannelCounter", 10, 25, CancellationToken.None);
while (await channel.WaitToReadAsync())
{
if (channel.TryRead(out var item))
{
LoginInfo($@"接收数据 {item}");
}
}
客户端至服务端
使用 IAsyncEnumerable
客户端
// 生成模拟的上传数据
async IAsyncEnumerable<int> clientStreamData()
{
for (var i = 0; i < 5; i++)
{
var result = await Task.FromResult(i);
yield return result;
}
}
// 上传数据
var datas = clientStreamData();
await hubConnection.SendAsync("UploadStream", datas);
服务端
## 接收上传数据
public async Task UploadStream(IAsyncEnumerable<int> stream)
{
await foreach (var item in stream)
{
await this.Clients.Caller.SendAsync("sendmessage", $@"服务端接收到消息 {item}");
}
}
使用 ChannelReader
客户端
var channel = Channel.CreateBounded<int>(10);
try
{
// 将 channel 传至数据器
await this.hubConnection.SendAsync("UploadChannelStream", channel.Reader);
// 向 channel 中写入数据
for (int i = 0; i < 5; i++)
{
await channel.Writer.WriteAsync(i);
}
// 完成数据写入
channel.Writer.Complete();
}
catch (Exception exception)
{
LoginInfo(@"发生错误", exception);
channel.Writer.Complete(exception);
}
服务端
// 定义使用 channel 方式上传数据
public async Task UploadChannelStream(ChannelReader<int> stream)
{
while (await stream.WaitToReadAsync())
{
while (stream.TryRead(out var item))
{
await this.Clients.Caller.SendAsync("sendmessage", $@"服务端接收到消息 {item}");
}
}
}
异步流
https://blog.csdn.net/mzl87/article/details/124859358