killapp/Assets/Scripts/Network/HttpRequestManager.cs
“虞渠成” 8568724c3f feat: 新增WiFi OTA升级功能与相关优化
1. 新增多语言文案用于OTA升级提示
2. 优化更新进度显示为整数百分比
3. 调整设备轮询间隔为5秒
4. 新增SSE流式请求处理与OTA数据模型
5. 实现WiFi环境下基于SSE的OTA升级流程
6. 增加蓝牙环境下OTA升级的前置校验与提示
2026-08-25 14:12:45 +08:00

846 lines
32 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
using Debug = UnityEngine.Debug;
// 添加 JSON 解析支持
using Newtonsoft.Json;
namespace Kill.Network
{
/// <summary>
/// HTTP请求管理器 - 统一管理所有HTTP请求
/// </summary>
public class HttpRequestManager : MonoBehaviour
{
public static HttpRequestManager Instance { get; private set; }
/// <summary>
/// 基础URL
/// </summary>
public string BaseUrl { get; set; } = "";
/// <summary>
/// 全局请求头
/// </summary>
private Dictionary<string, string> _globalHeaders = new Dictionary<string, string>();
/// <summary>
/// 请求拦截器
/// </summary>
public event Func<NetworkRequestTask, Task> OnRequestIntercept;
/// <summary>
/// 响应拦截器
/// </summary>
public event Func<string, Task<string>> OnResponseIntercept;
/// <summary>
/// 默认请求配置
/// </summary>
private HttpRequestConfig _defaultConfig = new HttpRequestConfig();
private void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
#region
/// <summary>
/// 设置基础URL
/// </summary>
public void SetBaseUrl(string baseUrl)
{
BaseUrl = baseUrl.TrimEnd('/');
}
/// <summary>
/// 添加全局请求头
/// </summary>
public void AddGlobalHeader(string key, string value)
{
_globalHeaders[key] = value;
}
/// <summary>
/// 移除全局请求头
/// </summary>
public void RemoveGlobalHeader(string key)
{
if (_globalHeaders.ContainsKey(key))
{
_globalHeaders.Remove(key);
}
}
/// <summary>
/// 设置默认配置
/// </summary>
public void SetDefaultConfig(HttpRequestConfig config)
{
_defaultConfig = config;
}
/// <summary>
/// 清除所有全局请求头
/// </summary>
public void ClearGlobalHeaders()
{
_globalHeaders.Clear();
}
#endregion
#region GET请求
/// <summary>
/// 发送GET请求
/// </summary>
public async Task<HttpResponse<T>> Get<T>(string url, Dictionary<string, string> queryParams = null, HttpRequestConfig config = null)
{
string fullUrl = BuildUrl(url, queryParams);
return await SendRequest<T>(fullUrl, HttpMethod.GET, null, config);
}
/// <summary>
/// 发送GET请求(返回原始字符串)
/// </summary>
public async Task<HttpResponse<string>> Get(string url, Dictionary<string, string> queryParams = null, HttpRequestConfig config = null)
{
string fullUrl = BuildUrl(url, queryParams);
return await SendRequest<string>(fullUrl, HttpMethod.GET, null, config);
}
#endregion
#region POST请求
/// <summary>
/// 发送POST请求(JSON数据)
/// </summary>
public async Task<HttpResponse<T>> Post<T>(string url, object data, HttpRequestConfig config = null)
{
string jsonData = data != null ? JsonConvert.SerializeObject(data) : "";
Debug.Log("发送数据:" + jsonData);
return await SendRequest<T>(url, HttpMethod.POST, jsonData, config, "application/json");
}
/// <summary>
/// 发送POST请求(表单数据)
/// </summary>
public async Task<HttpResponse<T>> PostForm<T>(string url, WWWForm formData, HttpRequestConfig config = null)
{
return await SendRequestWithForm<T>(url, HttpMethod.POST, formData, config);
}
/// <summary>
/// 发送POST请求(IMultipartFormSection)
/// </summary>
public async Task<HttpResponse<T>> PostFormData<T>(string url, List<IMultipartFormSection> formData, HttpRequestConfig config = null)
{
return await SendRequestWithFormData<T>(url, HttpMethod.POST, formData, config);
}
/// <summary>
/// 发送POST请求(原始字符串)
/// </summary>
public async Task<HttpResponse<string>> Post(string url, object data, HttpRequestConfig config = null)
{
return await Post<string>(url, data, config);
}
#endregion
#region PUT请求
/// <summary>
/// 发送PUT请求(JSON数据)
/// </summary>
public async Task<HttpResponse<T>> Put<T>(string url, object data, HttpRequestConfig config = null)
{
string jsonData = data != null ? JsonConvert.SerializeObject(data) : "";
Debug.Log("发送数据:" + jsonData);
return await SendRequest<T>(url, HttpMethod.PUT, jsonData, config, "application/json");
}
/// <summary>
/// 发送PUT请求(表单数据)
/// </summary>
public async Task<HttpResponse<T>> PutForm<T>(string url, WWWForm form, HttpRequestConfig config = null)
{
return await SendRequestWithForm<T>(url, HttpMethod.PUT, form, config);
}
/// <summary>
/// 发送PUT请求(原始字符串)
/// </summary>
public async Task<HttpResponse<string>> Put(string url, object data, HttpRequestConfig config = null)
{
return await Put<string>(url, data, config);
}
#endregion
#region DELETE请求
/// <summary>
/// 发送DELETE请求
/// </summary>
public async Task<HttpResponse<T>> Delete<T>(string url, HttpRequestConfig config = null)
{
return await SendRequest<T>(url, HttpMethod.DELETE, null, config);
}
/// <summary>
/// 发送DELETE请求(带body)
/// </summary>
public async Task<HttpResponse<T>> Delete<T>(string url, object data, HttpRequestConfig config = null)
{
string jsonData = data != null ? JsonUtility.ToJson(data) : "";
return await SendRequest<T>(url, HttpMethod.DELETE, jsonData, config, "application/json");
}
#endregion
#region
/// <summary>
/// 下载文件
/// </summary>
public async Task<HttpResponse<string>> DownloadFile(string url, string savePath, Action<DownloadProgress> onProgress = null, HttpRequestConfig config = null)
{
var mergedConfig = MergeConfig(config);
var stopwatch = Stopwatch.StartNew();
try
{
using (UnityWebRequest request = UnityWebRequest.Get(url))
{
request.timeout = mergedConfig.Timeout;
// 添加请求头
AddHeadersToRequest(request, mergedConfig.Headers);
// 发送请求
var operation = request.SendWebRequest();
// 监控进度
while (!operation.isDone)
{
long totalBytes = 0;
string contentLength = request.GetResponseHeader("Content-Length");
if (!string.IsNullOrEmpty(contentLength) && long.TryParse(contentLength, out long parsedLength))
{
totalBytes = parsedLength;
}
onProgress?.Invoke(new DownloadProgress
{
DownloadedBytes = (long)(request.downloadedBytes),
TotalBytes = totalBytes
});
await Task.Yield();
}
stopwatch.Stop();
if (request.result == UnityWebRequest.Result.Success)
{
// 确保目录存在
string directory = Path.GetDirectoryName(savePath);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
// 保存文件
File.WriteAllBytes(savePath, request.downloadHandler.data);
return new HttpResponse<string>
{
IsSuccess = true,
StatusCode = request.responseCode,
Data = savePath,
ElapsedMilliseconds = stopwatch.ElapsedMilliseconds
};
}
else
{
return HttpResponse<string>.Failure(request.error, request.responseCode);
}
}
}
catch (Exception ex)
{
stopwatch.Stop();
return HttpResponse<string>.Failure($"下载异常: {ex.Message}");
}
}
#endregion
#region SSE
/// <summary>
/// SSE 流式下载处理器:按事件分隔符 \n\n 解析 data: 行并回调
/// </summary>
public class SSEDownloadHandler : DownloadHandlerScript
{
private readonly Action<string> _onEventData;
private readonly StringBuilder _buffer = new StringBuilder();
public SSEDownloadHandler(Action<string> onEventData) : base(new byte[16384])
{
_onEventData = onEventData;
}
protected override bool ReceiveData(byte[] data, int dataLength)
{
if (data == null || dataLength == 0) return false;
_buffer.Append(Encoding.UTF8.GetString(data, 0, dataLength));
string content = _buffer.ToString();
int idx;
while ((idx = content.IndexOf("\n\n", StringComparison.Ordinal)) >= 0)
{
string evt = content.Substring(0, idx);
content = content.Substring(idx + 2);
foreach (string line in evt.Split('\n'))
{
string l = line.Trim();
if (l.StartsWith("data:"))
{
string payload = l.Substring(5).Trim();
if (!string.IsNullOrEmpty(payload) && payload != "[DONE]")
_onEventData?.Invoke(payload);
}
}
}
// 保留未完成的事件块,等待下次数据到达
_buffer.Clear();
_buffer.Append(content);
return true;
}
}
/// <summary>
/// 发起 SSE 流式请求(长连接)。
/// 事件数据通过 onEventData 逐条回调;连接结束(服务端关闭)或出错后协程结束。
/// </summary>
public IEnumerator StartSSE(string url, Dictionary<string, string> queryParams, Dictionary<string, string> headers, Action<string> onEventData, Action<string> onError)
{
string fullUrl = GetFullUrl(BuildUrl(url, queryParams));
var handler = new SSEDownloadHandler(onEventData);
using (UnityWebRequest request = new UnityWebRequest(fullUrl, UnityWebRequest.kHttpVerbGET, handler, null))
{
AddHeadersToRequest(request, headers);
// SSE 为长连接,大文件 OTA 传输耗时长,超时设大防止中途断开
request.timeout = 600;
yield return request.SendWebRequest();
if (request.result != UnityWebRequest.Result.Success && request.result != UnityWebRequest.Result.ProtocolError)
{
onError?.Invoke(request.error);
}
}
}
#endregion
#region
/// <summary>
/// 发送HTTP请求(核心方法)
/// </summary>
private async Task<HttpResponse<T>> SendRequest<T>(string url, HttpMethod method, string data, HttpRequestConfig config, string contentType = null)
{
var mergedConfig = MergeConfig(config);
string fullUrl = GetFullUrl(url);
// 检查网络状态
if (NetworkStateManager.Instance != null && !NetworkStateManager.Instance.IsConnected)
{
return HttpResponse<T>.Failure("网络未连接");
}
int retryCount = 0;
while (retryCount <= mergedConfig.MaxRetries)
{
var stopwatch = Stopwatch.StartNew();
try
{
using (UnityWebRequest request = CreateWebRequest(fullUrl, method, data, contentType))
{
request.timeout = mergedConfig.Timeout;
// 添加请求头
AddHeadersToRequest(request, mergedConfig.Headers);
// 发送请求
var operation = request.SendWebRequest();
while (!operation.isDone)
{
await Task.Yield();
}
stopwatch.Stop();
Debug.Log(request.uri.ToString());
// 处理响应
if (request.result == UnityWebRequest.Result.Success)
{
string responseText = request.downloadHandler.text;
Debug.Log("response:" + responseText);
// 响应拦截器
if (OnResponseIntercept != null)
{
responseText = await OnResponseIntercept.Invoke(responseText);
}
T resultData = default;
if (!string.IsNullOrEmpty(responseText) && typeof(T) != typeof(string))
{
try
{
// 使用 Newtonsoft.Json 解析,支持嵌套对象
resultData = JsonConvert.DeserializeObject<T>(responseText);
}
catch (Exception ex)
{
Debug.LogWarning($"Newtonsoft JSON解析失败: {ex.Message},尝试使用 Unity 的 JsonUtility");
try
{
// 降级使用 Unity 的 JsonUtility
resultData = JsonUtility.FromJson<T>(responseText);
}
catch (Exception ex2)
{
Debug.LogWarning($"JsonUtility 解析也失败: {ex2.Message}");
resultData = (T)(object)responseText;
}
}
}
else if (typeof(T) == typeof(string))
{
resultData = (T)(object)responseText;
}
return new HttpResponse<T>
{
IsSuccess = true,
StatusCode = request.responseCode,
Data = resultData,
RawResponse = responseText,
Headers = GetResponseHeaders(request),
ElapsedMilliseconds = stopwatch.ElapsedMilliseconds
};
}
else
{
// 请求失败,检查是否需要重试
if (retryCount < mergedConfig.MaxRetries && ShouldRetry(request.responseCode))
{
retryCount++;
int delay = mergedConfig.UseExponentialBackoff
? mergedConfig.RetryDelayMs * (int)Math.Pow(2, retryCount - 1)
: mergedConfig.RetryDelayMs;
Debug.Log($"请求失败,{delay}ms后重试({retryCount}/{mergedConfig.MaxRetries}): {fullUrl}");
await Task.Delay(delay);
continue;
}
return HttpResponse<T>.Failure(request.error, request.responseCode);
}
}
}
catch (Exception ex)
{
stopwatch.Stop();
if (retryCount < mergedConfig.MaxRetries)
{
retryCount++;
int delay = mergedConfig.UseExponentialBackoff
? mergedConfig.RetryDelayMs * (int)Math.Pow(2, retryCount - 1)
: mergedConfig.RetryDelayMs;
Debug.Log($"请求异常,{delay}ms后重试({retryCount}/{mergedConfig.MaxRetries}): {ex.Message}");
await Task.Delay(delay);
continue;
}
return HttpResponse<T>.Failure($"请求异常: {ex.Message}");
}
}
return HttpResponse<T>.Failure("请求失败,已重试所有次数");
}
/// <summary>
/// 发送带表单的请求
/// </summary>
private async Task<HttpResponse<T>> SendRequestWithForm<T>(string url, HttpMethod method, WWWForm form, HttpRequestConfig config)
{
var mergedConfig = MergeConfig(config);
string fullUrl = GetFullUrl(url);
int retryCount = 0;
while (retryCount <= mergedConfig.MaxRetries)
{
var stopwatch = Stopwatch.StartNew();
try
{
using (UnityWebRequest request = UnityWebRequest.Post(fullUrl, form))
{
if (method == HttpMethod.PUT)
{
request.method = "PUT";
}
request.timeout = mergedConfig.Timeout;
// 添加请求头(不要手动设置 Content-TypeWWWForm 会自动设置)
AddHeadersToRequest(request, mergedConfig.Headers);
var operation = request.SendWebRequest();
while (!operation.isDone)
{
await Task.Yield();
}
stopwatch.Stop();
if (request.result == UnityWebRequest.Result.Success)
{
string responseText = request.downloadHandler.text;
T resultData = default;
if (!string.IsNullOrEmpty(responseText) && typeof(T) != typeof(string))
{
try
{
resultData = JsonUtility.FromJson<T>(responseText);
}
catch
{
resultData = (T)(object)responseText;
}
}
else if (typeof(T) == typeof(string))
{
resultData = (T)(object)responseText;
}
return new HttpResponse<T>
{
IsSuccess = true,
StatusCode = request.responseCode,
Data = resultData,
RawResponse = responseText,
ElapsedMilliseconds = stopwatch.ElapsedMilliseconds
};
}
else
{
if (retryCount < mergedConfig.MaxRetries && ShouldRetry(request.responseCode))
{
retryCount++;
int delay = mergedConfig.UseExponentialBackoff
? mergedConfig.RetryDelayMs * (int)Math.Pow(2, retryCount - 1)
: mergedConfig.RetryDelayMs;
await Task.Delay(delay);
continue;
}
return HttpResponse<T>.Failure(request.error, request.responseCode);
}
}
}
catch (Exception ex)
{
stopwatch.Stop();
if (retryCount < mergedConfig.MaxRetries)
{
retryCount++;
int delay = mergedConfig.UseExponentialBackoff
? mergedConfig.RetryDelayMs * (int)Math.Pow(2, retryCount - 1)
: mergedConfig.RetryDelayMs;
await Task.Delay(delay);
continue;
}
return HttpResponse<T>.Failure($"请求异常: {ex.Message}");
}
}
return HttpResponse<T>.Failure("请求失败,已重试所有次数");
}
/// <summary>
/// 发送带表单的请求
/// </summary>
private async Task<HttpResponse<T>> SendRequestWithFormData<T>(string url, HttpMethod method, List<IMultipartFormSection> formData, HttpRequestConfig config)
{
var mergedConfig = MergeConfig(config);
string fullUrl = GetFullUrl(url);
int retryCount = 0;
while (retryCount <= mergedConfig.MaxRetries)
{
var stopwatch = Stopwatch.StartNew();
try
{
using (UnityWebRequest request = UnityWebRequest.Post(fullUrl, formData))
{
if (method == HttpMethod.PUT)
{
request.method = "PUT";
}
request.timeout = mergedConfig.Timeout;
// 添加请求头(不要手动设置 Content-TypeWWWForm 会自动设置)
AddHeadersToRequest(request, mergedConfig.Headers);
var operation = request.SendWebRequest();
while (!operation.isDone)
{
await Task.Yield();
}
stopwatch.Stop();
if (request.result == UnityWebRequest.Result.Success)
{
string responseText = request.downloadHandler.text;
T resultData = default;
if (!string.IsNullOrEmpty(responseText) && typeof(T) != typeof(string))
{
try
{
resultData = JsonUtility.FromJson<T>(responseText);
}
catch
{
resultData = (T)(object)responseText;
}
}
else if (typeof(T) == typeof(string))
{
resultData = (T)(object)responseText;
}
return new HttpResponse<T>
{
IsSuccess = true,
StatusCode = request.responseCode,
Data = resultData,
RawResponse = responseText,
ElapsedMilliseconds = stopwatch.ElapsedMilliseconds
};
}
else
{
if (retryCount < mergedConfig.MaxRetries && ShouldRetry(request.responseCode))
{
retryCount++;
int delay = mergedConfig.UseExponentialBackoff
? mergedConfig.RetryDelayMs * (int)Math.Pow(2, retryCount - 1)
: mergedConfig.RetryDelayMs;
await Task.Delay(delay);
continue;
}
return HttpResponse<T>.Failure(request.error, request.responseCode);
}
}
}
catch (Exception ex)
{
stopwatch.Stop();
if (retryCount < mergedConfig.MaxRetries)
{
retryCount++;
int delay = mergedConfig.UseExponentialBackoff
? mergedConfig.RetryDelayMs * (int)Math.Pow(2, retryCount - 1)
: mergedConfig.RetryDelayMs;
await Task.Delay(delay);
continue;
}
return HttpResponse<T>.Failure($"请求异常: {ex.Message}");
}
}
return HttpResponse<T>.Failure("请求失败,已重试所有次数");
}
#endregion
#region
/// <summary>
/// 创建WebRequest
/// </summary>
private UnityWebRequest CreateWebRequest(string url, HttpMethod method, string data, string contentType)
{
UnityWebRequest request;
switch (method)
{
case HttpMethod.GET:
request = UnityWebRequest.Get(url);
break;
case HttpMethod.POST:
request = UnityWebRequest.Post(url, data, contentType ?? "application/json");
break;
case HttpMethod.PUT:
byte[] bodyRaw = Encoding.UTF8.GetBytes(data ?? "");
request = UnityWebRequest.Put(url, bodyRaw);
request.SetRequestHeader("Content-Type", contentType ?? "application/json");
break;
case HttpMethod.DELETE:
request = UnityWebRequest.Delete(url);
if (!string.IsNullOrEmpty(data))
{
byte[] deleteBody = Encoding.UTF8.GetBytes(data);
request.uploadHandler = new UploadHandlerRaw(deleteBody);
request.SetRequestHeader("Content-Type", contentType ?? "application/json");
}
break;
default:
request = UnityWebRequest.Get(url);
break;
}
// 设置下载处理器
if (request.downloadHandler == null)
{
request.downloadHandler = new DownloadHandlerBuffer();
}
return request;
}
/// <summary>
/// 添加请求头
/// </summary>
private void AddHeadersToRequest(UnityWebRequest request, Dictionary<string, string> customHeaders)
{
// 添加全局请求头
foreach (var header in _globalHeaders)
{
request.SetRequestHeader(header.Key, header.Value);
}
// 添加自定义请求头
if (customHeaders != null)
{
foreach (var header in customHeaders)
{
request.SetRequestHeader(header.Key, header.Value);
}
}
}
/// <summary>
/// 获取响应头
/// </summary>
private Dictionary<string, string> GetResponseHeaders(UnityWebRequest request)
{
var headers = new Dictionary<string, string>();
if (request.GetResponseHeaders() != null)
{
foreach (var header in request.GetResponseHeaders())
{
headers[header.Key] = header.Value;
}
}
return headers;
}
/// <summary>
/// 合并配置
/// </summary>
private HttpRequestConfig MergeConfig(HttpRequestConfig customConfig)
{
if (customConfig == null) return _defaultConfig;
return new HttpRequestConfig
{
Timeout = customConfig.Timeout > 0 ? customConfig.Timeout : _defaultConfig.Timeout,
MaxRetries = customConfig.MaxRetries >= 0 ? customConfig.MaxRetries : _defaultConfig.MaxRetries,
RetryDelayMs = customConfig.RetryDelayMs > 0 ? customConfig.RetryDelayMs : _defaultConfig.RetryDelayMs,
UseExponentialBackoff = customConfig.UseExponentialBackoff,
Headers = customConfig.Headers ?? _defaultConfig.Headers,
Priority = customConfig.Priority,
EnableCache = customConfig.EnableCache,
CacheDuration = customConfig.CacheDuration > 0 ? customConfig.CacheDuration : _defaultConfig.CacheDuration
};
}
/// <summary>
/// 构建完整URL
/// </summary>
private string GetFullUrl(string url)
{
if (string.IsNullOrEmpty(BaseUrl) || url.StartsWith("http://") || url.StartsWith("https://"))
{
return url;
}
return $"{BaseUrl}/{url.TrimStart('/')}";
}
/// <summary>
/// 构建带查询参数的URL
/// </summary>
private string BuildUrl(string url, Dictionary<string, string> queryParams)
{
string fullUrl = GetFullUrl(url);
if (queryParams == null || queryParams.Count == 0)
{
return fullUrl;
}
StringBuilder sb = new StringBuilder(fullUrl);
sb.Append(fullUrl.Contains("?") ? "&" : "?");
bool first = true;
foreach (var param in queryParams)
{
if (!first) sb.Append("&");
sb.Append($"{UnityWebRequest.EscapeURL(param.Key)}={UnityWebRequest.EscapeURL(param.Value)}");
first = false;
}
return sb.ToString();
}
/// <summary>
/// 判断是否应该重试
/// </summary>
private bool ShouldRetry(long statusCode)
{
// 5xx 服务器错误或 0(网络错误) 时重试
return statusCode >= 500 || statusCode == 0 || statusCode == 408;
}
#endregion
}
}