.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
相关推荐
安静轨迹2 小时前
网口Bond模式详解:7种模式通俗解析
网络·网络协议
与遨游于天地2 小时前
HTTP的历史由来
网络·网络协议·http
wenha15 小时前
踩坑记录:UTF-8、UTF-8-BOM 与 GB2312 读取的乱码真相
utf-8·.net·编码·utf-8-bom
发光小北16 小时前
IEC104 转 Modbus TCP 网关如何应用?
网络·网络协议·tcp/ip
treesforest17 小时前
IP 反欺诈查询怎么落地更稳?Ipdatacloud 适用场景与实战决策闭环
网络·数据库·网络协议·tcp/ip·网络安全
lularible19 小时前
PTP协议精讲(2.18):遵循规则的艺术——Profile与一致性要求深度解析
网络·网络协议·开源·嵌入式·ptp
皮卡蛋炒饭.20 小时前
网络基础概念
服务器·网络协议
一川_20 小时前
前端驱动工业报警:基于 WebSocket 与网关的三色蜂鸣灯实时报警系统实战
javascript·websocket
彭于晏Yan20 小时前
Spring Boot整合WebSocket入门(一)
spring boot·后端·websocket