.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
相关推荐
2401_873479401 小时前
SOC告警日志中IP归属不明怎么办?部署IP离线库三步提升响应效率
网络·网络协议·tcp/ip
海盗12343 小时前
微软技术周报——2026-07-22
microsoft·c#·.net
海盗12343 小时前
微软技术日报 ——2026-07-21
microsoft·c#·.net
想学好C++的oMen4 小时前
socket编程UDP
网络协议·udp
半亩码田4 小时前
【.NET新特性·第8篇】.NET 9 AI 构建基块:Microsoft.Extensions.AI
人工智能·microsoft·.net
GitLqr14 小时前
玩转 WebSocket:从原理到 Flutter 实战
websocket·网络协议·flutter
微硬创新17 小时前
耐达讯自动化 CC-Link IE 转 PROFINET网关在污水厂智能化升级实战
人工智能·物联网·网络协议·自动化·信息与通信
Ctrl+Z侠20 小时前
.NET WebApi Windows / Linux Docker 全链路压测与瓶颈定位 0 到 1 教程
linux·windows·.net
TlSfoward21 小时前
抓包代理链路下的 TLS 指纹变化分析 TLSFOWARD抓包工具
数据库·爬虫·网络协议·搜索引擎·php
慧都小妮子1 天前
Aspose.CAD for .NET 26.6 更新发布
pdf·.net·3d渲染·cad·pdf导出·ifc编辑