You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
400 lines
17 KiB
400 lines
17 KiB
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;
|
|
using System.Linq;
|
|
|
|
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>
|
|
/// 工具方法:将address转换为合法文件名
|
|
/// </summary>
|
|
private static string SanitizeFileName(string address)
|
|
{
|
|
foreach (var c in System.IO.Path.GetInvalidFileNameChars())
|
|
{
|
|
address = address.Replace(c, '_');
|
|
}
|
|
return address;
|
|
}
|
|
|
|
/// <summary>
|
|
/// SSE推送客户端消息流
|
|
/// </summary>
|
|
[HttpGet("{address}/stream")]
|
|
public async Task StreamClientMessages(string address)
|
|
{
|
|
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(address);
|
|
if (client == null)
|
|
{
|
|
await SendSseEvent("error", new { message = "客户端未连接或不存在", address });
|
|
return;
|
|
}
|
|
|
|
await SendSseEvent("open", new {
|
|
message = "成功连接到服务器事件流",
|
|
address,
|
|
timestamp = DateTime.UtcNow
|
|
});
|
|
await Response.Body.FlushAsync(HttpContext.RequestAborted);
|
|
|
|
int lastSentCount = 0;
|
|
int lastReceivedCount = 0;
|
|
var cancellationToken = HttpContext.RequestAborted;
|
|
|
|
// 使用安全文件名
|
|
var safeAddress = SanitizeFileName(address);
|
|
var sentLogFilePath = Path.Combine(_logsDirectory, $"{safeAddress}_sent_messages.log");
|
|
var receivedLogFilePath = Path.Combine(_logsDirectory, $"{safeAddress}_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", address);
|
|
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", address);
|
|
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}", address);
|
|
await SendSseEvent("error", new {
|
|
message = "处理消息流时发生错误",
|
|
error = ex.Message,
|
|
address,
|
|
timestamp = DateTime.UtcNow
|
|
});
|
|
await Response.Body.FlushAsync(cancellationToken);
|
|
await Task.Delay(1000, cancellationToken);
|
|
}
|
|
}
|
|
await SendSseEvent("disconnected", new {
|
|
message = "客户端消息流连接已断开",
|
|
address,
|
|
timestamp = DateTime.UtcNow
|
|
});
|
|
await Response.Body.FlushAsync(cancellationToken);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
_logger.LogInformation("StreamClientMessages 连接被客户端取消,客户端: {ClientName}", address);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "StreamClientMessages 方法执行时发生未处理的异常,客户端: {ClientName}", address);
|
|
try
|
|
{
|
|
await SendSseEvent("fatal_error", new {
|
|
message = "服务器内部错误",
|
|
error = ex.Message,
|
|
address,
|
|
timestamp = DateTime.UtcNow
|
|
});
|
|
await Response.Body.FlushAsync();
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 发送消息到客户端
|
|
/// </summary>
|
|
[HttpPost("{address}/send")]
|
|
public ActionResult SendMessage(string address, [FromBody] JObject message)
|
|
{
|
|
var messageId = _webSocketManager.SendMessageToClient(address, message);
|
|
if (messageId >= 0)
|
|
return Ok(new { messageId, message = $"消息已发送到客户端 '{address}'" });
|
|
else
|
|
return BadRequest($"发送消息到客户端 '{address}' 失败");
|
|
}
|
|
|
|
/// <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),
|
|
address = 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 address)
|
|
{
|
|
try
|
|
{
|
|
if (messages == null || messages.Count == 0)
|
|
{
|
|
_logger.LogDebug("跳过空消息列表的日志写入: {MessageType}", messageType);
|
|
return;
|
|
}
|
|
|
|
var validMessages = messages.Where(msg => !string.IsNullOrWhiteSpace(msg)).ToList();
|
|
if (validMessages.Count == 0)
|
|
{
|
|
_logger.LogDebug("跳过无效消息的日志写入: {MessageType}", messageType);
|
|
return;
|
|
}
|
|
|
|
var logBuilder = new StringBuilder();
|
|
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
|
|
foreach (var message in validMessages)
|
|
{
|
|
logBuilder.AppendLine($"[{timestamp}] [{address}] [{messageType}] {message}");
|
|
logBuilder.AppendLine(new string('-', 80));
|
|
}
|
|
|
|
if (logBuilder.Length > 0)
|
|
{
|
|
await System.IO.File.AppendAllTextAsync(logFilePath, logBuilder.ToString(), Encoding.UTF8);
|
|
_logger.LogDebug("已记录 {Count} 条 {MessageType} 消息到文件: {FilePath}", validMessages.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);
|
|
}
|
|
}
|
|
}
|
|
}
|