diff --git a/Net/DownloadConfigurationManager.cs b/Net/DownloadConfigurationManager.cs new file mode 100644 index 00000000..c3cf1dd0 --- /dev/null +++ b/Net/DownloadConfigurationManager.cs @@ -0,0 +1,670 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using PCL.Core.IO; +using PCL.Core.Logging; + +namespace PCL.Core.Net; + +/// +/// 下载器配置文件结构 +/// +public class DownloadConfigurationProfile +{ + /// + /// 配置文件名称 + /// + public string Name { get; set; } = "Default"; + + /// + /// 配置描述 + /// + public string? Description { get; set; } + + /// + /// 是否为默认配置 + /// + public bool IsDefault { get; set; } = false; + + /// + /// 线程数 + /// + public int ThreadCount { get; set; } = 4; + + /// + /// 分块大小 (字节) + /// + public int ChunkSize { get; set; } = 1024 * 1024; // 1MB + + /// + /// 最大重试次数 + /// + public int MaxRetries { get; set; } = 3; + + /// + /// 超时时间 (毫秒) + /// + public int TimeoutMs { get; set; } = 30000; + + /// + /// 缓冲区大小 (字节) + /// + public int BufferSize { get; set; } = 8192; + + /// + /// 速度限制 (字节/秒,0表示无限制) + /// + public long SpeedLimit { get; set; } = 0; + + /// + /// 是否启用详细日志 + /// + public bool VerboseLogging { get; set; } = false; + + /// + /// 连接池大小 + /// + public int ConnectionPoolSize { get; set; } = 8; + + /// + /// 连接保持时间 (秒) + /// + public int KeepAliveTimeoutSeconds { get; set; } = 30; + + /// + /// 是否启用压缩 + /// + public bool EnableCompression { get; set; } = true; + + /// + /// 用户代理 + /// + public string UserAgent { get; set; } = "PCL.Core/1.0 (Enhanced Multi-Thread Downloader)"; + + /// + /// 自定义请求头 + /// + public Dictionary CustomHeaders { get; set; } = new(); + + /// + /// 是否验证SSL证书 + /// + public bool ValidateSSLCertificate { get; set; } = true; + + /// + /// 重试延迟策略 + /// + public RetryDelayStrategy RetryDelayStrategy { get; set; } = RetryDelayStrategy.ExponentialBackoff; + + /// + /// 基础重试延迟 (毫秒) + /// + public int BaseRetryDelayMs { get; set; } = 1000; + + /// + /// 最大重试延迟 (毫秒) + /// + public int MaxRetryDelayMs { get; set; } = 30000; + + /// + /// 适用场景标签 + /// + public List ScenarioTags { get; set; } = new(); + + /// + /// 创建时间 + /// + public DateTime CreatedAt { get; set; } = DateTime.Now; + + /// + /// 最后修改时间 + /// + public DateTime LastModified { get; set; } = DateTime.Now; + + /// + /// 创建配置的副本 + /// + public DownloadConfigurationProfile Clone() + { + var json = JsonSerializer.Serialize(this); + return JsonSerializer.Deserialize(json)!; + } + + /// + /// 验证配置的有效性 + /// + public void Validate() + { + if (ThreadCount <= 0 || ThreadCount > 64) + throw new ArgumentOutOfRangeException(nameof(ThreadCount), "线程数必须在1-64之间"); + + if (ChunkSize < 1024 || ChunkSize > 100 * 1024 * 1024) + throw new ArgumentOutOfRangeException(nameof(ChunkSize), "分块大小必须在1KB-100MB之间"); + + if (TimeoutMs < 1000 || TimeoutMs > 300000) + throw new ArgumentOutOfRangeException(nameof(TimeoutMs), "超时时间必须在1-300秒之间"); + + if (BufferSize < 1024 || BufferSize > 1024 * 1024) + throw new ArgumentOutOfRangeException(nameof(BufferSize), "缓冲区大小必须在1KB-1MB之间"); + + if (SpeedLimit < 0) + throw new ArgumentOutOfRangeException(nameof(SpeedLimit), "速度限制不能为负数"); + + if (MaxRetries < 0 || MaxRetries > 10) + throw new ArgumentOutOfRangeException(nameof(MaxRetries), "重试次数必须在0-10之间"); + } +} + +/// +/// 下载配置 +/// +public class DownloadConfiguration +{ + /// + /// 线程数 + /// + public int ThreadCount { get; set; } = 4; + + /// + /// 分块大小 (字节) + /// + public int ChunkSize { get; set; } = 1024 * 1024; // 1MB + + /// + /// 最大重试次数 + /// + public int MaxRetries { get; set; } = 3; + + /// + /// 超时时间 (毫秒) + /// + public int TimeoutMs { get; set; } = 30000; + + /// + /// 缓冲区大小 (字节) + /// + public int BufferSize { get; set; } = 8192; + + /// + /// 速度限制 (字节/秒,0表示无限制) + /// + public long SpeedLimit { get; set; } = 0; + + /// + /// 是否启用详细日志 + /// + public bool VerboseLogging { get; set; } = false; + + /// + /// 启用断点续传 + /// + public bool EnableResumeSupport { get; set; } = true; + + /// + /// 启用连接池复用 + /// + public bool EnableConnectionPooling { get; set; } = true; + + /// + /// 文件预分配 + /// + public bool PreAllocateFile { get; set; } = true; + + /// + /// 从配置文件创建下载配置 + /// + public static DownloadConfiguration FromProfile(DownloadConfigurationProfile profile) + { + return new DownloadConfiguration + { + ThreadCount = profile.ThreadCount, + ChunkSize = profile.ChunkSize, + MaxRetries = profile.MaxRetries, + TimeoutMs = profile.TimeoutMs, + BufferSize = profile.BufferSize, + SpeedLimit = profile.SpeedLimit, + VerboseLogging = profile.VerboseLogging, + EnableResumeSupport = true, // 默认启用断点续传 + EnableConnectionPooling = true, // 默认启用连接池 + PreAllocateFile = true // 默认启用文件预分配 + }; + } +} + +/// +/// 重试延迟策略 +/// +public enum RetryDelayStrategy +{ + /// + /// 固定延迟 + /// + Fixed, + + /// + /// 指数退避 + /// + ExponentialBackoff, + + /// + /// 线性增长 + /// + Linear, + + /// + /// 随机延迟 + /// + Random +} + +/// +/// 下载配置提供器 +/// 提供预定义的下载配置,遵循底层服务原则,不维护配置文件 +/// +public static class DownloadConfigurationProvider +{ + private const string LogModule = "DownloadConfigProvider"; + + /// + /// 当前活动配置 + /// + public static DownloadConfigurationProfile ActiveProfile { get; private set; } = GetPresetProfile("Default"); + + /// + /// 所有可用的预设配置 + /// + public static IReadOnlyCollection AvailableProfiles => GetAllPresetProfiles(); + + /// + /// 配置变更事件 + /// + public static event Action? ConfigurationChanged; + + /// + /// 切换活动配置 + /// + /// 配置名称 + public static bool SetActiveProfile(string profileName) + { + try + { + var profile = GetPresetProfile(profileName); + ActiveProfile = profile; + ConfigurationChanged?.Invoke(profile); + LogWrapper.Info(LogModule, $"已切换到配置: {profileName}"); + return true; + } + catch + { + LogWrapper.Warn(LogModule, $"配置不存在: {profileName}"); + return false; + } + } + + /// + /// 获取预定义配置 + /// + /// 配置名称 + /// 配置实例 + public static DownloadConfigurationProfile GetPresetProfile(string name) + { + return name.ToLower() switch + { + "high-speed" => CreateHighSpeedProfile(), + "low-bandwidth" => CreateLowBandwidthProfile(), + "stable" => CreateStableProfile(), + "mobile" => CreateMobileProfile(), + "server" => CreateServerProfile(), + _ => CreateDefaultProfile() + }; + } + + /// + /// 获取所有预设配置 + /// + public static List GetAllPresetProfiles() + { + return new List + { + GetPresetProfile("Default"), + GetPresetProfile("High-Speed"), + GetPresetProfile("Low-Bandwidth"), + GetPresetProfile("Stable"), + GetPresetProfile("Mobile"), + GetPresetProfile("Server") + }; + } + + /// + /// 根据网络条件自动选择最佳配置 + /// + /// 网络速度 (字节/秒) + /// 是否为计量网络 + /// 推荐的配置 + public static DownloadConfigurationProfile GetRecommendedProfile(long networkSpeed = 0, bool isMetered = false) + { + try + { + if (isMetered) + { + LogWrapper.Info(LogModule, "检测到计量网络,使用低带宽配置"); + return GetPresetProfile("Low-Bandwidth"); + } + + if (networkSpeed > 0) + { + // 根据网速推荐配置 + if (networkSpeed > 50 * 1024 * 1024) // 50MB/s + { + LogWrapper.Info(LogModule, "高速网络,使用高速配置"); + return GetPresetProfile("High-Speed"); + } + else if (networkSpeed < 1 * 1024 * 1024) // 1MB/s + { + LogWrapper.Info(LogModule, "低速网络,使用移动网络配置"); + return GetPresetProfile("Mobile"); + } + } + + LogWrapper.Info(LogModule, "使用稳定配置"); + return GetPresetProfile("Stable"); + } + catch (Exception ex) + { + LogWrapper.Warn(LogModule, $"获取推荐配置失败: {ex.Message},使用默认配置"); + return GetPresetProfile("Default"); + } + } + + /// + /// 根据文件大小推荐配置 + /// + /// 文件大小 + /// 推荐的配置 + public static DownloadConfigurationProfile GetProfileForFileSize(long fileSize) + { + try + { + if (fileSize < 10 * 1024 * 1024) // 小于10MB + { + LogWrapper.Info(LogModule, "小文件,使用移动网络配置"); + return GetPresetProfile("Mobile"); + } + else if (fileSize > 1024 * 1024 * 1024) // 大于1GB + { + LogWrapper.Info(LogModule, "大文件,使用高速配置"); + return GetPresetProfile("High-Speed"); + } + else + { + LogWrapper.Info(LogModule, "中等文件,使用稳定配置"); + return GetPresetProfile("Stable"); + } + } + catch (Exception ex) + { + LogWrapper.Warn(LogModule, $"获取文件大小配置失败: {ex.Message},使用默认配置"); + return GetPresetProfile("Default"); + } + } + + /// + /// 创建自定义配置(基于预设配置) + /// + /// 基础配置名称 + /// 自定义配置名称 + /// 自定义参数 + public static DownloadConfigurationProfile CreateCustomProfile(string baseName, string customName, Action customizations) + { + try + { + var baseProfile = GetPresetProfile(baseName); + var customProfile = baseProfile.Clone(); + + // 应用自定义设置 + customProfile.Name = customName; + customProfile.IsDefault = false; + customProfile.LastModified = DateTime.Now; + customizations(customProfile); + + // 验证配置 + customProfile.Validate(); + + LogWrapper.Info(LogModule, $"创建自定义配置: {customName} (基于 {baseName})"); + return customProfile; + } + catch (Exception ex) + { + LogWrapper.Error(ex, LogModule, $"创建自定义配置失败: {customName}"); + throw; + } + } + + /// + /// 创建默认配置 + /// + private static DownloadConfigurationProfile CreateDefaultProfile() + { + return new DownloadConfigurationProfile + { + Name = "Default", + Description = "默认下载配置,适用于大多数场景", + IsDefault = true, + ThreadCount = 4, + ChunkSize = 1024 * 1024, // 1MB + MaxRetries = 3, + TimeoutMs = 30000, + BufferSize = 8192, + SpeedLimit = 0, + VerboseLogging = false, + ConnectionPoolSize = 8, + KeepAliveTimeoutSeconds = 30, + EnableCompression = true, + ValidateSSLCertificate = true, + RetryDelayStrategy = RetryDelayStrategy.ExponentialBackoff, + BaseRetryDelayMs = 1000, + MaxRetryDelayMs = 30000, + ScenarioTags = new List { "general", "default" } + }; + } + + /// + /// 创建高速下载配置 + /// + private static DownloadConfigurationProfile CreateHighSpeedProfile() + { + return new DownloadConfigurationProfile + { + Name = "High-Speed", + Description = "高速下载配置,适用于高带宽网络环境", + ThreadCount = 8, + ChunkSize = 4 * 1024 * 1024, // 4MB + MaxRetries = 5, + TimeoutMs = 60000, + BufferSize = 32768, + SpeedLimit = 0, + VerboseLogging = false, + ConnectionPoolSize = 16, + KeepAliveTimeoutSeconds = 60, + EnableCompression = true, + ValidateSSLCertificate = true, + RetryDelayStrategy = RetryDelayStrategy.Fixed, + BaseRetryDelayMs = 500, + MaxRetryDelayMs = 5000, + ScenarioTags = new List { "high-speed", "high-bandwidth", "server" } + }; + } + + /// + /// 创建低带宽配置 + /// + private static DownloadConfigurationProfile CreateLowBandwidthProfile() + { + return new DownloadConfigurationProfile + { + Name = "Low-Bandwidth", + Description = "低带宽配置,适用于网络条件较差的环境", + ThreadCount = 2, + ChunkSize = 256 * 1024, // 256KB + MaxRetries = 5, + TimeoutMs = 120000, + BufferSize = 4096, + SpeedLimit = 512 * 1024, // 512KB/s + VerboseLogging = true, + ConnectionPoolSize = 4, + KeepAliveTimeoutSeconds = 15, + EnableCompression = true, + ValidateSSLCertificate = true, + RetryDelayStrategy = RetryDelayStrategy.ExponentialBackoff, + BaseRetryDelayMs = 2000, + MaxRetryDelayMs = 60000, + ScenarioTags = new List { "low-bandwidth", "slow-network", "conservative" } + }; + } + + /// + /// 创建稳定配置 + /// + private static DownloadConfigurationProfile CreateStableProfile() + { + return new DownloadConfigurationProfile + { + Name = "Stable", + Description = "稳定配置,平衡速度和稳定性", + ThreadCount = 6, + ChunkSize = 2 * 1024 * 1024, // 2MB + MaxRetries = 4, + TimeoutMs = 45000, + BufferSize = 16384, + SpeedLimit = 0, + VerboseLogging = false, + ConnectionPoolSize = 12, + KeepAliveTimeoutSeconds = 45, + EnableCompression = true, + ValidateSSLCertificate = true, + RetryDelayStrategy = RetryDelayStrategy.ExponentialBackoff, + BaseRetryDelayMs = 1500, + MaxRetryDelayMs = 20000, + ScenarioTags = new List { "stable", "balanced", "reliable" } + }; + } + + /// + /// 创建移动网络配置 + /// + private static DownloadConfigurationProfile CreateMobileProfile() + { + return new DownloadConfigurationProfile + { + Name = "Mobile", + Description = "移动网络配置,适用于移动设备和不稳定网络", + ThreadCount = 2, + ChunkSize = 512 * 1024, // 512KB + MaxRetries = 6, + TimeoutMs = 90000, + BufferSize = 4096, + SpeedLimit = 1024 * 1024, // 1MB/s + VerboseLogging = true, + ConnectionPoolSize = 4, + KeepAliveTimeoutSeconds = 20, + EnableCompression = true, + ValidateSSLCertificate = true, + RetryDelayStrategy = RetryDelayStrategy.ExponentialBackoff, + BaseRetryDelayMs = 3000, + MaxRetryDelayMs = 60000, + ScenarioTags = new List { "mobile", "unstable-network", "metered" } + }; + } + + /// + /// 创建服务器配置 + /// + private static DownloadConfigurationProfile CreateServerProfile() + { + return new DownloadConfigurationProfile + { + Name = "Server", + Description = "服务器配置,适用于服务器环境的批量下载", + ThreadCount = 16, + ChunkSize = 8 * 1024 * 1024, // 8MB + MaxRetries = 3, + TimeoutMs = 120000, + BufferSize = 65536, + SpeedLimit = 0, + VerboseLogging = false, + ConnectionPoolSize = 32, + KeepAliveTimeoutSeconds = 300, + EnableCompression = true, + ValidateSSLCertificate = true, + RetryDelayStrategy = RetryDelayStrategy.Fixed, + BaseRetryDelayMs = 1000, + MaxRetryDelayMs = 10000, + ScenarioTags = new List { "server", "batch", "high-performance" } + }; + } + + /// + /// 获取配置的性能评估 + /// + /// 配置文件 + /// 性能评估信息 + public static string GetPerformanceAssessment(DownloadConfigurationProfile profile) + { + var assessment = new List(); + + // 评估并发能力 + if (profile.ThreadCount >= 8) + assessment.Add("高并发"); + else if (profile.ThreadCount >= 4) + assessment.Add("中等并发"); + else + assessment.Add("低并发"); + + // 评估内存使用 + var memoryUsage = profile.ThreadCount * profile.ChunkSize / 1024 / 1024; + if (memoryUsage > 50) + assessment.Add("高内存使用"); + else if (memoryUsage > 20) + assessment.Add("中等内存使用"); + else + assessment.Add("低内存使用"); + + // 评估网络友好度 + if (profile.SpeedLimit > 0) + assessment.Add("带宽限制"); + if (profile.MaxRetries >= 5) + assessment.Add("高容错"); + + return string.Join(", ", assessment); + } + + /// + /// 获取适用场景建议 + /// + /// 配置文件 + /// 场景建议 + public static List GetScenarioRecommendations(DownloadConfigurationProfile profile) + { + var recommendations = new List(); + + if (profile.ThreadCount >= 8 && profile.ChunkSize >= 4 * 1024 * 1024) + recommendations.Add("大文件下载"); + + if (profile.SpeedLimit > 0) + recommendations.Add("带宽受限环境"); + + if (profile.MaxRetries >= 5) + recommendations.Add("不稳定网络"); + + if (profile.ConnectionPoolSize >= 16) + recommendations.Add("服务器批量下载"); + + if (profile.ThreadCount <= 2 && profile.ChunkSize <= 512 * 1024) + recommendations.Add("移动设备"); + + return recommendations; + } +} diff --git a/Net/DownloadErrorHandler.cs b/Net/DownloadErrorHandler.cs new file mode 100644 index 00000000..ef9a4ef9 --- /dev/null +++ b/Net/DownloadErrorHandler.cs @@ -0,0 +1,804 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using PCL.Core.Logging; + +namespace PCL.Core.Net; + +/// +/// 下载错误类型 +/// +public enum DownloadErrorType +{ + /// + /// 网络连接错误 + /// + NetworkConnection, + + /// + /// HTTP错误 + /// + HttpError, + + /// + /// 文件IO错误 + /// + FileIo, + + /// + /// 服务器错误 + /// + ServerError, + + /// + /// 认证错误 + /// + Authentication, + + /// + /// 超时错误 + /// + Timeout, + + /// + /// 取消操作 + /// + Cancelled, + + /// + /// 配置错误 + /// + Configuration, + + /// + /// 未知错误 + /// + Unknown +} + +/// +/// 错误严重程度 +/// +public enum ErrorSeverity +{ + /// + /// 信息级别 + /// + Info, + + /// + /// 警告级别 + /// + Warning, + + /// + /// 错误级别 + /// + Error, + + /// + /// 严重错误级别 + /// + Critical +} + +/// +/// 下载错误信息 +/// +public class DownloadError +{ + /// + /// 错误ID + /// + public string Id { get; set; } = Guid.NewGuid().ToString(); + + /// + /// 发生时间 + /// + public DateTime Timestamp { get; set; } = DateTime.Now; + + /// + /// 错误类型 + /// + public DownloadErrorType Type { get; set; } + + /// + /// 错误严重程度 + /// + public ErrorSeverity Severity { get; set; } + + /// + /// 错误代码 + /// + public string ErrorCode { get; set; } = ""; + + /// + /// 错误消息 + /// + public string Message { get; set; } = ""; + + /// + /// 详细描述 + /// + public string Details { get; set; } = ""; + + /// + /// 原始异常 + /// + public Exception? OriginalException { get; set; } + + /// + /// 上下文信息 + /// + public Dictionary Context { get; set; } = new(); + + /// + /// 建议的解决方案 + /// + public List SuggestedSolutions { get; set; } = new(); + + /// + /// 是否可重试 + /// + public bool IsRetryable { get; set; } = true; + + /// + /// 重试延迟(毫秒) + /// + public int RetryDelayMs { get; set; } = 1000; + + /// + /// 已重试次数 + /// + public int RetryCount { get; set; } = 0; + + /// + /// 最大重试次数 + /// + public int MaxRetries { get; set; } = 3; + + public override string ToString() + { + return $"[{Type}] {ErrorCode}: {Message}"; + } +} + +/// +/// 错误恢复策略 +/// +public enum ErrorRecoveryStrategy +{ + /// + /// 立即重试 + /// + ImmediateRetry, + + /// + /// 延迟重试 + /// + DelayedRetry, + + /// + /// 指数退避重试 + /// + ExponentialBackoff, + + /// + /// 降级处理(如切换到单线程) + /// + Fallback, + + /// + /// 终止操作 + /// + Abort, + + /// + /// 跳过当前任务 + /// + Skip +} + +/// +/// 错误恢复结果 +/// +public class ErrorRecoveryResult +{ + /// + /// 采用的策略 + /// + public ErrorRecoveryStrategy Strategy { get; set; } + + /// + /// 是否成功恢复 + /// + public bool IsRecovered { get; set; } + + /// + /// 恢复消息 + /// + public string Message { get; set; } = ""; + + /// + /// 额外参数 + /// + public Dictionary Parameters { get; set; } = new(); +} + +/// +/// 下载错误处理器 +/// 提供错误分析、诊断和自动恢复功能 +/// +public class DownloadErrorHandler +{ + private const string LogModule = "DownloadErrorHandler"; + + private readonly Dictionary> _recoveryHandlers = new(); + private readonly List _errorHistory = new(); + private readonly object _lockObject = new(); + + /// + /// 最大错误历史记录数 + /// + public int MaxErrorHistoryCount { get; set; } = 100; + + /// + /// 启用自动恢复 + /// + public bool EnableAutoRecovery { get; set; } = true; + + /// + /// 错误历史记录 + /// + public IReadOnlyList ErrorHistory + { + get + { + lock (_lockObject) + { + return _errorHistory.ToArray(); + } + } + } + + /// + /// 错误发生事件 + /// + public event Action? ErrorOccurred; + + /// + /// 错误恢复事件 + /// + public event Action? ErrorRecovered; + + public DownloadErrorHandler() + { + InitializeDefaultRecoveryHandlers(); + LogWrapper.Info(LogModule, "下载错误处理器已初始化"); + } + + /// + /// 处理异常并转换为下载错误 + /// + /// 异常实例 + /// 上下文信息 + /// 下载错误实例 + public DownloadError HandleException(Exception exception, Dictionary? context = null) + { + var error = AnalyzeException(exception); + + // 添加上下文信息 + if (context != null) + { + foreach (var kvp in context) + { + error.Context[kvp.Key] = kvp.Value; + } + } + + // 记录错误 + RecordError(error); + + // 触发事件 + ErrorOccurred?.Invoke(error); + + LogWrapper.Warn(LogModule, $"处理下载错误: {error}"); + + return error; + } + + /// + /// 尝试恢复错误 + /// + /// 错误实例 + /// 恢复结果 + public async Task TryRecoverAsync(DownloadError error) + { + if (!EnableAutoRecovery || !error.IsRetryable) + { + return new ErrorRecoveryResult + { + Strategy = ErrorRecoveryStrategy.Abort, + IsRecovered = false, + Message = "自动恢复已禁用或错误不可重试" + }; + } + + ErrorRecoveryResult result; + + if (_recoveryHandlers.TryGetValue(error.Type, out var handler)) + { + result = handler(error); + } + else + { + result = HandleGenericError(error); + } + + // 应用恢复策略 + if (result.IsRecovered && result.Strategy == ErrorRecoveryStrategy.DelayedRetry) + { + var delay = result.Parameters.ContainsKey("DelayMs") + ? (int)result.Parameters["DelayMs"] + : error.RetryDelayMs; + await Task.Delay(delay); + } + + // 更新错误状态 + error.RetryCount++; + + // 触发恢复事件 + ErrorRecovered?.Invoke(error, result); + + LogWrapper.Info(LogModule, $"错误恢复尝试: {error.Id}, 策略: {result.Strategy}, 成功: {result.IsRecovered}"); + + return result; + } + + /// + /// 分析异常并创建下载错误 + /// + private DownloadError AnalyzeException(Exception exception) + { + var error = new DownloadError + { + OriginalException = exception, + Message = exception.Message, + Details = exception.ToString() + }; + + // 分析异常类型 + switch (exception) + { + case HttpRequestException httpEx: + error.Type = DownloadErrorType.HttpError; + error.Severity = ErrorSeverity.Error; + error.ErrorCode = "HTTP_REQUEST_FAILED"; + AnalyzeHttpException(error, httpEx); + break; + + case TaskCanceledException cancelEx when cancelEx.InnerException is TimeoutException: + error.Type = DownloadErrorType.Timeout; + error.Severity = ErrorSeverity.Warning; + error.ErrorCode = "TIMEOUT"; + error.SuggestedSolutions.AddRange(new[] + { + "增加超时时间", + "检查网络连接", + "减少并发线程数" + }); + break; + + case OperationCanceledException: + error.Type = DownloadErrorType.Cancelled; + error.Severity = ErrorSeverity.Info; + error.ErrorCode = "OPERATION_CANCELLED"; + error.IsRetryable = false; + break; + + case UnauthorizedAccessException: + case System.Security.Authentication.AuthenticationException: + error.Type = DownloadErrorType.Authentication; + error.Severity = ErrorSeverity.Error; + error.ErrorCode = "AUTHENTICATION_FAILED"; + error.SuggestedSolutions.AddRange(new[] + { + "检查访问凭据", + "验证用户权限", + "检查认证配置" + }); + break; + + case System.IO.IOException ioEx: + error.Type = DownloadErrorType.FileIo; + error.Severity = ErrorSeverity.Error; + error.ErrorCode = "FILE_IO_ERROR"; + AnalyzeFileIOException(error, ioEx); + break; + + case System.Net.NetworkInformation.NetworkInformationException: + case System.Net.Sockets.SocketException: + error.Type = DownloadErrorType.NetworkConnection; + error.Severity = ErrorSeverity.Error; + error.ErrorCode = "NETWORK_CONNECTION_FAILED"; + error.SuggestedSolutions.AddRange(new[] + { + "检查网络连接", + "检查防火墙设置", + "尝试使用代理" + }); + break; + + case ArgumentException argEx: + error.Type = DownloadErrorType.Configuration; + error.Severity = ErrorSeverity.Error; + error.ErrorCode = "INVALID_CONFIGURATION"; + error.Message = $"配置错误: {argEx.Message}"; + error.IsRetryable = false; + break; + + default: + error.Type = DownloadErrorType.Unknown; + error.Severity = ErrorSeverity.Error; + error.ErrorCode = "UNKNOWN_ERROR"; + error.SuggestedSolutions.Add("查看详细错误信息以获取更多帮助"); + break; + } + + return error; + } + + /// + /// 分析HTTP异常 + /// + private void AnalyzeHttpException(DownloadError error, HttpRequestException httpEx) + { + if (httpEx.Message.Contains("404")) + { + error.ErrorCode = "HTTP_404_NOT_FOUND"; + error.Message = "文件未找到"; + error.IsRetryable = false; + error.SuggestedSolutions.Add("检查下载链接是否正确"); + } + else if (httpEx.Message.Contains("403")) + { + error.ErrorCode = "HTTP_403_FORBIDDEN"; + error.Message = "访问被禁止"; + error.IsRetryable = false; + error.SuggestedSolutions.AddRange(new[] + { + "检查访问权限", + "检查User-Agent设置", + "检查访问频率限制" + }); + } + else if (httpEx.Message.Contains("500")) + { + error.Type = DownloadErrorType.ServerError; + error.ErrorCode = "HTTP_500_SERVER_ERROR"; + error.Message = "服务器内部错误"; + error.SuggestedSolutions.AddRange(new[] + { + "稍后重试", + "联系服务器管理员", + "尝试其他下载源" + }); + } + else if (httpEx.Message.Contains("502") || httpEx.Message.Contains("503")) + { + error.Type = DownloadErrorType.ServerError; + error.ErrorCode = "HTTP_SERVER_UNAVAILABLE"; + error.Message = "服务器不可用"; + error.RetryDelayMs = 5000; // 5秒后重试 + } + } + + /// + /// 分析文件IO异常 + /// + private void AnalyzeFileIOException(DownloadError error, System.IO.IOException ioEx) + { + if (ioEx.Message.Contains("space") || ioEx.Message.Contains("disk")) + { + error.ErrorCode = "INSUFFICIENT_DISK_SPACE"; + error.Message = "磁盘空间不足"; + error.IsRetryable = false; + error.Severity = ErrorSeverity.Critical; + error.SuggestedSolutions.AddRange(new[] + { + "清理磁盘空间", + "选择其他存储位置", + "删除不必要的文件" + }); + } + else if (ioEx.Message.Contains("access") || ioEx.Message.Contains("permission")) + { + error.ErrorCode = "FILE_ACCESS_DENIED"; + error.Message = "文件访问被拒绝"; + error.IsRetryable = false; + error.SuggestedSolutions.AddRange(new[] + { + "检查文件权限", + "以管理员身份运行", + "选择其他存储位置" + }); + } + else if (ioEx.Message.Contains("use") || ioEx.Message.Contains("locked")) + { + error.ErrorCode = "FILE_IN_USE"; + error.Message = "文件正在被使用"; + error.RetryDelayMs = 2000; // 2秒后重试 + error.SuggestedSolutions.AddRange(new[] + { + "关闭正在使用该文件的程序", + "稍后重试", + "选择不同的文件名" + }); + } + } + + /// + /// 记录错误到历史记录 + /// + private void RecordError(DownloadError error) + { + lock (_lockObject) + { + _errorHistory.Add(error); + + // 限制历史记录数量 + while (_errorHistory.Count > MaxErrorHistoryCount) + { + _errorHistory.RemoveAt(0); + } + } + } + + /// + /// 初始化默认恢复处理器 + /// + private void InitializeDefaultRecoveryHandlers() + { + _recoveryHandlers[DownloadErrorType.NetworkConnection] = HandleNetworkError; + _recoveryHandlers[DownloadErrorType.HttpError] = HandleHttpError; + _recoveryHandlers[DownloadErrorType.ServerError] = HandleServerError; + _recoveryHandlers[DownloadErrorType.Timeout] = HandleTimeoutError; + _recoveryHandlers[DownloadErrorType.FileIo] = HandleFileIOError; + } + + /// + /// 处理网络错误 + /// + private ErrorRecoveryResult HandleNetworkError(DownloadError error) + { + if (error.RetryCount < error.MaxRetries) + { + return new ErrorRecoveryResult + { + Strategy = ErrorRecoveryStrategy.ExponentialBackoff, + IsRecovered = true, + Message = "网络错误,使用指数退避重试", + Parameters = new Dictionary + { + ["DelayMs"] = Math.Min(error.RetryDelayMs * (int)Math.Pow(2, error.RetryCount), 30000) + } + }; + } + + return new ErrorRecoveryResult + { + Strategy = ErrorRecoveryStrategy.Abort, + IsRecovered = false, + Message = "网络错误重试次数已达上限" + }; + } + + /// + /// 处理HTTP错误 + /// + private ErrorRecoveryResult HandleHttpError(DownloadError error) + { + // 4xx错误通常不需要重试 + if (error.ErrorCode.Contains("404") || error.ErrorCode.Contains("403")) + { + return new ErrorRecoveryResult + { + Strategy = ErrorRecoveryStrategy.Abort, + IsRecovered = false, + Message = "HTTP客户端错误,不需要重试" + }; + } + + // 其他HTTP错误可以重试 + if (error.RetryCount < error.MaxRetries) + { + return new ErrorRecoveryResult + { + Strategy = ErrorRecoveryStrategy.DelayedRetry, + IsRecovered = true, + Message = "HTTP错误,延迟重试", + Parameters = new Dictionary + { + ["DelayMs"] = error.RetryDelayMs + } + }; + } + + return new ErrorRecoveryResult + { + Strategy = ErrorRecoveryStrategy.Abort, + IsRecovered = false, + Message = "HTTP错误重试次数已达上限" + }; + } + + /// + /// 处理服务器错误 + /// + private ErrorRecoveryResult HandleServerError(DownloadError error) + { + if (error.RetryCount < error.MaxRetries) + { + return new ErrorRecoveryResult + { + Strategy = ErrorRecoveryStrategy.ExponentialBackoff, + IsRecovered = true, + Message = "服务器错误,使用指数退避重试", + Parameters = new Dictionary + { + ["DelayMs"] = Math.Min(5000 * (int)Math.Pow(2, error.RetryCount), 60000) + } + }; + } + + return new ErrorRecoveryResult + { + Strategy = ErrorRecoveryStrategy.Abort, + IsRecovered = false, + Message = "服务器错误重试次数已达上限" + }; + } + + /// + /// 处理超时错误 + /// + private ErrorRecoveryResult HandleTimeoutError(DownloadError error) + { + if (error.RetryCount < error.MaxRetries) + { + // 超时后切换到单线程模式 + return new ErrorRecoveryResult + { + Strategy = ErrorRecoveryStrategy.Fallback, + IsRecovered = true, + Message = "超时错误,切换到单线程模式", + Parameters = new Dictionary + { + ["ThreadCount"] = 1, + ["TimeoutMs"] = (int)(error.Context.ContainsKey("TimeoutMs") ? + (int)error.Context["TimeoutMs"] * 1.5 : 45000) + } + }; + } + + return new ErrorRecoveryResult + { + Strategy = ErrorRecoveryStrategy.Abort, + IsRecovered = false, + Message = "超时错误重试次数已达上限" + }; + } + + /// + /// 处理文件IO错误 + /// + private ErrorRecoveryResult HandleFileIOError(DownloadError error) + { + // 磁盘空间不足或权限错误不需要重试 + if (error.ErrorCode.Contains("DISK_SPACE") || error.ErrorCode.Contains("ACCESS_DENIED")) + { + return new ErrorRecoveryResult + { + Strategy = ErrorRecoveryStrategy.Abort, + IsRecovered = false, + Message = "文件系统错误,无法自动恢复" + }; + } + + // 文件被占用可以延迟重试 + if (error.ErrorCode.Contains("FILE_IN_USE") && error.RetryCount < error.MaxRetries) + { + return new ErrorRecoveryResult + { + Strategy = ErrorRecoveryStrategy.DelayedRetry, + IsRecovered = true, + Message = "文件被占用,延迟重试", + Parameters = new Dictionary + { + ["DelayMs"] = 3000 // 3秒 + } + }; + } + + return new ErrorRecoveryResult + { + Strategy = ErrorRecoveryStrategy.Abort, + IsRecovered = false, + Message = "文件IO错误无法恢复" + }; + } + + /// + /// 处理一般性错误 + /// + private ErrorRecoveryResult HandleGenericError(DownloadError error) + { + if (error.RetryCount < error.MaxRetries) + { + return new ErrorRecoveryResult + { + Strategy = ErrorRecoveryStrategy.DelayedRetry, + IsRecovered = true, + Message = "一般错误,延迟重试", + Parameters = new Dictionary + { + ["DelayMs"] = error.RetryDelayMs + } + }; + } + + return new ErrorRecoveryResult + { + Strategy = ErrorRecoveryStrategy.Abort, + IsRecovered = false, + Message = "错误重试次数已达上限" + }; + } + + /// + /// 获取错误统计信息 + /// + /// 错误统计 + public Dictionary GetErrorStatistics() + { + lock (_lockObject) + { + var stats = new Dictionary(); + + foreach (DownloadErrorType errorType in Enum.GetValues()) + { + stats[errorType] = 0; + } + + foreach (var error in _errorHistory) + { + stats[error.Type]++; + } + + return stats; + } + } + + /// + /// 清理错误历史记录 + /// + /// 清理指定时间之前的记录 + public void ClearErrorHistory(DateTime? olderThan = null) + { + lock (_lockObject) + { + if (olderThan.HasValue) + { + _errorHistory.RemoveAll(e => e.Timestamp < olderThan.Value); + } + else + { + _errorHistory.Clear(); + } + } + + LogWrapper.Info(LogModule, $"已清理错误历史记录"); + } +} diff --git a/Net/DownloadMonitor.cs b/Net/DownloadMonitor.cs new file mode 100644 index 00000000..8849f0f6 --- /dev/null +++ b/Net/DownloadMonitor.cs @@ -0,0 +1,642 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PCL.Core.App.Tasks; +using PCL.Core.Logging; + +namespace PCL.Core.Net; + +/// +/// 下载性能指标 +/// +public class DownloadMetrics +{ + public DateTime Timestamp { get; set; } = DateTime.Now; + public long BytesPerSecond { get; set; } + public int ActiveConnections { get; set; } + public double CpuUsagePercent { get; set; } + public long MemoryUsageBytes { get; set; } + public int RetryCount { get; set; } + public TimeSpan ResponseTime { get; set; } +} + +/// +/// 下载健康状态 +/// +public enum DownloadHealth +{ + Excellent, // > 80% 预期速度 + Good, // 50-80% 预期速度 + Fair, // 20-50% 预期速度 + Poor, // < 20% 预期速度 + Critical // 连接失败或严重错误 +} + +/// +/// 下载诊断信息 +/// +public class DownloadDiagnosis +{ + public DownloadHealth Health { get; set; } + public string HealthDescription { get; set; } = string.Empty; + public List Issues { get; set; } = new(); + public List Recommendations { get; set; } = new(); + public double EfficiencyScore { get; set; } // 0-100 +} + +/// +/// 下载监控器 +/// 提供实时监控、性能分析和诊断功能 +/// +public class DownloadMonitor : IDisposable +{ + private const string LogModule = "DownloadMonitor"; + + private readonly ConcurrentQueue _metricsHistory = new(); + private readonly ConcurrentDictionary _activeDownloads = new(); + private readonly Timer _monitoringTimer; + private readonly object _lockObject = new(); + private bool _disposed = false; + + /// + /// 监控间隔(毫秒) + /// + public int MonitoringInterval { get; set; } = 1000; + + /// + /// 性能指标历史记录保留数量 + /// + public int MaxHistoryCount { get; set; } = 300; // 5分钟历史 + + /// + /// 启用详细监控 + /// + public bool EnableDetailedMonitoring { get; set; } = true; + + /// + /// 性能警告阈值(字节/秒) + /// + public long PerformanceWarningThreshold { get; set; } = 100 * 1024; // 100KB/s + + /// + /// 性能指标历史 + /// + public IReadOnlyCollection MetricsHistory => _metricsHistory.ToArray(); + + /// + /// 当前活动下载数量 + /// + public int ActiveDownloadCount => _activeDownloads.Count; + + /// + /// 性能警告事件 + /// + public event Action? PerformanceWarning; + + /// + /// 下载完成事件 + /// + public event Action? DownloadCompleted; + + public DownloadMonitor() + { + _monitoringTimer = new Timer(MonitorPerformance, null, + TimeSpan.FromMilliseconds(MonitoringInterval), + TimeSpan.FromMilliseconds(MonitoringInterval)); + + LogWrapper.Info(LogModule, "下载监控器已启动"); + } + + /// + /// 注册下载任务进行监控 + /// + /// 任务ID + /// 下载任务 + public void RegisterDownload(string taskId, EnhancedMultiThreadDownloadTask task) + { + if (_disposed) return; + + var stats = task.GetDetailedStatus(); + _activeDownloads[taskId] = stats; + + // 订阅任务状态变化 + task.StateChanged += (sender, oldState, newState) => + { + if (newState is TaskState.Completed or TaskState.Failed or TaskState.Canceled) + { + OnDownloadCompleted(taskId, task); + } + }; + + LogWrapper.Info(LogModule, $"注册下载任务: {taskId}"); + } + + /// + /// 注销下载任务 + /// + /// 任务ID + public void UnregisterDownload(string taskId) + { + if (_activeDownloads.TryRemove(taskId, out var stats)) + { + LogWrapper.Info(LogModule, $"注销下载任务: {taskId}"); + } + } + + /// + /// 获取总体统计信息 + /// + /// 总体统计 + public (long TotalBytes, long DownloadedBytes, double AverageSpeed, int ActiveTasks) GetOverallStatistics() + { + lock (_lockObject) + { + var stats = _activeDownloads.Values.ToArray(); + var totalBytes = stats.Sum(s => s.TotalBytes); + var downloadedBytes = stats.Sum(s => s.DownloadedBytes); + var averageSpeed = stats.Where(s => s.CurrentSpeed > 0).DefaultIfEmpty().Average(s => s.CurrentSpeed); + + return (totalBytes, downloadedBytes, averageSpeed, stats.Length); + } + } + + /// + /// 获取性能分析报告 + /// + /// 性能报告 + public PerformanceReport GetPerformanceReport() + { + lock (_lockObject) + { + var metrics = _metricsHistory.ToArray(); + var stats = _activeDownloads.Values.ToArray(); + + return new PerformanceReport + { + GeneratedAt = DateTime.Now, + ActiveDownloads = stats.Length, + TotalBytesTransferred = stats.Sum(s => s.DownloadedBytes), + AverageSpeed = metrics.Where(m => m.BytesPerSecond > 0).DefaultIfEmpty().Average(m => m.BytesPerSecond), + PeakSpeed = metrics.DefaultIfEmpty().Max(m => m?.BytesPerSecond ?? 0), + TotalRetries = stats.Sum(s => s.RetryCount), + AverageConnections = metrics.Where(m => m.ActiveConnections > 0).DefaultIfEmpty().Average(m => m.ActiveConnections), + OverallHealth = CalculateOverallHealth(stats), + Recommendations = GenerateRecommendations(stats, metrics) + }; + } + } + + /// + /// 诊断下载性能 + /// + /// 任务ID + /// 诊断结果 + public DownloadDiagnosis DiagnoseDownload(string taskId) + { + if (!_activeDownloads.TryGetValue(taskId, out var stats)) + { + return new DownloadDiagnosis + { + Health = DownloadHealth.Critical, + HealthDescription = "下载任务未找到", + EfficiencyScore = 0 + }; + } + + var diagnosis = new DownloadDiagnosis(); + var issues = new List(); + var recommendations = new List(); + + // 速度分析 + var expectedSpeed = EstimateExpectedSpeed(stats.TotalBytes); + var speedRatio = stats.CurrentSpeed / expectedSpeed; + + if (speedRatio >= 0.8) + { + diagnosis.Health = DownloadHealth.Excellent; + diagnosis.HealthDescription = "下载速度优秀"; + } + else if (speedRatio >= 0.5) + { + diagnosis.Health = DownloadHealth.Good; + diagnosis.HealthDescription = "下载速度良好"; + } + else if (speedRatio >= 0.2) + { + diagnosis.Health = DownloadHealth.Fair; + diagnosis.HealthDescription = "下载速度一般"; + issues.Add("下载速度低于预期"); + recommendations.Add("考虑增加线程数或检查网络连接"); + } + else + { + diagnosis.Health = DownloadHealth.Poor; + diagnosis.HealthDescription = "下载速度较差"; + issues.Add("下载速度严重低于预期"); + recommendations.Add("检查网络连接、增加重试次数或更换下载源"); + } + + // 重试分析 + if (stats.RetryCount > 10) + { + issues.Add($"重试次数过多: {stats.RetryCount}"); + recommendations.Add("检查网络稳定性或服务器可靠性"); + } + + // 效率分析 + var progressRate = stats.ProgressPercentage / stats.ElapsedTime.TotalMinutes; + diagnosis.EfficiencyScore = Math.Min(100, speedRatio * 100); + + // 连接分析 + if (stats.ActiveThreads == 1 && stats.TotalBytes > 10 * 1024 * 1024) // 大于10MB + { + issues.Add("大文件使用单线程下载"); + recommendations.Add("检查服务器是否支持分块下载"); + } + + diagnosis.Issues = issues; + diagnosis.Recommendations = recommendations; + + return diagnosis; + } + + /// + /// 获取实时性能指标 + /// + /// 当前性能指标 + public DownloadMetrics GetCurrentMetrics() + { + lock (_lockObject) + { + var stats = _activeDownloads.Values.ToArray(); + + return new DownloadMetrics + { + Timestamp = DateTime.Now, + BytesPerSecond = (long)stats.Sum(s => s.CurrentSpeed), + ActiveConnections = stats.Sum(s => s.ActiveThreads), + RetryCount = stats.Sum(s => s.RetryCount), + CpuUsagePercent = GetCpuUsage(), + MemoryUsageBytes = GetMemoryUsage() + }; + } + } + + /// + /// 监控性能(定时器回调) + /// + private void MonitorPerformance(object? state) + { + if (_disposed) return; + + try + { + var currentMetrics = GetCurrentMetrics(); + + // 添加到历史记录 + _metricsHistory.Enqueue(currentMetrics); + + // 限制历史记录数量 + while (_metricsHistory.Count > MaxHistoryCount) + { + _metricsHistory.TryDequeue(out _); + } + + // 检查性能警告 + CheckPerformanceWarnings(currentMetrics); + + // 更新活动下载统计 + UpdateActiveDownloadStats(); + } + catch (Exception ex) + { + LogWrapper.Error(ex, LogModule, "性能监控出错"); + } + } + + /// + /// 更新活动下载统计信息 + /// + private void UpdateActiveDownloadStats() + { + // 这里可以从实际的下载任务中更新统计信息 + // TODO:暂时保持空实现,实际使用时需要与具体的下载任务集成 + } + + /// + /// 检查性能警告 + /// + private void CheckPerformanceWarnings(DownloadMetrics metrics) + { + if (!EnableDetailedMonitoring) return; + + var issues = new List(); + var recommendations = new List(); + var health = DownloadHealth.Good; + + // 检查下载速度 + if (metrics.BytesPerSecond > 0 && metrics.BytesPerSecond < PerformanceWarningThreshold) + { + health = DownloadHealth.Poor; + issues.Add($"下载速度过低: {metrics.BytesPerSecond / 1024:F1} KB/s"); + recommendations.Add("检查网络连接或增加并发数"); + } + + // 检查CPU使用率 + if (metrics.CpuUsagePercent > 80) + { + issues.Add($"CPU使用率过高: {metrics.CpuUsagePercent:F1}%"); + recommendations.Add("考虑减少并发线程数"); + } + + // 检查内存使用 + if (metrics.MemoryUsageBytes > 500 * 1024 * 1024) // 500MB + { + issues.Add($"内存使用过高: {metrics.MemoryUsageBytes / 1024 / 1024:F1} MB"); + recommendations.Add("检查是否有内存泄漏或减少缓冲区大小"); + } + + if (issues.Any()) + { + var diagnosis = new DownloadDiagnosis + { + Health = health, + HealthDescription = "检测到性能问题", + Issues = issues, + Recommendations = recommendations, + EfficiencyScore = CalculateEfficiencyScore(metrics) + }; + + PerformanceWarning?.Invoke(diagnosis); + } + } + + /// + /// 计算效率分数 + /// + private double CalculateEfficiencyScore(DownloadMetrics metrics) + { + var score = 100.0; + + // 速度评分 + if (metrics.BytesPerSecond < PerformanceWarningThreshold) + score -= 30; + + // CPU评分 + if (metrics.CpuUsagePercent > 80) + score -= 20; + else if (metrics.CpuUsagePercent > 60) + score -= 10; + + // 内存评分 + if (metrics.MemoryUsageBytes > 500 * 1024 * 1024) + score -= 20; + else if (metrics.MemoryUsageBytes > 200 * 1024 * 1024) + score -= 10; + + return Math.Max(0, score); + } + + /// + /// 下载完成处理 + /// + private void OnDownloadCompleted(string taskId, EnhancedMultiThreadDownloadTask task) + { + if (_activeDownloads.TryRemove(taskId, out var stats)) + { + var finalStats = task.GetDetailedStatus(); + DownloadCompleted?.Invoke(taskId, finalStats); + + LogWrapper.Info(LogModule, $"下载完成: {taskId}, " + + $"平均速度: {finalStats.AverageSpeed / 1024 / 1024:F2} MB/s, " + + $"重试次数: {finalStats.RetryCount}"); + } + } + + /// + /// 估算预期下载速度 + /// + private double EstimateExpectedSpeed(long fileSize) + { + // 基于文件大小的简单估算,实际应用中可以基于历史数据 + if (fileSize < 1024 * 1024) // < 1MB + return 500 * 1024; // 500KB/s + else if (fileSize < 100 * 1024 * 1024) // < 100MB + return 2 * 1024 * 1024; // 2MB/s + else + return 5 * 1024 * 1024; // 5MB/s + } + + /// + /// 计算整体健康状态 + /// + private DownloadHealth CalculateOverallHealth(DownloadStatistics[] stats) + { + if (!stats.Any()) return DownloadHealth.Good; + + var avgSpeed = stats.Average(s => s.CurrentSpeed); + var totalRetries = stats.Sum(s => s.RetryCount); + var avgProgress = stats.Average(s => s.ProgressPercentage); + + if (avgSpeed > 2 * 1024 * 1024 && totalRetries < 10) // > 2MB/s, < 10 retries + return DownloadHealth.Excellent; + else if (avgSpeed > 1024 * 1024 && totalRetries < 20) // > 1MB/s, < 20 retries + return DownloadHealth.Good; + else if (avgSpeed > 512 * 1024) // > 512KB/s + return DownloadHealth.Fair; + else + return DownloadHealth.Poor; + } + + /// + /// 生成性能建议 + /// + private List GenerateRecommendations(DownloadStatistics[] stats, DownloadMetrics[] metrics) + { + var recommendations = new List(); + + if (!stats.Any()) return recommendations; + + var avgSpeed = stats.Average(s => s.CurrentSpeed); + var totalRetries = stats.Sum(s => s.RetryCount); + var avgThreads = stats.Average(s => s.ActiveThreads); + + if (avgSpeed < 1024 * 1024) // < 1MB/s + { + recommendations.Add("考虑增加下载线程数以提高速度"); + } + + if (totalRetries > 50) + { + recommendations.Add("重试次数较多,建议检查网络稳定性"); + } + + if (avgThreads < 2 && stats.Any(s => s.TotalBytes > 10 * 1024 * 1024)) + { + recommendations.Add("大文件建议使用多线程下载"); + } + + if (metrics.Any(m => m.CpuUsagePercent > 70)) + { + recommendations.Add("CPU使用率较高,可适当减少并发数"); + } + + return recommendations; + } + + private static readonly PerformanceCounter? _cpuCounter; + private static readonly PerformanceCounter? _memoryCounter; + private static readonly Process _currentProcess = Process.GetCurrentProcess(); + private DateTime _lastCpuTime = DateTime.UtcNow; + private TimeSpan _lastProcessorTime = _currentProcess.TotalProcessorTime; + + static DownloadMonitor() + { + try + { + _cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total"); + _memoryCounter = new PerformanceCounter("Memory", "Available MBytes"); + // Initialize CPU counter (first call returns 0) + _cpuCounter.NextValue(); + } + catch (Exception ex) + { + LogWrapper.Warn("DownloadMonitor", $"无法初始化性能计数器: {ex.Message}"); + } + } + + /// + /// 获取CPU使用率 + /// + private double GetCpuUsage() + { + try + { + // 先使用方法1:使用PerformanceCounter(Windows系统) + if (_cpuCounter != null) + { + return _cpuCounter.NextValue(); + } + + // 再使用方法2:手动计算当前进程CPU使用率 + var currentTime = DateTime.UtcNow; + var currentProcessorTime = _currentProcess.TotalProcessorTime; + + var cpuUsedMs = (currentProcessorTime - _lastProcessorTime).TotalMilliseconds; + var totalMsPassed = (currentTime - _lastCpuTime).TotalMilliseconds; + var cpuUsageTotal = cpuUsedMs / (Environment.ProcessorCount * totalMsPassed); + + _lastCpuTime = currentTime; + _lastProcessorTime = currentProcessorTime; + + return Math.Max(0, Math.Min(100, cpuUsageTotal * 100)); + } + catch (Exception ex) + { + LogWrapper.Debug("DownloadMonitor", $"获取CPU使用率失败: {ex.Message}"); + return 0.0; + } + } + + /// + /// 获取内存使用量 + /// + private long GetMemoryUsage() + { + try + { + // 获取当前进程的工作集内存 + _currentProcess.Refresh(); + var workingSet = _currentProcess.WorkingSet64; + + // 获取.NET管理的内存 + var managedMemory = GC.GetTotalMemory(false); + + // 获取进程私有内存 + var privateMemory = _currentProcess.PrivateMemorySize64; + + // 返回工作集内存(实际物理内存使用量) + return workingSet; + } + catch (Exception ex) + { + LogWrapper.Debug("DownloadMonitor", $"获取内存使用量失败: {ex.Message}"); + // 降级到GC内存统计 + return GC.GetTotalMemory(false); + } + } + + /// + /// 获取详细内存信息 + /// + public (long WorkingSet, long PrivateMemory, long ManagedMemory, long AvailableMemory) GetDetailedMemoryInfo() + { + try + { + _currentProcess.Refresh(); + var workingSet = _currentProcess.WorkingSet64; + var privateMemory = _currentProcess.PrivateMemorySize64; + var managedMemory = GC.GetTotalMemory(false); + + long availableMemory = 0; + if (_memoryCounter != null) + { + availableMemory = (long)_memoryCounter.NextValue() * 1024 * 1024; // Convert MB to bytes + } + + return (workingSet, privateMemory, managedMemory, availableMemory); + } + catch (Exception ex) + { + LogWrapper.Debug("DownloadMonitor", $"获取详细内存信息失败: {ex.Message}"); + var managedMemory = GC.GetTotalMemory(false); + return (managedMemory, managedMemory, managedMemory, 0); + } + } + + /// + /// 释放资源 + /// + public void Dispose() + { + if (_disposed) return; + + _monitoringTimer?.Dispose(); + _activeDownloads.Clear(); + _disposed = true; + + LogWrapper.Info(LogModule, "下载监控器已释放"); + GC.SuppressFinalize(this); + } +} + +/// +/// 性能报告 +/// +public class PerformanceReport +{ + public DateTime GeneratedAt { get; set; } + public int ActiveDownloads { get; set; } + public long TotalBytesTransferred { get; set; } + public double AverageSpeed { get; set; } + public long PeakSpeed { get; set; } + public int TotalRetries { get; set; } + public double AverageConnections { get; set; } + public DownloadHealth OverallHealth { get; set; } + public List Recommendations { get; set; } = new(); + + public override string ToString() + { + return $"性能报告 ({GeneratedAt:yyyy-MM-dd HH:mm:ss})\n" + + $"活动下载: {ActiveDownloads}\n" + + $"总传输: {TotalBytesTransferred / 1024 / 1024:F1} MB\n" + + $"平均速度: {AverageSpeed / 1024 / 1024:F2} MB/s\n" + + $"峰值速度: {PeakSpeed / 1024 / 1024:F2} MB/s\n" + + $"总重试: {TotalRetries}\n" + + $"整体健康: {OverallHealth}"; + } +} diff --git a/Net/EnhancedMultiThreadDownloadTask.cs b/Net/EnhancedMultiThreadDownloadTask.cs new file mode 100644 index 00000000..ab4c3a1c --- /dev/null +++ b/Net/EnhancedMultiThreadDownloadTask.cs @@ -0,0 +1,799 @@ +using System; +using System.Buffers; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using PCL.Core.App.Tasks; +using PCL.Core.Logging; + +namespace PCL.Core.Net; + + +/// +/// 下载统计信息 +/// +public class DownloadStatistics +{ + public DateTime StartTime { get; set; } + public DateTime? EndTime { get; set; } + public long TotalBytes { get; set; } + public long DownloadedBytes { get; set; } + public double CurrentSpeed { get; set; } // bytes/second + public double AverageSpeed { get; set; } // bytes/second + public double PeakSpeed { get; set; } // bytes/second + public int ActiveThreads { get; set; } + public int RetryCount { get; set; } + public TimeSpan ElapsedTime => (EndTime ?? DateTime.Now) - StartTime; + public double ProgressPercentage => TotalBytes > 0 ? (double)DownloadedBytes / TotalBytes * 100 : 0; + public TimeSpan EstimatedTimeRemaining => CurrentSpeed > 0 ? + TimeSpan.FromSeconds((TotalBytes - DownloadedBytes) / CurrentSpeed) : TimeSpan.Zero; +} + +/// +/// 速度控制器 +/// +public class SpeedController +{ + private readonly long _speedLimit; + private readonly SemaphoreSlim _semaphore; + private DateTime _lastCheck = DateTime.Now; + private long _bytesInCurrentSecond = 0; + + public SpeedController(long speedLimit) + { + _speedLimit = speedLimit; + _semaphore = new SemaphoreSlim(1, 1); + } + + public async Task CanTransfer(int bytes, CancellationToken cancellationToken) + { + if (_speedLimit <= 0) return true; + + await _semaphore.WaitAsync(cancellationToken); + try + { + var now = DateTime.Now; + var elapsed = (now - _lastCheck).TotalMilliseconds; + + if (elapsed >= 1000) // Reset every second + { + _lastCheck = now; + _bytesInCurrentSecond = 0; + } + + if (_bytesInCurrentSecond + bytes <= _speedLimit) + { + _bytesInCurrentSecond += bytes; + return true; + } + + // Need to wait + var remainingTime = 1000 - elapsed; + if (remainingTime > 0) + { + await Task.Delay((int)remainingTime, cancellationToken); + _lastCheck = DateTime.Now; + _bytesInCurrentSecond = bytes; + } + + return true; + } + finally + { + _semaphore.Release(); + } + } +} + +/// +/// 多线程下载任务 +/// 支持断点续传、速度限制、连接池复用等高级功能 +/// +public class EnhancedMultiThreadDownloadTask : IObservableTaskStateSource, IObservableProgressSource +{ + private const string LogModule = "EnhancedMultiThreadDownload"; + + private readonly Uri _sourceUri; + private readonly string _targetPath; + private readonly string _tempPath; + private readonly DownloadConfiguration _config; + private readonly DownloadStatistics _stats = new(); + private readonly SpeedController _speedController; + private readonly ArrayPool _bufferPool = ArrayPool.Shared; + + private long _totalSize = 0; + private long _totalDownloaded = 0; + private readonly ConcurrentQueue _pendingChunks = new(); + private readonly ConcurrentDictionary _activeChunks = new(); + private readonly object _statsLock = new(); + private CancellationTokenSource? _internalCts; + private Timer? _speedCalculationTimer; + + // 性能计数器 + private long _lastSpeedCheck = 0; + private DateTime _lastSpeedTime = DateTime.Now; + + public DownloadStatistics Statistics => _stats; + + // 接口实现所需的字段和属性 + private TaskState _state = TaskState.Waiting; + private double _progress = 0.0; + private readonly CancellationToken? _cancellationToken; + + // 接口实现 - IObservableTaskStateSource + public string Name { get; } + public string? Description { get; } + + public TaskState State + { + get => _state; + private set + { + var oldState = _state; + _state = value; + StateChanged?.Invoke(this, oldState, value); + } + } + + // 接口实现 - IObservableProgressSource + public double Progress + { + get => _progress; + private set + { + var oldProgress = _progress; + _progress = Math.Max(0, Math.Min(1, value)); + ProgressChanged?.Invoke(this, oldProgress, _progress); + } + } + + // 接口实现 - 事件 + public event StateChangedHandler? StateChanged; + public event StateChangedHandler? ProgressChanged; + + // 显式接口实现 + event StateChangedHandler? IStateChangedSource.StateChanged + { + add => StateChanged += value; + remove => StateChanged -= value; + } + + event StateChangedHandler? IStateChangedSource.StateChanged + { + add => ProgressChanged += value; + remove => ProgressChanged -= value; + } + + /// + /// 创建多线程下载任务 + /// + public EnhancedMultiThreadDownloadTask( + Uri sourceUri, + string targetPath, + DownloadConfiguration? config = null, + CancellationToken? cancellationToken = null) + { + _sourceUri = sourceUri ?? throw new ArgumentNullException(nameof(sourceUri)); + _targetPath = Path.GetFullPath(targetPath); + _tempPath = _targetPath + ".download"; + _config = config ?? new DownloadConfiguration(); + _speedController = new SpeedController(_config.SpeedLimit); + _cancellationToken = cancellationToken; + + // 设置任务信息 + Name = $"下载 {Path.GetFileName(targetPath)}"; + Description = $"从 {sourceUri} 下载到 {targetPath}"; + + // 验证配置 + ValidateConfiguration(); + + LogWrapper.Info(LogModule, $"创建下载任务: {Name}, 配置: {_config.ThreadCount}线程, {_config.ChunkSize / 1024 / 1024}MB分块"); + } + + private void ValidateConfiguration() + { + _config.ThreadCount = Math.Max(1, Math.Min(_config.ThreadCount, 32)); + _config.ChunkSize = Math.Max(64 * 1024, _config.ChunkSize); // 最小64KB + _config.BufferSize = Math.Max(4096, _config.BufferSize); // 最小4KB + _config.MaxRetries = Math.Max(0, _config.MaxRetries); + _config.TimeoutMs = Math.Max(5000, _config.TimeoutMs); // 最小5秒 + } + + /// + /// 执行下载任务 + /// + public async Task RunAsync() + { + if (State != TaskState.Waiting) + throw new InvalidOperationException($"任务已执行,当前状态: {State}"); + + State = TaskState.Running; + Progress = 0; + _stats.StartTime = DateTime.Now; + + try + { + _internalCts = CancellationTokenSource.CreateLinkedTokenSource(_cancellationToken ?? System.Threading.CancellationToken.None); + var token = _internalCts.Token; + + LogWrapper.Info(LogModule, $"开始下载: {_sourceUri} -> {_targetPath}"); + + // 启动速度计算定时器 + StartSpeedCalculationTimer(); + + // 1. 检查断点续传 + Progress = 0.02; + var resumeInfo = await CheckResumeCapabilityAsync(token); + + // 2. 获取文件信息 + Progress = 0.05; + var fileInfo = await GetFileInfoAsync(token); + + if (!fileInfo.SupportsRange || fileInfo.ContentLength <= _config.ChunkSize) + { + LogWrapper.Info(LogModule, "使用单线程下载模式"); + return await DownloadSingleThreadAsync(token); + } + + _totalSize = fileInfo.ContentLength; + _stats.TotalBytes = _totalSize; + + // 3. 处理断点续传或创建新下载 + Progress = 0.08; + if (resumeInfo.CanResume && resumeInfo.ExistingSize > 0) + { + LogWrapper.Info(LogModule, $"断点续传: 已下载 {resumeInfo.ExistingSize:N0} / {_totalSize:N0} bytes"); + Interlocked.Exchange(ref _totalDownloaded, resumeInfo.ExistingSize); + CreateResumeChunks(resumeInfo.ExistingSize, _totalSize); + } + else + { + CreateDownloadChunks(_totalSize); + PrepareTargetFile(); + } + + // 4. 执行多线程下载 + Progress = 0.1; + await DownloadMultiThreadAsync(token); + + // 5. 验证和完成 + Progress = 0.95; + await FinalizeDownloadAsync(token); + + Progress = 1.0; + State = TaskState.Completed; + _stats.EndTime = DateTime.Now; + + var result = CreateSuccessResult(); + LogWrapper.Info(LogModule, $"下载完成: 大小={_totalSize:N0} bytes, 耗时={_stats.ElapsedTime.TotalSeconds:F1}s, 平均速度={result.AverageSpeed / 1024 / 1024:F2}MB/s"); + return result; + } + catch (OperationCanceledException) + { + State = TaskState.Canceled; + LogWrapper.Info(LogModule, $"下载已取消: {_targetPath}"); + return CreateCancelledResult(); + } + catch (Exception ex) + { + State = TaskState.Failed; + LogWrapper.Error(ex, LogModule, $"下载失败: {_targetPath}"); + return CreateFailedResult(ex); + } + finally + { + _speedCalculationTimer?.Dispose(); + _internalCts?.Dispose(); + } + } + + /// + /// 检查断点续传能力 + /// + private async Task<(bool CanResume, long ExistingSize)> CheckResumeCapabilityAsync(CancellationToken token) + { + if (!_config.EnableResumeSupport) + return (false, 0); + + try + { + if (File.Exists(_tempPath)) + { + var existingSize = new FileInfo(_tempPath).Length; + if (existingSize > 0) + { + // 验证服务器是否支持Range请求 + using var client = GetHttpClient(); + using var request = new HttpRequestMessage(HttpMethod.Head, _sourceUri); + using var response = await client.SendAsync(request, token); + + var supportsRange = response.Headers.AcceptRanges?.Contains("bytes") == true; + if (supportsRange) + { + LogWrapper.Info(LogModule, $"检测到断点续传文件: {existingSize:N0} bytes"); + return (true, existingSize); + } + } + } + else if (File.Exists(_targetPath)) + { + // 目标文件已存在,检查是否完整 + var existingSize = new FileInfo(_targetPath).Length; + using var client = GetHttpClient(); + using var request = new HttpRequestMessage(HttpMethod.Head, _sourceUri); + using var response = await client.SendAsync(request, token); + + var serverSize = response.Content.Headers.ContentLength ?? 0; + if (existingSize == serverSize) + { + LogWrapper.Info(LogModule, "文件已完整存在,跳过下载"); + return (true, existingSize); + } + } + } + catch (Exception ex) + { + LogWrapper.Warn(LogModule, $"检查断点续传失败: {ex.Message}"); + } + + return (false, 0); + } + + /// + /// 创建断点续传的下载分块 + /// + private void CreateResumeChunks(long existingSize, long totalSize) + { + if (existingSize >= totalSize) + { + // 文件已完整 + Interlocked.Exchange(ref _totalDownloaded, totalSize); + return; + } + + var remainingSize = totalSize - existingSize; + var chunkCount = Math.Min(_config.ThreadCount, (int)Math.Ceiling((double)remainingSize / _config.ChunkSize)); + var chunkSize = remainingSize / chunkCount; + + for (int i = 0; i < chunkCount; i++) + { + var start = existingSize + (i * chunkSize); + var end = i == chunkCount - 1 ? totalSize - 1 : start + chunkSize - 1; + + var chunk = new DownloadChunk + { + Id = i, + StartPosition = start, + EndPosition = end, + Status = ChunkStatus.Waiting + }; + + _pendingChunks.Enqueue(chunk); + } + + LogWrapper.Info(LogModule, $"断点续传: 创建 {chunkCount} 个分块下载剩余 {remainingSize:N0} bytes"); + } + + /// + /// 获取HttpClient + /// + private HttpClient GetHttpClient() + { + if (_config.EnableConnectionPooling) + { + return NetworkService.GetClient(); + } + else + { + var client = new HttpClient(); + client.Timeout = TimeSpan.FromMilliseconds(_config.TimeoutMs); + return client; + } + } + + /// + /// 多线程下载 + /// + private async Task DownloadMultiThreadAsync(CancellationToken token) + { + var downloadTasks = new List(); + + // 启动工作线程 + for (int i = 0; i < _config.ThreadCount; i++) + { + var threadId = i; + var task = Task.Run(async () => await OptimizedDownloadWorkerAsync(threadId, token), token); + downloadTasks.Add(task); + } + + // 等待所有下载任务完成 + await Task.WhenAll(downloadTasks); + } + + /// + /// 下载工作线程 + /// + private async Task OptimizedDownloadWorkerAsync(int workerId, CancellationToken token) + { + using var client = GetHttpClient(); + var buffer = _bufferPool.Rent(_config.BufferSize); + + try + { + if (_config.VerboseLogging) + LogWrapper.Debug(LogModule, $"工作线程 {workerId} 启动 (缓冲区: {_config.BufferSize:N0} bytes)"); + + while (!token.IsCancellationRequested && _pendingChunks.TryDequeue(out var chunk)) + { + _activeChunks[workerId] = chunk; + await DownloadChunkOptimizedAsync(client, chunk, buffer, workerId, token); + _activeChunks.TryRemove(workerId, out _); + } + + if (_config.VerboseLogging) + LogWrapper.Debug(LogModule, $"工作线程 {workerId} 完成"); + } + finally + { + _bufferPool.Return(buffer); + if (!_config.EnableConnectionPooling) + { + client.Dispose(); + } + } + } + + /// + /// 分块下载 + /// + private async Task DownloadChunkOptimizedAsync(HttpClient client, DownloadChunk chunk, byte[] buffer, int workerId, CancellationToken token) + { + for (int attempt = 0; attempt <= _config.MaxRetries; attempt++) + { + if (token.IsCancellationRequested) return; + + try + { + chunk.Status = ChunkStatus.Downloading; + chunk.LastError = null; + + using var request = new HttpRequestMessage(HttpMethod.Get, _sourceUri); + request.Headers.Range = new RangeHeaderValue( + chunk.StartPosition + chunk.DownloadedBytes, + chunk.EndPosition); + + using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token); + response.EnsureSuccessStatusCode(); + + using var contentStream = await response.Content.ReadAsStreamAsync(token); + + // 使用异步文件IO + using var fileStream = new FileStream( + _tempPath, + FileMode.OpenOrCreate, + FileAccess.Write, + FileShare.Write, + bufferSize: 4096, + useAsync: true); + + fileStream.Position = chunk.StartPosition + chunk.DownloadedBytes; + + int bytesRead; + while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length, token)) > 0) + { + // 速度限制检查 + await _speedController.CanTransfer(bytesRead, token); + + await fileStream.WriteAsync(buffer, 0, bytesRead, token); + await fileStream.FlushAsync(token); + + // 更新统计信息 + UpdateDownloadProgress(chunk, bytesRead); + } + + chunk.Status = ChunkStatus.Completed; + + if (_config.VerboseLogging) + LogWrapper.Debug(LogModule, $"线程 {workerId} 完成分块 {chunk.Id}"); + + return; + } + catch (Exception ex) when (attempt < _config.MaxRetries) + { + chunk.LastError = ex; + chunk.RetryCount = attempt + 1; + _stats.RetryCount++; + + LogWrapper.Warn(LogModule, $"线程 {workerId} 分块 {chunk.Id} 重试 {attempt + 1}/{_config.MaxRetries}: {ex.Message}"); + + // 指数退避 + var delay = Math.Min(1000 * (int)Math.Pow(2, attempt), 10000); + await Task.Delay(delay, token); + } + catch (Exception ex) + { + chunk.Status = ChunkStatus.Failed; + chunk.LastError = ex; + LogWrapper.Error(ex, LogModule, $"线程 {workerId} 分块 {chunk.Id} 最终失败"); + throw; + } + } + } + + /// + /// 更新下载进度 + /// + private void UpdateDownloadProgress(DownloadChunk chunk, int bytesRead) + { + lock (_statsLock) + { + chunk.DownloadedBytes += bytesRead; + Interlocked.Add(ref _totalDownloaded, bytesRead); + + // 更新统计 + _stats.DownloadedBytes = Interlocked.Read(ref _totalDownloaded); + _stats.ActiveThreads = _activeChunks.Count; + + // 实时更新进度 - Task机制会自动读取Progress属性 + if (_totalSize > 0) + { + var totalDownloaded = Interlocked.Read(ref _totalDownloaded); + var currentProgress = 0.1 + (0.85 * totalDownloaded / _totalSize); + Progress = Math.Min(currentProgress, 0.95); + } + } + } + + /// + /// 启动速度计算定时器 + /// + private void StartSpeedCalculationTimer() + { + _speedCalculationTimer = new Timer(CalculateSpeed, null, TimeSpan.Zero, TimeSpan.FromSeconds(1)); + } + + /// + /// 计算下载速度 + /// + private void CalculateSpeed(object? state) + { + var now = DateTime.Now; + var currentDownloaded = Interlocked.Read(ref _totalDownloaded); + + lock (_statsLock) + { + var timeDiff = (now - _lastSpeedTime).TotalSeconds; + if (timeDiff > 0) + { + var bytesDiff = currentDownloaded - _lastSpeedCheck; + _stats.CurrentSpeed = bytesDiff / timeDiff; + + // 更新峰值速度 + if (_stats.CurrentSpeed > _stats.PeakSpeed) + _stats.PeakSpeed = _stats.CurrentSpeed; + + // 计算平均速度 + var totalTime = _stats.ElapsedTime.TotalSeconds; + if (totalTime > 0) + _stats.AverageSpeed = currentDownloaded / totalTime; + } + + _lastSpeedCheck = currentDownloaded; + _lastSpeedTime = now; + } + } + + + /// + /// 获取文件信息 + /// + private async Task<(bool SupportsRange, long ContentLength)> GetFileInfoAsync(CancellationToken token) + { + using var client = GetHttpClient(); + using var request = new HttpRequestMessage(HttpMethod.Head, _sourceUri); + using var response = await client.SendAsync(request, token); + response.EnsureSuccessStatusCode(); + + var supportsRange = response.Headers.AcceptRanges?.Contains("bytes") == true; + var contentLength = response.Content.Headers.ContentLength ?? 0; + + LogWrapper.Info(LogModule, $"文件信息: 大小={contentLength:N0} bytes, 支持分块={supportsRange}"); + return (supportsRange, contentLength); + } + + /// + /// 创建下载分块 + /// + private void CreateDownloadChunks(long totalSize) + { + var chunkCount = Math.Min(_config.ThreadCount, (int)Math.Ceiling((double)totalSize / _config.ChunkSize)); + if (chunkCount <= 0) + { + LogWrapper.Error(LogModule, $"无法创建下载分块: chunkCount={chunkCount} (ThreadCount={_config.ThreadCount}, totalSize={totalSize}, ChunkSize={_config.ChunkSize})"); + return; + } + var chunkSize = totalSize / chunkCount; + var remainder = totalSize % chunkCount; + + for (int i = 0; i < chunkCount; i++) + { + var start = i * chunkSize; + var end = start + chunkSize - 1; + if (i == chunkCount - 1) + end += remainder; + + var chunk = new DownloadChunk + { + Id = i, + StartPosition = start, + EndPosition = end, + Status = ChunkStatus.Waiting + }; + + _pendingChunks.Enqueue(chunk); + } + + LogWrapper.Info(LogModule, $"创建 {chunkCount} 个下载分块"); + } + + /// + /// 准备目标文件 + /// + private void PrepareTargetFile() + { + var directory = Path.GetDirectoryName(_tempPath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + if (_config.PreAllocateFile && _totalSize > 0) + { + using var fs = new FileStream(_tempPath, FileMode.Create, FileAccess.Write); + fs.SetLength(_totalSize); + } + } + + /// + /// 完成下载 + /// + private async Task FinalizeDownloadAsync(CancellationToken token) + { + await Task.Run(() => + { + // 验证文件完整性 + if (!File.Exists(_tempPath)) + throw new FileNotFoundException("临时下载文件不存在"); + + var actualSize = new FileInfo(_tempPath).Length; + if (actualSize != _totalSize) + throw new InvalidDataException($"文件大小不匹配: 期望 {_totalSize:N0}, 实际 {actualSize:N0}"); + + // 移动到最终位置 + if (File.Exists(_targetPath)) + File.Delete(_targetPath); + + File.Move(_tempPath, _targetPath); + + LogWrapper.Info(LogModule, "文件验证和移动完成"); + }, token); + } + + /// + /// 单线程下载 + /// + private async Task DownloadSingleThreadAsync(CancellationToken token) + { + using var client = GetHttpClient(); + using var response = await client.GetAsync(_sourceUri, HttpCompletionOption.ResponseHeadersRead, token); + response.EnsureSuccessStatusCode(); + + _totalSize = response.Content.Headers.ContentLength ?? 0; + _stats.TotalBytes = _totalSize; + + using var contentStream = await response.Content.ReadAsStreamAsync(token); + using var fileStream = new FileStream(_targetPath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, true); + + var buffer = _bufferPool.Rent(_config.BufferSize); + try + { + int bytesRead; + while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length, token)) > 0) + { + await _speedController.CanTransfer(bytesRead, token); + await fileStream.WriteAsync(buffer, 0, bytesRead, token); + + Interlocked.Add(ref _totalDownloaded, bytesRead); + _stats.DownloadedBytes = Interlocked.Read(ref _totalDownloaded); + + if (_totalSize > 0) + { + var totalDownloaded = Interlocked.Read(ref _totalDownloaded); + Progress = 0.1 + (0.85 * totalDownloaded / _totalSize); + } + } + } + finally + { + _bufferPool.Return(buffer); + } + + // 设置完成状态 + Progress = 1.0; + State = TaskState.Completed; + _stats.EndTime = DateTime.Now; + + LogWrapper.Info(LogModule, $"单线程下载完成: 大小={_totalSize:N0} bytes, 耗时={_stats.ElapsedTime.TotalSeconds:F1}s"); + return CreateSuccessResult(); + } + + private MultiThreadDownloadResult CreateSuccessResult() + { + return new MultiThreadDownloadResult + { + FilePath = _targetPath, + TotalSize = _totalSize, + Duration = _stats.ElapsedTime, + AverageSpeed = _stats.AverageSpeed, + IsSuccess = true + }; + } + + private MultiThreadDownloadResult CreateCancelledResult() + { + return new MultiThreadDownloadResult + { + FilePath = _targetPath, + Duration = _stats.ElapsedTime, + IsSuccess = false, + ErrorMessage = "下载已取消" + }; + } + + private MultiThreadDownloadResult CreateFailedResult(Exception ex) + { + return new MultiThreadDownloadResult + { + FilePath = _targetPath, + Duration = _stats.ElapsedTime, + IsSuccess = false, + ErrorMessage = ex.Message + }; + } + + /// + /// 取消下载 + /// + public void CancelDownload() + { + _internalCts?.Cancel(); + LogWrapper.Info(LogModule, $"取消下载: {_targetPath}"); + } + + /// + /// 获取详细状态信息 + /// + public DownloadStatistics GetDetailedStatus() + { + lock (_statsLock) + { + return new DownloadStatistics + { + StartTime = _stats.StartTime, + EndTime = _stats.EndTime, + TotalBytes = _stats.TotalBytes, + DownloadedBytes = _stats.DownloadedBytes, + CurrentSpeed = _stats.CurrentSpeed, + AverageSpeed = _stats.AverageSpeed, + PeakSpeed = _stats.PeakSpeed, + ActiveThreads = _stats.ActiveThreads, + RetryCount = _stats.RetryCount + }; + } + } +} diff --git a/Net/MultiThreadDownloadManager.cs b/Net/MultiThreadDownloadManager.cs new file mode 100644 index 00000000..a23acb90 --- /dev/null +++ b/Net/MultiThreadDownloadManager.cs @@ -0,0 +1,318 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PCL.Core.App.Tasks; +using PCL.Core.Logging; + +namespace PCL.Core.Net; + +/// +/// 多线程下载管理器 +/// 提供简化的API用于管理多个下载任务 +/// +public class MultiThreadDownloadManager : IDisposable +{ + private const string LogModule = "MultiThreadDownloadManager"; + + private readonly ConcurrentDictionary _activeTasks = new(); + private readonly CancellationTokenSource _globalCts = new(); + private bool _disposed = false; + + /// + /// 默认线程数 + /// + public int DefaultThreadCount { get; set; } = 4; + + /// + /// 默认分块大小 (字节) + /// + public int DefaultChunkSize { get; set; } = 1024 * 1024; // 1MB + + /// + /// 默认最大重试次数 + /// + public int DefaultMaxRetries { get; set; } = 3; + + /// + /// 默认超时时间 (毫秒) + /// + public int DefaultTimeoutMs { get; set; } = 30000; // 30秒 + + /// + /// 当前活动任务数量 + /// + public int ActiveTaskCount => _activeTasks.Count; + + /// + /// 获取所有活动任务 + /// + public IEnumerable ActiveTasks => _activeTasks.Values.ToArray(); + + /// + /// 创建并启动下载任务 + /// + /// 下载地址 + /// 保存路径 + /// 线程数(可选,使用默认值) + /// 分块大小(可选,使用默认值) + /// 最大重试次数(可选,使用默认值) + /// 超时时间(可选,使用默认值) + /// 进度回调(可选) + /// 状态变化回调(可选) + /// 下载任务 + public async Task DownloadAsync( + string url, + string filePath, + int? threadCount = null, + int? chunkSize = null, + int? maxRetries = null, + int? timeoutMs = null, + Action? progressCallback = null, + Action? stateCallback = null) + { + if (_disposed) + throw new ObjectDisposedException(nameof(MultiThreadDownloadManager)); + + if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) + throw new ArgumentException($"无效的URL: {url}", nameof(url)); + + var taskId = Path.GetFileName(filePath) ?? Guid.NewGuid().ToString(); + if (_activeTasks.ContainsKey(taskId)) + { + taskId += "_" + DateTime.Now.Ticks; + } + + var downloadTask = new MultiThreadDownloadTask( + uri, + filePath, + threadCount ?? DefaultThreadCount, + chunkSize ?? DefaultChunkSize, + maxRetries ?? DefaultMaxRetries, + timeoutMs ?? DefaultTimeoutMs, + 0, // speedLimitBytesPerSecond - 默认无限制 + 1000, // baseRetryDelayMs - 默认1秒 + true, // useExponentialBackoff - 默认启用指数退避 + _globalCts.Token + ); + + // 订阅进度和状态变化事件 + if (progressCallback != null) + { + downloadTask.ProgressChanged += (sender, oldValue, newValue) => progressCallback(newValue); + } + + if (stateCallback != null) + { + downloadTask.StateChanged += (sender, oldValue, newValue) => stateCallback(newValue); + } + + // 任务完成后自动从活动列表中移除 + downloadTask.StateChanged += (sender, oldState, newState) => + { + if (newState is TaskState.Completed or TaskState.Failed or TaskState.Canceled) + { + _activeTasks.TryRemove(taskId, out _); + LogWrapper.Info(LogModule, $"任务已完成并从活动列表中移除: {taskId}"); + } + }; + + _activeTasks[taskId] = downloadTask; + LogWrapper.Info(LogModule, $"开始下载任务: {taskId} [{url} -> {filePath}]"); + + // 在后台运行任务 + _ = Task.Run(async () => + { + try + { + await downloadTask.RunAsync(); + } + catch (Exception ex) + { + LogWrapper.Error(ex, LogModule, $"下载任务执行异常: {taskId}"); + } + }, _globalCts.Token); + + return downloadTask; + } + + /// + /// 同步下载文件 (阻塞直到完成) + /// + /// 下载地址 + /// 保存路径 + /// 线程数(可选) + /// 分块大小(可选) + /// 最大重试次数(可选) + /// 超时时间(可选) + /// 进度回调(可选) + /// 下载结果 + public async Task DownloadFileAsync( + string url, + string filePath, + int? threadCount = null, + int? chunkSize = null, + int? maxRetries = null, + int? timeoutMs = null, + Action? progressCallback = null) + { + var task = await DownloadAsync(url, filePath, threadCount, chunkSize, maxRetries, timeoutMs, progressCallback); + + // 等待任务完成 + while (task.State is TaskState.Waiting or TaskState.Running) + { + await Task.Delay(100); + } + + return task.Result ?? new MultiThreadDownloadResult + { + FilePath = filePath, + IsSuccess = false, + ErrorMessage = "任务未正确完成" + }; + } + + /// + /// 批量下载文件 + /// + /// 下载项列表(URL, 文件路径) + /// 最大并发数 + /// 每个任务的线程数 + /// 整体进度回调 + /// 所有下载结果 + public async Task> DownloadBatchAsync( + IEnumerable<(string Url, string FilePath)> downloads, + int maxConcurrency = 3, + int threadCountPerTask = 2, + Action? progressCallback = null) + { + var downloadList = downloads.ToList(); + if (!downloadList.Any()) + return new List(); + + var results = new ConcurrentBag(); + var completed = 0; + + var semaphore = new SemaphoreSlim(maxConcurrency, maxConcurrency); + var tasks = downloadList.Select(async download => + { + await semaphore.WaitAsync(_globalCts.Token); + try + { + var result = await DownloadFileAsync( + download.Url, + download.FilePath, + threadCountPerTask); + + results.Add(result); + + var currentCompleted = Interlocked.Increment(ref completed); + var overallProgress = (double)currentCompleted / downloadList.Count; + progressCallback?.Invoke(overallProgress); + + return result; + } + finally + { + semaphore.Release(); + } + }); + + await Task.WhenAll(tasks); + return results.ToList(); + } + + /// + /// 取消指定任务 + /// + /// 任务ID + /// 是否成功取消 + public bool CancelTask(string taskId) + { + if (_activeTasks.TryGetValue(taskId, out var task)) + { + task.CancelDownload(); + return true; + } + return false; + } + + /// + /// 取消所有活动任务 + /// + public void CancelAllTasks() + { + LogWrapper.Info(LogModule, $"取消所有活动任务,共 {_activeTasks.Count} 个"); + + foreach (var task in _activeTasks.Values) + { + task.CancelDownload(); + } + + _globalCts.Cancel(); + } + + /// + /// 获取总体下载统计信息 + /// + /// 统计信息 + public (int ActiveTasks, long TotalDownloaded, long TotalSize, double OverallSpeed) GetOverallStatistics() + { + var activeTasks = _activeTasks.Values.ToArray(); + var totalDownloaded = 0L; + var totalSize = 0L; + var totalSpeed = 0.0; + + foreach (var task in activeTasks) + { + var (downloaded, total, speed, _) = task.GetDownloadStatus(); + totalDownloaded += downloaded; + totalSize += total; + totalSpeed += speed; + } + + return (activeTasks.Length, totalDownloaded, totalSize, totalSpeed); + } + + /// + /// 等待所有任务完成 + /// + /// 超时时间(可选) + /// 是否所有任务都成功完成 + public async Task WaitForAllTasksAsync(TimeSpan? timeout = null) + { + var startTime = DateTime.Now; + + while (_activeTasks.Any()) + { + if (timeout.HasValue && DateTime.Now - startTime > timeout.Value) + { + LogWrapper.Warn(LogModule, "等待所有任务完成超时"); + return false; + } + + await Task.Delay(100); + } + + LogWrapper.Info(LogModule, "所有任务已完成"); + return true; + } + + /// + /// 释放资源 + /// + public void Dispose() + { + if (_disposed) return; + + CancelAllTasks(); + _globalCts.Dispose(); + _disposed = true; + + LogWrapper.Info(LogModule, "多线程下载管理器已释放"); + GC.SuppressFinalize(this); + } +} diff --git a/Net/MultiThreadDownloadTask.cs b/Net/MultiThreadDownloadTask.cs new file mode 100644 index 00000000..16d1cc69 --- /dev/null +++ b/Net/MultiThreadDownloadTask.cs @@ -0,0 +1,602 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using PCL.Core.App.Tasks; +using PCL.Core.Logging; + +namespace PCL.Core.Net; + +/// +/// 多线程下载任务结果 +/// +public class MultiThreadDownloadResult +{ + public string FilePath { get; set; } = string.Empty; + public long TotalSize { get; set; } + public TimeSpan Duration { get; set; } + public double AverageSpeed { get; set; } // bytes per second + public bool IsSuccess { get; set; } + public string? ErrorMessage { get; set; } +} + +/// +/// 下载分片信息 +/// +public class DownloadChunk +{ + public int Id { get; set; } + public long StartPosition { get; set; } + public long EndPosition { get; set; } + public long DownloadedBytes { get; set; } + public ChunkStatus Status { get; set; } = ChunkStatus.Waiting; + public Exception? LastError { get; set; } + public int RetryCount { get; set; } = 0; +} + +/// +/// 分片状态 +/// +public enum ChunkStatus +{ + Waiting, + Downloading, + Completed, + Failed, + Cancelled +} + +/// +/// 速度限制控制器 +/// +public class SpeedLimiter +{ + private readonly long _speedLimitBytesPerSecond; + private readonly object _lockObject = new(); + private DateTime _lastResetTime = DateTime.Now; + private long _bytesInCurrentSecond = 0; + + public SpeedLimiter(long speedLimitBytesPerSecond) + { + _speedLimitBytesPerSecond = speedLimitBytesPerSecond; + } + + public async Task WaitIfNeeded(int bytesToTransfer, CancellationToken cancellationToken) + { + if (_speedLimitBytesPerSecond <= 0) return; + + await Task.Run(() => + { + lock (_lockObject) + { + var now = DateTime.Now; + var timeSinceReset = now - _lastResetTime; + + // Reset every second + if (timeSinceReset.TotalMilliseconds >= 1000) + { + _lastResetTime = now; + _bytesInCurrentSecond = 0; + } + + _bytesInCurrentSecond += bytesToTransfer; + + // Calculate delay if needed + if (_bytesInCurrentSecond > _speedLimitBytesPerSecond) + { + var remainingTime = 1000 - timeSinceReset.TotalMilliseconds; + if (remainingTime > 0) + { + Thread.Sleep((int)remainingTime); + _lastResetTime = DateTime.Now; + _bytesInCurrentSecond = bytesToTransfer; + } + } + } + }, cancellationToken); + } +} + +/// +/// 高性能多线程下载任务 +/// 继承TaskBase以完全适配PCL.Core架构,支持IObservableProgressSource接口 +/// 支持速度限制和可配置重试间隔 +/// +public class MultiThreadDownloadTask : TaskBase +{ + private const string LogModule = "MultiThreadDownload"; + + private readonly Uri _sourceUri; + private readonly string _targetPath; + private readonly int _threadCount; + private readonly int _chunkSize; + private readonly int _maxRetries; + private readonly int _timeoutMs; + private readonly long _speedLimitBytesPerSecond; + private readonly int _baseRetryDelayMs; + private readonly bool _useExponentialBackoff; + + private long _totalSize = 0; + private long _totalDownloaded = 0; + private readonly ConcurrentQueue _chunks = new(); + private readonly ConcurrentDictionary _activeChunks = new(); + private readonly object _progressLock = new(); + private DateTime _startTime; + private CancellationTokenSource? _internalCts; + private readonly SpeedLimiter _speedLimiter; + + /// + /// 创建多线程下载任务 + /// + /// 下载源地址 + /// 目标文件路径 + /// 线程数量(默认4) + /// 分块大小(默认1MB) + /// 最大重试次数(默认3) + /// 超时时间毫秒(默认30秒) + /// 速度限制(字节/秒,0表示无限制,默认无限制) + /// 基础重试延迟毫秒(默认1秒) + /// 使用指数退避重试(默认true) + /// 取消令牌 + public MultiThreadDownloadTask( + Uri sourceUri, + string targetPath, + int threadCount = 4, + int chunkSize = 1024 * 1024, // 1MB + int maxRetries = 3, + int timeoutMs = 30000, + long speedLimitBytesPerSecond = 0, // 0 = 无限制 + int baseRetryDelayMs = 1000, // 1秒 + bool useExponentialBackoff = true, + CancellationToken? cancellationToken = null) + : base($"下载 {Path.GetFileName(targetPath)}", cancellationToken, $"从 {sourceUri} 下载到 {targetPath}") + { + _sourceUri = sourceUri ?? throw new ArgumentNullException(nameof(sourceUri)); + _targetPath = Path.GetFullPath(targetPath); + _threadCount = Math.Max(1, Math.Min(threadCount, 16)); // 限制在1-16个线程 + _chunkSize = Math.Max(1024, chunkSize); // 至少1KB + _maxRetries = Math.Max(0, maxRetries); + _timeoutMs = Math.Max(5000, timeoutMs); // 至少5秒 + _speedLimitBytesPerSecond = Math.Max(0, speedLimitBytesPerSecond); + _baseRetryDelayMs = Math.Max(100, baseRetryDelayMs); // 至少100ms + _useExponentialBackoff = useExponentialBackoff; + + // 初始化速度限制器 + _speedLimiter = new SpeedLimiter(_speedLimitBytesPerSecond); + + LogWrapper.Info(LogModule, $"创建多线程下载任务: {Name}, 线程数: {_threadCount}, 速度限制: {(_speedLimitBytesPerSecond > 0 ? $"{_speedLimitBytesPerSecond / 1024 / 1024:F1}MB/s" : "无限制")}"); + } + + /// + /// 执行下载 + /// + public override async Task RunAsync(params object[] objects) + { + if (State != TaskState.Waiting) + throw new InvalidOperationException($"任务已执行,当前状态: {State}"); + + State = TaskState.Running; + Progress = 0; + _startTime = DateTime.Now; + + try + { + _internalCts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken ?? System.Threading.CancellationToken.None); + var token = _internalCts.Token; + + LogWrapper.Info(LogModule, $"开始下载: {_sourceUri} -> {_targetPath}"); + + // 1. 获取文件信息 + Progress = 0.05; + var fileInfo = await GetFileInfoAsync(token); + if (!fileInfo.SupportsRange || fileInfo.ContentLength <= 0) + { + // 不支持分块下载,使用单线程 + LogWrapper.Info(LogModule, "服务器不支持分块下载,使用单线程模式"); + return await DownloadSingleThreadAsync(token); + } + + _totalSize = fileInfo.ContentLength; + + // 2. 创建下载分块 + Progress = 0.1; + CreateDownloadChunks(fileInfo.ContentLength); + + // 3. 准备目标文件 + PrepareTargetFile(); + + // 4. 启动多线程下载 + Progress = 0.15; + await DownloadMultiThreadAsync(token); + + // 5. 验证下载完整性 + Progress = 0.95; + await ValidateDownloadAsync(token); + + Progress = 1.0; + State = TaskState.Completed; + + var duration = DateTime.Now - _startTime; + var result = new MultiThreadDownloadResult + { + FilePath = _targetPath, + TotalSize = _totalSize, + Duration = duration, + AverageSpeed = _totalSize / duration.TotalSeconds, + IsSuccess = true + }; + + LogWrapper.Info(LogModule, $"下载完成: {_targetPath}, 大小: {_totalSize:N0} bytes, 耗时: {duration.TotalSeconds:F1}s, 平均速度: {result.AverageSpeed / 1024 / 1024:F2} MB/s"); + return result; + } + catch (OperationCanceledException) + { + State = TaskState.Canceled; + LogWrapper.Info(LogModule, $"下载已取消: {_targetPath}"); + return new MultiThreadDownloadResult + { + FilePath = _targetPath, + Duration = DateTime.Now - _startTime, + IsSuccess = false, + ErrorMessage = "下载已取消" + }; + } + catch (Exception ex) + { + State = TaskState.Failed; + LogWrapper.Error(ex, LogModule, $"下载失败: {_targetPath}"); + return new MultiThreadDownloadResult + { + FilePath = _targetPath, + Duration = DateTime.Now - _startTime, + IsSuccess = false, + ErrorMessage = ex.Message + }; + } + finally + { + _internalCts?.Dispose(); + } + } + + /// + /// 获取文件信息 + /// + private async Task<(bool SupportsRange, long ContentLength)> GetFileInfoAsync(CancellationToken token) + { + using var client = NetworkService.GetClient(); + using var request = new HttpRequestMessage(HttpMethod.Head, _sourceUri); + + using var response = await client.SendAsync(request, token); + response.EnsureSuccessStatusCode(); + + var supportsRange = response.Headers.AcceptRanges?.Contains("bytes") == true; + var contentLength = response.Content.Headers.ContentLength ?? 0; + + LogWrapper.Info(LogModule, $"文件信息: 大小={contentLength:N0} bytes, 支持分块={supportsRange}"); + return (supportsRange, contentLength); + } + + /// + /// 创建下载分块 + /// + private void CreateDownloadChunks(long totalSize) + { + var chunkCount = Math.Min(_threadCount, (int)Math.Ceiling((double)totalSize / _chunkSize)); + var chunkSize = totalSize / chunkCount; + var remainder = totalSize % chunkCount; + + for (int i = 0; i < chunkCount; i++) + { + var start = i * chunkSize; + var end = start + chunkSize - 1; + if (i == chunkCount - 1) // 最后一块包含余数 + end += remainder; + + var chunk = new DownloadChunk + { + Id = i, + StartPosition = start, + EndPosition = end, + Status = ChunkStatus.Waiting + }; + + _chunks.Enqueue(chunk); + } + + LogWrapper.Info(LogModule, $"创建了 {chunkCount} 个下载分块,每块大小约 {chunkSize:N0} bytes"); + } + + /// + /// 准备目标文件 + /// + private void PrepareTargetFile() + { + var directory = Path.GetDirectoryName(_targetPath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + // 预分配文件空间 + using var fs = new FileStream(_targetPath, FileMode.Create, FileAccess.Write, FileShare.Read); + fs.SetLength(_totalSize); + fs.Close(); + } + + /// + /// 多线程下载 + /// + private async Task DownloadMultiThreadAsync(CancellationToken token) + { + var downloadTasks = new List(); + + // 启动下载线程 + for (int i = 0; i < _threadCount; i++) + { + var threadId = i; + var task = Task.Run(async () => await DownloadWorkerAsync(threadId, token), token); + downloadTasks.Add(task); + } + + // 启动进度更新任务 + var progressTask = Task.Run(async () => await UpdateProgressAsync(token), token); + downloadTasks.Add(progressTask); + + // 等待所有任务完成 + await Task.WhenAll(downloadTasks); + } + + /// + /// 下载工作线程 + /// + private async Task DownloadWorkerAsync(int workerId, CancellationToken token) + { + using var client = NetworkService.GetClient(); + client.Timeout = TimeSpan.FromMilliseconds(_timeoutMs); + + LogWrapper.Debug(LogModule, $"工作线程 {workerId} 已启动"); + + while (!token.IsCancellationRequested && _chunks.TryDequeue(out var chunk)) + { + _activeChunks[workerId] = chunk; + await DownloadChunkAsync(client, chunk, workerId, token); + _activeChunks.TryRemove(workerId, out _); + } + + LogWrapper.Debug(LogModule, $"工作线程 {workerId} 已完成"); + } + + /// + /// 下载单个分块 + /// + private async Task DownloadChunkAsync(HttpClient client, DownloadChunk chunk, int workerId, CancellationToken token) + { + var buffer = new byte[8192]; // 8KB读取缓冲区 + + for (int attempt = 0; attempt <= _maxRetries; attempt++) + { + if (token.IsCancellationRequested) + return; + + try + { + chunk.Status = ChunkStatus.Downloading; + chunk.LastError = null; + + using var request = new HttpRequestMessage(HttpMethod.Get, _sourceUri); + request.Headers.Range = new RangeHeaderValue( + chunk.StartPosition + chunk.DownloadedBytes, + chunk.EndPosition); + + using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token); + response.EnsureSuccessStatusCode(); + + using var contentStream = await response.Content.ReadAsStreamAsync(token); + using var fileStream = new FileStream(_targetPath, FileMode.Open, FileAccess.Write, FileShare.Write); + + fileStream.Position = chunk.StartPosition + chunk.DownloadedBytes; + + int bytesRead; + while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length, token)) > 0) + { + // 速度限制检查 + await _speedLimiter.WaitIfNeeded(bytesRead, token); + + await fileStream.WriteAsync(buffer, 0, bytesRead, token); + + lock (_progressLock) + { + chunk.DownloadedBytes += bytesRead; + _totalDownloaded += bytesRead; + } + } + + chunk.Status = ChunkStatus.Completed; + LogWrapper.Debug(LogModule, $"工作线程 {workerId} 完成分块 {chunk.Id} ({chunk.StartPosition}-{chunk.EndPosition})"); + return; + } + catch (Exception ex) when (attempt < _maxRetries) + { + chunk.LastError = ex; + chunk.RetryCount = attempt + 1; + LogWrapper.Warn(LogModule, $"工作线程 {workerId} 分块 {chunk.Id} 下载失败 (重试 {attempt + 1}/{_maxRetries}): {ex.Message}"); + + // 计算重试延迟时间 + int retryDelay; + if (_useExponentialBackoff) + { + // 指数退避: baseDelay * 2^attempt,最大30秒 + retryDelay = Math.Min(_baseRetryDelayMs * (int)Math.Pow(2, attempt), 30000); + } + else + { + // 固定延迟 + retryDelay = _baseRetryDelayMs; + } + + LogWrapper.Debug(LogModule, $"工作线程 {workerId} 分块 {chunk.Id} 将在 {retryDelay}ms 后重试"); + await Task.Delay(retryDelay, token); + } + catch (Exception ex) + { + chunk.Status = ChunkStatus.Failed; + chunk.LastError = ex; + LogWrapper.Error(ex, LogModule, $"工作线程 {workerId} 分块 {chunk.Id} 下载失败,已达最大重试次数"); + throw; + } + } + } + + /// + /// 更新进度 + /// + private async Task UpdateProgressAsync(CancellationToken token) + { + while (!token.IsCancellationRequested) + { + try + { + lock (_progressLock) + { + if (_totalSize > 0) + { + var currentProgress = 0.15 + (0.8 * _totalDownloaded / _totalSize); // 15%-95%的进度区间 + Progress = Math.Min(currentProgress, 0.95); + } + } + + await Task.Delay(100, token); // 每100ms更新一次进度 + } + catch (OperationCanceledException) + { + break; + } + } + } + + /// + /// 单线程下载(用于不支持分块的服务器) + /// + private async Task DownloadSingleThreadAsync(CancellationToken token) + { + using var client = NetworkService.GetClient(); + using var response = await client.GetAsync(_sourceUri, HttpCompletionOption.ResponseHeadersRead, token); + response.EnsureSuccessStatusCode(); + + _totalSize = response.Content.Headers.ContentLength ?? 0; + + using var contentStream = await response.Content.ReadAsStreamAsync(token); + using var fileStream = new FileStream(_targetPath, FileMode.Create, FileAccess.Write); + + var buffer = new byte[8192]; + int bytesRead; + long totalDownloaded = 0; + + while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length, token)) > 0) + { + // 速度限制检查 + await _speedLimiter.WaitIfNeeded(bytesRead, token); + + await fileStream.WriteAsync(buffer, 0, bytesRead, token); + totalDownloaded += bytesRead; + + if (_totalSize > 0) + { + Progress = 0.15 + (0.8 * totalDownloaded / _totalSize); + } + } + + Progress = 1.0; + State = TaskState.Completed; + + var duration = DateTime.Now - _startTime; + return new MultiThreadDownloadResult + { + FilePath = _targetPath, + TotalSize = totalDownloaded, + Duration = duration, + AverageSpeed = totalDownloaded / duration.TotalSeconds, + IsSuccess = true + }; + } + + /// + /// 验证下载完整性 + /// + private async Task ValidateDownloadAsync(CancellationToken token) + { + await Task.Run(() => + { + var fileInfo = new FileInfo(_targetPath); + if (!fileInfo.Exists) + throw new FileNotFoundException("下载的文件不存在"); + + if (fileInfo.Length != _totalSize) + throw new InvalidDataException($"文件大小不匹配: 期望 {_totalSize:N0} bytes, 实际 {fileInfo.Length:N0} bytes"); + + LogWrapper.Debug(LogModule, "文件完整性验证通过"); + }, token); + } + + /// + /// 取消下载 + /// + public void CancelDownload() + { + _internalCts?.Cancel(); + LogWrapper.Info(LogModule, $"取消下载: {_targetPath}"); + } + + /// + /// 获取当前下载状态信息 + /// + public (long Downloaded, long Total, double Speed, int ActiveThreads) GetDownloadStatus() + { + lock (_progressLock) + { + var elapsed = DateTime.Now - _startTime; + var speed = elapsed.TotalSeconds > 0 ? _totalDownloaded / elapsed.TotalSeconds : 0; + return (_totalDownloaded, _totalSize, speed, _activeChunks.Count); + } + } + + /// + /// 获取当前速度限制设置(字节/秒) + /// + public long GetSpeedLimit() => _speedLimitBytesPerSecond; + + /// + /// 获取重试配置信息 + /// + public (int BaseRetryDelayMs, bool UseExponentialBackoff, int MaxRetries) GetRetryConfig() + { + return (_baseRetryDelayMs, _useExponentialBackoff, _maxRetries); + } + + /// + /// 获取详细下载配置信息 + /// + public string GetConfigurationInfo() + { + var speedLimitText = _speedLimitBytesPerSecond > 0 + ? $"{_speedLimitBytesPerSecond / 1024.0 / 1024.0:F2} MB/s" + : "无限制"; + + var retryStrategyText = _useExponentialBackoff ? "指数退避" : "固定间隔"; + + return $"配置信息:\n" + + $" 线程数: {_threadCount}\n" + + $" 分块大小: {_chunkSize / 1024.0 / 1024.0:F2} MB\n" + + $" 速度限制: {speedLimitText}\n" + + $" 最大重试: {_maxRetries}\n" + + $" 重试策略: {retryStrategyText}\n" + + $" 基础延迟: {_baseRetryDelayMs} ms\n" + + $" 超时时间: {_timeoutMs} ms"; + } +}