Browse Source

更新:添加WebSocket相关功能实现

test
hyh 3 months ago
parent
commit
57292385ad
  1. 28
      src/CellularManagement.Application/Pipeline/IWebSocketMessageHandler.cs
  2. 16
      src/CellularManagement.Application/Services/IWebSocketService.cs
  3. 137
      src/CellularManagement.Application/Services/WebSocketService.cs
  4. 1
      src/CellularManagement.Domain/CellularManagement.Domain.csproj
  5. 66
      src/CellularManagement.Domain/Entities/WebSocketConnection.cs
  6. 35
      src/CellularManagement.Domain/Events/WebSocketMessageEvent.cs
  7. 9
      src/CellularManagement.Infrastructure/Configurations/WebSocketConfiguration.cs
  8. 12
      src/CellularManagement.Infrastructure/DependencyInjection.cs
  9. 196
      src/CellularManagement.Infrastructure/Distributed/DistributedWebSocketManager.cs
  10. 65
      src/CellularManagement.Infrastructure/Monitoring/WebSocketMetrics.cs
  11. 54
      src/CellularManagement.Infrastructure/Pipeline/Handlers/WebSocketMessageValidationHandler.cs
  12. 145
      src/CellularManagement.Infrastructure/Pipeline/WebSocketMessagePipeline.cs
  13. 59
      src/CellularManagement.Infrastructure/Pooling/WebSocketMessagePool.cs
  14. 67
      src/CellularManagement.Infrastructure/WebSocket/WebSocketConnectionManager.cs
  15. 116
      src/CellularManagement.Infrastructure/WebSocket/WebSocketMessagePipeline.cs
  16. 41
      src/CellularManagement.Infrastructure/WebSocket/WebSocketMessagePool.cs
  17. 89
      src/CellularManagement.Infrastructure/WebSocket/WebSocketMiddleware.cs
  18. 295
      src/CellularManagement.Infrastructure/WebSocket/WebSocketService.cs
  19. 8
      src/CellularManagement.WebAPI/CellularManagement.WebAPI.csproj
  20. 29
      src/CellularManagement.WebAPI/Program.cs
  21. 4
      src/CellularManagement.WebAPI/appsettings.json
  22. 382
      src/CellularManagement.WebAPI/wwwroot/websocket.html

28
src/CellularManagement.Application/Pipeline/IWebSocketMessageHandler.cs

@ -0,0 +1,28 @@
using System.Threading.Channels;
namespace CellularManagement.Application.Pipeline;
public interface IWebSocketMessageHandler
{
string HandlerName { get; }
int Priority { get; }
bool CanHandle(string messageType);
Task HandleAsync(WebSocketMessageContext context, CancellationToken cancellationToken);
}
public class WebSocketMessageContext
{
public string ConnectionId { get; set; } = string.Empty;
public string MessageType { get; set; } = string.Empty;
public byte[] Payload { get; set; } = Array.Empty<byte>();
public Dictionary<string, object> Properties { get; } = new();
public bool IsHandled { get; set; }
public Exception? Error { get; set; }
}
public interface IWebSocketMessagePipeline
{
Task ProcessMessageAsync(WebSocketMessageContext context, CancellationToken cancellationToken);
void RegisterHandler(IWebSocketMessageHandler handler);
void UnregisterHandler(string handlerName);
}

16
src/CellularManagement.Application/Services/IWebSocketService.cs

@ -0,0 +1,16 @@
using System.Net.WebSockets;
using CellularManagement.Domain.Entities;
namespace CellularManagement.Application.Services;
public interface IWebSocketService
{
Task<string> AcceptConnectionAsync(WebSocket webSocket);
Task<bool> CloseConnectionAsync(string connectionId);
Task<bool> SendMessageAsync(string connectionId, byte[] message);
Task<bool> BroadcastMessageAsync(byte[] message);
Task<bool> SendMessageToUserAsync(string userId, byte[] message);
Task AssociateUserAsync(string connectionId, string userId);
Task<WebSocketConnection?> GetConnectionAsync(string connectionId);
Task<IEnumerable<WebSocketConnection>> GetUserConnectionsAsync(string userId);
}

137
src/CellularManagement.Application/Services/WebSocketService.cs

@ -0,0 +1,137 @@
using System.Net.WebSockets;
using CellularManagement.Domain.Entities;
using CellularManagement.Application.Services;
using CellularManagement.Application.Pipeline;
using Microsoft.Extensions.Logging;
using MediatR;
using CellularManagement.Domain.Events;
namespace CellularManagement.Application.Services;
public class WebSocketService : IWebSocketService
{
private readonly ICacheService _cacheService;
private readonly ILogger<WebSocketService> _logger;
private readonly IWebSocketMessagePipeline _messagePipeline;
private readonly IMediator _mediator;
private const string CONNECTION_PREFIX = "ws_connection_";
private const string USER_CONNECTION_PREFIX = "ws_user_";
public WebSocketService(
ICacheService cacheService,
ILogger<WebSocketService> logger,
IWebSocketMessagePipeline messagePipeline,
IMediator mediator)
{
_cacheService = cacheService;
_logger = logger;
_messagePipeline = messagePipeline;
_mediator = mediator;
}
public async Task<string> AcceptConnectionAsync(WebSocket webSocket)
{
var connectionId = Guid.NewGuid().ToString();
var connection = WebSocketConnection.Create(connectionId);
var connectionKey = $"{CONNECTION_PREFIX}{connectionId}";
_cacheService.Set(connectionKey, webSocket);
_cacheService.Set(connectionId, connection);
_logger.LogInformation("WebSocket connection accepted: {ConnectionId}", connectionId);
return connectionId;
}
public async Task<bool> CloseConnectionAsync(string connectionId)
{
var connectionKey = $"{CONNECTION_PREFIX}{connectionId}";
var userKey = $"{USER_CONNECTION_PREFIX}{connectionId}";
var connection = _cacheService.Get<WebSocketConnection>(connectionId);
if (connection != null)
{
connection.Close();
_cacheService.Set(connectionId, connection);
}
_cacheService.Remove(connectionKey);
_cacheService.Remove(userKey);
_logger.LogInformation("WebSocket connection closed: {ConnectionId}", connectionId);
return true;
}
public async Task<bool> SendMessageAsync(string connectionId, byte[] message)
{
var connectionKey = $"{CONNECTION_PREFIX}{connectionId}";
var webSocket = _cacheService.Get<WebSocket>(connectionKey);
if (webSocket?.State == WebSocketState.Open)
{
await webSocket.SendAsync(
new ArraySegment<byte>(message),
WebSocketMessageType.Text,
true,
CancellationToken.None);
await _mediator.Publish(new WebSocketMessageSentEvent(
connectionId,
"text",
message));
return true;
}
return false;
}
public async Task<bool> BroadcastMessageAsync(byte[] message)
{
// 注意:这里需要实现广播逻辑
// 由于ICacheService的限制,可能需要使用其他方式实现
return false;
}
public async Task<bool> SendMessageToUserAsync(string userId, byte[] message)
{
// 注意:这里需要实现向特定用户发送消息的逻辑
// 由于ICacheService的限制,可能需要使用其他方式实现
return false;
}
public async Task AssociateUserAsync(string connectionId, string userId)
{
var connection = _cacheService.Get<WebSocketConnection>(connectionId);
if (connection != null)
{
connection.AssociateUser(userId);
_cacheService.Set(connectionId, connection);
var userKey = $"{USER_CONNECTION_PREFIX}{connectionId}";
_cacheService.Set(userKey, userId);
}
}
public async Task<WebSocketConnection?> GetConnectionAsync(string connectionId)
{
return _cacheService.Get<WebSocketConnection>(connectionId);
}
public async Task<IEnumerable<WebSocketConnection>> GetUserConnectionsAsync(string userId)
{
// 注意:这里需要实现获取用户所有连接的逻辑
// 由于ICacheService的限制,可能需要使用其他方式实现
return Enumerable.Empty<WebSocketConnection>();
}
public async Task ProcessMessageAsync(string connectionId, string messageType, byte[] payload)
{
var context = new WebSocketMessageContext
{
ConnectionId = connectionId,
MessageType = messageType,
Payload = payload
};
await _messagePipeline.ProcessMessageAsync(context, CancellationToken.None);
}
}

