.NET WebSocket Socket

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
相关推荐
HelloWorld工程师1 小时前
网站开启HTTPS:2步解决Chrome“不安全”提示
chrome·网络协议·https·ssl
xhtdj1 小时前
DuckDB Quack基于 HTTP的客户端 / 服务器协议面向多用户分析
服务器·网络协议·http
云中小生1 小时前
Scrutor:.NET 依赖注入自动化的优雅实现
c#·.net
Steadfast_GG1 小时前
详解HTTP中的URL
网络协议·http
李白你好1 小时前
Burp Suite 自动注入 HTTP Header 的插件
网络·网络协议·http
步步为营DotNet1 小时前
Semantic Kernel 在.NET AI 开发中的深度探索与实践
人工智能·.net
霸道流氓气质2 小时前
SSL Socket 通信与本地 Mock Server 实践指南
网络·网络协议·ssl
半亩码田2 小时前
【.NET新特性·第5篇】.NET 9 速览:云原生与性能之年
云原生·.net
宇砾2 小时前
HTTPS的工作流程
网络协议·http·https
.NET修仙日记2 小时前
.NET 领域驱动设计:用户角色更新如何从应用服务落地到领域实体(代码拆解)
c#·.net·领域驱动设计·微软技术·角色设计