news 2026/9/23 3:14:10

Aspire + Azure Web PubSub 构建高并发群聊系统

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Aspire + Azure Web PubSub 构建高并发群聊系统

1. 项目概述:为什么一个“群聊”值得用 Aspire + Azure Web PubSub 重做一遍?

我去年在给一家在线教育平台做实时互动模块时,被逼着把 WebSocket 服务从 SignalR 换成 Azure Web PubSub。当时心里是抵触的——不就是发个消息、推个通知?SignalR 跑得好好的,何必折腾?直到上线后第三周,我们遭遇了一次突发流量:某场免费公开课开课前5分钟,3.2万用户同时涌入聊天室,SignalR Hub 的 CPU 瞬间飙到98%,连接队列堆积,新用户连不上,老用户消息延迟超12秒。运维同事凌晨三点打电话说:“再不切,今天早上的课得直播道歉。”

那天我通宵研究 Azure Web PubSub 文档,第二天就搭出了最小可行原型。不是靠魔法,而是靠它彻底解耦了“业务逻辑”和“连接管理”——你的 ASP.NET Core 应用不再需要维护数万个 WebSocket 连接状态,也不用操心心跳、重连、断线清理这些脏活累活。Web PubSub 把连接层抽成独立的云服务,你只管写业务:谁发了什么消息、该转发给谁、要不要存数据库。Aspire 则把这套架构的部署、可观测性、依赖注入全给你兜底了。

标题里那个AddAzureWebPubSubHub方法,不是个花哨的语法糖,它是 Aspire 对 Web PubSub 的“语义封装”:它自动注册了 Hub 客户端、配置了连接字符串、绑定了默认的授权策略,并把 Hub 实例注入到 DI 容器里——你不用再手写IHttpClientFactory去调用 REST API,也不用自己拼接 SAS Token。它背后实际干的是三件事:

  1. 注册IWebPubSubClient(用于服务端主动推送);
  2. 注册IWebPubSubServiceClient(用于管理连接、分组、权限);
  3. 注册IWebPubSubHubClient<T>(泛型 Hub 客户端,类型安全地发消息)。

这玩意儿适合谁?如果你正在用 .NET 做以下任何一种场景,它就不是“可选”,而是“刚需”:

  • 在线协作工具(白板、文档协同);
  • 实时交易看板(股票、期货、加密货币行情);
  • 多人游戏状态同步(非高帧率动作类,比如棋牌、答题、投票);
  • IoT 设备状态广播(传感器数据汇总推送给监控大屏);
  • 客服系统坐席状态联动(坐席上线/离线/忙碌,实时同步给所有管理员)。

它不适合什么?别硬套:

  • 需要毫秒级响应的高频交易指令(Web PubSub 端到端延迟通常在 50–200ms,够用但不够极致);
  • 单机小项目(本地开发调试时,Web PubSub 的 Azure 依赖会拖慢启动速度,不如直接用 MemoryHub);
  • 强求 P2P 通信的场景(Web PubSub 是典型的 Pub/Sub 模型,所有消息必须经由服务端中转,没有客户端直连能力)。

我见过太多团队把 WebSocket 当成“高级 HTTP”来用:前端用new WebSocket(),后端用WebSocketManager手动管理连接池,结果一上生产就掉连接、丢消息、内存泄漏。这不是技术不行,是没看清 WebSocket 的本质——它不是“更快的 HTTP”,而是一种长生命周期、双向、低开销的通道协议。它的难点从来不在“怎么连”,而在“连上了之后怎么稳、怎么扩、怎么查”。Aspire + Web PubSub 的组合,就是把“怎么稳、怎么扩、怎么查”这三座大山,直接搬进 Azure 云服务里,让你专注写业务代码。

2. 架构设计与核心思路拆解:为什么放弃 SignalR,选择 Web PubSub?

2.1 信号模型的根本差异:从“Hub 中心化”到“Broker 解耦化”

