【Unity笔记】Unity超时检测器开发:支持自定义重试次数与事件触发

【Unity笔记】Unity超时检测器开发:支持自定义重试次数与事件触发

引言

超时检测器是 Unity 网络编程中的核心组件,用于监控异步操作(如 HTTP 请求、WebSocket 连接、AssetBundle 下载)的执行时间,防止长时间挂起导致应用卡死。超时检测器通过计时器 + 事件回调机制实现,支持自定义超时阈值、重试策略和异常处理,广泛应用于游戏联机、云存档、广告 SDK 集成等场景。

核心功能包括:超时检测自动重试事件通知资源清理。基于 Unity 的协程系统和 C# 事件委托实现,时间复杂度 O(1),支持 Unity 2020.3+。超时阈值通常设为 5-30 秒,视网络环境调整。

核心设计原理

1. 超时检测机制

  • 启动计时:异步操作开始时记录 startTime = Time.time
  • 周期检查:每帧/固定间隔检查 Time.time - startTime > timeoutThreshold
  • 超时触发:超出阈值时执行回调,取消原操作
  • 嵌套协程:使用 yield return new WaitUntil 或自定义 TimeoutCoroutine

2. 重试策略

策略描述适用场景
无重试超时即失败实时性要求高
固定间隔每次重试间隔固定简单场景
指数退避间隔呈指数增长网络波动大
随机抖动避免重试风暴多客户端

基础超时检测器实现

1. 核心 TimeoutDetector 组件

using UnityEngine;
using System;
using System.Collections;

public class TimeoutDetector : MonoBehaviour
{
    [System.Serializable]
    public class TimeoutConfig
    {
        [Header("超时设置")]
        public float timeoutSeconds = 10f;
        public int maxRetries = 3;
        public float retryDelay = 1f;
        public RetryStrategy retryStrategy = RetryStrategy.ExponentialBackoff;

        public enum RetryStrategy
        {
            None,
            FixedDelay,
            ExponentialBackoff,
            RandomJitter
        }
    }

    public TimeoutConfig config = new TimeoutConfig();

    // 事件系统
    public event Action<TimeoutResult> OnTimeout;
    public event Action<TimeoutResult> OnSuccess;
    public event Action<TimeoutResult> OnRetry;
    public event Action<TimeoutResult> OnFinalFailure;

    [System.Serializable]
    public class TimeoutResult
    {
        public bool IsSuccess { get; set; }
        public int RetryCount { get; set; }
        public float ElapsedTime { get; set; }
        public string ErrorMessage { get; set; }
        public object CustomData { get; set; }
    }

    private Coroutine timeoutCoroutine;

    /// <summary>
    /// 启动带超时的异步操作
    /// </summary>
    public Coroutine StartWithTimeout(Func<IEnumerator> operation, object customData = null)
    {
        StopCurrentTimeout();  // 停止现有超时

        TimeoutResult result = new TimeoutResult { CustomData = customData };
        timeoutCoroutine = StartCoroutine(TimeoutWrapper(operation, result));

        return timeoutCoroutine;
    }

    private IEnumerator TimeoutWrapper(Func<IEnumerator> operation, TimeoutResult result)
    {
        float startTime = Time.time;
        int retryCount = 0;

        while (retryCount <= config.maxRetries)
        {
            // 执行实际操作
            IEnumerator operationCoroutine = operation();
            bool operationCompleted = false;
            float operationStartTime = Time.time;

            // 超时检测协程
            Coroutine timeoutCheck = StartCoroutine(CheckTimeout(startTime, () => {
                operationCompleted = true;
                result.ErrorMessage = "Operation timed out";
            }));

            // 等待操作完成或超时
            yield return operationCoroutine;
            StopCoroutine(timeoutCheck);

            result.ElapsedTime = Time.time - operationStartTime;

            if (!operationCompleted)
            {
                // 操作成功完成
                result.IsSuccess = true;
                OnSuccess?.Invoke(result);
                yield break;
            }

            // 操作超时,处理重试
            retryCount++;
            result.RetryCount = retryCount;
            OnTimeout?.Invoke(result);
            OnRetry?.Invoke(result);

            if (retryCount > config.maxRetries)
            {
                result.IsSuccess = false;
                OnFinalFailure?.Invoke(result);
                yield break;
            }

            // 计算重试延迟
            float delay = CalculateRetryDelay(retryCount);
            yield return new WaitForSeconds(delay);
            startTime = Time.time;  // 重置超时计时
        }
    }

