468 lines
14 KiB
C#
468 lines
14 KiB
C#
using System.Text.Json;
|
||
|
||
using Microsoft.AspNetCore.SignalR.Client;
|
||
|
||
using ShengShengBuXi.ConsoleApp.Models;
|
||
|
||
namespace ShengShengBuXi.ConsoleApp.Services;
|
||
|
||
/// <summary>
|
||
/// SignalR客户端服务实现
|
||
/// </summary>
|
||
public class SignalRService : ISignalRService
|
||
{
|
||
private HubConnection? _hubConnection;
|
||
private readonly string _configBackupPath;
|
||
private PhoneBoothConfig _currentConfig;
|
||
private readonly SemaphoreSlim _semaphore = new(1, 1);
|
||
private string? _clientId;
|
||
private bool _isInitialConnection = true;
|
||
private const int MAX_RETRY_COUNT = 15;
|
||
private const int RETRY_INTERVAL_MS = 200;
|
||
private const int HEARTBEAT_INTERVAL_MS = 3000; // 3秒发送一次心跳
|
||
private Timer? _heartbeatTimer;
|
||
private DateTime _lastHeartbeatTime = DateTime.MinValue;
|
||
private const int HEARTBEAT_TIMEOUT_MS = 60000; // 60秒没有心跳就认为断开
|
||
|
||
/// <summary>
|
||
/// 连接状态改变事件
|
||
/// </summary>
|
||
public event EventHandler<bool>? ConnectionStateChanged;
|
||
|
||
/// <summary>
|
||
/// 配置更新事件
|
||
/// </summary>
|
||
public event EventHandler<PhoneBoothConfig>? ConfigUpdated;
|
||
|
||
/// <summary>
|
||
/// 音频文件更新事件
|
||
/// </summary>
|
||
public event EventHandler<(string FileName, byte[] FileData)>? AudioFileUpdated;
|
||
|
||
/// <summary>
|
||
/// 获取当前是否已连接
|
||
/// </summary>
|
||
public bool IsConnected => _hubConnection?.State == HubConnectionState.Connected;
|
||
|
||
/// <summary>
|
||
/// 构造函数
|
||
/// </summary>
|
||
/// <param name="configBackupPath">配置备份文件路径</param>
|
||
public SignalRService(string configBackupPath = "config.json")
|
||
{
|
||
_configBackupPath = configBackupPath;
|
||
|
||
// 加载本地配置
|
||
_currentConfig = PhoneBoothConfig.LoadFromFile(_configBackupPath);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 启动连接
|
||
/// </summary>
|
||
/// <param name="hubUrl">SignalR Hub URL</param>
|
||
public async Task StartConnectionAsync(string hubUrl)
|
||
{
|
||
if (_hubConnection != null)
|
||
{
|
||
try
|
||
{
|
||
await _hubConnection.StopAsync();
|
||
await _hubConnection.DisposeAsync();
|
||
_hubConnection = null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"停止旧连接失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
_hubConnection = new HubConnectionBuilder()
|
||
.WithUrl(hubUrl)
|
||
.WithAutomaticReconnect()
|
||
.Build();
|
||
|
||
// 启动心跳定时器
|
||
_heartbeatTimer?.Dispose();
|
||
_heartbeatTimer = new Timer(CheckHeartbeat, null, HEARTBEAT_INTERVAL_MS, HEARTBEAT_INTERVAL_MS);
|
||
_lastHeartbeatTime = DateTime.Now;
|
||
|
||
// 注册连接关闭事件
|
||
_hubConnection.Closed += async (error) =>
|
||
{
|
||
Console.WriteLine($"SignalR连接已关闭: {error?.Message ?? "未知原因"}");
|
||
|
||
if (_isInitialConnection)
|
||
{
|
||
// 初始连接失败,尝试重连
|
||
int retryCount = 0;
|
||
while (retryCount < MAX_RETRY_COUNT)
|
||
{
|
||
try
|
||
{
|
||
Console.WriteLine($"尝试重新连接服务器 (第{retryCount + 1}次)...");
|
||
await _hubConnection.StartAsync();
|
||
Console.WriteLine("重新连接成功");
|
||
ConnectionStateChanged?.Invoke(this, true);
|
||
return;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
retryCount++;
|
||
Console.WriteLine($"重连失败: {ex.Message}");
|
||
if (retryCount >= MAX_RETRY_COUNT)
|
||
{
|
||
Console.WriteLine($"达到最大重试次数({MAX_RETRY_COUNT}),程序将退出");
|
||
Environment.Exit(1);
|
||
}
|
||
await Task.Delay(RETRY_INTERVAL_MS);
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 运行时的连接断开,直接退出程序
|
||
Console.WriteLine("SignalR连接断开,程序将退出");
|
||
Environment.Exit(1);
|
||
}
|
||
};
|
||
|
||
// 注册连接恢复事件
|
||
_hubConnection.Reconnected += connectionId =>
|
||
{
|
||
_isInitialConnection = false;
|
||
ConnectionStateChanged?.Invoke(this, true);
|
||
return Task.CompletedTask;
|
||
};
|
||
|
||
// 注册接收配置更新事件
|
||
_hubConnection.On<string>("ReceiveConfigUpdate", async (configJson) =>
|
||
{
|
||
try
|
||
{
|
||
var config = JsonSerializer.Deserialize<PhoneBoothConfig>(configJson);
|
||
if (config != null)
|
||
{
|
||
await UpdateConfigAsync(config);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"处理配置更新失败: {ex.Message}");
|
||
}
|
||
});
|
||
|
||
// 注册接收音频文件更新事件
|
||
_hubConnection.On<string, byte[]>("ReceiveAudioFile", (fileName, fileData) =>
|
||
{
|
||
try
|
||
{
|
||
AudioFileUpdated?.Invoke(this, (fileName, fileData));
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"处理音频文件更新失败: {ex.Message}");
|
||
}
|
||
});
|
||
|
||
// 注册客户端ID确认事件
|
||
_hubConnection.On<string>("RegistrationConfirmed", (clientId) =>
|
||
{
|
||
_clientId = clientId;
|
||
Console.WriteLine($"客户端注册成功,ID: {clientId}");
|
||
});
|
||
|
||
try
|
||
{
|
||
await _hubConnection.StartAsync();
|
||
_isInitialConnection = false;
|
||
ConnectionStateChanged?.Invoke(this, true);
|
||
|
||
// 连接成功后,注册为控制器客户端
|
||
await RegisterAsControllerAsync();
|
||
|
||
// 连接成功后,尝试获取最新配置
|
||
await RequestLatestConfigAsync();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"SignalR连接失败: {ex.Message}");
|
||
ConnectionStateChanged?.Invoke(this, false);
|
||
try
|
||
{
|
||
await _hubConnection.StopAsync();
|
||
await _hubConnection.DisposeAsync();
|
||
_hubConnection = null;
|
||
}
|
||
catch { /* 忽略清理时的错误 */ }
|
||
throw; // 抛出异常,让调用者处理
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查心跳
|
||
/// </summary>
|
||
private async void CheckHeartbeat(object? state)
|
||
{
|
||
try
|
||
{
|
||
if (_hubConnection == null || _hubConnection.State != HubConnectionState.Connected)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// 检查上次心跳时间是否超时
|
||
if ((DateTime.Now - _lastHeartbeatTime).TotalMilliseconds > HEARTBEAT_TIMEOUT_MS)
|
||
{
|
||
Console.WriteLine("心跳超时,连接可能已断开");
|
||
await _hubConnection.StopAsync();
|
||
return;
|
||
}
|
||
|
||
// 发送心跳
|
||
await RequestLatestConfigAsync();
|
||
_lastHeartbeatTime = DateTime.Now;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"心跳检测失败: {ex.Message}");
|
||
if (_hubConnection != null)
|
||
{
|
||
await _hubConnection.StopAsync();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 注册为控制器客户端
|
||
/// </summary>
|
||
private async Task RegisterAsControllerAsync()
|
||
{
|
||
if (!IsConnected || _hubConnection == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
await _hubConnection.InvokeAsync("RegisterClient", ClientType.Controller, "电话亭控制器");
|
||
Console.WriteLine("已向服务器注册为控制器客户端");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"注册客户端失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 停止连接
|
||
/// </summary>
|
||
public async Task StopConnectionAsync()
|
||
{
|
||
if (_hubConnection != null)
|
||
{
|
||
try
|
||
{
|
||
// 如果有活动的音频流,先结束它
|
||
await EndAudioStreamAsync();
|
||
}
|
||
catch { /* 忽略错误 */ }
|
||
|
||
await _hubConnection.StopAsync();
|
||
await _hubConnection.DisposeAsync();
|
||
_hubConnection = null;
|
||
_clientId = null;
|
||
ConnectionStateChanged?.Invoke(this, false);
|
||
}
|
||
|
||
// 停止心跳定时器
|
||
_heartbeatTimer?.Dispose();
|
||
_heartbeatTimer = null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取最新配置
|
||
/// </summary>
|
||
public async Task<PhoneBoothConfig> GetConfigAsync()
|
||
{
|
||
await _semaphore.WaitAsync();
|
||
try
|
||
{
|
||
// 如果连接正常,尝试从服务器获取最新配置
|
||
if (IsConnected)
|
||
{
|
||
await RequestLatestConfigAsync();
|
||
}
|
||
|
||
return _currentConfig;
|
||
}
|
||
finally
|
||
{
|
||
_semaphore.Release();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 上传录音文件
|
||
/// </summary>
|
||
/// <param name="filePath">文件路径</param>
|
||
public async Task<bool> UploadRecordingAsync(string filePath)
|
||
{
|
||
// 检查配置是否允许上传录音文件
|
||
if (!_currentConfig.Recording.UploadRecordingToServer)
|
||
{
|
||
Console.WriteLine("根据配置设置,不上传录音文件");
|
||
return false;
|
||
}
|
||
|
||
if (!IsConnected || _hubConnection == null)
|
||
{
|
||
Console.WriteLine("无法上传录音文件:未连接到服务器");
|
||
return false;
|
||
}
|
||
|
||
try
|
||
{
|
||
if (!File.Exists(filePath))
|
||
{
|
||
Console.WriteLine($"无法上传录音文件:文件不存在 {filePath}");
|
||
return false;
|
||
}
|
||
|
||
string fileName = Path.GetFileName(filePath);
|
||
byte[] fileData = await File.ReadAllBytesAsync(filePath);
|
||
|
||
Console.WriteLine($"开始上传录音文件: {fileName}, 大小: {fileData.Length/1024} KB");
|
||
await _hubConnection.InvokeAsync("UploadRecording", fileName, fileData);
|
||
Console.WriteLine($"录音文件上传成功: {fileName}");
|
||
return true;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"上传录音文件失败: {ex.Message}");
|
||
return false;
|
||
}
|
||
|
||
}
|
||
|
||
/// <summary>
|
||
/// 开始音频流
|
||
/// </summary>
|
||
/// <param name="sampleRate">采样率</param>
|
||
/// <param name="channels">通道数</param>
|
||
public async Task StartAudioStreamAsync(int sampleRate, int channels)
|
||
{
|
||
if (!IsConnected || _hubConnection == null)
|
||
{
|
||
Console.WriteLine("无法开始音频流:未连接到服务器");
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
await _hubConnection.InvokeAsync("StartAudioStream", sampleRate, channels);
|
||
Console.WriteLine($"已通知服务器开始接收音频流,采样率: {sampleRate}, 通道数: {channels}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"开始音频流失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 结束音频流
|
||
/// </summary>
|
||
public async Task EndAudioStreamAsync()
|
||
{
|
||
if (!IsConnected || _hubConnection == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
await _hubConnection.InvokeAsync("EndAudioStream");
|
||
Console.WriteLine("已通知服务器结束音频流");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"结束音频流失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 实时上传音频数据
|
||
/// </summary>
|
||
/// <param name="audioData">音频数据字节数组</param>
|
||
public async Task<bool> UploadAudioDataRealtimeAsync(byte[] audioData)
|
||
{
|
||
if (!IsConnected || _hubConnection == null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
try
|
||
{
|
||
// 直接将音频数据发送到服务器
|
||
await _hubConnection.InvokeAsync("ReceiveAudioData", audioData);
|
||
return true;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"实时上传音频数据失败: {ex.Message}");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 请求最新配置
|
||
/// </summary>
|
||
private async Task RequestLatestConfigAsync()
|
||
{
|
||
if (!IsConnected || _hubConnection == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
string configJson = await _hubConnection.InvokeAsync<string>("GetLatestConfig");
|
||
var config = JsonSerializer.Deserialize<PhoneBoothConfig>(configJson);
|
||
if (config != null)
|
||
{
|
||
await UpdateConfigAsync(config);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"获取最新配置失败: {ex.Message}");
|
||
// 如果获取配置失败,可能是连接已断开
|
||
if (_hubConnection != null)
|
||
{
|
||
await _hubConnection.StopAsync();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新配置
|
||
/// </summary>
|
||
/// <param name="newConfig">新配置</param>
|
||
private async Task UpdateConfigAsync(PhoneBoothConfig newConfig)
|
||
{
|
||
await _semaphore.WaitAsync();
|
||
try
|
||
{
|
||
//_currentConfig = newConfig;
|
||
|
||
//// 保存到本地备份
|
||
//_currentConfig.SaveToFile(_configBackupPath);
|
||
|
||
//// 触发配置更新事件
|
||
//ConfigUpdated?.Invoke(this, _currentConfig);
|
||
}
|
||
finally
|
||
{
|
||
_semaphore.Release();
|
||
}
|
||
}
|
||
} |