Browse Source

fdsaffffffffffff

feature/MultiClientLog
root 1 month ago
parent
commit
96233d82be
  1. 11
      LTEMvcApp/Controllers/HomeController.cs
  2. 248
      LTEMvcApp/Controllers/StatisticsController.cs
  3. 336
      LTEMvcApp/Models/StatisticsData.cs
  4. 3
      LTEMvcApp/Program.cs
  5. 245
      LTEMvcApp/README_Statistics_Implementation.md
  6. 230
      LTEMvcApp/Services/StatisticsService.cs
  7. 108
      LTEMvcApp/Services/WebSocketManagerService.cs
  8. 122
      LTEMvcApp/Views/Home/StatisticsTest.cshtml
  9. 3
      LTEMvcApp/Views/Shared/_Layout.cshtml
  10. 344
      LTEMvcApp/Views/Statistics/Index.cshtml

11
LTEMvcApp/Controllers/HomeController.cs

@ -286,6 +286,9 @@ public class HomeController : Controller
return View();
}
/// <summary>
/// 网络配置页面
/// </summary>
public IActionResult NetworkConfig()
{
// 获取所有测试客户端配置和状态
@ -294,4 +297,12 @@ public class HomeController : Controller
ViewBag.IpGroups = ipGroups;
return View();
}
/// <summary>
/// 统计测试页面
/// </summary>
public IActionResult StatisticsTest()
{
return View();
}
}

248
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
{
/// <summary>
/// 统计控制器
/// </summary>
public class StatisticsController : Controller
{
private readonly WebSocketManagerService _webSocketManager;
private readonly ILogger<StatisticsController> _logger;
public StatisticsController(WebSocketManagerService webSocketManager, ILogger<StatisticsController> logger)
{
_webSocketManager = webSocketManager;
_logger = logger;
}
/// <summary>
/// 统计页面
/// </summary>
public IActionResult Index()
{
var summary = _webSocketManager.GetStatisticsSummary();
ViewBag.Summary = summary;
return View();
}
/// <summary>
/// 获取所有统计数据
/// </summary>
[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 });
}
}
/// <summary>
/// 获取最新统计数据
/// </summary>
[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 });
}
}
/// <summary>
/// 获取指定客户端的统计数据
/// </summary>
[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 });
}
}
/// <summary>
/// 获取指定客户端的历史统计数据
/// </summary>
[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 });
}
}
/// <summary>
/// 获取所有客户端的最新统计数据
/// </summary>
[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 });
}
}
/// <summary>
/// 清空统计数据
/// </summary>
[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 });
}
}
/// <summary>
/// 获取统计摘要
/// </summary>
[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 });
}
}
/// <summary>
/// SSE推送 - 所有统计数据
/// </summary>
[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推送统计数据时出错");
}
}
/// <summary>
/// SSE推送 - 指定客户端统计数据
/// </summary>
[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);
}
}
/// <summary>
/// 获取统计队列信息
/// </summary>
[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 });
}
}
}
}

