353 lines
12 KiB
C#
353 lines
12 KiB
C#
using System;
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using System.Text;
|
||
using UnityEngine;
|
||
using UnityEngine.Networking;
|
||
|
||
namespace NBF
|
||
{
|
||
/// <summary>
|
||
/// UnityWebRequest GET/POST 工具类:
|
||
/// - GET(query 参数)
|
||
/// - 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 JSON(Content-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)";
|
||
}
|
||
}
|
||
} |