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