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

Azure无服务器SignalR客户端调用失败及直连403问题咨询

客户端间RPC通信实现问题

目标

实现客户端兼具服务端与客户端角色的RPC通信,采用Azure无服务器SignalR服务搭配Azure Function触发器绑定。

当前问题

客户端无法调用相关方法,SignalR触发器的广播功能未触发;直接连接SignalR Service时返回403错误(未启用托管标识)。

疑问

  1. 实现思路是否正确?
  2. 是否必须通过Azure Function作为中转,才能向SignalR Service发送消息并由其推送给监听客户端?

Azure Function 代码

using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Azure.Functions.Worker.SignalRService;
using Microsoft.Extensions.Logging;

namespace DotnetIsolated_ClassBased
{
    [SignalRConnection("AzureSignalRConnectionString")]
    public class Functions : ServerlessHub<IChatClient>
    {
        private const string HubName = "<hub_name>"; // 修正原代码缺少的分号
        private readonly ILogger _logger;

        public Functions(IServiceProvider serviceProvider, ILogger<Functions> logger) : base(serviceProvider)
        {
            _logger = logger;
        }

        [Function(nameof(BroadcastToAll))]
        [SignalROutput(HubName = HubName, ConnectionStringSetting = "AzureSignalRConnectionString")]
        public static SignalRMessageAction BroadcastToAll([HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req)
        {
            using var bodyReader = new StreamReader(req.Body);
            return new SignalRMessageAction("newMessage")
            {
                Arguments = new[] { bodyReader.ReadToEnd() },
            };
        }

        [Function("negotiate")]
        public async Task<HttpResponseData> Negotiate([HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req)
        {
            _logger.LogInformation($"Negotiating with user: {req.Headers.GetValues("userId").FirstOrDefault()}");

            var negotiateResponse = await NegotiateAsync(new() { UserId = req.Headers.GetValues("userId").FirstOrDefault() });
            var response = req.CreateResponse();
            await response.WriteBytesAsync(negotiateResponse.ToArray());
            return response;
        }

        [Function("OnConnected")]
        public Task OnConnected([SignalRTrigger(HubName, "connections", "connected")] SignalRInvocationContext invocationContext)
        {
            invocationContext.Headers.TryGetValue("Authorization", out var auth);
            _logger.LogInformation($"{invocationContext.ConnectionId} has connected");
            return Clients.All.newConnection(new NewConnection(invocationContext.ConnectionId, auth));
        }

        [Function("Broadcast")]
        public Task Broadcast(
        [SignalRTrigger(HubName, "messages", "broadcast", "message")] SignalRInvocationContext invocationContext, string message)
        {
            _logger.LogInformation($"Broadcasting message: {message}");
            return Clients.All.newMessage(new NewMessage(invocationContext, message));
        }

        [Function("SendToGroup")]
        public Task SendToGroup([SignalRTrigger(HubName, "messages", "SendToGroup", "groupName", "message")] SignalRInvocationContext invocationContext, string groupName, string message)
        {
            return Clients.Group(groupName).newMessage(new NewMessage(invocationContext, message));
        }

        [Function("SendToUser")]
        public Task SendToUser([SignalRTrigger(HubName, "messages", "SendToUser", "userName", "message")] SignalRInvocationContext invocationContext, string userName, string message)
        {
            return Clients.User(userName).newMessage(new NewMessage(invocationContext, message));
        }

        [Function("SendToConnection")]
        public Task SendToConnection([SignalRTrigger(HubName, "messages", "SendToConnection", "connectionId", "message")] SignalRInvocationContext invocationContext, string connectionId, string message)
        {
            return Clients.Client(connectionId).newMessage(new NewMessage(invocationContext, message));
        }

        [Function("JoinGroup")]
        public Task JoinGroup([SignalRTrigger(HubName, "messages", "JoinGroup", "connectionId", "groupName")] SignalRInvocationContext invocationContext, string connectionId, string groupName)
        {
            return Groups.AddToGroupAsync(connectionId, groupName);
        }

        [Function("LeaveGroup")]
        public Task LeaveGroup([SignalRTrigger(HubName, "messages", "LeaveGroup", "connectionId", "groupName")] SignalRInvocationContext invocationContext, string connectionId, string groupName)
        {
            return Groups.RemoveFromGroupAsync(connectionId, groupName);
        }

        [Function("JoinUserToGroup")]
        public Task JoinUserToGroup([SignalRTrigger(HubName, "messages", "JoinUserToGroup", "userName", "groupName")] SignalRInvocationContext invocationContext, string userName, string groupName)
        {
            return UserGroups.AddToGroupAsync(userName, groupName);
        }

        [Function("LeaveUserFromGroup")]
        public Task LeaveUserFromGroup([SignalRTrigger(HubName, "messages", "LeaveUserFromGroup", "userName", "groupName")] SignalRInvocationContext invocationContext, string userName, string groupName)
        {
            return UserGroups.RemoveFromGroupAsync(userName, groupName);
        }

        [Function("OnDisconnected")]
        public void OnDisconnected([SignalRTrigger(HubName, "connections", "disconnected")] SignalRInvocationContext invocationContext)
        {
            _logger.LogInformation($"{invocationContext.ConnectionId} has disconnected");
        }
    }
    public class NewConnection
    {
        public string ConnectionId { get; }

        public string Authentication { get; }

        public NewConnection(string connectionId, string auth)
        {
            ConnectionId = connectionId;
            Authentication = auth;
        }
    }

    public class NewMessage
    {
        public string ConnectionId { get; }
        public string Sender { get; }
        public string Text { get; }

        public NewMessage(SignalRInvocationContext invocationContext, string message)
        {
            Sender = string.IsNullOrEmpty(invocationContext.UserId) ? string.Empty : invocationContext.UserId;
            ConnectionId = invocationContext.ConnectionId;
            Text = message;
        }
    }

    public interface IChatClient
    {
        Task newMessage(NewMessage message);
        Task newConnection(NewConnection connection);
    }
}

客户端代码

// 所有客户端代码结构如下
var connection = new HubConnectionBuilder()
    .WithUrl("<azure_function_endpoint>/api", (options) =>
    {
        options.Headers.Add("UserId", "<some_user_id>");
        options.AccessTokenProvider = async () =>
        {
            return await Task.FromResult("<access_token>"); // 已知硬编码token,仅用于POC测试
        };
    })
    .Build();

// 修正监听方法名称与参数类型
connection.On<NewMessage>("newMessage", msg => Console.WriteLine($"{msg.Sender}: {msg.Text}"));
await connection.StartAsync();
await connection.SendAsync("Broadcast", "This is a broadcast");

问题解答

1. 实现思路正确性

整体思路符合Azure无服务器SignalR的典型使用场景,是可行的。但当前代码存在几个关键问题导致功能失效:

  • 客户端监听的方法名称(Broadcast)与Function中广播的方法名称(newMessage)不匹配,导致客户端收不到消息。
  • Function代码存在语法错误(HubName常量缺少分号),会导致编译失败。
  • 连接字符串配置项不统一(类上用AzureSignalRConnectionString,BroadcastToAll方法用SignalRConnection),导致Function无法正确连接SignalR Service。

2. 是否必须通过Azure Function中转?

是的。无服务器模式下的Azure SignalR Service依赖Azure Function完成身份验证、连接管理和消息路由,不允许客户端直接建立连接,直接连接会返回403错误。所有客户端必须通过Function的协商端点获取合法的连接凭证后,才能与SignalR Service建立连接。

修复建议

  • 统一客户端监听方法与Function定义的客户端方法名称、参数类型(如示例代码中修正的connection.On<NewMessage>("newMessage", ...))。
  • 修正Function代码中的语法错误,确保编译通过。
  • 统一所有SignalR相关配置的连接字符串名称,避免配置不一致。
  • 确保Azure Function的系统分配身份已被授予SignalR Service的SignalR App Server角色,否则Function无法与SignalR Service交互。

内容的提问来源于stack exchange,提问作者Pultea Robert

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 05:29:54