using System; using System.Collections; using System.Collections.Generic; using System.Text; using UnityEngine; using UnityEngine.Networking; namespace NBF { /// /// UnityWebRequest GET/POST 工具类: /// - GET(query 参数) /// - POST JSON /// - POST 表单(x-www-form-urlencoded / multipart) /// - 上传文件(multipart) /// - 自定义 Header /// - 超时、取消 /// - 统一返回文本 / 原始字节 /// 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 responseHeaders; public HttpResult(bool ok, long statusCode, string text, byte[] data, string error, Dictionary 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}"; } /// /// 用于取消请求(调用 Cancel() 会中止 UnityWebRequest) /// 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 query = null, Dictionary headers = null, int timeoutSeconds = 15, Action 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)); } } /// /// POST JSON(Content-Type: application/json) /// public static IEnumerator PostJson( string url, string jsonBody, Dictionary headers = null, int timeoutSeconds = 15, Action callback = null, RequestHandle handle = null) { var bodyBytes = string.IsNullOrEmpty(jsonBody) ? Array.Empty() : 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)); } } /// /// POST 表单(application/x-www-form-urlencoded) /// public static IEnumerator PostFormUrlEncoded( string url, Dictionary form, Dictionary headers = null, int timeoutSeconds = 15, Action callback = null, RequestHandle handle = null) { // Unity 内部会自动按 x-www-form-urlencoded 处理字段 var formData = new List(); 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)); } } /// /// POST multipart(可用于普通表单 + 文件上传) /// files: (fieldName, fileName, bytes, mimeType) /// public static IEnumerator PostMultipart( string url, Dictionary fields, List<(string fieldName, string fileName, byte[] bytes, string mimeType)> files, Dictionary headers = null, int timeoutSeconds = 30, Action callback = null, RequestHandle handle = null) { var form = new List(); 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)); } } /// /// 下载二进制(比如图片、ab、配置包等) /// public static IEnumerator GetBytes( string url, Dictionary query = null, Dictionary headers = null, int timeoutSeconds = 30, Action 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( string url, Dictionary query = null, Dictionary headers = null, int timeoutSeconds = 15, Action 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(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 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 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)"; } } }