基于 ASP.NET Core 的高性能 WebSocket 服务端。你只需继承一个 WebSocketHandler 基类、重写几个事件方法,剩下的连接生命周期管理、并发发送、会话跟踪、广播、内存池化全部由框架处理。
命名空间:
EasilyNET.WebCore.WebSocket注册扩展位于Microsoft.Extensions.DependencyInjection/Microsoft.AspNetCore.Builder(using这两个即可) 程序集:EasilyNET.WebCore
保活说明:连接保活/死链检测采用 WebSocket 协议层 Ping/Pong(由运行时处理),通过
KeepAliveInterval+KeepAliveTimeout启用。不提供应用层"ping/pong 消息"心跳——避免无谓的复杂度与混淆。详见保活与死链检测。
// ① 定义处理程序:重写你关心的事件
public sealed class EchoHandler(ILogger<EchoHandler> logger) : WebSocketHandler
{
public override Task OnConnectedAsync(IWebSocketSession session)
{
logger.LogInformation("已连接: {Id}", session.Id);
return session.SendTextAsync("welcome");
}
// 唯一必须实现的方法
public override async Task OnMessageAsync(IWebSocketSession session, WebSocketMessage message)
{
if (message.MessageType == WebSocketMessageType.Text)
{
var text = Encoding.UTF8.GetString(message.Data.Span);
await session.SendTextAsync($"echo: {text}");
}
}
}// ② 注册:Handler 必须进 DI 容器;SessionManager 可选(需要广播时加)
builder.Services.AddSingleton<EchoHandler>();
builder.Services.AddWebSocketSessionManager(); // 可选
// ③ 映射:先 UseWebSockets(),再把 Handler 映射到路径
var app = builder.Build();
app.UseWebSockets();
app.MapWebSocketHandler<EchoHandler>("/ws/echo");
app.Run();客户端连 ws://your-host/ws/echo 即可。默认参数即生产可用:4MB 单条消息上限、自动会话清理。建议再配上协议层保活(见下文)。
每个连接的回调遵循严格顺序:
OnConnectedAsync → OnMessageAsync × N → OnDisconnectedAsync
(任意阶段出错则额外触发 OnErrorAsync)
OnConnectedAsync一定在任何OnMessageAsync之前完成。所以你可以放心在OnConnectedAsync里做鉴权、初始化session.Items,不用担心消息"插队"先到。OnMessageAsync按消息到达顺序串行调用(单分发循环),同一连接内不会并发回调。- 只有当
OnConnectedAsync成功返回后,框架才认为"已连接",断开时才会调用OnDisconnectedAsync。
接收循环只负责从 socket 读取、组装完整消息,然后入队给独立的分发循环调用 OnMessageAsync。因此你的 OnMessageAsync 再慢也不会阻塞接收循环。分发队列满(ReceiveDispatchQueueCapacity)时对接收侧背压,限制内存。
session.SendAsync / SendTextAsync / SendBinaryAsync 把消息放进有界队列,由唯一发送循环按顺序写出。多处并发调用同一 session 的发送是安全的(不会交错损坏帧)。发送为即发即弃:方法在入队成功后即返回,实际发送错误记入日志。
OnMessageAsync 的 message.Data 是该条消息的独立数组,在回调内可安全读取。需要长期保留时仍建议 ToArray() 拷贝。Encoding.UTF8.GetString(message.Data.Span) 是最常见的读法。
单条消息累计超过 MaxMessageSize(默认 4MB)时,框架会以 MessageTooBig 关闭该连接并记录告警——防止恶意或异常客户端用超大消息/永不结束的分片耗尽服务端内存。
WebSocketHandler 有 4 个方法,只有 OnMessageAsync 是 abstract 必须实现,其余 virtual 可选重写:
| 方法 | 必须 | 触发时机 |
|---|---|---|
OnConnectedAsync |
否 | 连接建立、进入消息循环前 |
OnMessageAsync |
是 | 每收到一条完整消息 |
OnDisconnectedAsync |
否 | 连接断开(仅当曾成功 OnConnected) |
OnErrorAsync |
否 | 接收/处理过程中发生异常 |
Handler 实例由 DI 解析。它通常是单例,被所有连接共享——因此不要在 Handler 字段里存单个连接的状态,按连接的数据请放在
session.Items里。
public sealed class ChatHandler(
IWebSocketSessionManager sessions,
ILogger<ChatHandler> logger) : WebSocketHandler
{
public override async Task OnConnectedAsync(IWebSocketSession session)
{
// 按连接的数据存在 session.Items(线程安全字典)
session.Items["JoinedAt"] = DateTime.UtcNow;
session.Items["Name"] = "匿名";
await session.SendTextAsync("欢迎加入聊天室!");
await sessions.BroadcastTextAsync($"用户 {session.Id} 加入了");
}
public override async Task OnMessageAsync(IWebSocketSession session, WebSocketMessage message)
{
if (message.MessageType != WebSocketMessageType.Text) return;
var text = Encoding.UTF8.GetString(message.Data.Span);
var name = session.Items["Name"] as string ?? session.Id;
await sessions.BroadcastTextAsync($"[{name}]: {text}");
}
public override async Task OnDisconnectedAsync(IWebSocketSession session)
=> await sessions.BroadcastTextAsync($"用户 {session.Id} 离开了");
public override Task OnErrorAsync(IWebSocketSession session, Exception exception)
{
logger.LogWarning(exception, "会话 {Id} 出错", session.Id);
return Task.CompletedTask;
}
}// Handler 必须注册到 DI(MapWebSocketHandler 会校验,未注册会抛异常)
builder.Services.AddSingleton<ChatHandler>();
// 会话管理器:需要广播 / 遍历在线连接时注册(单例)
builder.Services.AddWebSocketSessionManager();如果你用 EasilyNET 的
AutoDependencyInjection,也可以给 Handler 打[DependencyInjection(ServiceLifetime.Singleton)]特性自动注册。
var app = builder.Build();
app.UseWebSockets(); // ⚠️ 必须在 MapWebSocketHandler 之前调用
// 用默认配置
app.MapWebSocketHandler<ChatHandler>("/ws/chat");
// 或自定义配置
app.MapWebSocketHandler<ChatHandler>("/ws/chat", new WebSocketSessionOptions
{
ReceiveBufferSize = 16384,
MaxMessageSize = 4 * 1024 * 1024,
SendQueueCapacity = 1000,
ReceiveDispatchQueueCapacity = 1024,
CloseTimeout = TimeSpan.FromSeconds(5),
// 协议层保活 + 死链检测
KeepAliveInterval = TimeSpan.FromSeconds(15),
KeepAliveTimeout = TimeSpan.FromSeconds(10)
});
app.Run();忘记
app.UseWebSockets()会导致升级握手失败;Handler 未注册到 DI 则MapWebSocketHandler会抛InvalidOperationException明确提示。
注册 AddWebSocketSessionManager() 后,可注入 IWebSocketSessionManager 操作全部在线连接:
public interface IWebSocketSessionManager
{
int Count { get; } // 在线连接数
IReadOnlyCollection<IWebSocketSession> GetAllSessions(); // 所有连接快照
IWebSocketSession? GetSession(string id); // 按 ID 取连接
Task BroadcastAsync(ReadOnlyMemory<byte> message,
WebSocketMessageType messageType = WebSocketMessageType.Text,
CancellationToken cancellationToken = default); // 广播
Task BroadcastTextAsync(string text, CancellationToken cancellationToken = default);
}// 在任意服务中注入并使用
public class NotifyService(IWebSocketSessionManager sessions)
{
public Task PushToAllAsync(string msg) => sessions.BroadcastTextAsync(msg);
public Task PushToOneAsync(string sessionId, string msg)
=> sessions.GetSession(sessionId)?.SendTextAsync(msg) ?? Task.CompletedTask;
public int Online => sessions.Count;
}会话在中间件层自动注册/注销:连接进入即加入管理器,连接结束(无论正常还是异常)即移除,无需手动维护。 广播为即发即弃:
BroadcastTextAsync把消息入队到每个连接后即返回,不等待真正发送完成。
服务端的连接保活同样走 WebSocket 协议层 Ping/Pong,由运行时自动收发。通过两个选项启用:
| 选项 | 作用 |
|---|---|
KeepAliveInterval |
运行时按此间隔发送协议层 PING |
KeepAliveTimeout(.NET 8+) |
发送 PING 后超时无 PONG → 运行时 abort 连接 |
app.MapWebSocketHandler<ChatHandler>("/ws/chat", new WebSocketSessionOptions
{
KeepAliveInterval = TimeSpan.FromSeconds(15),
KeepAliveTimeout = TimeSpan.FromSeconds(10)
});为 null 时回退到 app.UseWebSockets(new WebSocketOptions { KeepAliveInterval = ..., KeepAliveTimeout = ... }) 配置的全局默认值,因此你也可以在 UseWebSockets 处统一设置、各路径不再单独配置。连接死掉时运行时 abort → 接收循环抛异常 → 框架走清理流程并调用 OnDisconnectedAsync。
与本库客户端对接:
ManagedWebSocketClient(EasilyNET.Core)同样使用协议层保活。两端都设置KeepAliveInterval+KeepAliveTimeout即可,无需在OnMessageAsync里处理任何心跳消息。
⚠️ 协议层 Pong 由对端运行时自动回复,只证明"传输 + 运行时存活"。若要验证对端业务循环真的在处理消息,请在业务消息里自行附带探活(属普通业务逻辑)。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
SendQueueCapacity |
int |
1000 | 每连接发送队列容量(满时发送背压等待) |
ReceiveBufferSize |
int |
16384 | 单次接收缓冲区大小(字节) |
MaxMessageSize |
long |
4MB | 单条入站消息上限,超限以 MessageTooBig 断连防内存耗尽;设 0 或负数禁用(不推荐) |
ReceiveDispatchQueueCapacity |
int |
1024 | 接收分发队列容量;解耦 OnMessageAsync 与接收循环,满时对接收侧背压 |
CloseTimeout |
TimeSpan |
5 秒 | 关闭握手超时,防止对端不响应导致挂起 |
KeepAliveInterval |
TimeSpan? |
null |
协议层 PING 间隔(接受连接时透传);null 用 UseWebSockets 的全局默认 |
KeepAliveTimeout |
TimeSpan? |
null |
协议层 PING/PONG 超时(.NET 8+);与 KeepAliveInterval 同设即开启死链检测 |
| 组件 | 说明 |
|---|---|
WebSocketHandler |
业务逻辑基类。重写 OnConnected/OnMessage/OnDisconnected/OnError 即可 |
IWebSocketSession |
单个客户端连接。提供 Id、State、Items 及 SendAsync/SendTextAsync/SendBinaryAsync/CloseAsync |
IWebSocketSessionManager |
会话管理器接口:Count、GetAllSessions、GetSession、BroadcastAsync/BroadcastTextAsync |
WebSocketSessionManager |
默认实现,线程安全,单例 |
WebSocketSessionOptions |
每个映射路径的会话配置 |
MapWebSocketHandler<T> |
把 Handler 映射到路径的扩展方法 |
AddWebSocketSessionManager |
注册会话管理器的扩展方法 |
IWebSocketSession 常用成员:
string Id { get; } // Ulid,全局唯一、时间有序
WebSocketState State { get; } // 底层连接状态
IDictionary<string, object?> Items { get; } // 按连接的线程安全数据袋
Task SendTextAsync(string text, ...);
Task SendBinaryAsync(byte[] bytes, ...);
Task SendAsync(ReadOnlyMemory<byte> message, WebSocketMessageType type, ...);
Task CloseAsync(WebSocketCloseStatus status, string? description, ...); // 主动关闭某连接- Handler 是共享单例:不要用字段存单连接状态,按连接的数据放
session.Items;跨连接的共享状态需自行保证线程安全。 - 开启协议层保活:生产环境建议设置
KeepAliveInterval+KeepAliveTimeout(或在UseWebSockets处全局设置),自动检测并清理死链。 - 回调里捕获异常:未捕获的异常会走
OnErrorAsync,但建议在业务代码内自行处理关键路径。 - 鉴权放在
OnConnectedAsync:因为它保证先于任何消息执行;鉴权失败可await session.CloseAsync(...)主动断开。 - 合理设置
MaxMessageSize:对外暴露的服务务必保留上限以防滥用;需要大消息时同时调大客户端与服务端上限。
Q:客户端连不上 / 升级握手失败?
A:确认在 MapWebSocketHandler 之前调用了 app.UseWebSockets(),且路径一致。
Q:启动即抛"handler ... is not registered"?
A:Handler 必须注册到 DI(AddSingleton<T>() 或 [DependencyInjection] 特性)后才能 MapWebSocketHandler<T>。
Q:怎么自动清理掉线/僵死的连接?
A:设置 KeepAliveInterval + KeepAliveTimeout 开启协议层死链检测;运行时 abort 后框架会触发清理与 OnDisconnectedAsync。
Q:OnMessageAsync 会并发调用吗?
A:同一连接内不会——按到达顺序串行。不同连接之间是并行的(各有独立循环)。
Q:超大消息为什么连接被关了?
A:超过 MaxMessageSize(默认 4MB)会以 MessageTooBig 主动关闭,调大该值或在客户端分块发送。