You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET Core SignalR连接时消息拼接致connection.on不触发问题求助

SignalR连接时空消息与配置消息拼接导致客户端回调失效的问题

问题详情

我基于.NET Core开发的SignalR应用中,客户端连接时会收到包含应用配置的消息,但SignalR同时会额外发送一条payload为{}的空消息。偶尔(不同环境下概率在1/5到1/25之间),这条空消息会和配置消息被拼接成一条发送,导致客户端connection.on方法完全无法触发。通过Chrome开发者工具可以看到,拼接后的消息包含空消息{}、帧分隔符以及配置消息的SignalR信封,但服务端和客户端的SignalR均未抛出任何错误。

已尝试但无效的方案:

  • 重构后端代码,使用连接ID定向发送消息替代Clients.Caller
  • 在客户端添加自定义格式化器移除前置空消息,推测因SignalR不识别处理后的payload而静默忽略
  • 考虑给配置消息添加100ms延迟,但这只是临时 workaround,希望找到根本解决办法

相关代码

Startup.cs

services.AddSignalR();

...

app.UseEndpoints(
    endpoints =>
    {
        endpoints.MapDefaultControllerRoute();
        endpoints.MapControllers();
        endpoints.MapRazorPages();
        endpoints.MapHub<MyHub>(Url.DefaultHub);
    });

服务端Hub实现

public sealed class MyHub(IAppSettingsChannel appSettingsChannel) : Hub
{
    /// <summary>
    /// 用户连接WebSocket时触发的事件处理程序,每个用户连接都会调用
    /// </summary>
    public override async Task OnConnectedAsync()
    {
        // 新用户连接或老用户重连时发送应用配置
        await appSettingsChannel.Send(Clients.Caller);

        await base.OnConnectedAsync();
    }
}

服务端消息通道

public class AppSettingsChannel : IAppSettingsChannel
{
    private static HubChannel Channel => HubChannel.AppSettings;

    internal AppSettingsModel AppSettings => new(...);

    public async Task Send(ISingleClientProxy caller)
    {
        await caller.SendAsync(Channel, AppSettings);
    }
}

客户端Hub封装

export default class Hub {
    /** 底层WebSocket连接 */
    private static connection: HubConnection | null = null;

    /** WebSocket连接的基础URL,不同Hub对应不同通道URL */
    private static defaultHubUrl: string = '/web-socket';

    /** 初始高频重试周期 */
    private static initialRetryPeriodMilliseconds: number = 60 * 1000;

    /** 初始重试周期内的短间隔重试时长 */
    private static initialRetryMilliseconds: number = 5 * 1000;

    /** 初始周期后的长间隔重试时长 */
    private static continuousRetryMilliseconds: number = 30 * 1000;

    /**
     * 监听指定通道的消息,若未建立连接则先创建连接
     * @param channel 要监听的服务端消息通道
     * @param onMessage 收到消息时执行的回调函数
     */
    static async on<T>(channel: HubChannel, onMessage: (data: T) => void) {
        // 若未连接到服务端WebSocket,先建立连接
        if (!this.connection)
            this.connection = await this.openConnection();

        // 收到指定通道消息时执行回调
        this.connection.on(channel.value, (msg: T) => {
            onMessage(msg);
        });
    }

    /**
     * 建立与服务端WebSocket的连接
     * @returns 已建立的连接
     */
    static async openConnection(): Promise<HubConnection> {
        // 创建WebSocket连接
        this.connection = new HubConnectionBuilder()
            .withUrl(this.defaultHubUrl)
            .withAutomaticReconnect({
                // 初始重试周期内用短间隔,之后用长间隔
                nextRetryDelayInMilliseconds: (retryContext) => (
                    retryContext.elapsedMilliseconds < this.initialRetryPeriodMilliseconds ? this.initialRetryMilliseconds : this.continuousRetryMilliseconds
                )
            })
            .build();

        // 连接关闭时若有错误则记录日志
        this.connection.onclose(async (error) => {
            if (error) {
                console.error('Hub连接关闭并报错:', error);
                const apiError = normalizeError(error);
                await logErrorSafe(apiError);
            }
        });

        // 启动连接
        await this.connection.start();

        return this.connection;
    }
}

可行解决方案

1. 升级SignalR版本并调整消息发送时机

这个帧拼接问题大概率是特定版本的SignalR底层Bug,建议先升级到最新的.NET Core SignalR服务端包和JavaScript客户端包。同时调整OnConnectedAsync的执行顺序,先完成基类的连接初始化再发送消息:

public override async Task OnConnectedAsync()
{
    await base.OnConnectedAsync();
    // 等待连接完全建立后再发送配置消息
    await appSettingsChannel.Send(Clients.Caller);
}

2. 客户端全局监听所有消息,手动拆分拼接内容

通过connection.on('*')监听所有原始消息,识别并拆分拼接的JSON结构后手动触发对应通道的回调:

this.connection.on('*', (invocationData) => {
    const rawPayload = JSON.stringify(invocationData);
    // 检测是否存在JSON拼接的特征标记
    if (rawPayload.includes('}{')) {
        // 拆分并修复每个JSON片段的格式
        const splitSegments = rawPayload.split('}{').map((seg, index, arr) => {
            if (index === 0) return seg + '}';
            if (index === arr.length - 1) return '{' + seg;
            return `{${seg}}`;
        });
        
        splitSegments.forEach(seg => {
            try {
                const parsed = JSON.parse(seg);
                // 触发对应通道的已注册回调
                const handlers = this.connection!.handlers[parsed.target];
                handlers?.forEach(handler => handler(...parsed.arguments));
            } catch (err) {
                console.error('处理拆分消息失败:', err);
            }
        });
    }
});

3. 启用SignalR的详细日志排查根因

在服务端开启SignalR的详细日志,追踪空消息的来源:

// 在Program.cs或Startup.cs中配置日志
builder.Logging.AddFilter("Microsoft.AspNetCore.SignalR", LogLevel.Debug);
builder.Logging.AddFilter("Microsoft.AspNetCore.Http.Connections", LogLevel.Debug);

查看日志中是否有异常的连接初始化流程,确认空消息是SignalR框架自身发送还是业务代码的隐性触发。


内容的提问来源于stack exchange,提问作者Chris Harrington

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 20:56:08