1
src/CellularManagement.Domain/CellularManagement.Domain.csproj

@ -8,6 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MediatR" Version="12.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />

66
src/CellularManagement.Domain/Entities/WebSocketConnection.cs

@ -0,0 +1,66 @@
using System.Net.WebSockets;
using System.Text.Json.Serialization;
namespace CellularManagement.Domain.Entities;
public class WebSocketConnection
{
[JsonPropertyName("connectionId")]
public string ConnectionId { get; set; }
[JsonPropertyName("userId")]
public string? UserId { get; set; }
[JsonPropertyName("connectedAt")]
public DateTime ConnectedAt { get; set; }
[JsonPropertyName("lastActivityAt")]
public DateTime? LastActivityAt { get; set; }
[JsonPropertyName("state")]
public WebSocketState State { get; set; }
// 添加无参构造函数用于反序列化
public WebSocketConnection()
{
ConnectionId = string.Empty;
ConnectedAt = DateTime.UtcNow;
State = WebSocketState.Open;
}
// 添加带参数的构造函数
[JsonConstructor]
public WebSocketConnection(string connectionId, string? userId, DateTime connectedAt, DateTime? lastActivityAt, WebSocketState state)
{
ConnectionId = connectionId;
UserId = userId;
ConnectedAt = connectedAt;
LastActivityAt = lastActivityAt;
State = state;
}
public static WebSocketConnection Create(string connectionId)
{
return new WebSocketConnection
{
ConnectionId = connectionId,
ConnectedAt = DateTime.UtcNow,
State = WebSocketState.Open
};
}
public void AssociateUser(string userId)
{
UserId = userId;
}
public void UpdateLastActivity()
{
LastActivityAt = DateTime.UtcNow;
}
public void Close()
{
State = WebSocketState.Closed;
}
}

35
src/CellularManagement.Domain/Events/WebSocketMessageEvent.cs

@ -0,0 +1,35 @@
using MediatR;
namespace CellularManagement.Domain.Events;
public class WebSocketMessageReceivedEvent : INotification
{
public string ConnectionId { get; }
public string MessageType { get; }
public byte[] Payload { get; }
public DateTime Timestamp { get; }
public WebSocketMessageReceivedEvent(string connectionId, string messageType, byte[] payload)
{
ConnectionId = connectionId;
MessageType = messageType;
Payload = payload;
Timestamp = DateTime.UtcNow;
}
}
public class WebSocketMessageSentEvent : INotification
{
public string ConnectionId { get; }
public string MessageType { get; }
public byte[] Payload { get; }
public DateTime Timestamp { get; }
public WebSocketMessageSentEvent(string connectionId, string messageType, byte[] payload)
{
ConnectionId = connectionId;
MessageType = messageType;
Payload = payload;
Timestamp = DateTime.UtcNow;
}
}

9
src/CellularManagement.Infrastructure/Configurations/WebSocketConfiguration.cs

@ -0,0 +1,9 @@
namespace CellularManagement.Infrastructure.Configurations;
public class WebSocketConfiguration
{
public const string SectionName = "WebSocket";
public int Port { get; set; } = 5202;
public string Path { get; set; } = "/ws";
}

12
src/CellularManagement.Infrastructure/DependencyInjection.cs