336
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
{
/// <summary>
/// 统计数据模型
/// </summary>
public class StatisticsData
{
/// <summary>
/// 消息类型
/// </summary>
public string Message { get; set; } = "";
/// <summary>
/// 实例ID
/// </summary>
public string InstanceId { get; set; } = "";
/// <summary>
/// CPU信息
/// </summary>
public CpuInfo Cpu { get; set; } = new CpuInfo();
/// <summary>
/// 小区信息
/// </summary>
public Dictionary<string, CellInfo> Cells { get; set; } = new Dictionary<string, CellInfo>();
/// <summary>
/// RF端口信息
/// </summary>
public Dictionary<string, RfPortInfo> RfPorts { get; set; } = new Dictionary<string, RfPortInfo>();
/// <summary>
/// 计数器信息
/// </summary>
public CountersInfo Counters { get; set; } = new CountersInfo();
/// <summary>
/// 持续时间
/// </summary>
public double Duration { get; set; }
/// <summary>
/// 消息ID
/// </summary>
public int MessageId { get; set; }
/// <summary>
/// 时间戳
/// </summary>
public double Time { get; set; }
/// <summary>
/// 接收时间
/// </summary>
public DateTime ReceivedAt { get; set; } = DateTime.Now;
/// <summary>
/// 客户端名称
/// </summary>
public string ClientName { get; set; } = "";
/// <summary>
/// 从JObject创建StatisticsData
/// </summary>
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<double>() ?? 0,
MessageId = data["message_id"]?.Value<int>() ?? 0,
Time = data["time"]?.Value<double>() ?? 0,
ClientName = clientName
};
// 解析CPU信息
if (data["cpu"] is JObject cpuObj)
{
stats.Cpu.Global = cpuObj["global"]?.Value<double>() ?? 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<double>() ?? 0,
UlBitrate = cellObj["ul_bitrate"]?.Value<double>() ?? 0,
DlTx = cellObj["dl_tx"]?.Value<double>() ?? 0,
UlTx = cellObj["ul_tx"]?.Value<double>() ?? 0,
DlRetx = cellObj["dl_retx"]?.Value<double>() ?? 0,
UlRetx = cellObj["ul_retx"]?.Value<double>() ?? 0,
DlUseMin = cellObj["dl_use_min"]?.Value<double>() ?? 0,
DlUseMax = cellObj["dl_use_max"]?.Value<double>() ?? 0,
DlUseAvg = cellObj["dl_use_avg"]?.Value<double>() ?? 0,
UlUseMin = cellObj["ul_use_min"]?.Value<double>() ?? 0,
UlUseMax = cellObj["ul_use_max"]?.Value<double>() ?? 0,
UlUseAvg = cellObj["ul_use_avg"]?.Value<double>() ?? 0,
DlSchedUsersMin = cellObj["dl_sched_users_min"]?.Value<double>() ?? 0,
DlSchedUsersMax = cellObj["dl_sched_users_max"]?.Value<double>() ?? 0,
DlSchedUsersAvg = cellObj["dl_sched_users_avg"]?.Value<double>() ?? 0,
UlSchedUsersMin = cellObj["ul_sched_users_min"]?.Value<double>() ?? 0,
UlSchedUsersMax = cellObj["ul_sched_users_max"]?.Value<double>() ?? 0,
UlSchedUsersAvg = cellObj["ul_sched_users_avg"]?.Value<double>() ?? 0,
UeCountMin = cellObj["ue_count_min"]?.Value<double>() ?? 0,
UeCountMax = cellObj["ue_count_max"]?.Value<double>() ?? 0,
UeCountAvg = cellObj["ue_count_avg"]?.Value<double>() ?? 0,
ErabCountMin = cellObj["erab_count_min"]?.Value<double>() ?? 0,
ErabCountMax = cellObj["erab_count_max"]?.Value<double>() ?? 0,
ErabCountAvg = cellObj["erab_count_avg"]?.Value<double>() ?? 0,
DlGbrUseMin = cellObj["dl_gbr_use_min"]?.Value<double>() ?? 0,
DlGbrUseMax = cellObj["dl_gbr_use_max"]?.Value<double>() ?? 0,
DlGbrUseAvg = cellObj["dl_gbr_use_avg"]?.Value<double>() ?? 0,
UlGbrUseMin = cellObj["ul_gbr_use_min"]?.Value<double>() ?? 0,
UlGbrUseMax = cellObj["ul_gbr_use_max"]?.Value<double>() ?? 0,
UlGbrUseAvg = cellObj["ul_gbr_use_avg"]?.Value<double>() ?? 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<int>() ?? 0;
}
}
if (countersObj["errors"] is JObject errorsObj)
{
foreach (var err in errorsObj)
{
cellInfo.Counters.Errors[err.Key] = err.Value?.Value<int>() ?? 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<double>() ?? 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<int>() ?? 0;
}
}
if (globalCountersObj["errors"] is JObject errorsObj)
{
foreach (var err in errorsObj)
{
stats.Counters.Errors[err.Key] = err.Value?.Value<int>() ?? 0;
}
}
}
return stats;
}
}
/// <summary>
/// CPU信息
/// </summary>
public class CpuInfo
{
public double Global { get; set; }
}
/// <summary>
/// 小区信息
/// </summary>
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();
}
/// <summary>
/// RF端口信息
/// </summary>
public class RfPortInfo
{
public Dictionary<string, double> RxtxDelay { get; set; } = new Dictionary<string, double>();
}
/// <summary>
/// 计数器信息
/// </summary>
public class CountersInfo
{
public Dictionary<string, int> Messages { get; set; } = new Dictionary<string, int>();
public Dictionary<string, int> Errors { get; set; } = new Dictionary<string, int>();
}
/// <summary>
/// 统计数据队列管理器
/// </summary>
public class StatisticsQueueManager
{
private readonly ConcurrentQueue<StatisticsData> _statsQueue = new ConcurrentQueue<StatisticsData>();
private readonly int _maxQueueSize = 1000; // 最大队列大小
private readonly object _lockObject = new object();
/// <summary>
/// 添加统计数据到队列
/// </summary>
public void EnqueueStats(StatisticsData stats)
{
lock (_lockObject)
{
_statsQueue.Enqueue(stats);
// 维持队列大小
while (_statsQueue.Count > _maxQueueSize)
{
_statsQueue.TryDequeue(out _);
}
}
}
/// <summary>
/// 获取队列中的所有统计数据
/// </summary>
public List<StatisticsData> GetAllStats()
{
lock (_lockObject)
{
return _statsQueue.ToList();
}
}
/// <summary>
/// 获取最新的统计数据
/// </summary>
public StatisticsData? GetLatestStats()
{
lock (_lockObject)
{
return _statsQueue.TryPeek(out var stats) ? stats : null;
}
}
/// <summary>
/// 清空队列
/// </summary>
public void Clear()
{
lock (_lockObject)
{
while (_statsQueue.TryDequeue(out _))
{
// 清空所有数据
}
}
}
/// <summary>
/// 获取队列大小
/// </summary>
public int Count
{
get
{
lock (_lockObject)
{
return _statsQueue.Count;
}
}
}
}
}