SignalR 的设计哲学是“Hub 即中心”。你在Startup.csProgram.cs里定义一个ChatHub : Hub,所有客户端都连到这个 Hub 实例。Hub 里既有OnConnectedAsync这种连接生命周期方法,也有SendMessage这种业务方法。好处是开发快、概念直观;坏处是——它把连接状态业务逻辑强耦合在了一起。

举个具体例子:假设你要实现“仅向同房间用户广播消息”。在 SignalR 里,你得这么写:

public class ChatHub : Hub { private readonly IHubContext<ChatHub> _hubContext; public ChatHub(IHubContext<ChatHub> hubContext) => _hubContext = hubContext; public async Task SendMessage(string room, string message) { // ❌ 错误示范:遍历所有连接,手动过滤 var connections = await _hubContext.Clients.All.GetConnections(); foreach (var conn in connections.Where(c => c.Room == room)) { await _hubContext.Clients.Client(conn.ConnectionId).SendAsync("ReceiveMessage", message); } } }

这段代码的问题在哪?

  • GetConnections()是 SignalR 的私有 API,官方不保证兼容性,.NET 6+ 已废弃;
  • 即使能用,它返回的是内存中的连接列表,集群部署时(多台服务器),你只能拿到本机的连接,跨节点消息就丢了;
  • 每次广播都要遍历全部连接,O(n) 复杂度,3万用户时性能断崖式下跌。

而 Web PubSub 的思路是“Broker 即规则引擎”。你不再定义一个“Hub 类”,而是定义一套“路由规则”和“事件处理器”。所有客户端连接到 Web PubSub 服务,服务根据 URL 路径、查询参数、JWT Token 自动分组。比如,你让前端连wss://your-pubsub.webpubsub.azure.com/client/hubs/chat?groupId=room123&userId=abc123,Web PubSub 就自动把这个连接加入room123分组,并关联userId=abc123元数据。

服务端发消息时,你只需调用:

await _hubClient.SendToGroupAsync("room123", new { type = "message", content = message });

Web PubSub 服务内部会自动:

  • 查找room123分组下的所有活跃连接;
  • 过滤掉已断开的连接;
  • 按 WebSocket 协议格式打包消息;
  • 通过最优路径推送给每个客户端。

这个过程完全脱离你的应用进程。你的 ASP.NET Core 应用只是个“消息生产者”,不保存任何连接状态,不参与任何网络 I/O。这就意味着:

  • 水平扩展无压力:加 10 台服务器,你的业务代码不用改一行,Web PubSub 自动负载均衡;
  • 故障隔离强:哪怕你的 API 服务挂了,已建立的 WebSocket 连接依然畅通(因为连接在 Web PubSub 服务上);
  • 可观测性好:Azure Portal 里直接看到每秒连接数、消息吞吐量、错误率,不用自己埋点。

2.2 Aspire 的价值:不是“又一个模板”,而是“生产就绪的胶水”

很多人以为 Aspire 就是个“带 UI 的 Docker Compose”,这是巨大误解。Aspire 的核心价值,在于它把分布式系统里最麻烦的三件事,变成了声明式配置:

  1. 依赖发现与连接字符串注入
    传统方式下,你得在appsettings.json里写死 Web PubSub 的连接字符串,或者用 Azure Key Vault 动态加载。Aspire 让你用一行代码声明依赖:

    var builder = DistributedApplication.CreateBuilder(args); var pubsub = builder.AddAzureWebPubSub("chat-pubsub"); // ✅ 自动创建资源,生成连接字符串 var api = builder.AddProject<Projects.ChatApi>("chat-api") .WithReference(pubsub); // ✅ 自动注入连接字符串到环境变量

    Aspire 后台会自动:

    • 调用 Azure ARM API 创建 Web PubSub 实例(或复用已有实例);
    • 生成具有Client.Connect权限的 SAS Token;
    • 把 Token 作为WebPubSubConnectionString环境变量注入到chat-api容器;
    • 在本地开发时,自动启动一个轻量级模拟器(azdCLI 支持),无需真实 Azure 账号。
  2. 健康检查与依赖拓扑可视化
    Aspire Dashboard 不是玩具。当你访问https://localhost:14001,能看到:

    • chat-api服务是否健康(HTTP GET/health);
    • chat-api是否能连上chat-pubsub(尝试建立 WebSocket 连接);
    • chat-pubsub的当前连接数、消息速率、错误日志摘要。
      这比你手写一堆HealthCheckILogger日志清晰十倍。
  3. 配置一致性保障
    开发、测试、生产环境的 Web PubSub 连接字符串,经常因手动复制出错。Aspire 用DistributedApplicationManifest(一个 JSON 文件)统一管理所有配置。你改一处,所有环境自动同步。

所以,AddAzureWebPubSubHub不是孤立的方法,它是 Aspire 整个依赖注入体系与 Web PubSub SDK 的深度集成点。它背后绑定的是:

  • IWebPubSubHubClient<ChatHub>(类型安全的 Hub 客户端);
  • IWebPubSubServiceClient(用于管理分组、用户、连接);
  • IWebPubSubClient(用于向特定连接 ID 发送消息);
  • IOptions<WebPubSubOptions>(包含 Hub 名称、连接字符串、重试策略等)。

这种设计,让“写业务”和“管基础设施”彻底分离。你写SendMessage方法时,脑子里想的是“我要把消息发给 room123”,而不是“我的连接字符串对不对”、“Token 过期了没”、“集群里其他实例能不能收到”。

2.3 WebSocket 原生支持:为什么不用 SignalR 客户端?

标题强调“原生 WebSocket”,这绝不是为了标新立异。SignalR 客户端(无论是 JS 还是 .NET)本质上是一个“WebSocket + Long Polling + Server-Sent Events”的兼容层。它在底层做了大量工作:

  • 自动降级(当 WebSocket 不可用时,切换到 SSE 或轮询);
  • 消息序列化/反序列化(JSON 格式,带方法名、参数、ID);
  • 连接恢复(断线后自动重连,重放未确认消息);
  • Hub 方法调用代理(hubConnection.invoke("SendMessage", ...))。

这些功能很强大,但代价是:

  • 协议不透明:你无法直接控制 WebSocket 的subprotocolheadersping/pong间隔;
  • 调试困难:浏览器 DevTools 的 Network 标签页里,看到的是一堆signalr的二进制帧,没法像原生 WebSocket 那样直接看到{"type":"message","content":"hello"}
  • 移动端兼容性风险:某些老旧 Android WebView 对 SignalR 的 SSE 降级支持不完善,导致连接失败。

而 Web PubSub 强制要求客户端使用标准 WebSocket 协议(RFC 6455)。这意味着:

  • 前端可以用最简代码连接:

    const ws = new WebSocket("wss://your-pubsub.webpubsub.azure.com/client/hubs/chat?groupId=room123&access_token=..."); ws.onmessage = (event) => { const data = JSON.parse(event.data); if (data.type === "message") { console.log("收到消息:", data.content); } };
  • 后端发的消息,前端收到的就是原始 JSON 字符串,零解析成本;

  • 所有 WebSocket 调试工具(如wscat、Postman WebSocket 客户端)都能直接连、直接测;

  • 移动端 App(iOS Swift / Android Kotlin)可以用系统原生 WebSocket API,不用引入 SignalR SDK。

我曾帮一个金融客户做合规审计,他们要求所有实时通信协议必须是 IETF 标准,不能用任何厂商私有协议。SignalR 被否决,而 Web PubSub 因为完全基于 RFC 6455,一次过审。

3. 核心细节解析与实操要点:从零搭建一个可运行的群聊

3.1 环境准备与 Aspire 项目初始化

先明确前提:你不需要 Azure 账号就能开始。Aspire 提供了本地模拟器,足够跑通整个流程。所需工具链非常干净:

  • .NET SDK 8.0+(必须,Web PubSub SDK 依赖 .NET 8 的System.Net.WebSockets新特性);
  • Visual Studio 2022 17.8+ 或 VS Code + C# Dev Kit
  • Docker Desktop(Aspire 默认用容器编排,本地模拟器也基于容器);
  • Azure CLI(可选,仅用于真机部署)

第一步,创建 Aspire 解决方案:

dotnet new aspire -n ChatApp cd ChatApp

这会生成三个项目:

  • ChatApp.AppHost:Aspire 主机,定义服务拓扑;
  • ChatApp.ApiService:你的 ASP.NET Core Web API;
  • ChatApp.ServiceDefaults:共享的中间件、认证、日志配置。

现在,把 Web PubSub 加进去。打开AppHost.cs,找到var builder = DistributedApplication.CreateBuilder(args);这行,在它下面添加:

// ✅ 添加 Web PubSub 服务(本地模拟器模式) var pubsub = builder.AddAzureWebPubSub("chat-hub") .WithAnnotation(new ContainerImageAnnotation { Registry = "mcr.microsoft.com", Image = "azure-webpubsub/azure-webpubsub" }) .WithHttpEndpoint(port: 8080, name: "http"); // 模拟器监听端口 // ✅ 添加 API 服务,并引用 PubSub var api = builder.AddProject<Projects.ChatApiService>("chat-api") .WithReference(pubsub) .WithExternalHttpEndpoints(); // 暴露 API 端口

关键点解释:

  • .WithAnnotation(...)是告诉 Aspire:本地开发时,不要去 Azure 创建真实资源,而是拉取微软官方镜像mcr.microsoft.com/azure-webpubsub/azure-webpubsub启动一个轻量容器;
  • .WithHttpEndpoint(port: 8080)是模拟器的管理端口(用于查看连接状态),不是 WebSocket 端口;
  • WithReference(pubsub)会自动把 Web PubSub 的连接字符串注入到chat-api的环境变量WebPubSubConnectionString中。

验证是否成功:运行dotnet run --project AppHost.csproj,你会看到终端输出类似:

Building and running application... Starting container 'webpubsub'... Container 'webpubsub' is running on http://localhost:8080 Starting project 'chat-api'... chat-api is ready at https://localhost:7192

此时,Web PubSub 模拟器已在http://localhost:8080运行,API 服务在https://localhost:7192

提示:如果遇到docker: command not found,请确认 Docker Desktop 已启动并登录。Aspire 模拟器完全依赖 Docker,没有替代方案。

3.2 API 服务集成:AddAzureWebPubSubHub的正确用法

打开ChatApiService项目,修改Program.cs。在builder.Services配置段,添加 Web PubSub Hub 注册:

// ✅ 正确注册:指定 Hub 名称为 "chat" builder.Services.AddAzureWebPubSubHub<ChatHub>(options => { options.ConnectionString = builder.Configuration.GetConnectionString("WebPubSub"); options.HubName = "chat"; // ⚠️ 必须与前端连接 URL 的 hub 名称一致 }); // ✅ 同时注册服务客户端(用于管理分组、用户) builder.Services.AddAzureWebPubSubClient(options => { options.ConnectionString = builder.Configuration.GetConnectionString("WebPubSub"); });

这里有两个易错点:

  1. HubName必须小写且无特殊字符:Web PubSub 服务强制要求 Hub 名称符合 DNS 标签规范(a-z, 0-9, -),且长度 1–63 字符。ChatHub类名可以是 PascalCase,但options.HubName必须是chat
  2. 连接字符串来源builder.Configuration.GetConnectionString("WebPubSub")会自动读取 Aspire 注入的环境变量WebPubSubConnectionString,你无需在appsettings.json里手动配置。

接着,创建ChatHub类。注意:它不是继承Hub,而是定义一个空类,仅作为泛型参数:

// ✅ ChatHub.cs - 纯标记类,无方法 public class ChatHub { }

为什么这样设计?因为 Web PubSub 的“Hub”概念,是服务端的一个逻辑命名空间,不是代码里的类。IWebPubSubHubClient<ChatHub>的作用,是告诉 SDK:“我要操作名为chat的 Hub”。它比字符串"chat"更类型安全,编译期就能检查。

现在,写一个发送消息的 API Controller:

[ApiController] [Route("api/[controller]")] public class ChatController : ControllerBase { private readonly IWebPubSubHubClient<ChatHub> _hubClient; public ChatController(IWebPubSubHubClient<ChatHub> hubClient) { _hubClient = hubClient; } [HttpPost("send")] public async Task<IActionResult> SendMessage([FromBody] SendMessageRequest request) { // ✅ 向指定分组发送消息(广播) await _hubClient.SendToGroupAsync(request.GroupId, new { type = "message", sender = request.Sender, content = request.Content, timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() }); return Ok(new { success = true }); } } public class SendMessageRequest { public string GroupId { get; set; } = "default"; // 默认分组 public string Sender { get; set; } = "anonymous"; public string Content { get; set; } = ""; }

关键参数说明:

  • request.GroupId:对应前端连接 URL 中的groupId参数;
  • new { ... }:匿名对象会被 JSON 序列化后发送,Web PubSub 服务不做任何修改,原样推送给客户端;
  • _hubClient.SendToGroupAsync:这是最常用的方法,适用于“房间聊天”场景。

注意:SendToGroupAsync不会返回错误,即使分组不存在。Web PubSub 服务会静默丢弃消息。所以,务必在前端连接时,确保groupId参数正确传递。

3.3 前端连接与消息收发:纯原生 WebSocket 实现

前端代码必须严格匹配 Web PubSub 的连接协议。核心是构造正确的 WebSocket URL:

wss://<your-pubsub-domain>/client/hubs/<hub-name>?groupId=<group-id>&userId=<user-id>&access_token=<sas-token>

在本地模拟器模式下,URL 是:

wss://localhost:8080/client/hubs/chat?groupId=room123&userId=user456&access_token=your-sas-token

access_token怎么获取?不能硬编码!Web PubSub 要求每次连接都用短期有效的 SAS Token。所以,你需要一个 API 接口,由后端生成 Token 并返回给前端:

// 在 ChatController 中添加 [HttpGet("connect-url")] public IActionResult GetConnectUrl([FromQuery] string groupId, [FromQuery] string userId) { // ✅ 使用服务客户端生成 Token var serviceClient = _serviceClientFactory.CreateClient(); var token = serviceClient.GenerateClientAccessToken( hubName: "chat", userId: userId, groupId: groupId, minutesToLive: 60); // Token 有效期 60 分钟 var url = $"wss://localhost:8080/client/hubs/chat?groupId={Uri.EscapeDataString(groupId)}&userId={Uri.EscapeDataString(userId)}&access_token={Uri.EscapeDataString(token.Token)}"; return Ok(new { url }); }

前端 JavaScript(Vue 3 Composition API 示例):

import { ref, onMounted } from 'vue'; export default { setup() { const ws = ref(null); const messages = ref([]); const inputMsg = ref(''); const groupId = 'room123'; const connect = async () => { try { // ✅ 第一步:获取连接 URL const res = await fetch(`/api/chat/connect-url?groupId=${groupId}&userId=${Date.now()}`); const { url } = await res.json(); // ✅ 第二步:建立 WebSocket 连接 ws.value = new WebSocket(url); ws.value.onopen = () => { console.log('WebSocket connected'); }; ws.value.onmessage = (event) => { const data = JSON.parse(event.data); if (data.type === 'message') { messages.value.push({ sender: data.sender, content: data.content, time: new Date(data.timestamp).toLocaleTimeString() }); } }; ws.value.onerror = (error) => { console.error('WebSocket error:', error); }; ws.value.onclose = () => { console.log('WebSocket closed'); }; } catch (err) { console.error('Failed to connect:', err); } }; const sendMessage = () => { if (!ws.value || ws.value.readyState !== WebSocket.OPEN) return; ws.value.send(JSON.stringify({ type: 'message', content: inputMsg.value, sender: 'current-user' })); inputMsg.value = ''; }; onMounted(() => { connect(); }); return { messages, inputMsg, sendMessage }; } };

关键细节:

  • ws.value.send(...)发送的是纯文本 JSON 字符串,不是 SignalR 那样的二进制帧;
  • onmessage回调里,event.data就是原始 JSON 字符串,直接JSON.parse即可;
  • groupIduserId必须和后端GenerateClientAccessToken时传入的一致,否则 Token 验证失败,连接被拒绝。

实操心得:我在第一次调试时,把groupId写成了room-123(带下划线),而 Token 里传的是room123,结果 WebSocket 连接立刻关闭,状态码401。Web PubSub 的错误日志非常友好,在 Aspire Dashboard 的webpubsub服务日志里,能看到Invalid group id in token的提示。记住:前后端groupIduserIdhubName必须完全一致,包括大小写和特殊字符。

3.4 消息路由与分组管理:超越简单广播的实战技巧

群聊的核心需求从来不是“发消息”,而是“精准投递”。Web PubSub 提供了四层路由能力,按优先级从高到低:

路由层级触发条件适用场景SDK 方法
连接 ID指定单个 WebSocket 连接私聊、系统通知(如“您的订单已支付”)SendToConnectionAsync(connectionId, ...)
用户 ID指定userId(Token 中设置)用户多设备同步(手机、PC 同时收到消息)SendToUserAsync(userId, ...)
分组 ID指定groupId(Token 中设置)房间聊天、频道订阅SendToGroupAsync(groupId, ...)
全 Hub不指定任何 ID全局公告、系统广播SendToAllAsync(...)

AddAzureWebPubSubHub默认支持SendToGroupAsync,但其他方法需要IWebPubSubServiceClient。我们来扩展一个“用户踢出”功能:

[HttpPost("kick/{userId}")] public async Task<IActionResult> KickUser(string userId, [FromQuery] string groupId) { var serviceClient = _serviceClientFactory.CreateClient(); // ✅ 从分组中移除用户(断开其所有连接) await serviceClient.RemoveUserFromGroupAsync("chat", groupId, userId); // ✅ 向该用户发送踢出通知 await serviceClient.SendToUserAsync("chat", userId, new { type = "kicked", reason = "You were removed from the group.", groupId = groupId }); return Ok(); }

这里用了两个关键 API:

  • RemoveUserFromGroupAsync:立即断开该用户在指定分组的所有连接;
  • SendToUserAsync:向该用户的所有在线连接(不限分组)发送消息。

这个组合,解决了“踢人”场景的终极难题:

  • 传统方案:发消息让前端自己断开,但用户可能忽略或伪造连接状态;
  • Web PubSub 方案:服务端直接操作连接状态,100% 可控。

另一个实用技巧:动态分组。很多群聊需要“按关键词加入房间”,比如搜索“#dotnet”就进入 .NET 技术讨论组。你可以用SendToGroupAsyncgroupId参数做文章:

// 前端连接时,groupId 传入哈希值 const keyword = '#dotnet'; const groupId = btoa(keyword).replace(/=/g, ''); // base64 编码,去掉 = 号 // 结果:IzE0Lm5ldA // 后端发送时,用相同算法生成 groupId string GetGroupId(string keyword) => Convert.ToBase64String(Encoding.UTF8.GetBytes(keyword)).Replace("=", "");

这样,不同关键词生成唯一groupId,Web PubSub 自动隔离流量,无需后端维护分组列表。

注意事项:groupId最大长度 128 字符,userId最大 128 字符。避免用长文本直接做 ID,建议用 MD5 或 SHA256 哈希后截取前 32 位。

4. 实操过程与核心环节实现:从本地调试到 Azure 部署

4.1 本地调试全流程:用 Postman 和 wscat 验证每一步

在把代码交给前端之前,务必用命令行工具逐层验证。这是避免“前端连不上、后端收不到”这类问题的黄金法则。

Step 1:验证 Web PubSub 模拟器是否就绪
打开浏览器,访问http://localhost:8080。你应该看到 Web PubSub 的管理界面,显示Hubs: chatConnections: 0。这是健康标志。

Step 2:用 wscat 测试 WebSocket 连接
安装wscat(Node.js 工具):

npm install -g wscat

生成一个临时 Token(用 Azure CLI 或在线工具),然后连接:

wscat -c "wss://localhost:8080/client/hubs/chat?groupId=test&userId=testuser&access_token=your-token-here"

如果连接成功,终端会显示connected (press CTRL+C to quit)。此时,回到http://localhost:8080页面,Connections数应该变成1

Step 3:用 Postman 测试 API 发送消息
启动 API 服务(dotnet run --project ChatApiService.csproj),然后用 Postman 发送 POST 请求:

  • URL:https://localhost:7192/api/chat/send
  • Body (JSON):
    { "groupId": "test", "sender": "server", "content": "Hello from API!" }

如果返回200 OK,且wscat终端收到了 JSON 消息,说明后端到 Web PubSub 的链路通了。

Step 4:用浏览器 DevTools 直接连接
打开 Chrome,F12 进入 Console,粘贴以下代码:

const ws = new WebSocket('wss://localhost:8080/client/hubs/chat?groupId=test&userId=dev&access_token=your-token'); ws.onmessage = e => console.log('Received:', e.data); ws.onopen = () => ws.send(JSON.stringify({type:'ping'}));

如果看到Received: {"type":"ping"},恭喜,你已经打通了“浏览器 → Web PubSub → API”全链路。

实操心得:我踩过的最大坑是 SSL。本地模拟器用的是自签名证书,Chrome 会拦截wss://localhost:8080。解决方案有两个:

  1. ws://localhost:8080(非加密,仅限本地开发);
  2. 在 Chrome 地址栏输入chrome://flags/#unsafely-treat-insecure-origin-as-secure,启用不安全源,然后访问http://localhost:8080(HTTP 管理界面)和ws://localhost:8080(WebSocket)。
    生产环境必须用wss,Azure Web PubSub 自带免费 TLS 证书,无需额外配置。

4.2 Azure 真机部署:从 Aspire 到 azd 的无缝迁移

Aspire 的魔力在于,本地代码几乎不用改,就能部署到 Azure。核心是azdCLI 工具。

Step 1:安装 azd 并登录 Azure

# 下载 azd(https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) azd login

Step 2:初始化 azd 环境
ChatApp根目录运行:

azd init --template aspire

这会生成azure.yaml配置文件,定义资源部署参数。

Step 3:修改 azure.yaml,指定 Web PubSub 实例名称

name: chat-app services: webpubsub: type: azure-webpubsub properties: sku: Free # 或 Standard_S1 hubName: chat api: type: azure-container-app properties: image: mcr.microsoft.com/dotnet/samples:aspire-chat-api env: - name: WebPubSubConnectionString value: ${{ services.webpubsub.connectionString }}

Step 4:一键部署

azd up

azd会自动:

  • 创建 Resource Group;
  • 部署 Azure Web PubSub 实例;
  • 构建ChatApiServiceDocker 镜像并推送到 Azure Container Registry;
  • 创建 Container App,并注入 Web PubSub 连接字符串;
  • 输出公网访问 URL。

部署完成后,azd show命令会显示所有服务的 URL。你的 API 服务地址类似https://chat-api-xxxx.eastus.azurecontainerapps.io,Web PubSub 的 WebSocket 地址是wss://chat-hub-xxxx.webpubsub.azure.com/client/hubs/chat

关键配置项说明:

  • sku: Free:免费层支持最多 100 个并发连接,适合测试;生产环境选Standard_S1(1000 连接/秒,100万消息/天);
  • hubName: chat:必须和代码里options.HubName一致;
  • value: ${{ services.webpubsub.connectionString }}azd的变量语法,自动注入生成的连接字符串。

提示:首次部署可能耗时 5–10 分钟。azd up会实时输出日志,看到Provisioning complete即表示成功。如果失败,azd logs可以查看详细错误。

4.3 生产环境加固:SSL、CORS、认证的必做配置

本地跑通只是开始,生产环境必须处理三座大山:HTTPS、跨域、认证。

SSL 配置(Azure 自动搞定)
Azure Web PubSub 默认启用 HTTPS,且提供免费的*.webpubsub.azure.com通配符证书。你无需上传证书或配置 TLS 版本。唯一要注意的是:前端连接 URL 必须用wss://,不能用ws://,否则浏览器会阻止。

CORS 配置(Aspire 时代已过时)
旧教程常教你配置app.UseCors(),但在 Web PubSub 场景下,CORS 由 Web PubSub 服务自身控制。你必须在 Azure Portal 的 Web PubSub 实例设置里,添加允许的 Origin:

  • 进入 Azure Portal → 你的 Web PubSub 实例 → Settings → CORS;
  • 添加https://your-frontend-domain.com(支持通配符https://*.example.com);
  • 保存。

如果前端域名不在白名单,WebSocket 连接会直接被拒绝,状态码403。Aspire 的appsettings.json里配置CORS对 Web PubSub 无效。

认证与授权(JWT Token 最佳实践)
Web PubSub 支持两种认证方式:

  • SAS Token(推荐):短期有效(最长 24 小时),由后端生成,前端只负责传递;
  • JWT Token(高级):需自己实现IWebPubSubAuthenticationProvider,验证用户身份。

SAS Token 已足够安全。关键是要避免 Token 泄露

  • 不要在前端存储 Token(如 localStorage);
  • Token 只在连接时使用,连接建立后即失效;
  • 后端生成 Token 时,务必设置userIdgroupId,利用 Web PubSub 的内置鉴权。

例如,一个恶意用户即使拿到了 Token,也只能连接到指定groupId,无法访问其他房间。这就是 Web PubSub 的“最小权限原则”。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 3:13:23

OpenClaw的tools与skills详解:用TaoToken统一Key跑通配置骨架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/23 3:13:15

Excel INT函数详解:从基础取整到高阶应用

1. INT函数基础认知&#xff1a;从数学定义到Excel实现INT函数作为Excel中最基础的数学函数之一&#xff0c;其官方定义是"将数字向下舍入到最接近的整数"。这个看似简单的定义背后&#xff0c;其实蕴含着严谨的数学逻辑和实用的数据处理哲学。在数学层面&#xff0c…

作者头像 李华
网站建设 2026/9/23 3:13:13

Blender新手入门:清空文件、网格与材质操作详解

说实话&#xff0c;我第一次跟着 oeasy 的 Blender 系列学习时&#xff0c;看到第 015 期的标题“清空文件_网格_mesh_材质_material”是有过一点困惑的&#xff1a;这三个词放在一块儿&#xff0c;像是不相关的碎片。结果等我把这一期完整过了一遍&#xff0c;才意识到它是给新…

作者头像 李华
网站建设 2026/9/23 3:12:03

2026年无代码平台TOP10选型指南:从数据模型到AI能力,避开五大坑

只列榜单不给标准的盘点文&#xff0c;这两年都快把读者坑出习惯了。我前前后后做过十几个无代码平台的选型&#xff0c;有给客户做的&#xff0c;也有自己团队内部用的。2026年这个时间点特别有意思&#xff1a;无代码平台的数量比三年前翻了几倍&#xff0c;可真正能接手正经…

作者头像 李华
网站建设 2026/9/23 3:11:49

AI+安全管理实战:从提示词设计到隐患排查报告生成

“这个隐患照片拍回来&#xff0c;报告谁来写&#xff1f;周一要交。”这是我这些年听过安全管理岗最真实的日常。台账、检查记录、整改回执、培训签到、演练方案、会议纪要&#xff0c;真正压垮安全员的往往不是现场跑得不够多&#xff0c;而是办公室里的文字活永远干不完。这…

作者头像 李华
网站建设 2026/9/23 3:09:27

5分钟搭建QQ AI机器人:Lighthouse+Deepseek+AstrBot+Docker实战

1. 为什么我要把AI塞进QQ里说实话&#xff0c;我一开始也是网页版AI的重度用户。每天开着浏览器标签页&#xff0c;写东西的时候切过去问两句&#xff0c;查资料的时候再切过去追问一轮。用久了就发现一个问题&#xff1a;我花在“打开AI”这件事上的时间&#xff0c;比用AI本身…

作者头像 李华