@ -8,6 +8,8 @@ using CellularManagement.Domain.Entities;
using CellularManagement.Domain.Repositories;
using CellularManagement.Application.Services;
using CellularManagement.Infrastructure.Services;
using CellularManagement.Infrastructure.WebSocket;
using CellularManagement.Infrastructure.Monitoring;
using Scrutor;
namespace CellularManagement.Infrastructure;
@ -110,10 +112,20 @@ public static class DependencyInjection
// 添加内存缓存
services.AddMemoryCache();
// 添加分布式缓存
services.AddDistributedMemoryCache();
// 注册缓存服务
services.AddScoped<ICacheService, CacheService>();
// 注册WebSocket相关服务
services.AddSingleton<WebSocketMetrics>();
services.AddSingleton<WebSocketMessagePool>();
services.AddScoped<IWebSocketService, Infrastructure.WebSocket.WebSocketService>();
services.AddSingleton<WebSocketConnectionManager>();
services.AddSingleton<WebSocketMessagePipeline>();
// 自动注册服务
services
.Scan(action =>

196
src/CellularManagement.Infrastructure/Distributed/DistributedWebSocketManager.cs

@ -0,0 +1,196 @@
using System.Net.WebSockets;
using CellularManagement.Domain.Entities;
using CellularManagement.Application.Services;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Caching.Distributed;
using System.Text.Json;
namespace CellularManagement.Infrastructure.Distributed;
public class DistributedWebSocketManager : IWebSocketService
{
private readonly IDistributedCache _distributedCache;
private readonly ILogger<DistributedWebSocketManager> _logger;
private readonly string _nodeId;
private const string CONNECTION_PREFIX = "ws_connection_";
private const string USER_PREFIX = "ws_user_";
private const string NODE_PREFIX = "ws_node_";
public DistributedWebSocketManager(
IDistributedCache distributedCache,
ILogger<DistributedWebSocketManager> logger)
{
_distributedCache = distributedCache;
_logger = logger;
_nodeId = Guid.NewGuid().ToString();
}
public async Task<string> AcceptConnectionAsync(System.Net.WebSockets.WebSocket webSocket)
{
var connectionId = Guid.NewGuid().ToString();
var connection = WebSocketConnection.Create(connectionId);
var connectionKey = $"{CONNECTION_PREFIX}{connectionId}";
var nodeKey = $"{NODE_PREFIX}{_nodeId}";
// 存储连接信息
await _distributedCache.SetStringAsync(
connectionKey,
JsonSerializer.Serialize(connection),
new DistributedCacheEntryOptions
{
SlidingExpiration = TimeSpan.FromMinutes(30)
});
// 更新节点连接列表
await AddConnectionToNodeAsync(connectionId);
_logger.LogInformation("WebSocket connection accepted: {ConnectionId} on node {NodeId}",
connectionId, _nodeId);
return connectionId;
}
public async Task<bool> CloseConnectionAsync(string connectionId)
{
var connectionKey = $"{CONNECTION_PREFIX}{connectionId}";
var userKey = $"{USER_PREFIX}{connectionId}";
// 获取连接信息
var connectionJson = await _distributedCache.GetStringAsync(connectionKey);
if (connectionJson != null)
{
var connection = JsonSerializer.Deserialize<WebSocketConnection>(connectionJson);
if (connection != null)
{
connection.Close();
await _distributedCache.SetStringAsync(
connectionKey,
JsonSerializer.Serialize(connection));
}
}
// 从节点连接列表中移除
await RemoveConnectionFromNodeAsync(connectionId);
// 清理缓存
await _distributedCache.RemoveAsync(connectionKey);
await _distributedCache.RemoveAsync(userKey);
_logger.LogInformation("WebSocket connection closed: {ConnectionId}", connectionId);
return true;
}
public async Task<bool> SendMessageAsync(string connectionId, byte[] message)
{
var connectionKey = $"{CONNECTION_PREFIX}{connectionId}";
var connectionJson = await _distributedCache.GetStringAsync(connectionKey);
if (connectionJson != null)
{
var connection = JsonSerializer.Deserialize<WebSocketConnection>(connectionJson);
if (connection?.State == WebSocketState.Open)
{
// 这里需要实现消息发送逻辑
// 可能需要使用消息队列或其他机制
return true;
}
}
return false;
}
public async Task<bool> BroadcastMessageAsync(byte[] message)
{
// 获取所有节点的连接列表
var nodes = await GetAllNodesAsync();
foreach (var node in nodes)
{
// 向每个节点发送广播消息
// 这里需要实现节点间的消息传递机制
}
return true;
}
public async Task<bool> SendMessageToUserAsync(string userId, byte[] message)
{
// 获取用户的所有连接
var connections = await GetUserConnectionsAsync(userId);
foreach (var connection in connections)
{
await SendMessageAsync(connection.ConnectionId, message);
}
return true;
}
public async Task AssociateUserAsync(string connectionId, string userId)
{
var connectionKey = $"{CONNECTION_PREFIX}{connectionId}";
var userKey = $"{USER_PREFIX}{connectionId}";
var connectionJson = await _distributedCache.GetStringAsync(connectionKey);
if (connectionJson != null)
{
var connection = JsonSerializer.Deserialize<WebSocketConnection>(connectionJson);
if (connection != null)
{
connection.AssociateUser(userId);
await _distributedCache.SetStringAsync(
connectionKey,
JsonSerializer.Serialize(connection));
await _distributedCache.SetStringAsync(userKey, userId);
}
}
}
public async Task<WebSocketConnection?> GetConnectionAsync(string connectionId)
{
var connectionKey = $"{CONNECTION_PREFIX}{connectionId}";
var connectionJson = await _distributedCache.GetStringAsync(connectionKey);
return connectionJson != null
? JsonSerializer.Deserialize<WebSocketConnection>(connectionJson)
: null;
}
public async Task<IEnumerable<WebSocketConnection>> GetUserConnectionsAsync(string userId)
{
var connections = new List<WebSocketConnection>();
// 这里需要实现获取用户所有连接的逻辑
return connections;
}
private async Task AddConnectionToNodeAsync(string connectionId)
{
var nodeKey = $"{NODE_PREFIX}{_nodeId}";
var connections = await GetNodeConnectionsAsync();
connections.Add(connectionId);
await _distributedCache.SetStringAsync(
nodeKey,
JsonSerializer.Serialize(connections));
}
private async Task RemoveConnectionFromNodeAsync(string connectionId)
{
var nodeKey = $"{NODE_PREFIX}{_nodeId}";
var connections = await GetNodeConnectionsAsync();
connections.Remove(connectionId);
await _distributedCache.SetStringAsync(
nodeKey,
JsonSerializer.Serialize(connections));
}
private async Task<List<string>> GetNodeConnectionsAsync()
{
var nodeKey = $"{NODE_PREFIX}{_nodeId}";
var connectionsJson = await _distributedCache.GetStringAsync(nodeKey);
return connectionsJson != null
? JsonSerializer.Deserialize<List<string>>(connectionsJson) ?? new List<string>()
: new List<string>();
}
private async Task<List<string>> GetAllNodesAsync()
{
// 这里需要实现获取所有节点的逻辑
// 可能需要使用服务发现或其他机制
return new List<string>();
}
}

65
src/CellularManagement.Infrastructure/Monitoring/WebSocketMetrics.cs

@ -0,0 +1,65 @@
using System.Diagnostics;
using System.Diagnostics.Metrics;
using Microsoft.Extensions.Logging;
namespace CellularManagement.Infrastructure.Monitoring;
public class WebSocketMetrics
{
private readonly ILogger<WebSocketMetrics> _logger;
private readonly Meter _meter;
private readonly Counter<int> _connectionsCounter;
private readonly Counter<int> _messagesCounter;
private readonly Histogram<double> _messageLatency;
private readonly Histogram<double> _messageSize;
private readonly Counter<int> _errorsCounter;
public WebSocketMetrics(ILogger<WebSocketMetrics> logger)
{
_logger = logger;
_meter = new Meter("CellularManagement.WebSocket", "1.0.0");
_connectionsCounter = _meter.CreateCounter<int>("websocket.connections", "个", "活跃连接数");
_messagesCounter = _meter.CreateCounter<int>("websocket.messages", "个", "消息处理数");
_messageLatency = _meter.CreateHistogram<double>("websocket.message.latency", "ms", "消息处理延迟");
_messageSize = _meter.CreateHistogram<double>("websocket.message.size", "bytes", "消息大小");
_errorsCounter = _meter.CreateCounter<int>("websocket.errors", "个", "错误数");
}
public void ConnectionEstablished()
{
_connectionsCounter.Add(1);
_logger.LogInformation("WebSocket connection established");
}
public void ConnectionClosed()
{
_connectionsCounter.Add(-1);
_logger.LogInformation("WebSocket connection closed");
}
public void MessageReceived(int size)
{
_messagesCounter.Add(1);
_messageSize.Record(size);
_logger.LogDebug("WebSocket message received, size: {Size} bytes", size);
}
public void MessageProcessed(TimeSpan latency)
{
_messageLatency.Record(latency.TotalMilliseconds);
_logger.LogDebug("WebSocket message processed, latency: {Latency}ms", latency.TotalMilliseconds);
}
public void ErrorOccurred(string errorType)
{
_errorsCounter.Add(1);
_logger.LogError("WebSocket error occurred: {ErrorType}", errorType);
}
public void RecordGauge(string name, double value)
{
var gauge = _meter.CreateObservableGauge<double>($"websocket.{name}", () => value);
_logger.LogDebug("WebSocket gauge recorded: {Name} = {Value}", name, value);
}
}

54
src/CellularManagement.Infrastructure/Pipeline/Handlers/WebSocketMessageValidationHandler.cs

@ -0,0 +1,54 @@
using CellularManagement.Application.Pipeline;
using Microsoft.Extensions.Logging;
namespace CellularManagement.Infrastructure.Pipeline.Handlers;
public class WebSocketMessageValidationHandler : IWebSocketMessageHandler
{
private readonly ILogger<WebSocketMessageValidationHandler> _logger;
public string HandlerName => "MessageValidationHandler";
public int Priority => 100;
public WebSocketMessageValidationHandler(ILogger<WebSocketMessageValidationHandler> logger)
{
_logger = logger;
}
public bool CanHandle(string messageType)
{
return true; // 处理所有消息类型
}
public async Task HandleAsync(WebSocketMessageContext context, CancellationToken cancellationToken)
{
try
{
// 验证消息格式
if (string.IsNullOrEmpty(context.MessageType))
{
throw new ArgumentException("Message type cannot be empty");
}
if (context.Payload == null || context.Payload.Length == 0)
{
throw new ArgumentException("Message payload cannot be empty");
}
// 验证消息大小
if (context.Payload.Length > 1024 * 1024) // 1MB
{
throw new ArgumentException("Message payload too large");
}
_logger.LogInformation("Message validation passed for connection: {ConnectionId}", context.ConnectionId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Message validation failed for connection: {ConnectionId}", context.ConnectionId);
context.Error = ex;
context.IsHandled = true;
throw;
}
}
}

145
src/CellularManagement.Infrastructure/Pipeline/WebSocketMessagePipeline.cs

@ -0,0 +1,145 @@
using System.Threading.Channels;
using CellularManagement.Application.Pipeline;
using Microsoft.Extensions.Logging;
using MediatR;
using CellularManagement.Domain.Events;
namespace CellularManagement.Infrastructure.Pipeline;
public class WebSocketMessagePipeline : IWebSocketMessagePipeline
{
private readonly Channel<WebSocketMessageContext> _messageChannel;
private readonly List<IWebSocketMessageHandler> _handlers = new();
private readonly ILogger<WebSocketMessagePipeline> _logger;
private readonly IMediator _mediator;
private readonly int _batchSize;
private readonly int _maxQueueSize;
public WebSocketMessagePipeline(
ILogger<WebSocketMessagePipeline> logger,
IMediator mediator,
int batchSize = 100,
int maxQueueSize = 10000)
{
_logger = logger;
_mediator = mediator;
_batchSize = batchSize;
_maxQueueSize = maxQueueSize;
var options = new BoundedChannelOptions(maxQueueSize)
{
FullMode = BoundedChannelFullMode.Wait,
SingleWriter = false,
SingleReader = false
};
_messageChannel = Channel.CreateBounded<WebSocketMessageContext>(options);
}
public async Task ProcessMessageAsync(WebSocketMessageContext context, CancellationToken cancellationToken)
{
try
{
await _messageChannel.Writer.WriteAsync(context, cancellationToken);
await _mediator.Publish(new WebSocketMessageReceivedEvent(
context.ConnectionId,
context.MessageType,
context.Payload), cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process message for connection: {ConnectionId}", context.ConnectionId);
throw;
}
}
public void RegisterHandler(IWebSocketMessageHandler handler)
{
_handlers.Add(handler);
_handlers.Sort((a, b) => b.Priority.CompareTo(a.Priority));
}
public void UnregisterHandler(string handlerName)
{
_handlers.RemoveAll(h => h.HandlerName == handlerName);
}
public async Task StartProcessingAsync(CancellationToken cancellationToken)
{
var batch = new List<WebSocketMessageContext>(_batchSize);
try
{
while (!cancellationToken.IsCancellationRequested)
{
batch.Clear();
while (batch.Count < _batchSize &&
await _messageChannel.Reader.WaitToReadAsync(cancellationToken))
{
if (_messageChannel.Reader.TryRead(out var message))
{
batch.Add(message);
}
}
if (batch.Count > 0)
{
await ProcessBatchAsync(batch, cancellationToken);
}
}
}
catch (OperationCanceledException)
{
_logger.LogInformation("Message processing pipeline stopped");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in message processing pipeline");
}
}
private async Task ProcessBatchAsync(List<WebSocketMessageContext> batch, CancellationToken cancellationToken)
{
foreach (var message in batch)
{
try
{
await ProcessMessageWithHandlersAsync(message, cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing message: {MessageId}", message.ConnectionId);
message.Error = ex;
}
}
}
private async Task ProcessMessageWithHandlersAsync(WebSocketMessageContext context, CancellationToken cancellationToken)
{
foreach (var handler in _handlers)
{
if (handler.CanHandle(context.MessageType) && !context.IsHandled)
{
try
{
await handler.HandleAsync(context, cancellationToken);
if (context.IsHandled)
{
break;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Handler {HandlerName} failed to process message", handler.HandlerName);
throw;
}
}
}
if (!context.IsHandled)
{
_logger.LogWarning("No handler found for message type: {MessageType}", context.MessageType);
}
}
}

59
src/CellularManagement.Infrastructure/Pooling/WebSocketMessagePool.cs

@ -0,0 +1,59 @@
using System.Buffers;
using System.Collections.Concurrent;
using CellularManagement.Application.Pipeline;
namespace CellularManagement.Infrastructure.Pooling;
public class WebSocketMessagePool
{
private readonly ConcurrentBag<WebSocketMessageContext> _pool = new();
private readonly ArrayPool<byte> _arrayPool = ArrayPool<byte>.Shared;
private readonly int _maxPoolSize;
private readonly int _bufferSize;
public WebSocketMessagePool(int maxPoolSize = 1000, int bufferSize = 1024 * 4)
{
_maxPoolSize = maxPoolSize;
_bufferSize = bufferSize;
}
public WebSocketMessageContext Rent()
{
if (_pool.TryTake(out var context))
{
return context;
}
return new WebSocketMessageContext
{
Payload = _arrayPool.Rent(_bufferSize)
};
}
public void Return(WebSocketMessageContext context)
{
if (_pool.Count < _maxPoolSize)
{
context.ConnectionId = string.Empty;
context.MessageType = string.Empty;
context.Properties.Clear();
context.IsHandled = false;
context.Error = null;
if (context.Payload != null)
{
_arrayPool.Return(context.Payload);
context.Payload = _arrayPool.Rent(_bufferSize);
}
_pool.Add(context);
}
else
{
if (context.Payload != null)
{
_arrayPool.Return(context.Payload);
}
}
}
}

67
src/CellularManagement.Infrastructure/WebSocket/WebSocketConnectionManager.cs

@ -0,0 +1,67 @@
using System.Net.WebSockets;
using System.Collections.Concurrent;
using Microsoft.Extensions.Logging;
using CellularManagement.Application.Services;
namespace CellularManagement.Infrastructure.WebSocket;
public class WebSocketConnectionManager
{
private readonly ILogger<WebSocketConnectionManager> _logger;
private readonly ConcurrentDictionary<string, System.Net.WebSockets.WebSocket> _sockets = new();
private readonly ConcurrentDictionary<string, string> _userConnections = new();
public WebSocketConnectionManager(ILogger<WebSocketConnectionManager> logger)
{
_logger = logger;
}
public string AddConnection(System.Net.WebSockets.WebSocket socket)
{
var connectionId = Guid.NewGuid().ToString();
_sockets.TryAdd(connectionId, socket);
_logger.LogInformation("WebSocket connection added: {ConnectionId}", connectionId);
return connectionId;
}
public bool RemoveConnection(string connectionId)
{
_sockets.TryRemove(connectionId, out _);
_userConnections.TryRemove(connectionId, out _);
_logger.LogInformation("WebSocket connection removed: {ConnectionId}", connectionId);
return true;
}
public System.Net.WebSockets.WebSocket? GetConnection(string connectionId)
{
_sockets.TryGetValue(connectionId, out var socket);
return socket;
}
public IEnumerable<System.Net.WebSockets.WebSocket> GetAllConnections()
{
return _sockets.Values;
}
public void AssociateUser(string connectionId, string userId)
{
_userConnections.TryAdd(connectionId, userId);
}
public string? GetUserId(string connectionId)
{
_userConnections.TryGetValue(connectionId, out var userId);
return userId;
}
public IEnumerable<System.Net.WebSockets.WebSocket> GetUserConnections(string userId)
{
var connections = _userConnections
.Where(kvp => kvp.Value == userId)
.Select(kvp => kvp.Key)
.Select(connectionId => GetConnection(connectionId))
.Where(socket => socket != null);
return connections!;
}
}

116
src/CellularManagement.Infrastructure/WebSocket/WebSocketMessagePipeline.cs

@ -0,0 +1,116 @@
using System.Threading.Channels;
using System.Buffers;
using Microsoft.Extensions.Logging;
namespace CellularManagement.Infrastructure.WebSocket;
public class WebSocketMessagePipeline
{
private readonly Channel<WebSocketMessage> _messageChannel;
private readonly ILogger<WebSocketMessagePipeline> _logger;
private readonly MemoryPool<byte> _memoryPool;
private readonly int _batchSize;
private readonly int _maxQueueSize;
public WebSocketMessagePipeline(
ILogger<WebSocketMessagePipeline> logger,
int batchSize = 100,
int maxQueueSize = 10000)
{
_logger = logger;
_batchSize = batchSize;
_maxQueueSize = maxQueueSize;
_memoryPool = MemoryPool<byte>.Shared;
var options = new BoundedChannelOptions(maxQueueSize)
{
FullMode = BoundedChannelFullMode.Wait,
SingleWriter = false,
SingleReader = false
};
_messageChannel = Channel.CreateBounded<WebSocketMessage>(options);
}
public async ValueTask<bool> WriteMessageAsync(WebSocketMessage message)
{
try
{
await _messageChannel.Writer.WriteAsync(message);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to write message to pipeline");
return false;
}
}
public async Task ProcessMessagesAsync(CancellationToken cancellationToken)
{
var batch = new List<WebSocketMessage>(_batchSize);
try
{
while (!cancellationToken.IsCancellationRequested)
{
batch.Clear();
// 批量读取消息
while (batch.Count < _batchSize &&
await _messageChannel.Reader.WaitToReadAsync(cancellationToken))
{
if (_messageChannel.Reader.TryRead(out var message))
{
batch.Add(message);
}
}
if (batch.Count > 0)
{
await ProcessBatchAsync(batch, cancellationToken);
}
}
}
catch (OperationCanceledException)
{
_logger.LogInformation("Message processing pipeline stopped");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in message processing pipeline");
}
}
private async Task ProcessBatchAsync(List<WebSocketMessage> batch, CancellationToken cancellationToken)
{
// 这里实现具体的消息处理逻辑
foreach (var message in batch)
{
try
{
// 处理消息
await ProcessMessageAsync(message, cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing message: {MessageId}", message.MessageId);
}
}
}
private async Task ProcessMessageAsync(WebSocketMessage message, CancellationToken cancellationToken)
{
// 实现具体的消息处理逻辑
// 这里可以添加消息验证、转换、业务处理等步骤
}
}
public class WebSocketMessage
{
public string MessageId { get; set; } = Guid.NewGuid().ToString();
public string ConnectionId { get; set; } = string.Empty;
public string MessageType { get; set; } = string.Empty;
public byte[] Payload { get; set; } = Array.Empty<byte>();
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
}

41
src/CellularManagement.Infrastructure/WebSocket/WebSocketMessagePool.cs

@ -0,0 +1,41 @@
using System.Collections.Concurrent;
using CellularManagement.Application.Pipeline;
namespace CellularManagement.Infrastructure.WebSocket;
public class WebSocketMessagePool
{
private readonly ConcurrentBag<WebSocketMessageContext> _pool = new();
private readonly int _maxPoolSize;
public WebSocketMessagePool(int maxPoolSize = 1000)
{
_maxPoolSize = maxPoolSize;
}
public WebSocketMessageContext Rent()
{
if (_pool.TryTake(out var context))
{
return context;
}
return new WebSocketMessageContext();
}
public void Return(WebSocketMessageContext context)
{
if (_pool.Count < _maxPoolSize)
{
// 重置上下文状态
context.ConnectionId = string.Empty;
context.MessageType = string.Empty;
context.Payload = Array.Empty<byte>();
context.Properties.Clear();
context.IsHandled = false;
context.Error = null;
_pool.Add(context);
}
}
}

89
src/CellularManagement.Infrastructure/WebSocket/WebSocketMiddleware.cs

@ -0,0 +1,89 @@
using System.Net.WebSockets;
using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using CellularManagement.Application.Services;
namespace CellularManagement.Infrastructure.WebSocket;
public class WebSocketMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<WebSocketMiddleware> _logger;
private readonly IServiceProvider _serviceProvider;
public WebSocketMiddleware(
RequestDelegate next,
ILogger<WebSocketMiddleware> logger,
IServiceProvider serviceProvider)
{
_next = next;
_logger = logger;
_serviceProvider = serviceProvider;
}
public async Task InvokeAsync(HttpContext context)
{
if (context.WebSockets.IsWebSocketRequest)
{
using var scope = _serviceProvider.CreateScope();
var webSocketService = scope.ServiceProvider.GetRequiredService<IWebSocketService>();
var webSocket = await context.WebSockets.AcceptWebSocketAsync();
var connectionId = await webSocketService.AcceptConnectionAsync(webSocket);
try
{
await HandleWebSocketConnection(webSocket, connectionId, webSocketService);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error handling WebSocket connection: {ConnectionId}", connectionId);
}
finally
{
await webSocketService.CloseConnectionAsync(connectionId);
}
}
else
{
await _next(context);
}
}
private async Task HandleWebSocketConnection(
System.Net.WebSockets.WebSocket webSocket,
string connectionId,
IWebSocketService webSocketService)
{
var buffer = new byte[1024 * 4];
var receiveResult = await webSocket.ReceiveAsync(
new ArraySegment<byte>(buffer), CancellationToken.None);
while (!receiveResult.CloseStatus.HasValue)
{
try
{
var message = buffer.Take(receiveResult.Count).ToArray();
await webSocketService.SendMessageAsync(connectionId, message);
receiveResult = await webSocket.ReceiveAsync(
new ArraySegment<byte>(buffer), CancellationToken.None);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing WebSocket message for connection: {ConnectionId}", connectionId);
break;
}
}
if (receiveResult.CloseStatus.HasValue)
{
await webSocket.CloseAsync(
receiveResult.CloseStatus.Value,
receiveResult.CloseStatusDescription,
CancellationToken.None);
}
}
}

295
src/CellularManagement.Infrastructure/WebSocket/WebSocketService.cs

@ -0,0 +1,295 @@
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
using CellularManagement.Application.Services;
using CellularManagement.Domain.Entities;
using CellularManagement.Infrastructure.Monitoring;
using CellularManagement.Infrastructure.Pooling;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
namespace CellularManagement.Infrastructure.WebSocket;
public class WebSocketService : IWebSocketService
{
private readonly ILogger<WebSocketService> _logger;
private readonly WebSocketMetrics _metrics;
private readonly WebSocketMessagePool _messagePool;
private readonly IDistributedCache _distributedCache;
private readonly ICacheService _cacheService;
private readonly string _nodeId;
private const string CONNECTION_PREFIX = "ws_connection_";
private const string WEBSOCKET_PREFIX = "ws_socket_";
private const string USER_PREFIX = "ws_user_";
private const string NODE_PREFIX = "ws_node_";
public WebSocketService(
ILogger<WebSocketService> logger,
WebSocketMetrics metrics,
WebSocketMessagePool messagePool,
IDistributedCache distributedCache,
ICacheService cacheService)
{
_logger = logger;
_metrics = metrics;
_messagePool = messagePool;
_distributedCache = distributedCache;
_cacheService = cacheService;
_nodeId = Guid.NewGuid().ToString();
}
public async Task<string> AcceptConnectionAsync(System.Net.WebSockets.WebSocket webSocket)
{
var connectionId = Guid.NewGuid().ToString();
var connection = WebSocketConnection.Create(connectionId);
var connectionKey = $"{CONNECTION_PREFIX}{connectionId}";
var webSocketKey = $"{WEBSOCKET_PREFIX}{connectionId}";
var nodeKey = $"{NODE_PREFIX}{_nodeId}";
await _distributedCache.SetStringAsync(
connectionKey,
JsonSerializer.Serialize(connection),
new DistributedCacheEntryOptions
{
SlidingExpiration = TimeSpan.FromMinutes(30)
});
_cacheService.Set(webSocketKey, webSocket, new MemoryCacheEntryOptions
{
SlidingExpiration = TimeSpan.FromMinutes(30)
});
await AddConnectionToNodeAsync(connectionId);
_metrics.ConnectionEstablished();
_logger.LogInformation("WebSocket connection accepted: {ConnectionId} on node {NodeId}",
connectionId, _nodeId);
return connectionId;
}
public async Task<bool> CloseConnectionAsync(string connectionId)
{
var connectionKey = $"{CONNECTION_PREFIX}{connectionId}";
var webSocketKey = $"{WEBSOCKET_PREFIX}{connectionId}";
var userKey = $"{USER_PREFIX}{connectionId}";
var connectionJson = await _distributedCache.GetStringAsync(connectionKey);
if (_cacheService.TryGetValue(webSocketKey, out System.Net.WebSockets.WebSocket? webSocket))
{
try
{
if (connectionJson != null)
{
var connection = JsonSerializer.Deserialize<WebSocketConnection>(connectionJson);
if (connection != null && webSocket != null)
{
try
{
if (webSocket.State == WebSocketState.Open || webSocket.State == WebSocketState.CloseReceived)
{
await webSocket.CloseAsync(
WebSocketCloseStatus.NormalClosure,
"Connection closed by server",
CancellationToken.None);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error closing WebSocket connection: {ConnectionId}", connectionId);
}
connection.Close();
await _distributedCache.SetStringAsync(connectionKey, JsonSerializer.Serialize(connection));
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to deserialize WebSocketConnection for connection: {ConnectionId}, continuing with cleanup", connectionId);
}
}
await RemoveConnectionFromNodeAsync(connectionId);
await _distributedCache.RemoveAsync(connectionKey);
_cacheService.Remove(webSocketKey);
await _distributedCache.RemoveAsync(userKey);
_metrics.ConnectionClosed();
_logger.LogInformation("WebSocket connection closed: {ConnectionId}", connectionId);
return true;
}
public async Task<bool> SendMessageAsync(string connectionId, byte[] message)
{
var connectionKey = $"{CONNECTION_PREFIX}{connectionId}";
var webSocketKey = $"{WEBSOCKET_PREFIX}{connectionId}";
var connectionJson = await _distributedCache.GetStringAsync(connectionKey);
if (_cacheService.TryGetValue(webSocketKey, out System.Net.WebSockets.WebSocket? webSocket))
{
try
{
if (connectionJson != null)
{
var connection = JsonSerializer.Deserialize<WebSocketConnection>(connectionJson);
if (connection?.State == WebSocketState.Open && webSocket != null)
{
try
{
await webSocket.SendAsync(
new ArraySegment<byte>(message),
WebSocketMessageType.Text,
true,
CancellationToken.None);
_metrics.MessageProcessed(TimeSpan.Zero);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error sending message to connection: {ConnectionId}", connectionId);
_metrics.ErrorOccurred("SendMessage");
}
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to deserialize WebSocketConnection for connection: {ConnectionId}, skipping message send", connectionId);
}
}
return false;
}
public async Task<bool> BroadcastMessageAsync(byte[] message)
{
var nodes = await GetAllNodesAsync();
var success = true;
foreach (var node in nodes)
{
var nodeKey = $"{NODE_PREFIX}{node}";
var connectionsJson = await _distributedCache.GetStringAsync(nodeKey);
if (connectionsJson != null)
{
var connections = JsonSerializer.Deserialize<List<string>>(connectionsJson);
foreach (var connectionId in connections)
{
if (!await SendMessageAsync(connectionId, message))
{
success = false;
}
}
}
}
return success;
}
public async Task<bool> SendMessageToUserAsync(string userId, byte[] message)
{
var userConnections = await GetUserConnectionsAsync(userId);
var success = true;
foreach (var connection in userConnections)
{
if (!await SendMessageAsync(connection.ConnectionId, message))
{
success = false;
}
}
return success;
}
public async Task AssociateUserAsync(string connectionId, string userId)
{
var connectionKey = $"{CONNECTION_PREFIX}{connectionId}";
var userKey = $"{USER_PREFIX}{connectionId}";
var connectionJson = await _distributedCache.GetStringAsync(connectionKey);
if (connectionJson != null)
{
var connection = JsonSerializer.Deserialize<WebSocketConnection>(connectionJson);
if (connection != null)
{
connection.AssociateUser(userId);
await _distributedCache.SetStringAsync(connectionKey, JsonSerializer.Serialize(connection));
await _distributedCache.SetStringAsync(userKey, userId);
}
}
}
public async Task<WebSocketConnection?> GetConnectionAsync(string connectionId)
{
var connectionKey = $"{CONNECTION_PREFIX}{connectionId}";
var connectionJson = await _distributedCache.GetStringAsync(connectionKey);
return connectionJson != null
? JsonSerializer.Deserialize<WebSocketConnection>(connectionJson)
: null;
}
public async Task<IEnumerable<WebSocketConnection>> GetUserConnectionsAsync(string userId)
{
var connections = new List<WebSocketConnection>();
var nodes = await GetAllNodesAsync();
foreach (var node in nodes)
{
var nodeKey = $"{NODE_PREFIX}{node}";
var connectionsJson = await _distributedCache.GetStringAsync(nodeKey);
if (connectionsJson != null)
{
var connectionIds = JsonSerializer.Deserialize<List<string>>(connectionsJson);
foreach (var connectionId in connectionIds)
{
var connection = await GetConnectionAsync(connectionId);
if (connection?.UserId == userId)
{
connections.Add(connection);
}
}
}
}
return connections;
}
private async Task AddConnectionToNodeAsync(string connectionId)
{
var nodeKey = $"{NODE_PREFIX}{_nodeId}";
var connections = await GetNodeConnectionsAsync();
connections.Add(connectionId);
await _distributedCache.SetStringAsync(
nodeKey,
JsonSerializer.Serialize(connections));
}
private async Task RemoveConnectionFromNodeAsync(string connectionId)
{
var nodeKey = $"{NODE_PREFIX}{_nodeId}";
var connections = await GetNodeConnectionsAsync();
connections.Remove(connectionId);
await _distributedCache.SetStringAsync(
nodeKey,
JsonSerializer.Serialize(connections));
}
private async Task<List<string>> GetNodeConnectionsAsync()
{
var nodeKey = $"{NODE_PREFIX}{_nodeId}";
var connectionsJson = await _distributedCache.GetStringAsync(nodeKey);
return connectionsJson != null
? JsonSerializer.Deserialize<List<string>>(connectionsJson) ?? new List<string>()
: new List<string>();
}
private async Task<List<string>> GetAllNodesAsync()
{
// 这里需要实现服务发现机制
// 可以使用Redis的Pub/Sub或其他服务发现机制
return new List<string> { _nodeId };
}
}

8
src/CellularManagement.WebAPI/CellularManagement.WebAPI.csproj

@ -14,7 +14,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>
<ItemGroup>
@ -22,4 +22,10 @@
<ProjectReference Include="..\CellularManagement.Presentation\CellularManagement.Presentation.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="wwwroot\**\*">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</None>
</ItemGroup>
</Project>

29
src/CellularManagement.WebAPI/Program.cs

@ -6,6 +6,11 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using CellularManagement.Infrastructure.Configurations;
using CellularManagement.Application;
using CellularManagement.Infrastructure.WebSocket;
using CellularManagement.Infrastructure.Monitoring;
using CellularManagement.Infrastructure.Pooling;
using CellularManagement.Application.Services;
using Microsoft.Extensions.Options;
// 创建 Web 应用程序构建器
var builder = WebApplication.CreateBuilder(args);
@ -44,6 +49,17 @@ builder.Services.AddCors();
// 配置认证服务
// 从配置文件中读取认证相关配置
builder.Services.Configure<AuthConfiguration>(builder.Configuration.GetSection("Auth"));
builder.Services.Configure<WebSocketConfiguration>(builder.Configuration.GetSection(WebSocketConfiguration.SectionName));
// 注册WebSocket相关服务
builder.Services.AddSingleton<WebSocketMetrics>();
builder.Services.AddSingleton<CellularManagement.Infrastructure.Pooling.WebSocketMessagePool>();
builder.Services.AddSingleton<WebSocketConnectionManager>();
builder.Services.AddSingleton<WebSocketMessagePipeline>();
builder.Services.AddScoped<IWebSocketService, CellularManagement.Infrastructure.WebSocket.WebSocketService>();
// 添加静态文件服务
builder.Services.AddDirectoryBrowser();
// 构建应用程序
var app = builder.Build();
@ -70,6 +86,19 @@ app.UseCors(x => x.AllowAnyHeader().SetIsOriginAllowed(p => true).AllowAnyMethod
// 处理用户认证和授权
app.UseAuthorization();
// 配置 WebSocket 中间件
var webSocketConfig = app.Services.GetRequiredService<IOptions<WebSocketConfiguration>>().Value;
app.UseWebSockets(new Microsoft.AspNetCore.Builder.WebSocketOptions
{
KeepAliveInterval = TimeSpan.FromMinutes(2)
});
app.UseMiddleware<WebSocketMiddleware>();
// 配置静态文件中间件
app.UseStaticFiles();
app.UseDirectoryBrowser();
// 映射控制器路由
app.MapControllers();

4
src/CellularManagement.WebAPI/appsettings.json

@ -34,5 +34,9 @@
"DefaultUserRole": "User",
"AccessTokenExpirationMinutes": 60,
"RefreshTokenExpirationDays": 7
},
"WebSocket": {
"Port": 5202,
"Path": "/ws"
}
}

382
src/CellularManagement.WebAPI/wwwroot/websocket.html

@ -0,0 +1,382 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket 3D 聊天客户端</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
@keyframes slideIn {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.status-dot {
animation: pulse 2s infinite;
}
.message {
animation: slideIn 0.3s ease-out;
}
#three-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
opacity: 0.3;
}
.content-container {
position: relative;
z-index: 1;
}
</style>
</head>
<body class="bg-gray-50 dark:bg-gray-900 h-screen flex flex-col">
<!-- Three.js 容器 -->
<div id="three-container"></div>
<!-- 内容容器 -->
<div class="content-container flex flex-col h-full">
<!-- 顶部状态栏 -->
<div class="bg-white dark:bg-gray-800 shadow-sm">
<div class="max-w-4xl mx-auto px-4 py-3">
<!-- WebSocket地址输入区域 -->
<div class="flex items-center space-x-2 mb-3">
<input type="text"
id="wsAddress"
value="ws://localhost:5202/ws"
placeholder="输入WebSocket地址"
class="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600
rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500
dark:bg-gray-700 dark:text-white transition-colors duration-200">
</div>
<div class="flex justify-between items-center">
<div class="flex items-center space-x-2">
<div id="status" class="flex items-center space-x-2">
<div class="w-2 h-2 rounded-full bg-red-500 status-dot"></div>
<span class="text-sm text-gray-600 dark:text-gray-300">未连接</span>
</div>
</div>
<div class="flex space-x-2">
<button id="connectBtn"
class="px-4 py-2 bg-indigo-500 hover:bg-indigo-600 text-white rounded-lg shadow-sm
transition-colors duration-200">
连接
</button>
<button id="disconnectBtn"
class="px-4 py-2 bg-red-500 hover:bg-red-600 text-white rounded-lg shadow-sm
transition-colors duration-200">
断开
</button>
</div>
</div>
</div>
</div>
<!-- 消息区域 -->
<div class="flex-1 overflow-hidden">
<div class="max-w-4xl mx-auto h-full px-4 py-4">
<div class="flex justify-between items-center mb-2">
<h2 class="text-lg font-semibold text-gray-700 dark:text-gray-300">消息记录</h2>
<button id="clearBtn"
class="px-3 py-1 bg-gray-500 hover:bg-gray-600 text-white rounded-lg
shadow-sm transition-colors duration-200">
清空消息
</button>
</div>
<div id="messages" class="h-full overflow-y-auto space-y-4">
<!-- 消息将在这里动态添加 -->
</div>
</div>
</div>
<!-- 输入区域 -->
<div class="bg-white dark:bg-gray-800 shadow-sm">
<div class="max-w-4xl mx-auto px-4 py-4">
<div class="flex space-x-2">
<input type="text"
id="messageInput"
placeholder="输入消息..."
class="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600
rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500
dark:bg-gray-700 dark:text-white transition-colors duration-200">
<button id="sendBtn"
class="px-4 py-2 bg-indigo-500 hover:bg-indigo-600 text-white rounded-lg
shadow-sm transition-colors duration-200">
发送
</button>
</div>
</div>
</div>
</div>
<script>
// Three.js 场景初始化
let scene, camera, renderer, particles;
const particleCount = 1000;
const particleGeometry = new THREE.BufferGeometry();
const particlePositions = new Float32Array(particleCount * 3);
const particleColors = new Float32Array(particleCount * 3);
const particleVelocities = new Float32Array(particleCount * 3);
function initThreeJS() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById('three-container').appendChild(renderer.domElement);
// 初始化粒子
for (let i = 0; i < particleCount; i++) {
particlePositions[i * 3] = (Math.random() - 0.5) * 20;
particlePositions[i * 3 + 1] = (Math.random() - 0.5) * 20;
particlePositions[i * 3 + 2] = (Math.random() - 0.5) * 20;
particleColors[i * 3] = Math.random();
particleColors[i * 3 + 1] = Math.random();
particleColors[i * 3 + 2] = Math.random();
particleVelocities[i * 3] = (Math.random() - 0.5) * 0.02;
particleVelocities[i * 3 + 1] = (Math.random() - 0.5) * 0.02;
particleVelocities[i * 3 + 2] = (Math.random() - 0.5) * 0.02;
}
particleGeometry.setAttribute('position', new THREE.BufferAttribute(particlePositions, 3));
particleGeometry.setAttribute('color', new THREE.BufferAttribute(particleColors, 3));
const particleMaterial = new THREE.PointsMaterial({
size: 0.1,
vertexColors: true,
transparent: true,
opacity: 0.8
});
particles = new THREE.Points(particleGeometry, particleMaterial);
scene.add(particles);
camera.position.z = 15;
// 添加环境光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
// 添加点光源
const pointLight = new THREE.PointLight(0xffffff, 1);
pointLight.position.set(10, 10, 10);
scene.add(pointLight);
animate();
}
function animate() {
requestAnimationFrame(animate);
// 更新粒子位置
const positions = particleGeometry.attributes.position.array;
for (let i = 0; i < particleCount; i++) {
positions[i * 3] += particleVelocities[i * 3];
positions[i * 3 + 1] += particleVelocities[i * 3 + 1];
positions[i * 3 + 2] += particleVelocities[i * 3 + 2];
// 边界检查
if (Math.abs(positions[i * 3]) > 10) particleVelocities[i * 3] *= -1;
if (Math.abs(positions[i * 3 + 1]) > 10) particleVelocities[i * 3 + 1] *= -1;
if (Math.abs(positions[i * 3 + 2]) > 10) particleVelocities[i * 3 + 2] *= -1;
}
particleGeometry.attributes.position.needsUpdate = true;
// 旋转场景
scene.rotation.y += 0.001;
renderer.render(scene, camera);
}
// 处理窗口大小变化
window.addEventListener('resize', function() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// WebSocket 相关代码
let socket;
const statusElement = document.getElementById('status');
const statusDot = statusElement.querySelector('.status-dot');
const statusText = statusElement.querySelector('span:last-child');
const messagesContainer = document.getElementById('messages');
const messageInput = document.getElementById('messageInput');
const connectBtn = document.getElementById('connectBtn');
const disconnectBtn = document.getElementById('disconnectBtn');
const sendBtn = document.getElementById('sendBtn');
const wsAddressInput = document.getElementById('wsAddress');
const clearBtn = document.getElementById('clearBtn');
// 更新状态显示
function updateStatus(connected) {
if (connected) {
statusDot.classList.remove('bg-red-500');
statusDot.classList.add('bg-green-500');
statusText.textContent = '已连接';
statusText.classList.remove('text-gray-600');
statusText.classList.add('text-green-600');
} else {
statusDot.classList.remove('bg-green-500');
statusDot.classList.add('bg-red-500');
statusText.textContent = '未连接';
statusText.classList.remove('text-green-600');
statusText.classList.add('text-gray-600');
}
}
// 添加消息到显示区域
function addMessage(message, isReceived = false) {
const messageElement = document.createElement('div');
messageElement.className = `message flex ${isReceived ? 'justify-start' : 'justify-end'}`;
const messageContent = document.createElement('div');
messageContent.className = `flex items-start space-x-2 max-w-[80%]`;
const avatar = document.createElement('div');
avatar.className = `flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center
text-white font-bold ${isReceived ? 'bg-gray-500' : 'bg-indigo-500'}`;
avatar.textContent = isReceived ? 'S' : '我';
const bubble = document.createElement('div');
bubble.className = `p-3 rounded-lg shadow-sm ${
isReceived
? 'bg-white dark:bg-gray-700 text-gray-800 dark:text-gray-200'
: 'bg-indigo-500 text-white'
}`;
bubble.textContent = message;
if (isReceived) {
messageContent.appendChild(avatar);
messageContent.appendChild(bubble);
} else {
messageContent.appendChild(bubble);
messageContent.appendChild(avatar);
}
messageElement.appendChild(messageContent);
messagesContainer.appendChild(messageElement);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
// 收到消息时更新3D场景
if (isReceived) {
updateParticles();
}
}
// 更新粒子效果
function updateParticles() {
const positions = particleGeometry.attributes.position.array;
const colors = particleGeometry.attributes.color.array;
// 随机更新一些粒子的位置和颜色
for (let i = 0; i < particleCount; i += 10) {
positions[i * 3] = (Math.random() - 0.5) * 20;
positions[i * 3 + 1] = (Math.random() - 0.5) * 20;
positions[i * 3 + 2] = (Math.random() - 0.5) * 20;
colors[i * 3] = Math.random();
colors[i * 3 + 1] = Math.random();
colors[i * 3 + 2] = Math.random();
}
particleGeometry.attributes.position.needsUpdate = true;
particleGeometry.attributes.color.needsUpdate = true;
}
// 初始化 WebSocket 连接
function initWebSocket() {
const wsAddress = wsAddressInput.value.trim();
if (!wsAddress) {
addMessage('请输入WebSocket地址', true);
return;
}
try {
// 确保地址以 ws:// 开头
const normalizedAddress = wsAddress.startsWith('ws://')
? wsAddress
: `ws://${wsAddress}`;
socket = new WebSocket(normalizedAddress);
socket.onopen = function() {
updateStatus(true);
addMessage('WebSocket 连接已建立', true);
};
socket.onclose = function(event) {
updateStatus(false);
addMessage(`WebSocket 连接已关闭 (${event.code})`, true);
};
socket.onerror = function(error) {
addMessage(`连接错误: ${error.message || '未知错误'}`, true);
};
socket.onmessage = function(event) {
addMessage(event.data, true);
};
} catch (error) {
addMessage(`连接失败: ${error.message}`, true);
}
}
// 按钮事件处理
connectBtn.addEventListener('click', function() {
if (!socket || socket.readyState === WebSocket.CLOSED) {
initWebSocket();
}
});
disconnectBtn.addEventListener('click', function() {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.close();
}
});
sendBtn.addEventListener('click', function() {
if (socket && socket.readyState === WebSocket.OPEN) {
const message = messageInput.value.trim();
if (message) {
socket.send(message);
addMessage(message);
messageInput.value = '';
}
} else {
addMessage('WebSocket 未连接', true);
}
});
// 按回车发送消息
messageInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
sendBtn.click();
}
});
// 按回车连接WebSocket
wsAddressInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
connectBtn.click();
}
});
// 清空消息
clearBtn.addEventListener('click', function() {
messagesContainer.innerHTML = '';
});
// 初始化Three.js场景
initThreeJS();
</script>
</body>
</html>
Loading…
Cancel
Save