3
LTEMvcApp/Program.cs

@ -21,6 +21,9 @@ builder.Services.AddSingleton<WebSocketManagerService>(sp =>
sp
));
// 注册统计服务
builder.Services.AddSingleton<StatisticsService>();
var app = builder.Build();
// Configure the HTTP request pipeline.

245
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接口

230
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
{
/// <summary>
/// 统计服务 - 管理统计数据的队列和SSE推送
/// </summary>
public class StatisticsService
{
private readonly ILogger<StatisticsService> _logger;
private readonly StatisticsQueueManager _statsQueue;
private readonly ConcurrentDictionary<string, StatisticsData> _latestStatsByClient;
private readonly ConcurrentDictionary<string, List<StatisticsData>> _statsHistoryByClient;
private readonly int _maxHistorySize = 100; // 每个客户端最多保存100条历史记录
/// <summary>
/// 统计数据更新事件
/// </summary>
public event EventHandler<StatisticsData>? StatsUpdated;
/// <summary>
/// 构造函数
/// </summary>
public StatisticsService(ILogger<StatisticsService> logger)
{
_logger = logger;
_statsQueue = new StatisticsQueueManager();
_latestStatsByClient = new ConcurrentDictionary<string, StatisticsData>();
_statsHistoryByClient = new ConcurrentDictionary<string, List<StatisticsData>>();
_logger.LogInformation("StatisticsService 初始化完成");
}
/// <summary>
/// 处理接收到的统计数据
/// </summary>
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}");
}
}
/// <summary>
/// 添加统计数据到历史记录
/// </summary>
private void AddToHistory(string clientName, StatisticsData statsData)
{
if (!_statsHistoryByClient.TryGetValue(clientName, out var history))
{
history = new List<StatisticsData>();
_statsHistoryByClient[clientName] = history;
}
lock (history)
{
history.Add(statsData);
// 维持历史记录大小
while (history.Count > _maxHistorySize)
{
history.RemoveAt(0);
}
}
}
/// <summary>
/// 获取所有统计数据
/// </summary>
public List<StatisticsData> GetAllStats()
{
return _statsQueue.GetAllStats();
}
/// <summary>
/// 获取最新的统计数据
/// </summary>
public StatisticsData? GetLatestStats()
{
return _statsQueue.GetLatestStats();
}
/// <summary>
/// 获取指定客户端的最新统计数据
/// </summary>
public StatisticsData? GetLatestStatsByClient(string clientName)
{
_latestStatsByClient.TryGetValue(clientName, out var stats);
return stats;
}
/// <summary>
/// 获取指定客户端的历史统计数据
/// </summary>
public List<StatisticsData> GetStatsHistoryByClient(string clientName)
{
_statsHistoryByClient.TryGetValue(clientName, out var history);
return history ?? new List<StatisticsData>();
}
/// <summary>
/// 获取所有客户端的最新统计数据
/// </summary>
public Dictionary<string, StatisticsData> GetAllLatestStats()
{
return _latestStatsByClient.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}
/// <summary>
/// 清空统计数据
/// </summary>
public void ClearStats()
{
_statsQueue.Clear();
_latestStatsByClient.Clear();
_statsHistoryByClient.Clear();
_logger.LogInformation("统计数据已清空");
}
/// <summary>
/// 获取统计摘要信息
/// </summary>
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;
}
/// <summary>
/// 获取SSE格式的统计数据
/// </summary>
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";
}
/// <summary>
/// 获取指定客户端的SSE格式统计数据
/// </summary>
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";
}
/// <summary>
/// 获取队列大小
/// </summary>
public int GetQueueCount()
{
return _statsQueue.Count;
}
/// <summary>
/// 获取客户端数量
/// </summary>
public int GetClientCount()
{
return _latestStatsByClient.Count;
}
}
}

