22 changed files with 1853 additions and 1 deletions
@ -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); |
||||
|
} |
@ -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); |
||||
|
} |
@ -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); |
||||
|
} |
||||
|
} |
@ -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; |
||||
|
} |
||||
|
} |
@ -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; |
||||
|
} |
||||
|
} |
@ -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"; |
||||
|
} |
@ -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>(); |
||||
|
} |
||||
|
} |
@ -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); |
||||
|
} |
||||
|
} |
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
@ -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!; |
||||
|
} |
||||
|
} |
@ -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; |
||||
|
} |
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -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 }; |
||||
|
} |
||||
|
} |
@ -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…
Reference in new issue