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.
320 lines
9.7 KiB
320 lines
9.7 KiB
using AuroraDesk.Presentation.ViewModels.Base;
|
|
using Microsoft.Extensions.Logging;
|
|
using ReactiveUI;
|
|
using System;
|
|
using System.Collections.ObjectModel;
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
using System.Reactive;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Avalonia.Threading;
|
|
|
|
namespace AuroraDesk.Presentation.ViewModels.Pages;
|
|
|
|
/// <summary>
|
|
/// UDP 客户端页面 ViewModel
|
|
/// </summary>
|
|
public class UdpClientPageViewModel : RoutableViewModel
|
|
{
|
|
private readonly ILogger<UdpClientPageViewModel>? _logger;
|
|
private UdpClient? _udpClient;
|
|
private CancellationTokenSource? _receiveCancellationTokenSource;
|
|
private bool _isConnected;
|
|
private string _serverIp = "192.168.14.58";
|
|
private int _serverPort = 50505;
|
|
private int _localPort = 0; // 0 表示系统自动分配
|
|
private string _message = string.Empty;
|
|
private string _statusMessage = "未连接";
|
|
private readonly ObservableCollection<ReceivedMessageEntry> _receivedMessages = new();
|
|
private readonly ObservableCollection<string> _sentMessages = new();
|
|
private int _receivedMessageIndex;
|
|
private bool _isAutoScrollToBottom = true;
|
|
|
|
public UdpClientPageViewModel(
|
|
IScreen hostScreen,
|
|
ILogger<UdpClientPageViewModel>? logger = null)
|
|
: base(hostScreen, "UdpClient")
|
|
{
|
|
_logger = logger;
|
|
|
|
// 创建命令
|
|
ConnectCommand = ReactiveCommand.CreateFromTask(ConnectAsync,
|
|
this.WhenAnyValue(x => x.IsConnected, isConnected => !isConnected));
|
|
DisconnectCommand = ReactiveCommand.Create(Disconnect,
|
|
this.WhenAnyValue(x => x.IsConnected));
|
|
SendMessageCommand = ReactiveCommand.CreateFromTask(SendMessageAsync,
|
|
this.WhenAnyValue(x => x.IsConnected, x => x.Message,
|
|
(isConnected, message) => isConnected && !string.IsNullOrWhiteSpace(message)));
|
|
ClearMessagesCommand = ReactiveCommand.Create(ClearMessages);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 是否已连接
|
|
/// </summary>
|
|
public bool IsConnected
|
|
{
|
|
get => _isConnected;
|
|
set => this.RaiseAndSetIfChanged(ref _isConnected, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 服务器 IP 地址
|
|
/// </summary>
|
|
public string ServerIp
|
|
{
|
|
get => _serverIp;
|
|
set => this.RaiseAndSetIfChanged(ref _serverIp, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 服务器端口
|
|
/// </summary>
|
|
public int ServerPort
|
|
{
|
|
get => _serverPort;
|
|
set => this.RaiseAndSetIfChanged(ref _serverPort, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 本地端口
|
|
/// </summary>
|
|
public int LocalPort
|
|
{
|
|
get => _localPort;
|
|
set => this.RaiseAndSetIfChanged(ref _localPort, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 要发送的消息
|
|
/// </summary>
|
|
public string Message
|
|
{
|
|
get => _message;
|
|
set => this.RaiseAndSetIfChanged(ref _message, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 状态消息
|
|
/// </summary>
|
|
public string StatusMessage
|
|
{
|
|
get => _statusMessage;
|
|
set => this.RaiseAndSetIfChanged(ref _statusMessage, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 是否自动滚动到底部
|
|
/// </summary>
|
|
public bool IsAutoScrollToBottom
|
|
{
|
|
get => _isAutoScrollToBottom;
|
|
set => this.RaiseAndSetIfChanged(ref _isAutoScrollToBottom, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 接收到的消息列表
|
|
/// </summary>
|
|
public ObservableCollection<ReceivedMessageEntry> ReceivedMessages => _receivedMessages;
|
|
|
|
/// <summary>
|
|
/// 已发送的消息列表
|
|
/// </summary>
|
|
public ObservableCollection<string> SentMessages => _sentMessages;
|
|
|
|
/// <summary>
|
|
/// 连接命令
|
|
/// </summary>
|
|
public ReactiveCommand<Unit, Unit> ConnectCommand { get; }
|
|
|
|
/// <summary>
|
|
/// 断开连接命令
|
|
/// </summary>
|
|
public ReactiveCommand<Unit, Unit> DisconnectCommand { get; }
|
|
|
|
/// <summary>
|
|
/// 发送消息命令
|
|
/// </summary>
|
|
public ReactiveCommand<Unit, Unit> SendMessageCommand { get; }
|
|
|
|
/// <summary>
|
|
/// 清空消息命令
|
|
/// </summary>
|
|
public ReactiveCommand<Unit, Unit> ClearMessagesCommand { get; }
|
|
|
|
/// <summary>
|
|
/// 连接到服务器
|
|
/// </summary>
|
|
private async Task ConnectAsync()
|
|
{
|
|
try
|
|
{
|
|
if (!IPAddress.TryParse(ServerIp, out var ipAddress))
|
|
{
|
|
StatusMessage = $"无效的 IP 地址: {ServerIp}";
|
|
_logger?.LogWarning("无效的 IP 地址: {Ip}", ServerIp);
|
|
return;
|
|
}
|
|
|
|
if (ServerPort < 1 || ServerPort > 65535)
|
|
{
|
|
StatusMessage = $"无效的端口: {ServerPort}";
|
|
_logger?.LogWarning("无效的端口: {Port}", ServerPort);
|
|
return;
|
|
}
|
|
|
|
// 创建 UDP 客户端
|
|
_udpClient = new UdpClient(LocalPort);
|
|
_udpClient.EnableBroadcast = true;
|
|
|
|
var localEndPoint = (IPEndPoint)_udpClient.Client.LocalEndPoint!;
|
|
LocalPort = localEndPoint.Port;
|
|
|
|
IsConnected = true;
|
|
StatusMessage = $"已连接到 {ServerIp}:{ServerPort} (本地端口: {LocalPort})";
|
|
_logger?.LogInformation("UDP 客户端已连接到 {ServerIp}:{ServerPort}, 本地端口: {LocalPort}",
|
|
ServerIp, ServerPort, LocalPort);
|
|
|
|
// 启动接收任务
|
|
_receiveCancellationTokenSource = new CancellationTokenSource();
|
|
_ = Task.Run(() => ReceiveMessagesAsync(_receiveCancellationTokenSource.Token));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger?.LogError(ex, "连接失败");
|
|
StatusMessage = $"连接失败: {ex.Message}";
|
|
Disconnect();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 断开连接
|
|
/// </summary>
|
|
private void Disconnect()
|
|
{
|
|
try
|
|
{
|
|
_receiveCancellationTokenSource?.Cancel();
|
|
_receiveCancellationTokenSource?.Dispose();
|
|
_receiveCancellationTokenSource = null;
|
|
|
|
_udpClient?.Close();
|
|
_udpClient?.Dispose();
|
|
_udpClient = null;
|
|
|
|
IsConnected = false;
|
|
StatusMessage = "已断开连接";
|
|
_logger?.LogInformation("UDP 客户端已断开连接");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger?.LogError(ex, "断开连接时出错");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 发送消息
|
|
/// </summary>
|
|
private async Task SendMessageAsync()
|
|
{
|
|
if (_udpClient == null || string.IsNullOrWhiteSpace(Message))
|
|
return;
|
|
|
|
try
|
|
{
|
|
if (!IPAddress.TryParse(ServerIp, out var ipAddress))
|
|
{
|
|
StatusMessage = $"无效的 IP 地址: {ServerIp}";
|
|
return;
|
|
}
|
|
|
|
var remoteEndPoint = new IPEndPoint(ipAddress, ServerPort);
|
|
var data = Encoding.UTF8.GetBytes(Message);
|
|
|
|
await _udpClient.SendAsync(data, data.Length, remoteEndPoint);
|
|
|
|
var timestamp = DateTime.Now.ToString("HH:mm:ss");
|
|
var displayMessage = $"[{timestamp}] 发送: {Message}";
|
|
|
|
await Dispatcher.UIThread.InvokeAsync(() =>
|
|
{
|
|
SentMessages.Add(displayMessage);
|
|
});
|
|
|
|
_logger?.LogInformation("发送消息到 {ServerIp}:{ServerPort}: {Message}",
|
|
ServerIp, ServerPort, Message);
|
|
|
|
Message = string.Empty; // 清空输入框
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger?.LogError(ex, "发送消息失败");
|
|
StatusMessage = $"发送失败: {ex.Message}";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 接收消息(后台任务)
|
|
/// </summary>
|
|
private async Task ReceiveMessagesAsync(CancellationToken cancellationToken)
|
|
{
|
|
while (!cancellationToken.IsCancellationRequested && _udpClient != null)
|
|
{
|
|
try
|
|
{
|
|
var result = await _udpClient.ReceiveAsync();
|
|
var receivedData = Encoding.UTF8.GetString(result.Buffer);
|
|
var remoteEndPoint = result.RemoteEndPoint;
|
|
var timestamp = DateTime.Now.ToString("HH:mm:ss");
|
|
var displayMessage = $"[{timestamp}] 来自 {remoteEndPoint.Address}:{remoteEndPoint.Port}: {receivedData}";
|
|
|
|
var index = Interlocked.Increment(ref _receivedMessageIndex);
|
|
|
|
await Dispatcher.UIThread.InvokeAsync(() =>
|
|
{
|
|
ReceivedMessages.Add(new ReceivedMessageEntry(index, displayMessage));
|
|
});
|
|
|
|
_logger?.LogDebug("收到消息: {Message} 来自 {EndPoint}", receivedData, remoteEndPoint);
|
|
}
|
|
catch (ObjectDisposedException)
|
|
{
|
|
// UDP 客户端已关闭,正常退出
|
|
break;
|
|
}
|
|
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.Interrupted)
|
|
{
|
|
// 操作被取消,正常退出
|
|
break;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger?.LogError(ex, "接收消息时出错");
|
|
await Task.Delay(1000, cancellationToken); // 等待后重试
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 清空消息
|
|
/// </summary>
|
|
private void ClearMessages()
|
|
{
|
|
ReceivedMessages.Clear();
|
|
Interlocked.Exchange(ref _receivedMessageIndex, 0);
|
|
SentMessages.Clear();
|
|
_logger?.LogInformation("已清空消息列表");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 清理资源
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
Disconnect();
|
|
}
|
|
}
|
|
|
|
public sealed record ReceivedMessageEntry(int Index, string DisplayText);
|
|
|
|
|