.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
相关推荐
网络小江3 小时前
上网第四十八课:抓包实战入门——Wireshark 读懂第一个包
网络协议
墨香幽梦客4 小时前
SSL/TLS握手过程全拆解:从ClientHello到Finish的关键步骤与性能优化
网络协议·性能优化·ssl
QUDONG_biubiubiu4 小时前
.NET Runtime 6.0/7.0/8.0/9.0下载修复教程,软件打不开这样解决
.net·net runtime下载·net运行库修复·net runtime安装
神一样的老师4 小时前
WS63 访问 HTTPS 握手失败(-0x7780)根治
数据库·网络协议·https
宝桥南山5 小时前
.NET 11 - 尝试创建一下AGUIServer和AGUIClient(基于新版.NET SDK for AG-UI)
microsoft·ai·微软·c#·aigc·.net
波力海苔夹心脆6755 小时前
C# LINQ 入门到上手:一篇讲透查询语法、常用操作符与延迟执行(含示例与速查表
经验分享·c#·.net·solr·linq
木子n114 小时前
车载以太网与SOME/IP服务化通信实战-01.从字节到服务:SOME/IP报文解析与四通道开销账
网络·网络协议·tcp/ip·车载以太网·some/ip
Knight_AL17 小时前
基于 Netty 实现的 WebSocket 服务端
网络·websocket·网络协议
沫璃染墨19 小时前
从零入门计算机网络系列(一):计算机网络初识——从网络发展史到TCP/IP协议》
linux·网络·网络协议·tcp/ip·计算机网络
是个西兰花19 小时前
TCP套接字编程实战:从服务器搭建到多进程处理
服务器·网络协议·tcp/ip