    private IEnumerator CheckTimeout(float startTime, Action onTimeout)
    {
        yield return new WaitUntil(() => Time.time - startTime > config.timeoutSeconds);
        onTimeout?.Invoke();
    }

    private float CalculateRetryDelay(int retryCount)
    {
        switch (config.retryStrategy)
        {
            case TimeoutConfig.RetryStrategy.FixedDelay:
                return config.retryDelay;

            case TimeoutConfig.RetryStrategy.ExponentialBackoff:
                return config.retryDelay * Mathf.Pow(2, retryCount);

            case TimeoutConfig.RetryStrategy.RandomJitter:
                return config.retryDelay + UnityEngine.Random.Range(0f, config.retryDelay);

            default:
                return 0f;
        }
    }

    public void StopCurrentTimeout()
    {
        if (timeoutCoroutine != null)
        {
            StopCoroutine(timeoutCoroutine);
            timeoutCoroutine = null;
        }
    }

    void OnDestroy()
    {
        StopCurrentTimeout();
    }
}

2. HTTP 请求超时示例

using UnityEngine;
using UnityEngine.Networking;
using System.Collections;

public class HTTPTimeoutExample : MonoBehaviour
{
    public TimeoutDetector timeoutDetector;
    public string apiUrl = "https://api.example.com/data";

    [ContextMenu("Test HTTP with Timeout")]
    void TestHTTPRequest()
    {
        StartCoroutine(HTTPRequestWithTimeout());
    }

    IEnumerator HTTPRequestWithTimeout()
    {
        // 配置超时:10秒,3次重试
        timeoutDetector.config.timeoutSeconds = 10f;
        timeoutDetector.config.maxRetries = 3;
        timeoutDetector.config.retryStrategy = TimeoutDetector.TimeoutConfig.RetryStrategy.ExponentialBackoff;

        return timeoutDetector.StartWithTimeout(() => MakeHTTPRequest(apiUrl));
    }

    private IEnumerator MakeHTTPRequest(string url)
    {
        using (UnityWebRequest request = UnityWebRequest.Get(url))
        {
            // 设置超时(UnityWebRequest 自带超时,配合使用)
            request.timeout = 8;  // 8秒硬件超时

            yield return request.SendWebRequest();

            if (request.result == UnityWebRequest.Result.Success)
            {
                Debug.Log("HTTP Success: " + request.downloadHandler.text);
            }
            else
            {
                Debug.LogError("HTTP Error: " + request.error);
                throw new System.Exception(request.error);  // 触发超时检测
            }
        }
    }
}

高级功能实现

3. AssetBundle 下载超时

public class AssetBundleTimeoutLoader : MonoBehaviour
{
    public TimeoutDetector timeoutDetector;

    public void LoadAssetBundleWithTimeout(string bundleURL, string bundleName)
    {
        timeoutDetector.StartWithTimeout(() => DownloadAssetBundle(bundleURL, bundleName));
    }

    private IEnumerator DownloadAssetBundle(string url, string bundleName)
    {
        using (UnityWebRequest request = UnityWebRequestAssetBundle.GetAssetBundle(url))
        {
            request.timeout = 30;  // 30秒下载超时
            yield return request.SendWebRequest();

            if (request.result != UnityWebRequest.Result.Success)
            {
                yield break;
            }

            AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request);
            if (bundle == null)
            {
                Debug.LogError("Failed to load AssetBundle");
                yield break;
            }

            // 使用 Bundle...
            bundle.Unload(false);
        }
    }
}

4. WebSocket 连接超时

using WebSocketSharp;
using System;

public class WebSocketTimeoutExample : MonoBehaviour
{
    public TimeoutDetector timeoutDetector;
    private WebSocket ws;

    public void ConnectWithTimeout(string wsUrl)
    {
        timeoutDetector.StartWithTimeout(() => ConnectWebSocket(wsUrl), wsUrl);

        timeoutDetector.OnFinalFailure += OnConnectionFailed;
        timeoutDetector.OnSuccess += OnConnectionSuccess;
    }

