Files
BabyVideo/Assets/Scripts/Utils/HttpUtil.cs
2026-02-09 20:10:14 +08:00

353 lines
12 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.Text;
using UnityEngine;
using UnityEngine.Networking;
namespace NBF
{
/// <summary>
/// UnityWebRequest GET/POST 工具类:
/// - GETquery 参数)
/// - POST JSON
/// - POST 表单x-www-form-urlencoded / multipart
/// - 上传文件multipart
/// - 自定义 Header
/// - 超时、取消
/// - 统一返回文本 / 原始字节
/// </summary>
public static class HttpUtil
{
// ========== 公共返回结构 ==========
public readonly struct HttpResult
{
public readonly bool ok;
public readonly long statusCode;
public readonly string text;
public readonly byte[] data;
public readonly string error; // 包含网络错误/HTTP错误/取消/超时等信息
public readonly Dictionary<string, string> responseHeaders;
public HttpResult(bool ok, long statusCode, string text, byte[] data, string error,
Dictionary<string, string> responseHeaders)
{
this.ok = ok;
this.statusCode = statusCode;
this.text = text;
this.data = data;
this.error = error;
this.responseHeaders = responseHeaders;
}
public override string ToString() =>
$"ok={ok}, status={statusCode}, error={error}, textLen={text?.Length ?? 0}";
}
/// <summary>
/// 用于取消请求(调用 Cancel() 会中止 UnityWebRequest
/// </summary>
public sealed class RequestHandle
{
internal UnityWebRequest req;
public bool IsDone => req == null || req.isDone;
public void Cancel()
{
try
{
req?.Abort();
}
catch
{
/* ignore */
}
}
}
// ========== 对外 API协程 ==========
public static IEnumerator Get(
string url,
Dictionary<string, string> query = null,
Dictionary<string, string> headers = null,
int timeoutSeconds = 15,
Action<HttpResult> callback = null,
RequestHandle handle = null)
{
var finalUrl = BuildUrlWithQuery(url, query);
using (var req = UnityWebRequest.Get(finalUrl))
{
ApplyCommon(req, headers, timeoutSeconds, handle);
yield return req.SendWebRequest();
callback?.Invoke(BuildResult(req));
}
}
/// <summary>
/// POST JSONContent-Type: application/json
/// </summary>
public static IEnumerator PostJson(
string url,
string jsonBody,
Dictionary<string, string> headers = null,
int timeoutSeconds = 15,
Action<HttpResult> callback = null,
RequestHandle handle = null)
{
var bodyBytes = string.IsNullOrEmpty(jsonBody) ? Array.Empty<byte>() : Encoding.UTF8.GetBytes(jsonBody);
using (var req = new UnityWebRequest(url, UnityWebRequest.kHttpVerbPOST))
{
req.uploadHandler = new UploadHandlerRaw(bodyBytes);
req.downloadHandler = new DownloadHandlerBuffer();
req.SetRequestHeader("Content-Type", "application/json; charset=utf-8");
ApplyCommon(req, headers, timeoutSeconds, handle);
yield return req.SendWebRequest();
callback?.Invoke(BuildResult(req));
}
}
/// <summary>
/// POST 表单application/x-www-form-urlencoded
/// </summary>
public static IEnumerator PostFormUrlEncoded(
string url,
Dictionary<string, string> form,
Dictionary<string, string> headers = null,
int timeoutSeconds = 15,
Action<HttpResult> callback = null,
RequestHandle handle = null)
{
// Unity 内部会自动按 x-www-form-urlencoded 处理字段
var formData = new List<IMultipartFormSection>();
if (form != null)
{
foreach (var kv in form)
formData.Add(new MultipartFormDataSection(kv.Key, kv.Value));
}
// 这里用 Post + 手动覆盖 content-type
using (var req = UnityWebRequest.Post(url, formData))
{
req.downloadHandler = new DownloadHandlerBuffer();
req.SetRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8");
ApplyCommon(req, headers, timeoutSeconds, handle);
yield return req.SendWebRequest();
callback?.Invoke(BuildResult(req));
}
}
/// <summary>
/// POST multipart可用于普通表单 + 文件上传)
/// files: (fieldName, fileName, bytes, mimeType)
/// </summary>
public static IEnumerator PostMultipart(
string url,
Dictionary<string, string> fields,
List<(string fieldName, string fileName, byte[] bytes, string mimeType)> files,
Dictionary<string, string> headers = null,
int timeoutSeconds = 30,
Action<HttpResult> callback = null,
RequestHandle handle = null)
{
var form = new List<IMultipartFormSection>();
if (fields != null)
{
foreach (var kv in fields)
form.Add(new MultipartFormDataSection(kv.Key, kv.Value));
}
if (files != null)
{
foreach (var f in files)
{
var mime = string.IsNullOrEmpty(f.mimeType) ? "application/octet-stream" : f.mimeType;
form.Add(new MultipartFormFileSection(f.fieldName, f.bytes, f.fileName, mime));
}
}
using (var req = UnityWebRequest.Post(url, form))
{
req.downloadHandler = new DownloadHandlerBuffer();
ApplyCommon(req, headers, timeoutSeconds, handle);
yield return req.SendWebRequest();
callback?.Invoke(BuildResult(req));
}
}
/// <summary>
/// 下载二进制比如图片、ab、配置包等
/// </summary>
public static IEnumerator GetBytes(
string url,
Dictionary<string, string> query = null,
Dictionary<string, string> headers = null,
int timeoutSeconds = 30,
Action<HttpResult> callback = null,
RequestHandle handle = null)
{
var finalUrl = BuildUrlWithQuery(url, query);
using (var req = UnityWebRequest.Get(finalUrl))
{
req.downloadHandler = new DownloadHandlerBuffer();
ApplyCommon(req, headers, timeoutSeconds, handle);
yield return req.SendWebRequest();
callback?.Invoke(BuildResult(req, preferBytes: true));
}
}
// ========== 便捷:返回 JSON 反序列化JsonUtility ==========
// 注意JsonUtility 不支持 Dictionary / 顶层数组等,你若需要更强建议用 Newtonsoft.Json
public static IEnumerator GetJson<T>(
string url,
Dictionary<string, string> query = null,
Dictionary<string, string> headers = null,
int timeoutSeconds = 15,
Action<bool, T, HttpResult> callback = null,
RequestHandle handle = null)
{
yield return Get(url, query, headers, timeoutSeconds, r =>
{
if (!r.ok)
{
callback?.Invoke(false, default, r);
return;
}
try
{
var obj = JsonUtility.FromJson<T>(r.text);
callback?.Invoke(true, obj, r);
}
catch (Exception e)
{
callback?.Invoke(false, default, new HttpResult(false, r.statusCode, r.text, r.data,
"JSON parse error: " + e.Message, r.responseHeaders));
}
}, handle);
}
// ========== 内部工具 ==========
static void ApplyCommon(UnityWebRequest req, Dictionary<string, string> headers, int timeoutSeconds,
RequestHandle handle)
{
req.timeout = Mathf.Max(1, timeoutSeconds);
req.downloadHandler ??= new DownloadHandlerBuffer();
if (headers != null)
{
foreach (var kv in headers)
{
if (!string.IsNullOrEmpty(kv.Key))
req.SetRequestHeader(kv.Key, kv.Value);
}
}
if (handle != null) handle.req = req;
}
static HttpResult BuildResult(UnityWebRequest req, bool preferBytes = false)
{
// Unity 2020+ : result enum
bool isNetworkOrHttpError =
req.result == UnityWebRequest.Result.ConnectionError ||
req.result == UnityWebRequest.Result.ProtocolError ||
req.result == UnityWebRequest.Result.DataProcessingError;
var code = req.responseCode;
var headers = req.GetResponseHeaders();
// 下载内容
byte[] data = null;
string text = null;
if (req.downloadHandler != null)
{
data = req.downloadHandler.data;
// preferBytes==false 时优先给 text
if (!preferBytes)
{
try
{
text = req.downloadHandler.text;
}
catch
{
text = data != null ? Encoding.UTF8.GetString(data) : null;
}
}
else
{
// preferBytes==true 时 text 也尽量给一份(方便日志)
try
{
text = req.downloadHandler.text;
}
catch
{
/* ignore */
}
}
}
// 错误信息(包含 HTTP 错误)
string err = null;
if (isNetworkOrHttpError)
{
err = req.error;
// 有些后端会把错误细节放在 body
if (!string.IsNullOrEmpty(text))
err = $"{err}\nBody: {TrimForLog(text, 1200)}";
}
bool ok = !isNetworkOrHttpError && code >= 200 && code < 300;
return new HttpResult(ok, code, text, data, err, headers);
}
static string BuildUrlWithQuery(string url, Dictionary<string, string> query)
{
if (query == null || query.Count == 0) return url;
var sb = new StringBuilder(url);
sb.Append(url.Contains("?") ? "&" : "?");
bool first = true;
foreach (var kv in query)
{
if (!first) sb.Append('&');
first = false;
sb.Append(UnityWebRequest.EscapeURL(kv.Key));
sb.Append('=');
sb.Append(UnityWebRequest.EscapeURL(kv.Value ?? ""));
}
return sb.ToString();
}
static string TrimForLog(string s, int maxLen)
{
if (string.IsNullOrEmpty(s)) return s;
if (s.Length <= maxLen) return s;
return s.Substring(0, maxLen) + "...(truncated)";
}
}
}