108
LTEMvcApp/Services/WebSocketManagerService.cs

@ -27,6 +27,7 @@ namespace LTEMvcApp.Services
private const int LogCacheSize = 10000; // 服务器最多缓存10000条最新日志
private readonly ConcurrentQueue<LTELog> _logCache = new ConcurrentQueue<LTELog>();
private readonly string _configsFilePath = "test_client_configs.json"; // 只保留多个配置文件路径
private readonly StatisticsService _statisticsService; // 添加统计服务
#endregion
@ -52,6 +53,11 @@ namespace LTEMvcApp.Services
/// </summary>
public event EventHandler<(string clientName, ClientState state)>? StateChanged;
/// <summary>
/// 统计数据更新事件
/// </summary>
public event EventHandler<StatisticsData>? StatisticsUpdated;
#endregion
#region 构造函数
@ -66,9 +72,13 @@ namespace LTEMvcApp.Services
_logger = logger;
_serviceProvider = serviceProvider;
_testClientConfigs = new List<ClientConfig>(); // 初始化测试配置列表
_statisticsService = serviceProvider.GetRequiredService<StatisticsService>(); // 通过依赖注入获取统计服务
LoadTestClientConfigs(); // 加载多个测试配置
// 订阅统计服务事件
_statisticsService.StatsUpdated += (sender, stats) => StatisticsUpdated?.Invoke(this, stats);
_logger.LogInformation("WebSocketManagerService 初始化");
}
@ -715,6 +725,98 @@ namespace LTEMvcApp.Services
}
}
#region 统计相关方法
/// <summary>
/// 获取所有统计数据
/// </summary>
public List<StatisticsData> GetAllStatistics()
{
return _statisticsService.GetAllStats();
}
/// <summary>
/// 获取最新的统计数据
/// </summary>
public StatisticsData? GetLatestStatistics()
{
return _statisticsService.GetLatestStats();
}
/// <summary>
/// 获取指定客户端的最新统计数据
/// </summary>
public StatisticsData? GetClientStatistics(string clientName)
{
return _statisticsService.GetLatestStatsByClient(clientName);
}
/// <summary>
/// 获取指定客户端的历史统计数据
/// </summary>
public List<StatisticsData> GetClientStatisticsHistory(string clientName)
{
return _statisticsService.GetStatsHistoryByClient(clientName);
}
/// <summary>
/// 获取所有客户端的最新统计数据
/// </summary>
public Dictionary<string, StatisticsData> GetAllClientStatistics()
{
return _statisticsService.GetAllLatestStats();
}
/// <summary>
/// 清空统计数据
/// </summary>
public void ClearStatistics()
{
_statisticsService.ClearStats();
}
/// <summary>
/// 获取统计摘要信息
/// </summary>
public object GetStatisticsSummary()
{
return _statisticsService.GetStatsSummary();
}
/// <summary>
/// 获取SSE格式的统计数据
/// </summary>
public string GetStatisticsAsSSE()
{
return _statisticsService.GetStatsAsSSE();
}
/// <summary>
/// 获取指定客户端的SSE格式统计数据
/// </summary>
public string GetClientStatisticsAsSSE(string clientName)
{
return _statisticsService.GetClientStatsAsSSE(clientName);
}
/// <summary>
/// 获取统计队列大小
/// </summary>
public int GetStatisticsQueueCount()
{
return _statisticsService.GetQueueCount();
}
/// <summary>
/// 获取统计客户端数量
/// </summary>
public int GetStatisticsClientCount()
{
return _statisticsService.GetClientCount();
}
#endregion
#endregion
#region 私有方法
@ -785,8 +887,10 @@ namespace LTEMvcApp.Services
/// </summary>
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

