生产环境必须用 Microsoft.AspNetCore.WebSockets,因其内置 HTTP 升级、连接管理、超时控制与 DI 集成;裸用 System.Net.WebSockets 需手动处理握手、生命周期等,易出错且不支持高并发。

WebSocket 服务端用 Microsoft.AspNetCore.WebSockets 还是 System.Net.WebSockets?
直接上结论:生产环境必须用 Microsoft.AspNetCore.WebSockets(即 ASP.NET Core 内置 WebSocket 中间件),而不是裸用 System.Net.WebSockets。后者只是底层协议封装,不处理 HTTP 升级、连接生命周期、并发调度、TLS 终止等关键环节——你得自己写握手、解析 Upgrade 头、管理 socket 池,极易出错且无法承载高并发。
ASP.NET Core 的 UseWebSockets 中间件已内置超时控制、缓冲区复用、连接限流(WebSocketOptions 可配 KeepAliveInterval 和 ReceiveBufferSize),且天然集成 DI、日志、中间件管道。
-
WebSocketOptions.KeepAliveInterval = TimeSpan.FromSeconds(30):避免 NAT/防火墙静默断连 -
WebSocketOptions.ReceiveBufferSize = 4 * 1024:小包多频场景下比默认 4KB 更省内存 - 务必在
Startup.Configure中调用app.UseWebSockets(options => { ... }),且位置要在UseRouting之后、UseEndpoints之前
如何安全地广播消息给所有在线连接?别用静态集合存 WebSocket
WebSocket 对象不是线程安全的,且不可跨请求重用。常见错误是把 WebSocket 直接塞进 static ConcurrentDictionary,然后在另一个线程里直接 SendAsync——这会触发 InvalidOperationException: "The WebSocket is in an invalid state",因为连接可能已在其他线程关闭或正被读取。
正确做法是:为每个连接分配唯一 ID(如 GUID),用 ConcurrentDictionary 存储包装类,内部封装 WebSocket + CancellationTokenSource + 状态标记,并在发送前检查 State == WebSocketState.Open。
public class WebSocketConnection
{
public WebSocket Socket { get; }
public CancellationTokenSource CloseToken { get; } = new();
public WebSocketConnection(WebSocket socket) => Socket = socket;
public async Task SendAsync(byte[] data)
{
if (Socket.State != WebSocketState.Open) return;
try
{
await Socket.SendAsync(new ArraySegmentzuojiankuohaophpcnbyteyoujiankuohaophpcn(data),
WebSocketMessageType.Binary, true, CloseToken.Token);
}
catch (OperationCanceledException) { }
catch (WebSocketException) { /* 连接已断,后续清理 */ }
}}
ReceiveAsync 阻塞模型怎么应对高并发读?必须配合 MemoryPool
每个 WebSocket 连接默认独占一个后台线程执行 ReceiveAsync 循环,若用 new byte[bufferSize] 分配缓冲区,在万级连接下会引发 GC 压力暴增和内存碎片。实测 5000 连接持续收 1KB 消息时,Gen2 GC 频率从 2 分钟一次飙升至每秒多次。
mallcloud商城
mallcloud商城基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba并采用前后端分离vue的企业级微服务敏捷开发系统架构。并引入组件化的思想实现高内聚低耦合,项目代码简洁注释丰富上手容易,适合学习和企业中使用。真正实现了基于RBAC、jwt和oauth2的无状态统一权限认证的解决方案,面向互联网设计同时适合B端和C端用户,支持CI/CD多环境部署,并提
下载
解决方案:用 MemoryPool.Shared.Rent(8192) 替代 new byte[8192],并在处理完后调用 .Return() 归还缓冲区。注意 ArraySegment 必须指向 Memory.Span ,不能直接传 MemoryPool.Rent().Memory 的 ToArray()(会触发拷贝)。
- 缓冲区大小建议设为 2^n(如 4096、8192),匹配 MemoryPool 默认块大小
-
ReceiveAsync返回的WebSocketReceiveResult中Count是实际接收字节数,不是缓冲区长度 - 必须用
while (!token.IsCancellationRequested)包裹接收循环,否则连接断开时线程不会退出
为什么用了 ConcurrentDictionary 还出现连接丢失?检查 OnConnectedAsync 异常捕获
很多人把 WebSocket 接入逻辑写在 MapGet("/ws", async context => { ... }) 里,但没包裹 try/catch。一旦 AcceptWebSocketAsync() 后的初始化代码(如鉴权、DB 查询)抛异常,连接会被静默关闭,客户端收到 1006 错误,而服务端日志里只有未捕获异常堆栈,找不到对应连接 ID。
必须确保整个 WebSocket 生命周期都在 try/catch 内,且异常时主动调用 websocket.CloseAsync(WebSocketCloseStatus.InternalServerError, "...", CancellationToken.None),再清理字典中对应项。
- 不要在
AcceptWebSocketAsync前做耗时操作(如查 DB),否则会阻塞 HTTP 升级响应 - 客户端重连间隔建议用指数退避(如 1s → 2s → 4s),避免雪崩式重连冲击
- Kestrel 默认单连接最大请求体是 30MB,若需传大文件,要显式配置
options.Limits.MaxRequestBodySize = null
真正难的不是写通 WebSocket,而是让成千上万个连接在内存、GC、线程调度、网络丢包、客户端异常断连这些边界条件下稳定跑满 7×24 小时。每个 WebSocket 实例背后都是操作系统 socket、TLS 状态、.NET 线程池资源的精确配比,漏掉任意一环,压测时都会在凌晨三点给你发告警。








