1. WebSocketHelper
cs
复制代码
public class WebSocketsHelper
{
// 用户ID -> 多个 WebSocket 连接(支持多设备)
private static readonly ConcurrentDictionary<string, HashSet<WebSocket>> _connections = new();
private readonly ILogger<WebSocketsHelper> _logger;
public WebSocketsHelper(ILogger<WebSocketsHelper> logger)
{
_logger = logger;
}
public static async Task AddConnection(string userId, WebSocket socket)
{
_connections.AddOrUpdate(userId,
new HashSet<WebSocket> { socket },
(key, existing) => { existing.Add(socket); return existing; });
}
public static async Task RemoveConnection(string userId, WebSocket socket)
{
if (socket == null) return;
// 👇 关键:只在有效状态下才尝试关闭
if (socket.State == WebSocketState.Open ||
socket.State == WebSocketState.CloseReceived ||
socket.State == WebSocketState.CloseSent)
{
try
{
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "已从连接池中移除", CancellationToken.None);
}
catch (WebSocketException ex)
{
Log.Error($"警告:无法关闭WebSocket:{ex.Message}");
}
}
// 无论是否成功关闭,都从连接池移除
lock (_connections)
{
if (_connections.TryGetValue(userId, out var userSockets))
{
userSockets.Remove(socket);
if (userSockets.Count == 0)
_connections.Remove(userId, out _);
}
}
}
/// <summary>
/// 针对单个用户发消息
/// </summary>
/// <param name="userId"></param>
/// <param name="message"></param>
/// <returns></returns>
public static async Task SendMessageToUser(string userId, string message)
{
if (_connections.TryGetValue(userId, out var sockets))
{
var tasks = new List<Task>();
foreach (var socket in sockets.ToList()) // ToList 防止并发修改
{
if (socket.State == WebSocketState.Open)
{
var buffer = Encoding.UTF8.GetBytes(message);
tasks.Add(socket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None));
}
else
{
sockets.Remove(socket); // 清理已关闭的连接
}
}
await Task.WhenAll(tasks);
}
}
/// <summary>
/// 广播(群发消息)
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
public static async Task BroadcastMessage(string message)
{
var tasks = new List<Task>();
foreach (var (_, sockets) in _connections)
{
foreach (var socket in sockets.ToList())
{
if (socket.State == WebSocketState.Open)
{
var buffer = Encoding.UTF8.GetBytes(message);
tasks.Add(socket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None));
}
}
}
await Task.WhenAll(tasks);
}
}
2. Program.cs
cs
复制代码
#region WebSocket 中心路由
builder.Services.AddWebSockets(options =>
{
// 开发环境可放宽限制
if (builder.Environment.IsDevelopment())
{
options.AllowedOrigins.Add("*");
}
else
{
options.AllowedOrigins.Add("http://IP地址:端口号"); // 前端开发地址
options.AllowedOrigins.Add("http://域名"); // 前端开发地址
}
});
#endregion
#region WebSocket 中心路由
app.UseWebSockets();
static async Task<string?> ValidateTokenAndGetUserId(string token, IConfiguration configuration, HttpContext context)
{
try
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["SecurityKey"]));
var validationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = configuration["issuer"],
ValidAudience = configuration["audience"],
IssuerSigningKey = key,
ClockSkew = TimeSpan.Zero // 严格过期
};
var principal = tokenHandler.ValidateToken(token, validationParameters, out _);
var userId = principal.FindFirst(ClaimTypes.Name)?.Value;
// 可选:验证 Redis 中是否存在该 token(防止登出后仍有效)
var redis = context.RequestServices.GetRequiredService<RedisHelper>();
var storedToken = await redis.GetCurrentUserToken(userId);
if (storedToken != token)
return null; // token 不匹配,视为无效
return userId;
}
catch
{
return null;
}
}
// 注册 WebSocket 通知中心路由
app.Map("/ws/notification", async context =>
{
if (!context.WebSockets.IsWebSocketRequest)
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
return;
}
// 从查询字符串获取 token
var token = context.Request.Query["token"].ToString();
if (string.IsNullOrEmpty(token))
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsync("缺少令牌");
return;
}
// 验证 JWT Token 并提取用户ID
var userId = await ValidateTokenAndGetUserId(token, builder.Configuration, context);
if (string.IsNullOrEmpty(userId))
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsync("无效的令牌");
return;
}
// 接受 WebSocket 连接
var webSocket = await context.WebSockets.AcceptWebSocketAsync();
// 将用户加入在线连接池
await WebSocketsHelper.AddConnection(userId, webSocket);
try
{
var buffer = new byte[1024 * 4];
while (webSocket.State == WebSocketState.Open)
{
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Close)
{
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed by client", CancellationToken.None);
break;
}
// 可选:处理客户端发来的消息(如心跳、订阅等)
// 例如解析 JSON 消息
if (result.EndOfMessage && result.Count > 0)
{
var messageStr = Encoding.UTF8.GetString(buffer, 0, result.Count);
//await Console.Out.WriteLineAsync($"{messageStr},userId={userId}");
// 可在此处理客户端指令,如 { type: "subscribe", topic: "notification" }
await WebSocketsHelper.ProcessMessage(messageStr, userId);
}
}
}
finally
{
//await Console.Out.WriteLineAsync($"用户断开连接,移除{userId}");
// 用户断开连接,移除
await WebSocketsHelper.RemoveConnection(userId, webSocket);
}
});
#endregion