122
LTEMvcApp/Views/Home/StatisticsTest.cshtml

@ -0,0 +1,122 @@
@{
ViewData["Title"] = "统计测试";
}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">统计功能测试</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h5>API测试</h5>
<button class="btn btn-primary mb-2" onclick="testGetAllStats()">获取所有统计数据</button>
<button class="btn btn-info mb-2" onclick="testGetLatestStats()">获取最新统计数据</button>
<button class="btn btn-success mb-2" onclick="testGetSummary()">获取统计摘要</button>
<button class="btn btn-warning mb-2" onclick="testClearStats()">清空统计数据</button>
</div>
<div class="col-md-6">
<h5>SSE测试</h5>
<button class="btn btn-primary mb-2" onclick="testSSE()">测试SSE连接</button>
<button class="btn btn-danger mb-2" onclick="stopSSE()">停止SSE</button>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<h5>测试结果</h5>
<pre id="testResult" style="background-color: #f8f9fa; padding: 10px; border-radius: 5px; max-height: 400px; overflow-y: auto;"></pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
<script>
let sseConnection = null;
function logResult(message) {
const result = document.getElementById('testResult');
const timestamp = new Date().toLocaleTimeString();
result.textContent += `[${timestamp}] ${message}\n`;
result.scrollTop = result.scrollHeight;
}
function testGetAllStats() {
logResult('测试获取所有统计数据...');
$.get('/Statistics/GetAllStats', function(response) {
logResult('响应: ' + JSON.stringify(response, null, 2));
}).fail(function(xhr, status, error) {
logResult('错误: ' + error);
});
}
function testGetLatestStats() {
logResult('测试获取最新统计数据...');
$.get('/Statistics/GetLatestStats', function(response) {
logResult('响应: ' + JSON.stringify(response, null, 2));
}).fail(function(xhr, status, error) {
logResult('错误: ' + error);
});
}
function testGetSummary() {
logResult('测试获取统计摘要...');
$.get('/Statistics/GetSummary', function(response) {
logResult('响应: ' + JSON.stringify(response, null, 2));
}).fail(function(xhr, status, error) {
logResult('错误: ' + error);
});
}
function testClearStats() {
logResult('测试清空统计数据...');
$.post('/Statistics/ClearStats', function(response) {
logResult('响应: ' + JSON.stringify(response, null, 2));
}).fail(function(xhr, status, error) {
logResult('错误: ' + error);
});
}
function testSSE() {
logResult('测试SSE连接...');
if (sseConnection) {
sseConnection.close();
}
sseConnection = new EventSource('/Statistics/SSEStats');
sseConnection.onopen = function(event) {
logResult('SSE连接已建立');
};
sseConnection.onmessage = function(event) {
logResult('SSE消息: ' + event.data);
};
sseConnection.onerror = function(event) {
logResult('SSE错误: ' + JSON.stringify(event));
};
}
function stopSSE() {
logResult('停止SSE连接...');
if (sseConnection) {
sseConnection.close();
sseConnection = null;
logResult('SSE连接已关闭');
}
}
// 页面加载时清空结果
$(document).ready(function() {
document.getElementById('testResult').textContent = '';
});
</script>
}

3
LTEMvcApp/Views/Shared/_Layout.cshtml

@ -233,6 +233,9 @@
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Logs">实时日志</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Statistics" asp-action="Index">统计数据</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="TestClientConfig">测试客户端配置</a>
</li>

344
LTEMvcApp/Views/Statistics/Index.cshtml

