From 96233d82befbe7f080bbcb89373edf2d6ca33f1f Mon Sep 17 00:00:00 2001 From: root <295172551@qq.com> Date: Sat, 28 Jun 2025 15:38:09 +0800 Subject: [PATCH] fdsaffffffffffff --- LTEMvcApp/Controllers/HomeController.cs | 11 + LTEMvcApp/Controllers/StatisticsController.cs | 248 +++++++++++++ LTEMvcApp/Models/StatisticsData.cs | 336 +++++++++++++++++ LTEMvcApp/Program.cs | 3 + LTEMvcApp/README_Statistics_Implementation.md | 245 +++++++++++++ LTEMvcApp/Services/StatisticsService.cs | 230 ++++++++++++ LTEMvcApp/Services/WebSocketManagerService.cs | 108 +++++- LTEMvcApp/Views/Home/StatisticsTest.cshtml | 122 +++++++ LTEMvcApp/Views/Shared/_Layout.cshtml | 3 + LTEMvcApp/Views/Statistics/Index.cshtml | 344 ++++++++++++++++++ 10 files changed, 1648 insertions(+), 2 deletions(-) create mode 100644 LTEMvcApp/Controllers/StatisticsController.cs create mode 100644 LTEMvcApp/Models/StatisticsData.cs create mode 100644 LTEMvcApp/README_Statistics_Implementation.md create mode 100644 LTEMvcApp/Services/StatisticsService.cs create mode 100644 LTEMvcApp/Views/Home/StatisticsTest.cshtml create mode 100644 LTEMvcApp/Views/Statistics/Index.cshtml diff --git a/LTEMvcApp/Controllers/HomeController.cs b/LTEMvcApp/Controllers/HomeController.cs index 997e92a..67941f8 100644 --- a/LTEMvcApp/Controllers/HomeController.cs +++ b/LTEMvcApp/Controllers/HomeController.cs @@ -286,6 +286,9 @@ public class HomeController : Controller return View(); } + /// + /// 网络配置页面 + /// public IActionResult NetworkConfig() { // 获取所有测试客户端配置和状态 @@ -294,4 +297,12 @@ public class HomeController : Controller ViewBag.IpGroups = ipGroups; return View(); } + + /// + /// 统计测试页面 + /// + public IActionResult StatisticsTest() + { + return View(); + } } diff --git a/LTEMvcApp/Controllers/StatisticsController.cs b/LTEMvcApp/Controllers/StatisticsController.cs new file mode 100644 index 0000000..5ed017b --- /dev/null +++ b/LTEMvcApp/Controllers/StatisticsController.cs @@ -0,0 +1,248 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using LTEMvcApp.Services; +using LTEMvcApp.Models; +using System.Text; +using Microsoft.AspNetCore.Http; + +namespace LTEMvcApp.Controllers +{ + /// + /// 统计控制器 + /// + public class StatisticsController : Controller + { + private readonly WebSocketManagerService _webSocketManager; + private readonly ILogger _logger; + + public StatisticsController(WebSocketManagerService webSocketManager, ILogger logger) + { + _webSocketManager = webSocketManager; + _logger = logger; + } + + /// + /// 统计页面 + /// + public IActionResult Index() + { + var summary = _webSocketManager.GetStatisticsSummary(); + ViewBag.Summary = summary; + return View(); + } + + /// + /// 获取所有统计数据 + /// + [HttpGet] + public IActionResult GetAllStats() + { + try + { + var stats = _webSocketManager.GetAllStatistics(); + return Json(new { success = true, data = stats }); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取所有统计数据时出错"); + return Json(new { success = false, message = ex.Message }); + } + } + + /// + /// 获取最新统计数据 + /// + [HttpGet] + public IActionResult GetLatestStats() + { + try + { + var stats = _webSocketManager.GetLatestStatistics(); + return Json(new { success = true, data = stats }); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取最新统计数据时出错"); + return Json(new { success = false, message = ex.Message }); + } + } + + /// + /// 获取指定客户端的统计数据 + /// + [HttpGet] + public IActionResult GetClientStats(string clientName) + { + try + { + var stats = _webSocketManager.GetClientStatistics(clientName); + if (stats == null) + { + return Json(new { success = false, message = "客户端未找到" }); + } + return Json(new { success = true, data = stats }); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取客户端统计数据时出错: {ClientName}", clientName); + return Json(new { success = false, message = ex.Message }); + } + } + + /// + /// 获取指定客户端的历史统计数据 + /// + [HttpGet] + public IActionResult GetClientStatsHistory(string clientName) + { + try + { + var stats = _webSocketManager.GetClientStatisticsHistory(clientName); + return Json(new { success = true, data = stats }); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取客户端历史统计数据时出错: {ClientName}", clientName); + return Json(new { success = false, message = ex.Message }); + } + } + + /// + /// 获取所有客户端的最新统计数据 + /// + [HttpGet] + public IActionResult GetAllClientStats() + { + try + { + var stats = _webSocketManager.GetAllClientStatistics(); + return Json(new { success = true, data = stats }); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取所有客户端统计数据时出错"); + return Json(new { success = false, message = ex.Message }); + } + } + + /// + /// 清空统计数据 + /// + [HttpPost] + public IActionResult ClearStats() + { + try + { + _webSocketManager.ClearStatistics(); + return Json(new { success = true, message = "统计数据已清空" }); + } + catch (Exception ex) + { + _logger.LogError(ex, "清空统计数据时出错"); + return Json(new { success = false, message = ex.Message }); + } + } + + /// + /// 获取统计摘要 + /// + [HttpGet] + public IActionResult GetSummary() + { + try + { + var summary = _webSocketManager.GetStatisticsSummary(); + return Json(new { success = true, data = summary }); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取统计摘要时出错"); + return Json(new { success = false, message = ex.Message }); + } + } + + /// + /// SSE推送 - 所有统计数据 + /// + [HttpGet] + public async Task SSEStats() + { + Response.Headers.Add("Content-Type", "text/event-stream"); + Response.Headers.Add("Cache-Control", "no-cache"); + Response.Headers.Add("Connection", "keep-alive"); + Response.Headers.Add("Access-Control-Allow-Origin", "*"); + + try + { + while (!HttpContext.RequestAborted.IsCancellationRequested) + { + var sseData = _webSocketManager.GetStatisticsAsSSE(); + var bytes = Encoding.UTF8.GetBytes(sseData); + await Response.Body.WriteAsync(bytes, 0, bytes.Length); + await Response.Body.FlushAsync(); + + await Task.Delay(1000); // 每秒推送一次 + } + } + catch (Exception ex) + { + _logger.LogError(ex, "SSE推送统计数据时出错"); + } + } + + /// + /// SSE推送 - 指定客户端统计数据 + /// + [HttpGet] + public async Task SSEClientStats(string clientName) + { + Response.Headers.Add("Content-Type", "text/event-stream"); + Response.Headers.Add("Cache-Control", "no-cache"); + Response.Headers.Add("Connection", "keep-alive"); + Response.Headers.Add("Access-Control-Allow-Origin", "*"); + + try + { + while (!HttpContext.RequestAborted.IsCancellationRequested) + { + var sseData = _webSocketManager.GetClientStatisticsAsSSE(clientName); + var bytes = Encoding.UTF8.GetBytes(sseData); + await Response.Body.WriteAsync(bytes, 0, bytes.Length); + await Response.Body.FlushAsync(); + + await Task.Delay(1000); // 每秒推送一次 + } + } + catch (Exception ex) + { + _logger.LogError(ex, "SSE推送客户端统计数据时出错: {ClientName}", clientName); + } + } + + /// + /// 获取统计队列信息 + /// + [HttpGet] + public IActionResult GetQueueInfo() + { + try + { + var queueCount = _webSocketManager.GetStatisticsQueueCount(); + var clientCount = _webSocketManager.GetStatisticsClientCount(); + + return Json(new { + success = true, + data = new { + queueCount = queueCount, + clientCount = clientCount + } + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取统计队列信息时出错"); + return Json(new { success = false, message = ex.Message }); + } + } + } +} \ No newline at end of file diff --git a/LTEMvcApp/Models/StatisticsData.cs b/LTEMvcApp/Models/StatisticsData.cs new file mode 100644 index 0000000..365c186 --- /dev/null +++ b/LTEMvcApp/Models/StatisticsData.cs @@ -0,0 +1,336 @@ +using System; +using System.Collections.Generic; +using System.Collections.Concurrent; +using Newtonsoft.Json.Linq; + +namespace LTEMvcApp.Models +{ + /// + /// 统计数据模型 + /// + public class StatisticsData + { + /// + /// 消息类型 + /// + public string Message { get; set; } = ""; + + /// + /// 实例ID + /// + public string InstanceId { get; set; } = ""; + + /// + /// CPU信息 + /// + public CpuInfo Cpu { get; set; } = new CpuInfo(); + + /// + /// 小区信息 + /// + public Dictionary Cells { get; set; } = new Dictionary(); + + /// + /// RF端口信息 + /// + public Dictionary RfPorts { get; set; } = new Dictionary(); + + /// + /// 计数器信息 + /// + public CountersInfo Counters { get; set; } = new CountersInfo(); + + /// + /// 持续时间 + /// + public double Duration { get; set; } + + /// + /// 消息ID + /// + public int MessageId { get; set; } + + /// + /// 时间戳 + /// + public double Time { get; set; } + + /// + /// 接收时间 + /// + public DateTime ReceivedAt { get; set; } = DateTime.Now; + + /// + /// 客户端名称 + /// + public string ClientName { get; set; } = ""; + + /// + /// 从JObject创建StatisticsData + /// + public static StatisticsData FromJObject(JObject data, string clientName) + { + var stats = new StatisticsData + { + Message = data["message"]?.ToString() ?? "", + InstanceId = data["instance_id"]?.ToString() ?? "", + Duration = data["duration"]?.Value() ?? 0, + MessageId = data["message_id"]?.Value() ?? 0, + Time = data["time"]?.Value() ?? 0, + ClientName = clientName + }; + + // 解析CPU信息 + if (data["cpu"] is JObject cpuObj) + { + stats.Cpu.Global = cpuObj["global"]?.Value() ?? 0; + } + + // 解析小区信息 + if (data["cells"] is JObject cellsObj) + { + foreach (var cell in cellsObj) + { + if (cell.Value is JObject cellObj) + { + var cellInfo = new CellInfo + { + DlBitrate = cellObj["dl_bitrate"]?.Value() ?? 0, + UlBitrate = cellObj["ul_bitrate"]?.Value() ?? 0, + DlTx = cellObj["dl_tx"]?.Value() ?? 0, + UlTx = cellObj["ul_tx"]?.Value() ?? 0, + DlRetx = cellObj["dl_retx"]?.Value() ?? 0, + UlRetx = cellObj["ul_retx"]?.Value() ?? 0, + DlUseMin = cellObj["dl_use_min"]?.Value() ?? 0, + DlUseMax = cellObj["dl_use_max"]?.Value() ?? 0, + DlUseAvg = cellObj["dl_use_avg"]?.Value() ?? 0, + UlUseMin = cellObj["ul_use_min"]?.Value() ?? 0, + UlUseMax = cellObj["ul_use_max"]?.Value() ?? 0, + UlUseAvg = cellObj["ul_use_avg"]?.Value() ?? 0, + DlSchedUsersMin = cellObj["dl_sched_users_min"]?.Value() ?? 0, + DlSchedUsersMax = cellObj["dl_sched_users_max"]?.Value() ?? 0, + DlSchedUsersAvg = cellObj["dl_sched_users_avg"]?.Value() ?? 0, + UlSchedUsersMin = cellObj["ul_sched_users_min"]?.Value() ?? 0, + UlSchedUsersMax = cellObj["ul_sched_users_max"]?.Value() ?? 0, + UlSchedUsersAvg = cellObj["ul_sched_users_avg"]?.Value() ?? 0, + UeCountMin = cellObj["ue_count_min"]?.Value() ?? 0, + UeCountMax = cellObj["ue_count_max"]?.Value() ?? 0, + UeCountAvg = cellObj["ue_count_avg"]?.Value() ?? 0, + ErabCountMin = cellObj["erab_count_min"]?.Value() ?? 0, + ErabCountMax = cellObj["erab_count_max"]?.Value() ?? 0, + ErabCountAvg = cellObj["erab_count_avg"]?.Value() ?? 0, + DlGbrUseMin = cellObj["dl_gbr_use_min"]?.Value() ?? 0, + DlGbrUseMax = cellObj["dl_gbr_use_max"]?.Value() ?? 0, + DlGbrUseAvg = cellObj["dl_gbr_use_avg"]?.Value() ?? 0, + UlGbrUseMin = cellObj["ul_gbr_use_min"]?.Value() ?? 0, + UlGbrUseMax = cellObj["ul_gbr_use_max"]?.Value() ?? 0, + UlGbrUseAvg = cellObj["ul_gbr_use_avg"]?.Value() ?? 0 + }; + + // 解析计数器 + if (cellObj["counters"] is JObject countersObj) + { + if (countersObj["messages"] is JObject messagesObj) + { + foreach (var msg in messagesObj) + { + cellInfo.Counters.Messages[msg.Key] = msg.Value?.Value() ?? 0; + } + } + if (countersObj["errors"] is JObject errorsObj) + { + foreach (var err in errorsObj) + { + cellInfo.Counters.Errors[err.Key] = err.Value?.Value() ?? 0; + } + } + } + + stats.Cells[cell.Key] = cellInfo; + } + } + } + + // 解析RF端口信息 + if (data["rf_ports"] is JObject rfPortsObj) + { + foreach (var port in rfPortsObj) + { + if (port.Value is JObject portObj) + { + var rfPortInfo = new RfPortInfo(); + if (portObj["rxtx_delay"] is JObject delayObj) + { + foreach (var delay in delayObj) + { + rfPortInfo.RxtxDelay[delay.Key] = delay.Value?.Value() ?? 0; + } + } + stats.RfPorts[port.Key] = rfPortInfo; + } + } + } + + // 解析全局计数器 + if (data["counters"] is JObject globalCountersObj) + { + if (globalCountersObj["messages"] is JObject messagesObj) + { + foreach (var msg in messagesObj) + { + stats.Counters.Messages[msg.Key] = msg.Value?.Value() ?? 0; + } + } + if (globalCountersObj["errors"] is JObject errorsObj) + { + foreach (var err in errorsObj) + { + stats.Counters.Errors[err.Key] = err.Value?.Value() ?? 0; + } + } + } + + return stats; + } + } + + /// + /// CPU信息 + /// + public class CpuInfo + { + public double Global { get; set; } + } + + /// + /// 小区信息 + /// + public class CellInfo + { + public double DlBitrate { get; set; } + public double UlBitrate { get; set; } + public double DlTx { get; set; } + public double UlTx { get; set; } + public double DlRetx { get; set; } + public double UlRetx { get; set; } + public double DlUseMin { get; set; } + public double DlUseMax { get; set; } + public double DlUseAvg { get; set; } + public double UlUseMin { get; set; } + public double UlUseMax { get; set; } + public double UlUseAvg { get; set; } + public double DlSchedUsersMin { get; set; } + public double DlSchedUsersMax { get; set; } + public double DlSchedUsersAvg { get; set; } + public double UlSchedUsersMin { get; set; } + public double UlSchedUsersMax { get; set; } + public double UlSchedUsersAvg { get; set; } + public double UeCountMin { get; set; } + public double UeCountMax { get; set; } + public double UeCountAvg { get; set; } + public double ErabCountMin { get; set; } + public double ErabCountMax { get; set; } + public double ErabCountAvg { get; set; } + public double DlGbrUseMin { get; set; } + public double DlGbrUseMax { get; set; } + public double DlGbrUseAvg { get; set; } + public double UlGbrUseMin { get; set; } + public double UlGbrUseMax { get; set; } + public double UlGbrUseAvg { get; set; } + public CountersInfo Counters { get; set; } = new CountersInfo(); + } + + /// + /// RF端口信息 + /// + public class RfPortInfo + { + public Dictionary RxtxDelay { get; set; } = new Dictionary(); + } + + /// + /// 计数器信息 + /// + public class CountersInfo + { + public Dictionary Messages { get; set; } = new Dictionary(); + public Dictionary Errors { get; set; } = new Dictionary(); + } + + /// + /// 统计数据队列管理器 + /// + public class StatisticsQueueManager + { + private readonly ConcurrentQueue _statsQueue = new ConcurrentQueue(); + private readonly int _maxQueueSize = 1000; // 最大队列大小 + private readonly object _lockObject = new object(); + + /// + /// 添加统计数据到队列 + /// + public void EnqueueStats(StatisticsData stats) + { + lock (_lockObject) + { + _statsQueue.Enqueue(stats); + + // 维持队列大小 + while (_statsQueue.Count > _maxQueueSize) + { + _statsQueue.TryDequeue(out _); + } + } + } + + /// + /// 获取队列中的所有统计数据 + /// + public List GetAllStats() + { + lock (_lockObject) + { + return _statsQueue.ToList(); + } + } + + /// + /// 获取最新的统计数据 + /// + public StatisticsData? GetLatestStats() + { + lock (_lockObject) + { + return _statsQueue.TryPeek(out var stats) ? stats : null; + } + } + + /// + /// 清空队列 + /// + public void Clear() + { + lock (_lockObject) + { + while (_statsQueue.TryDequeue(out _)) + { + // 清空所有数据 + } + } + } + + /// + /// 获取队列大小 + /// + public int Count + { + get + { + lock (_lockObject) + { + return _statsQueue.Count; + } + } + } + } +} \ No newline at end of file diff --git a/LTEMvcApp/Program.cs b/LTEMvcApp/Program.cs index e5be280..98b6e9f 100644 --- a/LTEMvcApp/Program.cs +++ b/LTEMvcApp/Program.cs @@ -21,6 +21,9 @@ builder.Services.AddSingleton(sp => sp )); +// 注册统计服务 +builder.Services.AddSingleton(); + var app = builder.Build(); // Configure the HTTP request pipeline. diff --git a/LTEMvcApp/README_Statistics_Implementation.md b/LTEMvcApp/README_Statistics_Implementation.md new file mode 100644 index 0000000..f8a4cac --- /dev/null +++ b/LTEMvcApp/README_Statistics_Implementation.md @@ -0,0 +1,245 @@ +# LTE统计数据监控系统实现 + +## 概述 + +本系统实现了对LTE客户端统计数据的完整监控功能,包括数据队列管理、实时显示和SSE推送。 + +## 功能特性 + +### 1. 数据结构模型 +- **StatisticsData**: 主要统计数据模型 +- **CellInfo**: 小区信息模型 +- **CpuInfo**: CPU信息模型 +- **RfPortInfo**: RF端口信息模型 +- **CountersInfo**: 计数器信息模型 + +### 2. 数据队列管理 +- **StatisticsQueueManager**: 线程安全的统计数据队列 +- 支持最大队列大小限制(默认1000条) +- 自动维护队列大小,防止内存溢出 + +### 3. 统计服务 +- **StatisticsService**: 核心统计服务 +- 处理WebSocket接收的统计数据 +- 管理客户端历史记录 +- 提供SSE格式数据推送 + +### 4. WebSocket集成 +- 在**WebSocketManagerService**中集成统计服务 +- 自动处理`OnStatsReceived`事件 +- 支持多客户端统计数据管理 + +### 5. API接口 +- **StatisticsController**: 提供完整的REST API +- 支持获取所有统计数据 +- 支持获取指定客户端数据 +- 支持清空统计数据 +- 支持SSE实时推送 + +### 6. 前端界面 +- 响应式统计数据显示页面 +- 实时数据更新 +- 小区详细信息查看 +- SSE连接状态监控 + +## 数据结构说明 + +### 统计数据格式 +```json +{ + "message": "stats", + "instance_id": "685f9485", + "cpu": { + "global": 328.32038445519004 + }, + "cells": { + "1": { + "dl_bitrate": 0, + "ul_bitrate": 0, + "dl_use_avg": 0.003762019146681548, + "ul_use_avg": 0.08999999798834324, + "ue_count_avg": 0, + "erab_count_avg": 0, + "counters": { + "messages": {}, + "errors": {} + } + } + }, + "rf_ports": { + "0": { + "rxtx_delay": {} + } + }, + "counters": { + "messages": { + "s1_setup_request": 2, + "ng_setup_request": 2 + }, + "errors": {} + }, + "duration": 1.04, + "message_id": 33, + "time": 146.155 +} +``` + +## API接口说明 + +### 1. 获取所有统计数据 +``` +GET /Statistics/GetAllStats +``` + +### 2. 获取最新统计数据 +``` +GET /Statistics/GetLatestStats +``` + +### 3. 获取指定客户端统计数据 +``` +GET /Statistics/GetClientStats?clientName={clientName} +``` + +### 4. 获取客户端历史数据 +``` +GET /Statistics/GetClientStatsHistory?clientName={clientName} +``` + +### 5. 获取所有客户端最新数据 +``` +GET /Statistics/GetAllClientStats +``` + +### 6. 清空统计数据 +``` +POST /Statistics/ClearStats +``` + +### 7. 获取统计摘要 +``` +GET /Statistics/GetSummary +``` + +### 8. SSE实时推送 +``` +GET /Statistics/SSEStats +``` + +### 9. 指定客户端SSE推送 +``` +GET /Statistics/SSEClientStats?clientName={clientName} +``` + +## 使用说明 + +### 1. 启动统计监控 +1. 访问 `/Statistics/Index` 页面 +2. 点击"启动SSE"按钮开始实时监控 +3. 统计数据会自动更新显示 + +### 2. 查看小区详情 +1. 在统计表格中点击"查看小区"按钮 +2. 系统会显示该客户端的所有小区详细信息 + +### 3. 测试功能 +1. 访问 `/Home/StatisticsTest` 页面 +2. 使用各种测试按钮验证功能 + +### 4. API测试 +可以使用Postman或其他工具测试API接口: +```bash +# 获取统计摘要 +curl http://localhost:15001/Statistics/GetSummary + +# 获取所有统计数据 +curl http://localhost:15001/Statistics/GetAllStats + +# 清空统计数据 +curl -X POST http://localhost:15001/Statistics/ClearStats +``` + +## 技术实现 + +### 1. 线程安全 +- 使用`ConcurrentDictionary`和`ConcurrentQueue` +- 所有数据操作都是线程安全的 + +### 2. 内存管理 +- 自动限制队列大小 +- 定期清理历史数据 +- 防止内存泄漏 + +### 3. 实时推送 +- 使用Server-Sent Events (SSE) +- 支持多客户端同时连接 +- 自动重连机制 + +### 4. 错误处理 +- 完整的异常捕获和日志记录 +- 优雅的错误恢复机制 + +## 配置说明 + +### 1. 队列大小配置 +在`StatisticsQueueManager`中可以调整: +```csharp +private readonly int _maxQueueSize = 1000; // 最大队列大小 +``` + +### 2. 历史记录大小 +在`StatisticsService`中可以调整: +```csharp +private readonly int _maxHistorySize = 100; // 每个客户端最多保存100条历史记录 +``` + +### 3. SSE推送间隔 +在`StatisticsController`中可以调整: +```csharp +await Task.Delay(1000); // 每秒推送一次 +``` + +## 扩展功能 + +### 1. 数据持久化 +可以添加数据库存储功能,将统计数据保存到数据库。 + +### 2. 图表显示 +可以集成Chart.js等图表库,显示统计数据的趋势图。 + +### 3. 告警功能 +可以添加阈值告警功能,当某些指标超过阈值时发送通知。 + +### 4. 数据导出 +可以添加数据导出功能,支持CSV、Excel等格式。 + +## 注意事项 + +1. **性能考虑**: 大量数据时注意内存使用 +2. **网络带宽**: SSE推送会占用一定带宽 +3. **浏览器兼容性**: 确保浏览器支持SSE +4. **数据准确性**: 统计数据可能有延迟,注意时间同步 + +## 故障排除 + +### 1. SSE连接失败 +- 检查浏览器是否支持SSE +- 检查网络连接 +- 查看服务器日志 + +### 2. 数据不更新 +- 检查WebSocket连接状态 +- 确认客户端正在发送统计数据 +- 查看浏览器控制台错误 + +### 3. 内存使用过高 +- 调整队列大小限制 +- 检查是否有内存泄漏 +- 重启应用程序 + +## 更新日志 + +- **v1.0**: 初始版本,实现基本统计功能 +- 支持多客户端数据管理 +- 实现SSE实时推送 +- 添加完整的API接口 \ No newline at end of file diff --git a/LTEMvcApp/Services/StatisticsService.cs b/LTEMvcApp/Services/StatisticsService.cs new file mode 100644 index 0000000..8cee246 --- /dev/null +++ b/LTEMvcApp/Services/StatisticsService.cs @@ -0,0 +1,230 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using LTEMvcApp.Models; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; +using System.Text; + +namespace LTEMvcApp.Services +{ + /// + /// 统计服务 - 管理统计数据的队列和SSE推送 + /// + public class StatisticsService + { + private readonly ILogger _logger; + private readonly StatisticsQueueManager _statsQueue; + private readonly ConcurrentDictionary _latestStatsByClient; + private readonly ConcurrentDictionary> _statsHistoryByClient; + private readonly int _maxHistorySize = 100; // 每个客户端最多保存100条历史记录 + + /// + /// 统计数据更新事件 + /// + public event EventHandler? StatsUpdated; + + /// + /// 构造函数 + /// + public StatisticsService(ILogger logger) + { + _logger = logger; + _statsQueue = new StatisticsQueueManager(); + _latestStatsByClient = new ConcurrentDictionary(); + _statsHistoryByClient = new ConcurrentDictionary>(); + + _logger.LogInformation("StatisticsService 初始化完成"); + } + + /// + /// 处理接收到的统计数据 + /// + public void ProcessStatsData(JObject data, string clientName) + { + try + { + var statsData = StatisticsData.FromJObject(data, clientName); + + // 添加到队列 + _statsQueue.EnqueueStats(statsData); + + // 更新最新统计数据 + _latestStatsByClient[clientName] = statsData; + + // 添加到历史记录 + AddToHistory(clientName, statsData); + + // 触发事件 + StatsUpdated?.Invoke(this, statsData); + + _logger.LogDebug($"处理统计数据: 客户端 {clientName}, 消息ID {statsData.MessageId}, 小区数量 {statsData.Cells.Count}"); + } + catch (Exception ex) + { + _logger.LogError(ex, $"处理统计数据时出错: 客户端 {clientName}"); + } + } + + /// + /// 添加统计数据到历史记录 + /// + private void AddToHistory(string clientName, StatisticsData statsData) + { + if (!_statsHistoryByClient.TryGetValue(clientName, out var history)) + { + history = new List(); + _statsHistoryByClient[clientName] = history; + } + + lock (history) + { + history.Add(statsData); + + // 维持历史记录大小 + while (history.Count > _maxHistorySize) + { + history.RemoveAt(0); + } + } + } + + /// + /// 获取所有统计数据 + /// + public List GetAllStats() + { + return _statsQueue.GetAllStats(); + } + + /// + /// 获取最新的统计数据 + /// + public StatisticsData? GetLatestStats() + { + return _statsQueue.GetLatestStats(); + } + + /// + /// 获取指定客户端的最新统计数据 + /// + public StatisticsData? GetLatestStatsByClient(string clientName) + { + _latestStatsByClient.TryGetValue(clientName, out var stats); + return stats; + } + + /// + /// 获取指定客户端的历史统计数据 + /// + public List GetStatsHistoryByClient(string clientName) + { + _statsHistoryByClient.TryGetValue(clientName, out var history); + return history ?? new List(); + } + + /// + /// 获取所有客户端的最新统计数据 + /// + public Dictionary GetAllLatestStats() + { + return _latestStatsByClient.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + } + + /// + /// 清空统计数据 + /// + public void ClearStats() + { + _statsQueue.Clear(); + _latestStatsByClient.Clear(); + _statsHistoryByClient.Clear(); + _logger.LogInformation("统计数据已清空"); + } + + /// + /// 获取统计摘要信息 + /// + public object GetStatsSummary() + { + var summary = new + { + TotalStatsCount = _statsQueue.Count, + ClientCount = _latestStatsByClient.Count, + Clients = _latestStatsByClient.Keys.ToList(), + LastUpdateTime = _latestStatsByClient.Values + .OrderByDescending(s => s.ReceivedAt) + .FirstOrDefault()?.ReceivedAt + }; + + return summary; + } + + /// + /// 获取SSE格式的统计数据 + /// + public string GetStatsAsSSE() + { + var latestStats = GetAllLatestStats(); + var sseData = new + { + type = "stats_update", + timestamp = DateTime.UtcNow, + data = latestStats + }; + + var json = System.Text.Json.JsonSerializer.Serialize(sseData, new System.Text.Json.JsonSerializerOptions + { + PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase + }); + + return $"data: {json}\n\n"; + } + + /// + /// 获取指定客户端的SSE格式统计数据 + /// + public string GetClientStatsAsSSE(string clientName) + { + var stats = GetLatestStatsByClient(clientName); + if (stats == null) + { + return $"data: {System.Text.Json.JsonSerializer.Serialize(new { type = "error", message = "Client not found" })}\n\n"; + } + + var sseData = new + { + type = "client_stats_update", + clientName = clientName, + timestamp = DateTime.UtcNow, + data = stats + }; + + var json = System.Text.Json.JsonSerializer.Serialize(sseData, new System.Text.Json.JsonSerializerOptions + { + PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase + }); + + return $"data: {json}\n\n"; + } + + /// + /// 获取队列大小 + /// + public int GetQueueCount() + { + return _statsQueue.Count; + } + + /// + /// 获取客户端数量 + /// + public int GetClientCount() + { + return _latestStatsByClient.Count; + } + } +} \ No newline at end of file diff --git a/LTEMvcApp/Services/WebSocketManagerService.cs b/LTEMvcApp/Services/WebSocketManagerService.cs index 9a2c1cd..5564a8f 100644 --- a/LTEMvcApp/Services/WebSocketManagerService.cs +++ b/LTEMvcApp/Services/WebSocketManagerService.cs @@ -27,6 +27,7 @@ namespace LTEMvcApp.Services private const int LogCacheSize = 10000; // 服务器最多缓存10000条最新日志 private readonly ConcurrentQueue _logCache = new ConcurrentQueue(); private readonly string _configsFilePath = "test_client_configs.json"; // 只保留多个配置文件路径 + private readonly StatisticsService _statisticsService; // 添加统计服务 #endregion @@ -52,6 +53,11 @@ namespace LTEMvcApp.Services /// public event EventHandler<(string clientName, ClientState state)>? StateChanged; + /// + /// 统计数据更新事件 + /// + public event EventHandler? StatisticsUpdated; + #endregion #region 构造函数 @@ -66,9 +72,13 @@ namespace LTEMvcApp.Services _logger = logger; _serviceProvider = serviceProvider; _testClientConfigs = new List(); // 初始化测试配置列表 + _statisticsService = serviceProvider.GetRequiredService(); // 通过依赖注入获取统计服务 LoadTestClientConfigs(); // 加载多个测试配置 + // 订阅统计服务事件 + _statisticsService.StatsUpdated += (sender, stats) => StatisticsUpdated?.Invoke(this, stats); + _logger.LogInformation("WebSocketManagerService 初始化"); } @@ -715,6 +725,98 @@ namespace LTEMvcApp.Services } } + #region 统计相关方法 + + /// + /// 获取所有统计数据 + /// + public List GetAllStatistics() + { + return _statisticsService.GetAllStats(); + } + + /// + /// 获取最新的统计数据 + /// + public StatisticsData? GetLatestStatistics() + { + return _statisticsService.GetLatestStats(); + } + + /// + /// 获取指定客户端的最新统计数据 + /// + public StatisticsData? GetClientStatistics(string clientName) + { + return _statisticsService.GetLatestStatsByClient(clientName); + } + + /// + /// 获取指定客户端的历史统计数据 + /// + public List GetClientStatisticsHistory(string clientName) + { + return _statisticsService.GetStatsHistoryByClient(clientName); + } + + /// + /// 获取所有客户端的最新统计数据 + /// + public Dictionary GetAllClientStatistics() + { + return _statisticsService.GetAllLatestStats(); + } + + /// + /// 清空统计数据 + /// + public void ClearStatistics() + { + _statisticsService.ClearStats(); + } + + /// + /// 获取统计摘要信息 + /// + public object GetStatisticsSummary() + { + return _statisticsService.GetStatsSummary(); + } + + /// + /// 获取SSE格式的统计数据 + /// + public string GetStatisticsAsSSE() + { + return _statisticsService.GetStatsAsSSE(); + } + + /// + /// 获取指定客户端的SSE格式统计数据 + /// + public string GetClientStatisticsAsSSE(string clientName) + { + return _statisticsService.GetClientStatsAsSSE(clientName); + } + + /// + /// 获取统计队列大小 + /// + public int GetStatisticsQueueCount() + { + return _statisticsService.GetQueueCount(); + } + + /// + /// 获取统计客户端数量 + /// + public int GetStatisticsClientCount() + { + return _statisticsService.GetClientCount(); + } + + #endregion + #endregion #region 私有方法 @@ -785,8 +887,10 @@ namespace LTEMvcApp.Services /// private void OnStatsReceived(string clientName, JObject data) { - _logger.LogInformation($"客户端 {clientName} 状态变更: {data.ToString()}"); - //StateChanged?.Invoke(this, (clientName, state)); + _logger.LogInformation($"客户端 {clientName} 收到统计数据: {data.ToString()}"); + + // 处理统计数据 + _statisticsService.ProcessStatsData(data, clientName); } #endregion diff --git a/LTEMvcApp/Views/Home/StatisticsTest.cshtml b/LTEMvcApp/Views/Home/StatisticsTest.cshtml new file mode 100644 index 0000000..5f3c4e6 --- /dev/null +++ b/LTEMvcApp/Views/Home/StatisticsTest.cshtml @@ -0,0 +1,122 @@ +@{ + ViewData["Title"] = "统计测试"; +} + +
+
+
+
+
+