    private IEnumerator ConnectWebSocket(string url)
    {
        ws = new WebSocket(url);

        var tcs = new System.Threading.Tasks.TaskCompletionSource<bool>();

        ws.OnOpen += (sender, e) => {
            tcs.SetResult(true);
        };

        ws.OnError += (sender, e) => {
            tcs.SetException(new Exception(e.Message));
        };

        ws.Connect();

        yield return tcs.Task;
    }

    private void OnConnectionSuccess(TimeoutDetector.TimeoutResult result)
    {
        Debug.Log("WebSocket connected successfully");
        // 开始心跳检测
    }

    private void OnConnectionFailed(TimeoutDetector.TimeoutResult result)
    {
        Debug.LogError("WebSocket connection failed after timeout");
    }
}

5. 自定义超时上下文管理

public class TimeoutContext<T>
{
    public TimeoutDetector Detector { get; private set; }
    public T OperationData { get; set; }
    public bool IsActive => Detector.timeoutCoroutine != null;

    public TimeoutContext(TimeoutDetector detector)
    {
        Detector = detector;
    }

    public Coroutine ExecuteWithTimeout(Func<IEnumerator> operation)
    {
        return Detector.StartWithTimeout(operation, this);
    }

    public void Cancel()
    {
        Detector.StopCurrentTimeout();
    }
}

// 使用示例
public class GameSaveManager : MonoBehaviour
{
    private TimeoutContext<SaveData> saveContext;

    void Start()
    {
        saveContext = new TimeoutContext<SaveData>(timeoutDetector);
    }

    public void SaveGameWithTimeout(SaveData data)
    {
        saveContext.OperationData = data;
        saveContext.ExecuteWithTimeout(() => SaveToCloud(data));
    }
}

事件驱动与回调系统

6. 高级事件处理

public class AdvancedTimeoutEvents : MonoBehaviour
{
    public TimeoutDetector timeoutDetector;

    void Start()
    {
        // 注册详细事件
        timeoutDetector.OnTimeout += HandleTimeout;
        timeoutDetector.OnRetry += HandleRetry;
        timeoutDetector.OnSuccess += HandleSuccess;
        timeoutDetector.OnFinalFailure += HandleFinalFailure;
    }

    private void HandleTimeout(TimeoutDetector.TimeoutResult result)
    {
        Debug.LogWarning($"Operation timed out. Retry: {result.RetryCount}, Elapsed: {result.ElapsedTime}s");

        // UI 反馈
        ShowTimeoutWarning();

        // 分析超时原因
        AnalyzeTimeout(result);
    }

    private void HandleRetry(TimeoutDetector.TimeoutResult result)
    {
        float nextDelay = timeoutDetector.config.retryDelay * Mathf.Pow(2, result.RetryCount);
        Debug.Log($"Retrying in {nextDelay}s... (Attempt {result.RetryCount}/{timeoutDetector.config.maxRetries})");

        // 动态调整策略
        if (result.RetryCount > 2)
        {
            timeoutDetector.config.retryStrategy = TimeoutDetector.TimeoutConfig.RetryStrategy.RandomJitter;
        }
    }

    private void HandleSuccess(TimeoutDetector.TimeoutResult result)
    {
        Debug.Log($"Operation succeeded after {result.RetryCount} retries");
        HideTimeoutWarning();
    }

    private void HandleFinalFailure(TimeoutDetector.TimeoutResult result)
    {
        Debug.LogError($"Final failure after {timeoutDetector.config.maxRetries} retries: {result.ErrorMessage}");
        ShowOfflineMode();
    }
}

性能优化与配置

7. 全局超时管理器

public class GlobalTimeoutManager : MonoBehaviour
{
    public static GlobalTimeoutManager Instance;

    [Header("全局配置")]
    public TimeoutConfig defaultConfig;
    public int maxConcurrentOperations = 10;

    private TimeoutDetector[] detectors;