@ -0,0 +1,344 @@
@{
ViewData["Title"] = "统计数据";
}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">LTE统计数据监控</h3>
<div class="card-tools">
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="refreshStats()">
<i class="fas fa-sync-alt"></i> 刷新
</button>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="clearStats()">
<i class="fas fa-trash"></i> 清空
</button>
<button type="button" class="btn btn-sm btn-outline-info" onclick="toggleSSE()">
<i class="fas fa-broadcast-tower"></i> <span id="sseToggleText">启动SSE</span>
</button>
</div>
</div>
<div class="card-body">
<!-- 统计摘要 -->
<div class="row mb-3">
<div class="col-md-3">
<div class="info-box">
<span class="info-box-icon bg-info"><i class="fas fa-server"></i></span>
<div class="info-box-content">
<span class="info-box-text">客户端数量</span>
<span class="info-box-number" id="clientCount">0</span>
</div>
</div>
</div>
<div class="col-md-3">
<div class="info-box">
<span class="info-box-icon bg-success"><i class="fas fa-database"></i></span>
<div class="info-box-content">
<span class="info-box-text">队列大小</span>
<span class="info-box-number" id="queueCount">0</span>
</div>
</div>
</div>
<div class="col-md-3">
<div class="info-box">
<span class="info-box-icon bg-warning"><i class="fas fa-clock"></i></span>
<div class="info-box-content">
<span class="info-box-text">最后更新</span>
<span class="info-box-number" id="lastUpdate">-</span>
</div>
</div>
</div>
<div class="col-md-3">
<div class="info-box">
<span class="info-box-icon bg-primary"><i class="fas fa-signal"></i></span>
<div class="info-box-content">
<span class="info-box-text">SSE状态</span>
<span class="info-box-number" id="sseStatus">未连接</span>
</div>
</div>
</div>
</div>
<!-- 客户端选择 -->
<div class="row mb-3">
<div class="col-md-6">
<div class="form-group">
<label for="clientSelect">选择客户端:</label>
<select class="form-control" id="clientSelect" onchange="loadClientStats()">
<option value="">所有客户端</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="refreshInterval">刷新间隔 (秒):</label>
<input type="number" class="form-control" id="refreshInterval" value="5" min="1" max="60" onchange="updateRefreshInterval()">
</div>
</div>
</div>
<!-- 统计数据表格 -->
<div class="table-responsive">
<table class="table table-bordered table-striped" id="statsTable">
<thead>
<tr>
<th>客户端</th>
<th>实例ID</th>
<th>CPU使用率</th>
<th>小区数量</th>
<th>RF端口</th>
<th>消息ID</th>
<th>持续时间</th>
<th>接收时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="statsTableBody">
<!-- 数据将通过JavaScript动态填充 -->
</tbody>
</table>
</div>
<!-- 小区详细信息 -->
<div class="row mt-4" id="cellDetails" style="display: none;">
<div class="col-12">
<div class="card">
<div class="card-header">
<h4 class="card-title">小区详细信息 - <span id="selectedClientName"></span></h4>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered table-striped" id="cellTable">
<thead>
<tr>
<th>小区ID</th>
<th>下行比特率</th>
<th>上行比特率</th>
<th>下行使用率(平均)</th>
<th>上行使用率(平均)</th>
<th>UE数量(平均)</th>
<th>ERAB数量(平均)</th>
<th>调度用户(下行)</th>
<th>调度用户(上行)</th>
</tr>
</thead>
<tbody id="cellTableBody">
<!-- 小区数据将通过JavaScript动态填充 -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
<script>
let sseConnection = null;
let refreshTimer = null;
let currentStats = {};
// 页面加载完成后初始化
$(document).ready(function() {
loadSummary();
loadAllStats();
startRefreshTimer();
});
// 加载统计摘要
function loadSummary() {
$.get('/Statistics/GetSummary', function(response) {
if (response.success) {
$('#clientCount').text(response.data.clientCount || 0);
$('#queueCount').text(response.data.totalStatsCount || 0);
if (response.data.lastUpdateTime) {
$('#lastUpdate').text(new Date(response.data.lastUpdateTime).toLocaleString());
}
}
});
}
// 加载所有统计数据
function loadAllStats() {
$.get('/Statistics/GetAllClientStats', function(response) {
if (response.success) {
currentStats = response.data;
updateStatsTable();
updateClientSelect();
}
});
}
// 更新统计表格
function updateStatsTable() {
const tbody = $('#statsTableBody');
tbody.empty();
Object.keys(currentStats).forEach(clientName => {
const stats = currentStats[clientName];
const row = `
<tr>
<td>${clientName}</td>
<td>${stats.instanceId || '-'}</td>
<td>${(stats.cpu?.global || 0).toFixed(2)}%</td>
<td>${Object.keys(stats.cells || {}).length}</td>
<td>${Object.keys(stats.rfPorts || {}).length}</td>
<td>${stats.messageId || '-'}</td>
<td>${(stats.duration || 0).toFixed(2)}s</td>
<td>${new Date(stats.receivedAt).toLocaleString()}</td>
<td>
<button class="btn btn-sm btn-info" onclick="showCellDetails('${clientName}')">
<i class="fas fa-eye"></i> 查看小区
</button>
</td>
</tr>
`;
tbody.append(row);
});
}
// 更新客户端选择下拉框
function updateClientSelect() {
const select = $('#clientSelect');
select.find('option:not(:first)').remove();
Object.keys(currentStats).forEach(clientName => {
select.append(`<option value="${clientName}">${clientName}</option>`);
});
}
// 显示小区详细信息
function showCellDetails(clientName) {
const stats = currentStats[clientName];
if (!stats || !stats.cells) return;
$('#selectedClientName').text(clientName);
const tbody = $('#cellTableBody');
tbody.empty();
Object.keys(stats.cells).forEach(cellId => {
const cell = stats.cells[cellId];
const row = `
<tr>
<td>${cellId}</td>
<td>${(cell.dlBitrate || 0).toFixed(2)}</td>
<td>${(cell.ulBitrate || 0).toFixed(2)}</td>
<td>${(cell.dlUseAvg || 0).toFixed(2)}%</td>
<td>${(cell.ulUseAvg || 0).toFixed(2)}%</td>
<td>${(cell.ueCountAvg || 0).toFixed(0)}</td>
<td>${(cell.erabCountAvg || 0).toFixed(0)}</td>
<td>${(cell.dlSchedUsersAvg || 0).toFixed(0)}</td>
<td>${(cell.ulSchedUsersAvg || 0).toFixed(0)}</td>
</tr>
`;
tbody.append(row);
});
$('#cellDetails').show();
}
// 加载指定客户端的统计数据
function loadClientStats() {
const clientName = $('#clientSelect').val();
if (!clientName) {
loadAllStats();
return;
}
$.get(`/Statistics/GetClientStats?clientName=${encodeURIComponent(clientName)}`, function(response) {
if (response.success) {
currentStats = { [clientName]: response.data };
updateStatsTable();
}
});
}
// 刷新统计数据
function refreshStats() {
loadSummary();
loadAllStats();
}
// 清空统计数据
function clearStats() {
if (confirm('确定要清空所有统计数据吗?')) {
$.post('/Statistics/ClearStats', function(response) {
if (response.success) {
alert('统计数据已清空');
refreshStats();
} else {
alert('清空失败: ' + response.message);
}
});
}
}
// 切换SSE连接
function toggleSSE() {
if (sseConnection) {
sseConnection.close();
sseConnection = null;
$('#sseToggleText').text('启动SSE');
$('#sseStatus').text('未连接').removeClass('text-success').addClass('text-danger');
} else {
startSSE();
$('#sseToggleText').text('停止SSE');
$('#sseStatus').text('已连接').removeClass('text-danger').addClass('text-success');
}
}
// 启动SSE连接
function startSSE() {
sseConnection = new EventSource('/Statistics/SSEStats');
sseConnection.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
if (data.type === 'stats_update') {
currentStats = data.data;
updateStatsTable();
loadSummary();
}
} catch (e) {
console.error('解析SSE数据失败:', e);
}
};
sseConnection.onerror = function(event) {
console.error('SSE连接错误:', event);
$('#sseStatus').text('连接错误').removeClass('text-success').addClass('text-danger');
};
}
// 启动刷新定时器
function startRefreshTimer() {
const interval = $('#refreshInterval').val() * 1000;
if (refreshTimer) {
clearInterval(refreshTimer);
}
refreshTimer = setInterval(refreshStats, interval);
}
// 更新刷新间隔
function updateRefreshInterval() {
startRefreshTimer();
}
// 页面卸载时清理资源
$(window).on('beforeunload', function() {
if (sseConnection) {
sseConnection.close();
}
if (refreshTimer) {
clearInterval(refreshTimer);
}
});
</script>
}
Loading…
Cancel
Save