Skip to content

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)

IdentityServer4系列 | 初识基础知识点 - 艾三元 - 博客园 (cnblogs.com)

ASP.NET Core 上的 Identity 简介 | Microsoft Learn


# 客户端提供 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