    void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);
            InitializeDetectors();
        }
        else
        {
            Destroy(gameObject);
        }
    }

    private void InitializeDetectors()
    {
        detectors = new TimeoutDetector[maxConcurrentOperations];
        for (int i = 0; i < maxConcurrentOperations; i++)
        {
            GameObject detectorObj = new GameObject($"TimeoutDetector_{i}");
            detectorObj.transform.SetParent(transform);
            detectors[i] = detectorObj.AddComponent<TimeoutDetector>();
            detectors[i].config = defaultConfig;
        }
    }

    public Coroutine ExecuteWithTimeout(Func<IEnumerator> operation, TimeoutConfig customConfig = null)
    {
        TimeoutDetector availableDetector = GetAvailableDetector();
        if (availableDetector == null)
        {
            Debug.LogWarning("No available timeout detectors");
            return null;
        }

        if (customConfig != null)
            availableDetector.config = customConfig;

        return availableDetector.StartWithTimeout(operation);
    }

    private TimeoutDetector GetAvailableDetector()
    {
        foreach (var detector in detectors)
        {
            if (detector.timeoutCoroutine == null)
                return detector;
        }
        return null;
    }
}

8. 配置优化建议

// 不同场景的推荐配置
public static class TimeoutPresets
{
    public static TimeoutConfig CriticalOperation()
    {
        return new TimeoutConfig
        {
            timeoutSeconds = 5f,      // 关键操作严格超时
            maxRetries = 0,           // 不重试
            retryStrategy = TimeoutConfig.RetryStrategy.None
        };
    }

    public static TimeoutConfig NetworkRequest()
    {
        return new TimeoutConfig
        {
            timeoutSeconds = 15f,     // 网络请求宽松
            maxRetries = 3,
            retryStrategy = TimeoutConfig.RetryStrategy.ExponentialBackoff
        };
    }

    public static TimeoutConfig AssetDownload()
    {
        return new TimeoutConfig
        {
            timeoutSeconds = 60f,     // 大文件下载长超时
            maxRetries = 2,
            retryStrategy = TimeoutConfig.RetryStrategy.FixedDelay
        };
    }
}

使用示例与集成

9. 完整使用流程

public class GameNetworkManager : MonoBehaviour
{
    private TimeoutDetector timeoutDetector;

    void Start()
    {
        timeoutDetector = GetComponent<TimeoutDetector>();

        // 玩家登录
        LoginWithTimeout();

        // 房间匹配
        StartCoroutine(MatchmakingWithTimeout());
    }

    void LoginWithTimeout()
    {
        var config = new TimeoutDetector.TimeoutConfig
        {
            timeoutSeconds = 10f,
            maxRetries = 2
        };

        timeoutDetector.config = config;
        timeoutDetector.StartWithTimeout(LoginOperation);
    }

    private IEnumerator LoginOperation()
    {
        // 模拟登录请求
        yield return new WaitForSeconds(UnityEngine.Random.Range(2f, 12f));

        if (UnityEngine.Random.value > 0.7f)  // 30% 模拟失败
            throw new System.Exception("Login failed");
    }
}

调试与监控

10. 超时统计与日志

public class TimeoutMonitor : MonoBehaviour
{
    [System.Serializable]
    public class TimeoutStats
    {
        public int totalOperations;
        public int timeoutCount;
        public int successCount;
        public int retryCount;
        public float avgResponseTime;
    }

    public TimeoutStats stats = new TimeoutStats();

    public void LogTimeout(TimeoutDetector.TimeoutResult result)
    {
        stats.totalOperations++;

        if (!result.IsSuccess)
            stats.timeoutCount++;
        else
            stats.successCount++;

        stats.retryCount += result.RetryCount;
        stats.avgResponseTime = (stats.avgResponseTime * (stats.totalOperations - 1) + result.ElapsedTime) / stats.totalOperations;

        // 持久化统计
        PlayerPrefs.SetInt("Timeout_Total", stats.totalOperations);
    }
}

常见问题解决

问题原因解决方案
协程提前结束超时检测与操作协程竞争使用嵌套协程统一控制
重试过于频繁策略配置不当使用指数退避 + 抖动
内存泄漏事件未注销在 OnDestroy 中清理事件
平台差异Time.time vs UnscaledTime根据需求选择时间源
多线程冲突异步操作线程安全使用主线程回调

此超时检测器提供完整的异步操作监控方案,支持灵活配置和事件驱动。如需特定网络库集成(如 Photon、Mirror)或性能调优,请提供更多细节!

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注