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<string websocket></string>,然后在另一个线程里直接
SendAsync——这会触发
InvalidOperationException: "The WebSocket is in an invalid state",因为连接可能已在其他线程关闭或正被读取。
正确做法是:为每个连接分配唯一 ID(如 GUID),用
ConcurrentDictionary<string websocketconnection></string>存储包装类,内部封装
WebSocket+
CancellationTokenSource+ 状态标记,并在发送前检查
State == WebSocketState.Open。
public class WebSocketConnection
{
public WebSocket Socket { get; }
public CancellationTokenSource CloseToken { get; } = new();
<pre class='brush:php;toolbar:false;'>public WebSocketConnection(WebSocket socket) => Socket = socket;
public async Task SendAsync(byte[] data)
{
if (Socket.State != WebSocketState.Open) return;
try
{
await Socket.SendAsync(new ArraySegment<byte>(data),
WebSocketMessageType.Binary, true, CloseToken.Token);
}
catch (OperationCanceledException) { }
catch (WebSocketException) { /* 连接已断,后续清理 */ }
}}
ReceiveAsync
阻塞模型怎么应对高并发读?必须配合 MemoryPool<byte></byte>
每个 WebSocket 连接默认独占一个后台线程执行
ReceiveAsync循环,若用
new byte[bufferSize]分配缓冲区,在万级连接下会引发 GC 压力暴增和内存碎片。实测 5000 连接持续收 1KB 消息时,Gen2 GC 频率从 2 分钟一次飙升至每秒多次。
解决方案:用
MemoryPool<byte>.Shared.Rent(8192)</byte>替代
new byte[8192],并在处理完后调用
.Return()归还缓冲区。注意
ArraySegment<byte></byte>必须指向
Memory<byte>.Span</byte>,不能直接传
MemoryPool<byte>.Rent().Memory</byte>的
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 线程池资源的精确配比,漏掉任意一环,压测时都会在凌晨三点给你发告警。
