结论:C#中WebSocket实时通信应优先使用Microsoft.AspNetCore.SignalR而非System.Net.WebSockets原生API;SignalR内置路由、广播、重连、心跳及多协议回退,而原生API仅提供底层连接,需手动处理帧、并发、心跳等,适用场景极窄。

直接说结论:C# 里做 WebSocket 实时通信,别碰 System.Net.WebSockets 原生 API 写服务端——它只提供底层连接管理,没路由、没消息分发、没心跳、没并发安全封装,硬刚等于自己造半个 SignalR。
用 Microsoft.AspNetCore.SignalR 而不是裸 WebSocket
你真正需要的不是“WebSocket 协议支持”,而是“客户端连上来后能发消息、能按用户/组广播、断线自动重连、服务端能主动推”。这些 SignalR 全包了,且默认优先用 WebSocket 传输(Fallback 到 Server-Sent Events 或 Long Polling)。
-
Startup.cs或Program.cs中调用AddSignalR()和MapHub<myhub>("/hub")</myhub> - 写一个继承
Hub的类,方法名会自动暴露给 JS 客户端调用,比如public async Task SendMessage(string user, string msg) - JS 端用
new HubConnectionBuilder().withUrl("/hub").build()连接,不用管协议切换 - 注意:Hub 实例是**每次调用新建**的,不要在字段里存状态;要用
IHubContext<myhub></myhub>从 DI 容器获取上下文做跨请求推送
WebSocket 原生 API 只适合极少数场景
除非你在写代理网关、协议转换桥接、或嵌入式设备直连(且明确禁用 SignalR),否则不建议用 HttpContext.WebSockets.AcceptWebSocketAsync() 手动处理帧。
- 收到
WebSocketCloseStatus后必须显式调用CloseAsync(),否则连接卡在 CLOSE_WAIT -
ReceiveAsync()是单次读取,需循环调用;SendAsync()不保证原子性,大数据要分帧、加锁或用MemoryStream缓冲 - 没有内置心跳,得自己发
Ping帧(opcode = 9)并监听Pong(opcode = 10),超时就关连接 - .NET 6+ 支持
WebSocketOptions.KeepAliveInterval,但仅影响底层 TCP KeepAlive,不替代应用层心跳
客户端 JS 连不上?先查这三处
SignalR 连接失败,80% 是环境或配置问题,不是代码逻辑错。
- 检查浏览器控制台是否报
Failed to start the connection: Error: Unable to initialize any of the available transports—— 很可能是 Kestrel 没启用 WebSocket:WebHost.CreateDefaultBuilder().ConfigureKestrel(o => o.ConfigureEndpointDefaults(opt => opt.Protocols = HttpProtocols.Http1AndHttp2AndHttp3))不够,必须加opt.WebSocket = new WebSocketOptions { KeepAliveInterval = TimeSpan.FromSeconds(60) } - 反向代理(Nginx / IIS)未透传 Upgrade / Connection 头,Nginx 需显式配置:
proxy_set_header Upgrade $http_upgrade;和proxy_set_header Connection "upgrade"; - 客户端 URL 写成
https://host/hub但服务端映射的是/hub/(带尾斜杠),会导致 404;SignalR 客户端对路径敏感,务必保持前后一致
真正难的不是“怎么连上”,而是“连接稳定后如何安全地共享状态、避免并发修改 Hub 上下文、处理百万级连接时的内存与 GC 压力”。这些 SignalR 默认不帮你扛,得结合 IDistributedCache、ChannelReader 或专门的流处理库来补足。


















