17 changed files with 2078 additions and 1548 deletions
@ -0,0 +1,173 @@ |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using LTEMvcApp.Models; |
|||
using LTEMvcApp.Services; |
|||
using Microsoft.Extensions.Logging; |
|||
|
|||
namespace LTEMvcApp.Controllers |
|||
{ |
|||
/// <summary>
|
|||
/// 客户端管理控制器 - 负责客户端的基本管理功能
|
|||
/// </summary>
|
|||
[ApiController] |
|||
[Route("api/[controller]")]
|
|||
public class ClientController : ControllerBase |
|||
{ |
|||
private readonly WebSocketManagerService _webSocketManager; |
|||
private readonly ILogger<ClientController> _logger; |
|||
|
|||
public ClientController(WebSocketManagerService webSocketManager, ILogger<ClientController> logger) |
|||
{ |
|||
_webSocketManager = webSocketManager; |
|||
_logger = logger; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取所有客户端状态
|
|||
/// </summary>
|
|||
/// <returns>客户端状态列表</returns>
|
|||
[HttpGet("states")] |
|||
public ActionResult<Dictionary<string, ClientState>> GetClientStates() |
|||
{ |
|||
_logger.LogInformation("获取所有客户端状态"); |
|||
var states = _webSocketManager.GetAllClientStates(); |
|||
return Ok(states); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 启动客户端
|
|||
/// </summary>
|
|||
/// <param name="clientName">客户端名称</param>
|
|||
/// <returns>操作结果</returns>
|
|||
[HttpPost("{clientName}/start")] |
|||
public ActionResult StartClient(string clientName) |
|||
{ |
|||
_logger.LogInformation($"API请求: 启动客户端 {clientName}"); |
|||
var success = _webSocketManager.StartClient(clientName); |
|||
if (success) |
|||
{ |
|||
_logger.LogInformation($"客户端 {clientName} 启动成功"); |
|||
return Ok(new { message = $"客户端 '{clientName}' 已启动" }); |
|||
} |
|||
else |
|||
{ |
|||
_logger.LogWarning($"客户端 {clientName} 启动失败"); |
|||
return BadRequest($"启动客户端 '{clientName}' 失败"); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 停止客户端
|
|||
/// </summary>
|
|||
/// <param name="clientName">客户端名称</param>
|
|||
/// <returns>操作结果</returns>
|
|||
[HttpPost("{clientName}/stop")] |
|||
public ActionResult StopClient(string clientName) |
|||
{ |
|||
var success = _webSocketManager.StopClient(clientName); |
|||
if (success) |
|||
return Ok(new { message = $"客户端 '{clientName}' 已停止" }); |
|||
else |
|||
return BadRequest($"停止客户端 '{clientName}' 失败"); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 播放/暂停客户端
|
|||
/// </summary>
|
|||
/// <param name="clientName">客户端名称</param>
|
|||
/// <returns>操作结果</returns>
|
|||
[HttpPost("{clientName}/playpause")] |
|||
public ActionResult PlayPauseClient(string clientName) |
|||
{ |
|||
var success = _webSocketManager.PlayPauseClient(clientName); |
|||
if (success) |
|||
return Ok(new { message = $"客户端 '{clientName}' 播放/暂停状态已切换" }); |
|||
else |
|||
return BadRequest($"切换客户端 '{clientName}' 播放/暂停状态失败"); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 重置客户端日志
|
|||
/// </summary>
|
|||
/// <param name="clientName">客户端名称</param>
|
|||
/// <returns>操作结果</returns>
|
|||
[HttpPost("{clientName}/reset-logs")] |
|||
public ActionResult ResetClientLogs(string clientName) |
|||
{ |
|||
var success = _webSocketManager.ResetClientLogs(clientName); |
|||
if (success) |
|||
return Ok(new { message = $"客户端 '{clientName}' 日志已重置" }); |
|||
else |
|||
return BadRequest($"重置客户端 '{clientName}' 日志失败"); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取客户端日志
|
|||
/// </summary>
|
|||
/// <param name="clientName">客户端名称</param>
|
|||
/// <param name="limit">日志数量限制</param>
|
|||
/// <returns>日志列表</returns>
|
|||
[HttpGet("{clientName}/logs")] |
|||
public ActionResult<List<LTELog>?> GetClientLogs(string clientName, [FromQuery] int limit = 100) |
|||
{ |
|||
var logs = _webSocketManager.GetClientLogs(clientName); |
|||
if (logs == null) |
|||
return NotFound($"客户端 '{clientName}' 不存在或未连接"); |
|||
|
|||
// 限制返回的日志数量
|
|||
var limitedLogs = logs.TakeLast(limit).ToList(); |
|||
return Ok(limitedLogs); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 设置客户端日志配置
|
|||
/// </summary>
|
|||
/// <param name="clientName">客户端名称</param>
|
|||
/// <param name="request">请求体</param>
|
|||
[HttpPost("{clientName}/logs-config")] |
|||
public ActionResult SetClientLogsConfig(string clientName, [FromBody] ClientLogsConfig request) |
|||
{ |
|||
var success = _webSocketManager.SetClientLogsConfig(clientName, request); |
|||
if (success) |
|||
{ |
|||
return Ok(new { message = "日志配置已更新" }); |
|||
} |
|||
else |
|||
{ |
|||
return NotFound(new { message = $"客户端 '{clientName}' 未找到或更新失败" }); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取连接统计信息
|
|||
/// </summary>
|
|||
/// <returns>统计信息</returns>
|
|||
[HttpGet("statistics")] |
|||
public ActionResult<ConnectionStatistics> GetStatistics() |
|||
{ |
|||
var stats = _webSocketManager.GetConnectionStatistics(); |
|||
return Ok(stats); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 启动所有已配置的客户端
|
|||
/// </summary>
|
|||
/// <returns>操作结果</returns>
|
|||
[HttpPost("start-all")] |
|||
public ActionResult StartAllClients() |
|||
{ |
|||
_webSocketManager.StartAllConfiguredClients(); |
|||
return Ok(new { message = "所有已配置的客户端已启动" }); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 停止所有客户端
|
|||
/// </summary>
|
|||
/// <returns>操作结果</returns>
|
|||
[HttpPost("stop-all")] |
|||
public ActionResult StopAllClients() |
|||
{ |
|||
_webSocketManager.StopAllClients(); |
|||
return Ok(new { message = "所有客户端已停止" }); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,83 @@ |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using LTEMvcApp.Models; |
|||
using LTEMvcApp.Services; |
|||
using Microsoft.Extensions.Logging; |
|||
|
|||
namespace LTEMvcApp.Controllers |
|||
{ |
|||
/// <summary>
|
|||
/// 配置管理控制器 - 负责普通客户端配置管理
|
|||
/// </summary>
|
|||
[ApiController] |
|||
[Route("api/[controller]")]
|
|||
public class ConfigController : ControllerBase |
|||
{ |
|||
private readonly WebSocketManagerService _webSocketManager; |
|||
private readonly ILogger<ConfigController> _logger; |
|||
|
|||
public ConfigController(WebSocketManagerService webSocketManager, ILogger<ConfigController> logger) |
|||
{ |
|||
_webSocketManager = webSocketManager; |
|||
_logger = logger; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取客户端配置
|
|||
/// </summary>
|
|||
/// <param name="clientName">客户端名称</param>
|
|||
/// <returns>客户端配置</returns>
|
|||
[HttpGet("{clientName}")] |
|||
public ActionResult<ClientConfig?> GetClientConfig(string clientName) |
|||
{ |
|||
var config = _webSocketManager.GetClientConfig(clientName); |
|||
if (config == null) |
|||
return NotFound($"客户端 '{clientName}' 不存在"); |
|||
|
|||
return Ok(config); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取所有客户端配置
|
|||
/// </summary>
|
|||
/// <returns>客户端配置列表</returns>
|
|||
[HttpGet] |
|||
public ActionResult<List<ClientConfig>> GetAllConfigs() |
|||
{ |
|||
var configs = _webSocketManager.GetAllClientConfigs(); |
|||
return Ok(configs); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 添加客户端配置
|
|||
/// </summary>
|
|||
/// <param name="config">客户端配置</param>
|
|||
/// <returns>操作结果</returns>
|
|||
[HttpPost] |
|||
public ActionResult AddClientConfig([FromBody] ClientConfig config) |
|||
{ |
|||
if (string.IsNullOrEmpty(config.Name)) |
|||
return BadRequest("客户端名称不能为空"); |
|||
|
|||
var success = _webSocketManager.AddClientConfig(config); |
|||
if (success) |
|||
return Ok(new { message = $"客户端 '{config.Name}' 配置已添加" }); |
|||
else |
|||
return BadRequest("添加客户端配置失败"); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 移除客户端配置
|
|||
/// </summary>
|
|||
/// <param name="clientName">客户端名称</param>
|
|||
/// <returns>操作结果</returns>
|
|||
[HttpDelete("{clientName}")] |
|||
public ActionResult RemoveClientConfig(string clientName) |
|||
{ |
|||
var success = _webSocketManager.RemoveClientConfig(clientName); |
|||
if (success) |
|||
return Ok(new { message = $"客户端 '{clientName}' 配置已移除" }); |
|||
else |
|||
return BadRequest($"移除客户端 '{clientName}' 配置失败"); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,487 @@ |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using LTEMvcApp.Models; |
|||
using LTEMvcApp.Services; |
|||
using Microsoft.Extensions.Logging; |
|||
using System.Threading.Tasks; |
|||
using System.Threading; |
|||
using System.IO; |
|||
using System.Text; |
|||
|
|||
namespace LTEMvcApp.Controllers |
|||
{ |
|||
/// <summary>
|
|||
/// 日志管理控制器 - 负责日志相关的管理功能
|
|||
/// </summary>
|
|||
[ApiController] |
|||
[Route("api/[controller]")]
|
|||
public class LogController : ControllerBase |
|||
{ |
|||
private readonly WebSocketManagerService _webSocketManager; |
|||
private readonly ILogger<LogController> _logger; |
|||
private readonly string _logsDirectory = "ClientMessageLogs"; |
|||
|
|||
public LogController(WebSocketManagerService webSocketManager, ILogger<LogController> logger) |
|||
{ |
|||
_webSocketManager = webSocketManager; |
|||
_logger = logger; |
|||
|
|||
// 确保日志目录存在
|
|||
if (!Directory.Exists(_logsDirectory)) |
|||
{ |
|||
Directory.CreateDirectory(_logsDirectory); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取日志缓存统计信息
|
|||
/// </summary>
|
|||
/// <returns>统计信息</returns>
|
|||
[HttpGet("stats")] |
|||
public ActionResult<object> GetLogCacheStats() |
|||
{ |
|||
try |
|||
{ |
|||
var stats = new |
|||
{ |
|||
totalLogs = _webSocketManager.GetLogCacheCount(), |
|||
cacheSize = 10000, // LogCacheSize
|
|||
timestamp = DateTime.UtcNow |
|||
}; |
|||
return Ok(stats); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "获取日志缓存统计信息时发生错误"); |
|||
return StatusCode(500, new { message = "获取统计信息失败", error = ex.Message }); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 清空全局日志缓存
|
|||
/// </summary>
|
|||
/// <returns>操作结果</returns>
|
|||
[HttpPost("clear")] |
|||
public ActionResult ClearLogCache() |
|||
{ |
|||
try |
|||
{ |
|||
_webSocketManager.ClearLogCache(); |
|||
return Ok(new { message = "日志缓存已清空" }); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "清空日志缓存时发生错误"); |
|||
return StatusCode(500, new { message = "清空日志缓存失败", error = ex.Message }); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 重置全局日志缓存
|
|||
/// </summary>
|
|||
/// <returns>操作结果</returns>
|
|||
[HttpPost("reset")] |
|||
public ActionResult ResetLogCache() |
|||
{ |
|||
try |
|||
{ |
|||
_webSocketManager.ResetLogCache(); |
|||
return Ok(new { message = "日志缓存已重置" }); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "重置日志缓存时发生错误"); |
|||
return StatusCode(500, new { message = "重置日志缓存失败", error = ex.Message }); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 添加测试日志数据
|
|||
/// </summary>
|
|||
/// <returns>操作结果</returns>
|
|||
[HttpPost("add-test-data")] |
|||
public ActionResult AddTestLogData() |
|||
{ |
|||
try |
|||
{ |
|||
var testLogs = new List<LTELog>(); |
|||
_webSocketManager.AddLogsToCache(testLogs); |
|||
return Ok(new { message = $"已添加 {testLogs.Count} 条测试日志" }); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "添加测试日志数据时发生错误"); |
|||
return StatusCode(500, new { message = "添加测试日志失败", error = ex.Message }); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 使用 Server-Sent Events (SSE) 实时推送全局日志
|
|||
/// </summary>
|
|||
[HttpGet("stream")] |
|||
public async Task StreamLogs(CancellationToken cancellationToken) |
|||
{ |
|||
try |
|||
{ |
|||
Response.ContentType = "text/event-stream"; |
|||
Response.Headers.Append("Cache-Control", "no-cache"); |
|||
Response.Headers.Append("Connection", "keep-alive"); |
|||
Response.Headers.Append("Access-Control-Allow-Origin", "*"); |
|||
|
|||
// 发送连接成功事件
|
|||
await SendSseEvent("connected", new { message = "日志流连接已建立", timestamp = DateTime.UtcNow }); |
|||
await Response.Body.FlushAsync(cancellationToken); |
|||
|
|||
int lastLogCount = 0; |
|||
var lastLogs = new List<LTELog>(); |
|||
var lastLogHash = string.Empty; |
|||
|
|||
// 首先,一次性推送所有已缓存的日志
|
|||
try |
|||
{ |
|||
var initialLogs = _webSocketManager.GetLogCache()?.ToList() ?? new List<LTELog>(); |
|||
_logger.LogInformation("StreamLogs: 获取到初始日志 {Count} 条", initialLogs.Count); |
|||
|
|||
if (initialLogs.Any()) |
|||
{ |
|||
_logger.LogInformation("StreamLogs: 发送历史日志事件,日志数量: {Count}", initialLogs.Count); |
|||
await SendSseEvent("history", new { logs = initialLogs, totalCount = initialLogs.Count }); |
|||
await Response.Body.FlushAsync(cancellationToken); |
|||
lastLogCount = initialLogs.Count; |
|||
lastLogs = initialLogs.ToList(); |
|||
lastLogHash = CalculateLogsHash(initialLogs); |
|||
} |
|||
else |
|||
{ |
|||
_logger.LogInformation("StreamLogs: 没有历史日志数据"); |
|||
} |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "获取初始日志时发生错误"); |
|||
await SendSseEvent("error", new { message = "获取初始日志失败", error = ex.Message }); |
|||
await Response.Body.FlushAsync(cancellationToken); |
|||
} |
|||
|
|||
while (!cancellationToken.IsCancellationRequested) |
|||
{ |
|||
try |
|||
{ |
|||
var currentLogs = _webSocketManager.GetLogCache()?.ToList() ?? new List<LTELog>(); |
|||
var currentLogHash = CalculateLogsHash(currentLogs); |
|||
|
|||
bool hasNewLogs = false; |
|||
List<LTELog> newLogs = new List<LTELog>(); |
|||
|
|||
if (currentLogs.Count > lastLogCount) |
|||
{ |
|||
newLogs = currentLogs.Skip(lastLogCount).ToList(); |
|||
hasNewLogs = newLogs.Any(); |
|||
} |
|||
else if (currentLogs.Count == lastLogCount && currentLogHash != lastLogHash) |
|||
{ |
|||
newLogs = GetChangedLogs(lastLogs, currentLogs); |
|||
hasNewLogs = newLogs.Any(); |
|||
} |
|||
else if (currentLogs.Count < lastLogCount) |
|||
{ |
|||
_logger.LogInformation("检测到日志缓存被重置,重新同步"); |
|||
await SendSseEvent("reset", new { |
|||
message = "日志缓存已重置", |
|||
totalCount = currentLogs.Count |
|||
}); |
|||
await Response.Body.FlushAsync(cancellationToken); |
|||
|
|||
lastLogCount = currentLogs.Count; |
|||
lastLogs = currentLogs.ToList(); |
|||
lastLogHash = currentLogHash; |
|||
continue; |
|||
} |
|||
|
|||
if (hasNewLogs && newLogs.Any()) |
|||
{ |
|||
_logger.LogInformation("StreamLogs: 发送新日志事件,新增日志数量: {NewCount}, 总日志数量: {TotalCount}", newLogs.Count, currentLogs.Count); |
|||
|
|||
var eventData = new { |
|||
logs = newLogs, |
|||
totalCount = currentLogs.Count, |
|||
newCount = newLogs.Count |
|||
}; |
|||
|
|||
await SendSseEvent("new_logs", eventData); |
|||
await Response.Body.FlushAsync(cancellationToken); |
|||
|
|||
lastLogCount = currentLogs.Count; |
|||
lastLogs = currentLogs.ToList(); |
|||
lastLogHash = currentLogHash; |
|||
} |
|||
|
|||
await Task.Delay(250, cancellationToken); |
|||
} |
|||
catch (OperationCanceledException) |
|||
{ |
|||
break; |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "StreamLogs 循环中发生错误"); |
|||
await SendSseEvent("error", new { |
|||
message = "处理日志流时发生错误", |
|||
error = ex.Message, |
|||
timestamp = DateTime.UtcNow |
|||
}); |
|||
await Response.Body.FlushAsync(cancellationToken); |
|||
|
|||
await Task.Delay(1000, cancellationToken); |
|||
} |
|||
} |
|||
|
|||
await SendSseEvent("disconnected", new { |
|||
message = "日志流连接已断开", |
|||
timestamp = DateTime.UtcNow |
|||
}); |
|||
await Response.Body.FlushAsync(cancellationToken); |
|||
} |
|||
catch (OperationCanceledException) |
|||
{ |
|||
_logger.LogInformation("StreamLogs 连接被客户端取消"); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "StreamLogs 方法执行时发生未处理的异常"); |
|||
try |
|||
{ |
|||
await SendSseEvent("fatal_error", new { |
|||
message = "服务器内部错误", |
|||
error = ex.Message, |
|||
timestamp = DateTime.UtcNow |
|||
}); |
|||
await Response.Body.FlushAsync(); |
|||
} |
|||
catch |
|||
{ |
|||
// 忽略发送错误事件时的异常
|
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取日志缓存详细状态(调试用)
|
|||
/// </summary>
|
|||
/// <returns>日志缓存详细状态</returns>
|
|||
[HttpGet("debug")] |
|||
public ActionResult<object> GetLogCacheDebugInfo() |
|||
{ |
|||
try |
|||
{ |
|||
var logs = _webSocketManager.GetLogCache()?.ToList() ?? new List<LTELog>(); |
|||
var logCount = _webSocketManager.GetLogCacheCount(); |
|||
var cacheStatus = _webSocketManager.GetLogCacheStatus(); |
|||
|
|||
var debugInfo = new |
|||
{ |
|||
totalLogs = logCount, |
|||
actualLogsCount = logs.Count, |
|||
cacheSize = 10000, |
|||
timestamp = DateTime.UtcNow, |
|||
sampleLogs = logs.TakeLast(5).Select(log => new |
|||
{ |
|||
timestamp = log.Timestamp, |
|||
layer = log.Layer, |
|||
message = log.Message?.Substring(0, Math.Min(100, log.Message?.Length ?? 0)) + "..." |
|||
}).ToList(), |
|||
logHash = CalculateLogsHash(logs), |
|||
cacheStatus = cacheStatus |
|||
}; |
|||
|
|||
return Ok(debugInfo); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "获取日志缓存调试信息时发生错误"); |
|||
return StatusCode(500, new { message = "获取调试信息失败", error = ex.Message }); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取SSE连接状态(调试用)
|
|||
/// </summary>
|
|||
/// <returns>连接状态信息</returns>
|
|||
[HttpGet("connection-status")] |
|||
public ActionResult<object> GetSseConnectionStatus() |
|||
{ |
|||
try |
|||
{ |
|||
var status = new |
|||
{ |
|||
timestamp = DateTime.UtcNow, |
|||
requestHeaders = Request.Headers.ToDictionary(h => h.Key, h => h.Value.ToString()), |
|||
userAgent = Request.Headers["User-Agent"].ToString(), |
|||
accept = Request.Headers["Accept"].ToString(), |
|||
cacheControl = Request.Headers["Cache-Control"].ToString(), |
|||
connection = Request.Headers["Connection"].ToString(), |
|||
isHttps = Request.IsHttps, |
|||
host = Request.Host.ToString(), |
|||
path = Request.Path.ToString(), |
|||
queryString = Request.QueryString.ToString(), |
|||
method = Request.Method, |
|||
contentType = Request.ContentType, |
|||
contentLength = Request.ContentLength |
|||
}; |
|||
|
|||
return Ok(status); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "获取SSE连接状态时发生错误"); |
|||
return StatusCode(500, new { message = "获取连接状态失败", error = ex.Message }); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 测试SSE连接(调试用)
|
|||
/// </summary>
|
|||
/// <returns>测试结果</returns>
|
|||
[HttpGet("test-connection")] |
|||
public ActionResult<object> TestSseConnection() |
|||
{ |
|||
try |
|||
{ |
|||
var testResult = new |
|||
{ |
|||
message = "SSE连接测试成功", |
|||
timestamp = DateTime.UtcNow, |
|||
serverTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"), |
|||
logCacheCount = _webSocketManager.GetLogCacheCount(), |
|||
testData = new { test = true, message = "这是一个测试消息" } |
|||
}; |
|||
|
|||
return Ok(testResult); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "测试SSE连接时发生错误"); |
|||
return StatusCode(500, new { message = "测试连接失败", error = ex.Message }); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 强制推送测试日志(调试用)
|
|||
/// </summary>
|
|||
/// <returns>操作结果</returns>
|
|||
[HttpPost("force-push-test")] |
|||
public ActionResult ForcePushTestLogs() |
|||
{ |
|||
try |
|||
{ |
|||
var testLogs = new List<LTELog>(); |
|||
_webSocketManager.AddLogsToCache(testLogs); |
|||
|
|||
_logger.LogInformation("强制推送测试日志: {Message}", testLogs[0].Message); |
|||
|
|||
return Ok(new { |
|||
message = $"已强制推送测试日志: {testLogs[0].Message}", |
|||
timestamp = testLogs[0].Timestamp |
|||
}); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "强制推送测试日志时发生错误"); |
|||
return StatusCode(500, new { message = "强制推送失败", error = ex.Message }); |
|||
} |
|||
} |
|||
|
|||
private async Task SendSseEvent(string eventName, object data) |
|||
{ |
|||
try |
|||
{ |
|||
if (string.IsNullOrEmpty(eventName)) |
|||
{ |
|||
_logger.LogWarning("尝试发送空事件名称的SSE事件"); |
|||
return; |
|||
} |
|||
|
|||
if (data == null) |
|||
{ |
|||
_logger.LogWarning("尝试发送空数据的SSE事件: {EventName}", eventName); |
|||
return; |
|||
} |
|||
|
|||
var json = Newtonsoft.Json.JsonConvert.SerializeObject(data); |
|||
var eventData = $"event: {eventName}\ndata: {json}\n\n"; |
|||
|
|||
await Response.WriteAsync(eventData); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "发送SSE事件时发生错误: {EventName}", eventName); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 计算日志列表的哈希值,用于检测内容变化
|
|||
/// </summary>
|
|||
/// <param name="logs">日志列表</param>
|
|||
/// <returns>哈希值字符串</returns>
|
|||
private string CalculateLogsHash(List<LTELog> logs) |
|||
{ |
|||
if (logs == null || !logs.Any()) |
|||
return string.Empty; |
|||
|
|||
try |
|||
{ |
|||
var hashInput = string.Join("|", logs.Select(log => |
|||
$"{log.Timestamp}_{log.Layer}_{log.Message}")); |
|||
|
|||
using (var sha256 = System.Security.Cryptography.SHA256.Create()) |
|||
{ |
|||
var bytes = System.Text.Encoding.UTF8.GetBytes(hashInput); |
|||
var hash = sha256.ComputeHash(bytes); |
|||
return Convert.ToBase64String(hash); |
|||
} |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "计算日志哈希值时发生错误"); |
|||
return string.Empty; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取变化的日志项
|
|||
/// </summary>
|
|||
/// <param name="oldLogs">旧日志列表</param>
|
|||
/// <param name="newLogs">新日志列表</param>
|
|||
/// <returns>变化的日志列表</returns>
|
|||
private List<LTELog> GetChangedLogs(List<LTELog> oldLogs, List<LTELog> newLogs) |
|||
{ |
|||
var changedLogs = new List<LTELog>(); |
|||
|
|||
if (oldLogs.Count != newLogs.Count) |
|||
{ |
|||
return newLogs; |
|||
} |
|||
|
|||
for (int i = 0; i < newLogs.Count; i++) |
|||
{ |
|||
if (i < oldLogs.Count) |
|||
{ |
|||
var oldLog = oldLogs[i]; |
|||
var newLog = newLogs[i]; |
|||
|
|||
if (oldLog.Timestamp != newLog.Timestamp || |
|||
oldLog.Layer != newLog.Layer || |
|||
oldLog.Message != newLog.Message) |
|||
{ |
|||
changedLogs.Add(newLog); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
changedLogs.Add(newLogs[i]); |
|||
} |
|||
} |
|||
|
|||
return changedLogs; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,368 @@ |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using LTEMvcApp.Models; |
|||
using LTEMvcApp.Services; |
|||
using Microsoft.Extensions.Logging; |
|||
using Newtonsoft.Json.Linq; |
|||
using System.Threading.Tasks; |
|||
using System.Threading; |
|||
using System.IO; |
|||
using System.Text; |
|||
|
|||
namespace LTEMvcApp.Controllers |
|||
{ |
|||
/// <summary>
|
|||
/// 客户端消息管理控制器
|
|||
/// </summary>
|
|||
[ApiController] |
|||
[Route("api/[controller]")]
|
|||
public class MessageController : ControllerBase |
|||
{ |
|||
private readonly WebSocketManagerService _webSocketManager; |
|||
private readonly ILogger<MessageController> _logger; |
|||
private readonly string _logsDirectory = "ClientMessageLogs"; |
|||
|
|||
public MessageController(WebSocketManagerService webSocketManager, ILogger<MessageController> logger) |
|||
{ |
|||
_webSocketManager = webSocketManager; |
|||
_logger = logger; |
|||
if (!Directory.Exists(_logsDirectory)) |
|||
{ |
|||
Directory.CreateDirectory(_logsDirectory); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// SSE推送客户端消息流
|
|||
/// </summary>
|
|||
[HttpGet("{clientName}/stream")] |
|||
public async Task StreamClientMessages(string clientName) |
|||
{ |
|||
try |
|||
{ |
|||
Response.ContentType = "text/event-stream"; |
|||
Response.Headers.Append("Cache-Control", "no-cache"); |
|||
Response.Headers.Append("Connection", "keep-alive"); |
|||
Response.Headers.Append("Access-Control-Allow-Origin", "*"); |
|||
|
|||
var client = _webSocketManager.GetClientInstance(clientName); |
|||
if (client == null) |
|||
{ |
|||
await SendSseEvent("error", new { message = "客户端未连接或不存在", clientName }); |
|||
return; |
|||
} |
|||
|
|||
await SendSseEvent("open", new { |
|||
message = "成功连接到服务器事件流", |
|||
clientName, |
|||
timestamp = DateTime.UtcNow |
|||
}); |
|||
await Response.Body.FlushAsync(HttpContext.RequestAborted); |
|||
|
|||
int lastSentCount = 0; |
|||
int lastReceivedCount = 0; |
|||
var cancellationToken = HttpContext.RequestAborted; |
|||
|
|||
var sentLogFilePath = Path.Combine(_logsDirectory, $"{clientName}_sent_messages.log"); |
|||
var receivedLogFilePath = Path.Combine(_logsDirectory, $"{clientName}_received_messages.log"); |
|||
|
|||
while (!cancellationToken.IsCancellationRequested) |
|||
{ |
|||
try |
|||
{ |
|||
bool hasNewMessages = false; |
|||
var currentSentCount = client.SentMessagesCount; |
|||
if (currentSentCount > lastSentCount) |
|||
{ |
|||
var sentMessages = client.SentMessages?.ToList() ?? new List<string>(); |
|||
if (sentMessages.Count > lastSentCount) |
|||
{ |
|||
var newMessages = sentMessages.Skip(lastSentCount).ToList(); |
|||
if (newMessages.Any()) |
|||
{ |
|||
await LogMessagesToFile(sentLogFilePath, newMessages, "SENT", clientName); |
|||
await SendSseEvent("update", new { |
|||
type = "sent", |
|||
messages = newMessages, |
|||
totalCount = currentSentCount, |
|||
newCount = newMessages.Count |
|||
}); |
|||
lastSentCount = currentSentCount; |
|||
hasNewMessages = true; |
|||
} |
|||
} |
|||
} |
|||
var currentReceivedCount = client.ReceivedMessagesCount; |
|||
if (currentReceivedCount > lastReceivedCount) |
|||
{ |
|||
var receivedMessages = client.ReceivedMessages?.ToList() ?? new List<string>(); |
|||
if (receivedMessages.Count > lastReceivedCount) |
|||
{ |
|||
var newMessages = receivedMessages.Skip(lastReceivedCount).ToList(); |
|||
if (newMessages.Any()) |
|||
{ |
|||
await LogMessagesToFile(receivedLogFilePath, newMessages, "RECEIVED", clientName); |
|||
await SendSseEvent("update", new { |
|||
type = "received", |
|||
messages = newMessages, |
|||
totalCount = currentReceivedCount, |
|||
newCount = newMessages.Count |
|||
}); |
|||
lastReceivedCount = currentReceivedCount; |
|||
hasNewMessages = true; |
|||
} |
|||
} |
|||
} |
|||
if (hasNewMessages) |
|||
{ |
|||
await Response.Body.FlushAsync(cancellationToken); |
|||
} |
|||
await Task.Delay(250, cancellationToken); |
|||
} |
|||
catch (OperationCanceledException) |
|||
{ |
|||
break; |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "StreamClientMessages 循环中发生错误,客户端: {ClientName}", clientName); |
|||
await SendSseEvent("error", new { |
|||
message = "处理消息流时发生错误", |
|||
error = ex.Message, |
|||
clientName, |
|||
timestamp = DateTime.UtcNow |
|||
}); |
|||
await Response.Body.FlushAsync(cancellationToken); |
|||
await Task.Delay(1000, cancellationToken); |
|||
} |
|||
} |
|||
await SendSseEvent("disconnected", new { |
|||
message = "客户端消息流连接已断开", |
|||
clientName, |
|||
timestamp = DateTime.UtcNow |
|||
}); |
|||
await Response.Body.FlushAsync(cancellationToken); |
|||
} |
|||
catch (OperationCanceledException) |
|||
{ |
|||
_logger.LogInformation("StreamClientMessages 连接被客户端取消,客户端: {ClientName}", clientName); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "StreamClientMessages 方法执行时发生未处理的异常,客户端: {ClientName}", clientName); |
|||
try |
|||
{ |
|||
await SendSseEvent("fatal_error", new { |
|||
message = "服务器内部错误", |
|||
error = ex.Message, |
|||
clientName, |
|||
timestamp = DateTime.UtcNow |
|||
}); |
|||
await Response.Body.FlushAsync(); |
|||
} |
|||
catch { } |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 发送消息到客户端
|
|||
/// </summary>
|
|||
[HttpPost("{clientName}/send")] |
|||
public ActionResult SendMessage(string clientName, [FromBody] JObject message) |
|||
{ |
|||
var messageId = _webSocketManager.SendMessageToClient(clientName, message); |
|||
if (messageId >= 0) |
|||
return Ok(new { messageId, message = $"消息已发送到客户端 '{clientName}'" }); |
|||
else |
|||
return BadRequest($"发送消息到客户端 '{clientName}' 失败"); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取客户端消息日志文件列表
|
|||
/// </summary>
|
|||
[HttpGet("logs")] |
|||
public ActionResult<object> GetClientMessageLogFiles() |
|||
{ |
|||
try |
|||
{ |
|||
if (!Directory.Exists(_logsDirectory)) |
|||
{ |
|||
return Ok(new { files = new List<object>(), message = "日志目录不存在" }); |
|||
} |
|||
var logFiles = Directory.GetFiles(_logsDirectory, "*.log") |
|||
.Select(filePath => new |
|||
{ |
|||
fileName = Path.GetFileName(filePath), |
|||
filePath = filePath, |
|||
size = new FileInfo(filePath).Length, |
|||
lastModified = System.IO.File.GetLastWriteTime(filePath), |
|||
clientName = Path.GetFileNameWithoutExtension(filePath).Replace("_sent_messages", "").Replace("_received_messages", ""), |
|||
type = filePath.Contains("_sent_messages") ? "发送消息" : "接收消息" |
|||
}) |
|||
.OrderByDescending(f => f.lastModified) |
|||
.ToList(); |
|||
return Ok(new { files = logFiles, totalCount = logFiles.Count }); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "获取客户端消息日志文件列表时发生错误"); |
|||
return StatusCode(500, new { message = "获取日志文件列表失败", error = ex.Message }); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取客户端消息日志文件内容
|
|||
/// </summary>
|
|||
[HttpGet("logs/{fileName}")] |
|||
public ActionResult<object> GetClientMessageLogContent(string fileName, [FromQuery] int lines = 100) |
|||
{ |
|||
try |
|||
{ |
|||
var filePath = Path.Combine(_logsDirectory, fileName); |
|||
if (!System.IO.File.Exists(filePath)) |
|||
{ |
|||
return NotFound(new { message = $"日志文件 '{fileName}' 不存在" }); |
|||
} |
|||
var fileInfo = new System.IO.FileInfo(filePath); |
|||
var allLines = System.IO.File.ReadAllLines(filePath, Encoding.UTF8); |
|||
var lastLines = allLines.TakeLast(lines).ToList(); |
|||
return Ok(new |
|||
{ |
|||
fileName = fileName, |
|||
filePath = filePath, |
|||
totalLines = allLines.Length, |
|||
returnedLines = lastLines.Count, |
|||
fileSize = fileInfo.Length, |
|||
lastModified = fileInfo.LastWriteTime, |
|||
content = lastLines |
|||
}); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "获取客户端消息日志文件内容时发生错误: {FileName}", fileName); |
|||
return StatusCode(500, new { message = "获取日志文件内容失败", error = ex.Message }); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 清空客户端消息日志文件
|
|||
/// </summary>
|
|||
[HttpDelete("logs")] |
|||
public ActionResult ClearClientMessageLogs([FromQuery] string? fileName = null) |
|||
{ |
|||
try |
|||
{ |
|||
if (!Directory.Exists(_logsDirectory)) |
|||
{ |
|||
return Ok(new { message = "日志目录不存在,无需清空" }); |
|||
} |
|||
int clearedCount = 0; |
|||
if (string.IsNullOrEmpty(fileName)) |
|||
{ |
|||
var logFiles = Directory.GetFiles(_logsDirectory, "*.log"); |
|||
foreach (var filePath in logFiles) |
|||
{ |
|||
System.IO.File.WriteAllText(filePath, string.Empty); |
|||
clearedCount++; |
|||
} |
|||
_logger.LogInformation("已清空 {Count} 个客户端消息日志文件", clearedCount); |
|||
return Ok(new { message = $"已清空 {clearedCount} 个客户端消息日志文件" }); |
|||
} |
|||
else |
|||
{ |
|||
var filePath = Path.Combine(_logsDirectory, fileName); |
|||
if (!System.IO.File.Exists(filePath)) |
|||
{ |
|||
return NotFound(new { message = $"日志文件 '{fileName}' 不存在" }); |
|||
} |
|||
System.IO.File.WriteAllText(filePath, string.Empty); |
|||
_logger.LogInformation("已清空客户端消息日志文件: {FileName}", fileName); |
|||
return Ok(new { message = $"已清空客户端消息日志文件 '{fileName}'" }); |
|||
} |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "清空客户端消息日志文件时发生错误"); |
|||
return StatusCode(500, new { message = "清空日志文件失败", error = ex.Message }); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 删除客户端消息日志文件
|
|||
/// </summary>
|
|||
[HttpDelete("logs/delete")] |
|||
public ActionResult DeleteClientMessageLogs([FromQuery] string? fileName = null) |
|||
{ |
|||
try |
|||
{ |
|||
if (!Directory.Exists(_logsDirectory)) |
|||
{ |
|||
return Ok(new { message = "日志目录不存在,无需删除" }); |
|||
} |
|||
int deletedCount = 0; |
|||
if (string.IsNullOrEmpty(fileName)) |
|||
{ |
|||
var logFiles = Directory.GetFiles(_logsDirectory, "*.log"); |
|||
foreach (var filePath in logFiles) |
|||
{ |
|||
System.IO.File.Delete(filePath); |
|||
deletedCount++; |
|||
} |
|||
_logger.LogInformation("已删除 {Count} 个客户端消息日志文件", deletedCount); |
|||
return Ok(new { message = $"已删除 {deletedCount} 个客户端消息日志文件" }); |
|||
} |
|||
else |
|||
{ |
|||
var filePath = Path.Combine(_logsDirectory, fileName); |
|||
if (!System.IO.File.Exists(filePath)) |
|||
{ |
|||
return NotFound(new { message = $"日志文件 '{fileName}' 不存在" }); |
|||
} |
|||
System.IO.File.Delete(filePath); |
|||
_logger.LogInformation("已删除客户端消息日志文件: {FileName}", fileName); |
|||
return Ok(new { message = $"已删除客户端消息日志文件 '{fileName}'" }); |
|||
} |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "删除客户端消息日志文件时发生错误"); |
|||
return StatusCode(500, new { message = "删除日志文件失败", error = ex.Message }); |
|||
} |
|||
} |
|||
|
|||
private async Task LogMessagesToFile(string logFilePath, List<string> messages, string messageType, string clientName) |
|||
{ |
|||
try |
|||
{ |
|||
var logBuilder = new StringBuilder(); |
|||
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); |
|||
foreach (var message in messages) |
|||
{ |
|||
logBuilder.AppendLine($"[{timestamp}] [{clientName}] [{messageType}] {message}"); |
|||
logBuilder.AppendLine(new string('-', 80)); |
|||
} |
|||
await System.IO.File.AppendAllTextAsync(logFilePath, logBuilder.ToString(), Encoding.UTF8); |
|||
_logger.LogDebug("已记录 {Count} 条 {MessageType} 消息到文件: {FilePath}", messages.Count, messageType, logFilePath); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "记录消息到文件时发生错误: {FilePath}", logFilePath); |
|||
} |
|||
} |
|||
|
|||
private async Task SendSseEvent(string eventName, object data) |
|||
{ |
|||
try |
|||
{ |
|||
if (string.IsNullOrEmpty(eventName) || data == null) |
|||
return; |
|||
var json = Newtonsoft.Json.JsonConvert.SerializeObject(data); |
|||
var eventData = $"event: {eventName}\ndata: {json}\n\n"; |
|||
await Response.WriteAsync(eventData); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "发送SSE事件时发生错误: {EventName}", eventName); |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,175 @@ |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using LTEMvcApp.Models; |
|||
using LTEMvcApp.Services; |
|||
using Microsoft.Extensions.Logging; |
|||
|
|||
namespace LTEMvcApp.Controllers |
|||
{ |
|||
/// <summary>
|
|||
/// 测试配置管理控制器 - 负责测试客户端配置管理
|
|||
/// </summary>
|
|||
[ApiController] |
|||
[Route("api/[controller]")]
|
|||
public class TestConfigController : ControllerBase |
|||
{ |
|||
private readonly WebSocketManagerService _webSocketManager; |
|||
private readonly ILogger<TestConfigController> _logger; |
|||
|
|||
public TestConfigController(WebSocketManagerService webSocketManager, ILogger<TestConfigController> logger) |
|||
{ |
|||
_webSocketManager = webSocketManager; |
|||
_logger = logger; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取默认测试客户端配置
|
|||
/// </summary>
|
|||
/// <returns>测试客户端配置</returns>
|
|||
[HttpGet("default")] |
|||
public ActionResult<ClientConfig> GetDefaultTestClientConfig() |
|||
{ |
|||
var testConfig = _webSocketManager.GetDefaultTestClientConfig(); |
|||
return Ok(testConfig); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 设置测试客户端配置
|
|||
/// </summary>
|
|||
/// <param name="config">测试客户端配置</param>
|
|||
/// <returns>操作结果</returns>
|
|||
[HttpPost("default")] |
|||
public ActionResult SetTestClientConfig([FromBody] ClientConfig config) |
|||
{ |
|||
if (string.IsNullOrEmpty(config.Name)) |
|||
return BadRequest("客户端名称不能为空"); |
|||
|
|||
var success = _webSocketManager.SetTestClientConfig(config); |
|||
if (success) |
|||
return Ok(new { message = "测试客户端配置已更新" }); |
|||
else |
|||
return BadRequest("更新测试客户端配置失败"); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取所有测试客户端配置
|
|||
/// </summary>
|
|||
/// <returns>测试客户端配置列表</returns>
|
|||
[HttpGet] |
|||
public ActionResult<List<ClientConfig>> GetAllTestClientConfigs() |
|||
{ |
|||
var configs = _webSocketManager.GetAllTestClientConfigs(); |
|||
return Ok(configs); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 根据地址获取测试客户端配置
|
|||
/// </summary>
|
|||
/// <param name="address">服务器地址</param>
|
|||
/// <returns>测试客户端配置</returns>
|
|||
[HttpGet("address/{address}")] |
|||
public ActionResult<ClientConfig?> GetTestClientConfigByAddress(string address) |
|||
{ |
|||
var config = _webSocketManager.GetTestClientConfigByAddress(address); |
|||
if (config == null) |
|||
return NotFound($"测试客户端配置 (地址: {address}) 不存在"); |
|||
|
|||
return Ok(config); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 删除测试客户端配置
|
|||
/// </summary>
|
|||
/// <param name="address">服务器地址</param>
|
|||
/// <returns>操作结果</returns>
|
|||
[HttpDelete("address/{address}")] |
|||
public ActionResult RemoveTestClientConfig(string address) |
|||
{ |
|||
var success = _webSocketManager.RemoveTestClientConfig(address); |
|||
if (success) |
|||
return Ok(new { message = $"测试客户端配置 (地址: {address}) 已删除" }); |
|||
else |
|||
return NotFound($"测试客户端配置 (地址: {address}) 不存在"); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 启动测试客户端
|
|||
/// </summary>
|
|||
/// <param name="request">启动请求</param>
|
|||
/// <returns>操作结果</returns>
|
|||
[HttpPost("start")] |
|||
public ActionResult StartTestClient([FromBody] StartStopRequest? request = null) |
|||
{ |
|||
string? address = request?.Address; |
|||
|
|||
if (string.IsNullOrEmpty(address)) |
|||
{ |
|||
// 使用默认配置启动
|
|||
var success = _webSocketManager.StartTestClient(); |
|||
if (success) |
|||
return Ok(new { message = "测试客户端已启动" }); |
|||
else |
|||
return BadRequest("启动测试客户端失败"); |
|||
} |
|||
else |
|||
{ |
|||
// 根据地址查找配置并启动
|
|||
var config = _webSocketManager.GetTestClientConfigByAddress(address); |
|||
if (config == null) |
|||
return NotFound($"未找到地址为 {address} 的测试客户端配置"); |
|||
|
|||
var success = _webSocketManager.StartClient(config.Name); |
|||
if (success) |
|||
return Ok(new { message = $"测试客户端 {config.Name} 已启动" }); |
|||
else |
|||
return BadRequest($"启动测试客户端 {config.Name} 失败"); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 停止测试客户端
|
|||
/// </summary>
|
|||
/// <param name="request">停止请求</param>
|
|||
/// <returns>操作结果</returns>
|
|||
[HttpPost("stop")] |
|||
public ActionResult StopTestClient([FromBody] StartStopRequest? request = null) |
|||
{ |
|||
string? address = request?.Address; |
|||
|
|||
if (string.IsNullOrEmpty(address)) |
|||
{ |
|||
// 使用默认配置停止
|
|||
_logger.LogInformation("API 请求: 停止测试客户端"); |
|||
var success = _webSocketManager.StopTestClient(); |
|||
if (success) |
|||
return Ok(new { message = "测试客户端停止成功" }); |
|||
else |
|||
return BadRequest("停止测试客户端失败"); |
|||
} |
|||
else |
|||
{ |
|||
// 根据地址查找配置并停止
|
|||
var config = _webSocketManager.GetTestClientConfigByAddress(address); |
|||
if (config == null) |
|||
return NotFound($"未找到地址为 {address} 的测试客户端配置"); |
|||
|
|||
_logger.LogInformation($"API 请求: 停止测试客户端 {config.Name}"); |
|||
var success = _webSocketManager.StopClient(config.Name); |
|||
if (success) |
|||
return Ok(new { message = $"测试客户端 {config.Name} 停止成功" }); |
|||
else |
|||
return BadRequest($"停止测试客户端 {config.Name} 失败"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 启动/停止请求
|
|||
/// </summary>
|
|||
public class StartStopRequest |
|||
{ |
|||
/// <summary>
|
|||
/// 服务器地址
|
|||
/// </summary>
|
|||
public string? Address { get; set; } |
|||
} |
|||
} |
File diff suppressed because it is too large
@ -0,0 +1,174 @@ |
|||
# 多个测试客户端配置功能 |
|||
|
|||
## 功能概述 |
|||
|
|||
本功能允许系统保存和管理多个测试客户端配置,使用服务器地址(Address)作为唯一标识符。系统已简化为只保留多个配置的管理功能。 |
|||
|
|||
## 主要特性 |
|||
|
|||
### 1. 基于Address的唯一性管理 |
|||
- 使用服务器地址(Address)作为配置的唯一key |
|||
- 保存前自动检查Address是否存在 |
|||
- 存在则更新,不存在则添加新配置 |
|||
|
|||
### 2. 自动配置加载 |
|||
- 页面加载时显示默认配置(第一个配置) |
|||
- 输入Address时自动查询并加载对应配置 |
|||
- 支持实时配置切换 |
|||
|
|||
### 3. 简化的配置文件管理 |
|||
- 只使用 `test_client_configs.json` 存储所有配置 |
|||
- 移除了单个配置文件的管理 |
|||
- 自动创建默认配置(如果没有配置存在) |
|||
|
|||
## API接口 |
|||
|
|||
### 获取默认测试客户端配置 |
|||
``` |
|||
GET /api/testconfig/default |
|||
``` |
|||
|
|||
### 获取所有测试客户端配置 |
|||
``` |
|||
GET /api/testconfig |
|||
``` |
|||
|
|||
### 根据地址获取测试客户端配置 |
|||
``` |
|||
GET /api/testconfig/address/{address} |
|||
``` |
|||
|
|||
### 设置默认测试客户端配置 |
|||
``` |
|||
POST /api/testconfig/default |
|||
``` |
|||
|
|||
### 删除测试客户端配置 |
|||
``` |
|||
DELETE /api/testconfig/address/{address} |
|||
``` |
|||
|
|||
### 启动测试客户端 |
|||
``` |
|||
POST /api/testconfig/start |
|||
Content-Type: application/json |
|||
|
|||
{ |
|||
"address": "192.168.13.12:9001" // 可选,如果不提供则使用默认配置 |
|||
} |
|||
``` |
|||
|
|||
### 停止测试客户端 |
|||
``` |
|||
POST /api/testconfig/stop |
|||
Content-Type: application/json |
|||
|
|||
{ |
|||
"address": "192.168.13.12:9001" // 可选,如果不提供则使用默认配置 |
|||
} |
|||
``` |
|||
|
|||
## 前端功能 |
|||
|
|||
### 自动配置查询 |
|||
- 在地址输入框失去焦点时自动查询配置 |
|||
- 如果找到配置则自动填充表单 |
|||
- 如果未找到则准备创建新配置 |
|||
|
|||
### 智能保存 |
|||
- 保存时自动检查Address是否存在 |
|||
- 存在则更新现有配置 |
|||
- 不存在则创建新配置 |
|||
|
|||
## 使用示例 |
|||
|
|||
### 1. 创建新配置 |
|||
1. 在配置页面输入新的服务器地址 |
|||
2. 填写其他配置信息 |
|||
3. 点击保存,系统会自动创建新配置 |
|||
|
|||
### 2. 更新现有配置 |
|||
1. 输入已存在的服务器地址 |
|||
2. 系统会自动加载现有配置 |
|||
3. 修改配置信息 |
|||
4. 点击保存,系统会更新现有配置 |
|||
|
|||
### 3. 切换配置 |
|||
1. 在地址输入框中输入不同的地址 |
|||
2. 系统会自动加载对应的配置 |
|||
3. 可以查看和编辑不同地址的配置 |
|||
|
|||
## 技术实现 |
|||
|
|||
### 后端实现 |
|||
- `WebSocketManagerService` 简化为只管理多个配置 |
|||
- 使用 `List<ClientConfig>` 存储所有配置 |
|||
- 基于Address的唯一性检查 |
|||
- 自动文件持久化 |
|||
- 移除了单个配置的管理逻辑 |
|||
|
|||
### 前端实现 |
|||
- JavaScript自动查询功能 |
|||
- 表单自动填充 |
|||
- 实时配置切换 |
|||
|
|||
## 文件结构 |
|||
|
|||
``` |
|||
Services/ |
|||
├── WebSocketManagerService.cs # 简化的多配置管理逻辑 |
|||
Controllers/ |
|||
├── WebSocketController.cs # API接口 |
|||
Views/Home/ |
|||
├── TestClientConfig.cshtml # 配置管理页面 |
|||
``` |
|||
|
|||
## 配置文件格式 |
|||
|
|||
### test_client_configs.json |
|||
```json |
|||
[ |
|||
{ |
|||
"name": "TestClient1", |
|||
"address": "192.168.13.12:9001", |
|||
"enabled": true, |
|||
"password": "test123", |
|||
"reconnectDelay": 15000, |
|||
"ssl": false, |
|||
"readonly": false, |
|||
"mode": "ran", |
|||
"logs": { |
|||
"layers": { |
|||
"PHY": { |
|||
"level": "debug", |
|||
"filter": "debug", |
|||
"maxSize": 1000, |
|||
"payload": true |
|||
} |
|||
} |
|||
} |
|||
} |
|||
] |
|||
``` |
|||
|
|||
## 简化设计说明 |
|||
|
|||
### 移除的功能 |
|||
- 单个测试配置文件 (`test_client_config.json`) |
|||
- 单个配置的独立管理方法 |
|||
- 复杂的配置切换逻辑 |
|||
|
|||
### 保留的功能 |
|||
- 多个配置的管理 |
|||
- 基于Address的唯一性检查 |
|||
- 自动配置加载和保存 |
|||
- 默认配置的自动创建 |
|||
|
|||
## 注意事项 |
|||
|
|||
1. Address必须是唯一的,不能重复 |
|||
2. 系统会自动创建默认配置(如果没有配置存在) |
|||
3. 删除配置时使用Address作为标识 |
|||
4. 页面刷新时会加载默认配置(第一个配置) |
|||
5. 所有配置都会持久化到 `test_client_configs.json` 文件中 |
|||
6. 系统已简化为只管理多个配置,不再维护单个配置文件 |
@ -1,153 +0,0 @@ |
|||
{ |
|||
"name": "TestClient", |
|||
"address": "192.168.13.12:9001", |
|||
"enabled": true, |
|||
"password": "test123", |
|||
"reconnectDelay": 15000, |
|||
"ssl": false, |
|||
"logs": { |
|||
"layers": { |
|||
"PHY": { |
|||
"level": "info", |
|||
"maxSize": 1000, |
|||
"payload": true, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"MAC": { |
|||
"level": "warn", |
|||
"maxSize": 1000, |
|||
"payload": true, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"RLC": { |
|||
"level": "warn", |
|||
"maxSize": 1000, |
|||
"payload": false, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"PDCP": { |
|||
"level": "warn", |
|||
"maxSize": 1000, |
|||
"payload": false, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"RRC": { |
|||
"level": "debug", |
|||
"maxSize": 1000, |
|||
"payload": true, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"NAS": { |
|||
"level": "debug", |
|||
"maxSize": 1000, |
|||
"payload": true, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"S1AP": { |
|||
"level": "debug", |
|||
"maxSize": 1000, |
|||
"payload": false, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"NGAP": { |
|||
"level": "debug", |
|||
"maxSize": 1000, |
|||
"payload": false, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"GTPU": { |
|||
"level": "warn", |
|||
"maxSize": 1000, |
|||
"payload": false, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"X2AP": { |
|||
"level": "debug", |
|||
"maxSize": 1000, |
|||
"payload": false, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"XnAP": { |
|||
"level": "warn", |
|||
"maxSize": 1000, |
|||
"payload": false, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"M2AP": { |
|||
"level": "info", |
|||
"maxSize": 1000, |
|||
"payload": false, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
} |
|||
}, |
|||
"signal": null, |
|||
"cch": null, |
|||
"extensionData": null |
|||
}, |
|||
"pause": false, |
|||
"readonly": false, |
|||
"skipLogMenu": false, |
|||
"locked": false, |
|||
"active": false, |
|||
"model": null |
|||
} |
@ -0,0 +1,310 @@ |
|||
[ |
|||
{ |
|||
"name": "TestClient", |
|||
"address": "192.168.13.12:9001", |
|||
"enabled": true, |
|||
"password": "test123", |
|||
"reconnectDelay": 15000, |
|||
"ssl": false, |
|||
"logs": { |
|||
"layers": { |
|||
"PHY": { |
|||
"level": "warn", |
|||
"maxSize": 1, |
|||
"payload": false, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"MAC": { |
|||
"level": "warn", |
|||
"maxSize": 1, |
|||
"payload": false, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"RLC": { |
|||
"level": "warn", |
|||
"maxSize": 1, |
|||
"payload": false, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"PDCP": { |
|||
"level": "warn", |
|||
"maxSize": 1, |
|||
"payload": false, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"RRC": { |
|||
"level": "warn", |
|||
"maxSize": 1, |
|||
"payload": false, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"NAS": { |
|||
"level": "warn", |
|||
"maxSize": 1, |
|||
"payload": false, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"S1AP": { |
|||
"level": "warn", |
|||
"maxSize": 1, |
|||
"payload": false, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"NGAP": { |
|||
"level": "warn", |
|||
"maxSize": 1, |
|||
"payload": false, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"GTPU": { |
|||
"level": "warn", |
|||
"maxSize": 1, |
|||
"payload": false, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"X2AP": { |
|||
"level": "warn", |
|||
"maxSize": 1, |
|||
"payload": false, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"XnAP": { |
|||
"level": "warn", |
|||
"maxSize": 1, |
|||
"payload": false, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"M2AP": { |
|||
"level": "warn", |
|||
"maxSize": 1, |
|||
"payload": false, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
} |
|||
}, |
|||
"signal": null, |
|||
"cch": null, |
|||
"extensionData": null |
|||
}, |
|||
"pause": false, |
|||
"readonly": false, |
|||
"skipLogMenu": false, |
|||
"locked": false, |
|||
"active": false, |
|||
"mode": "ran", |
|||
"model": null |
|||
}, |
|||
{ |
|||
"name": "TestClient", |
|||
"address": "192.168.13.12:9002", |
|||
"enabled": true, |
|||
"password": "test123", |
|||
"reconnectDelay": 15000, |
|||
"ssl": false, |
|||
"logs": { |
|||
"layers": { |
|||
"PHY": { |
|||
"level": "warn", |
|||
"maxSize": 1, |
|||
"payload": false, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"MAC": { |
|||
"level": "warn", |
|||
"maxSize": 1, |
|||
"payload": false, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"RLC": { |
|||
"level": "warn", |
|||
"maxSize": 1, |
|||
"payload": false, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"PDCP": { |
|||
"level": "warn", |
|||
"maxSize": 1, |
|||
"payload": false, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"RRC": { |
|||
"level": "warn", |
|||
"maxSize": 1, |
|||
"payload": false, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"NAS": { |
|||
"level": "warn", |
|||
"maxSize": 1, |
|||
"payload": false, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"S1AP": { |
|||
"level": "warn", |
|||
"maxSize": 1, |
|||
"payload": false, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"NGAP": { |
|||
"level": "warn", |
|||
"maxSize": 1, |
|||
"payload": false, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"GTPU": { |
|||
"level": "warn", |
|||
"maxSize": 1, |
|||
"payload": false, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"X2AP": { |
|||
"level": "warn", |
|||
"maxSize": 1, |
|||
"payload": false, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"XnAP": { |
|||
"level": "warn", |
|||
"maxSize": 1, |
|||
"payload": false, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
}, |
|||
"M2AP": { |
|||
"level": "warn", |
|||
"maxSize": 1, |
|||
"payload": false, |
|||
"filter": "warn", |
|||
"color": "#000000", |
|||
"direction": {}, |
|||
"epc": false, |
|||
"debug": null, |
|||
"max": null |
|||
} |
|||
}, |
|||
"signal": null, |
|||
"cch": null, |
|||
"extensionData": null |
|||
}, |
|||
"pause": false, |
|||
"readonly": false, |
|||
"skipLogMenu": false, |
|||
"locked": false, |
|||
"active": false, |
|||
"mode": "ran", |
|||
"model": null |
|||
} |
|||
] |
Loading…
Reference in new issue