统计功能测试

+
+
+
+
+
API测试
+ + + + +
+
+
SSE测试
+ + +
+
+ +
+
+
测试结果
+

+                        
+
+
+
+
+
+
+ +@section Scripts { + +} \ No newline at end of file diff --git a/LTEMvcApp/Views/Shared/_Layout.cshtml b/LTEMvcApp/Views/Shared/_Layout.cshtml index b58a200..af3ca29 100644 --- a/LTEMvcApp/Views/Shared/_Layout.cshtml +++ b/LTEMvcApp/Views/Shared/_Layout.cshtml @@ -233,6 +233,9 @@ + diff --git a/LTEMvcApp/Views/Statistics/Index.cshtml b/LTEMvcApp/Views/Statistics/Index.cshtml new file mode 100644 index 0000000..c787f0d --- /dev/null +++ b/LTEMvcApp/Views/Statistics/Index.cshtml @@ -0,0 +1,344 @@ +@{ + ViewData["Title"] = "统计数据"; +} + +
+
+
+
+
+

LTE统计数据监控

+
+ + + +
+
+
+ +
+
+
+ +
+ 客户端数量 + 0 +
+
+
+
+
+ +
+ 队列大小 + 0 +
+
+
+
+
+ +
+ 最后更新 + - +
+
+
+
+
+ +
+ SSE状态 + 未连接 +
+
+
+
+ + +
+
+
+ + +
+
+
+
+ + +
+
+
+ + +
+ + + + + + + + + + + + + + + + + +
客户端实例IDCPU使用率小区数量RF端口消息ID持续时间接收时间操作
+
+ + + +
+
+
+
+
+ +@section Scripts { + +} \ No newline at end of file