674 lines
32 KiB
C#
674 lines
32 KiB
C#
using Best.HTTP;
|
|
using Best.HTTP.Response;
|
|
using Best.HTTP.Shared.PlatformSupport.Memory;
|
|
using Cysharp.Threading.Tasks;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Text;
|
|
using UnityEngine;
|
|
using UVC.Json;
|
|
using UVC.Log;
|
|
|
|
namespace UVC.Network
|
|
{
|
|
/// <summary>
|
|
/// HTTP 요청을 처리하기 위한 유틸리티 클래스
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 이 클래스는 REST API 호출을 위한 메소드들을 제공합니다.
|
|
/// GET, POST 요청 및 파일 다운로드를 지원하며 요청/응답 로깅 기능도 포함합니다.
|
|
/// 요청은 비동기(UniTask)로 처리됩니다.
|
|
///
|
|
/// 기본 사용법:
|
|
/// <code>
|
|
/// // 도메인 설정
|
|
/// HttpRequester.Domain = "https://api.example.com";
|
|
///
|
|
/// // GET 요청 예제
|
|
/// var products = await HttpRequester.RequestGet<List<Product>>("/api/products");
|
|
///
|
|
/// // POST 요청 예제
|
|
/// var loginData = new Dictionary<string, object>
|
|
/// {
|
|
/// { "username", "user@example.com" },
|
|
/// { "password", "password123" }
|
|
/// };
|
|
/// var response = await HttpRequester.RequestPost<LoginResponse>("/api/login", loginData);
|
|
/// </code>
|
|
/// </remarks>
|
|
public class HttpRequester
|
|
{
|
|
/// <summary>
|
|
/// 기본 도메인 주소입니다.
|
|
/// </summary>
|
|
public static string Domain { get; set; } = "";
|
|
|
|
/// <summary>
|
|
/// Dictionary 형태의 본문을 가진 사용자 정의 HTTP 메소드로 API 요청을 수행합니다.
|
|
/// </summary>
|
|
/// <typeparam name="T">응답 데이터를 변환할 타입</typeparam>
|
|
/// <param name="url">요청할 URL (http가 포함되지 않은 경우 Domain과 결합)</param>
|
|
/// <param name="method">HTTP 메소드 (get, post, put 등)</param>
|
|
/// <param name="body">요청 본문으로 전송할 Dictionary 데이터 (null 가능)</param>
|
|
/// <param name="header">추가할 헤더 정보 (null 가능)</param>
|
|
/// <param name="useAuth">인증 토큰 사용 여부</param>
|
|
/// <returns>지정된 타입으로 변환된 응답 데이터</returns>
|
|
/// <example>
|
|
/// <code>
|
|
/// // PUT 요청 예제
|
|
/// public async UniTask<ProductResponse> UpdateProductAsync(int productId, string name, float price)
|
|
/// {
|
|
/// var body = new Dictionary<string, object>
|
|
/// {
|
|
/// { "Id", productId },
|
|
/// { "name", name },
|
|
/// { "price", price }
|
|
/// };
|
|
///
|
|
/// return await HttpRequester.Request<ProductResponse>("/api/products", "put", body);
|
|
/// }
|
|
/// </code>
|
|
/// </example>
|
|
public static async UniTask<T> Request<T>(string url, string method, Dictionary<string, object> body = null, Dictionary<string, string> header = null, bool useAuth = false)
|
|
{
|
|
return await Request_<T>(url, body == null ? null : JsonHelper.ToJson(body), method, header, useAuth);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 문자열 형태의 본문을 가진 사용자 정의 HTTP 메소드로 API 요청을 수행합니다.
|
|
/// </summary>
|
|
/// <typeparam name="T">응답 데이터를 변환할 타입</typeparam>
|
|
/// <param name="url">요청할 URL (http가 포함되지 않은 경우 Domain과 결합)</param>
|
|
/// <param name="method">HTTP 메소드 (get, post, put 등)</param>
|
|
/// <param name="body">요청 본문으로 전송할 JSON 문자열 (null 가능)</param>
|
|
/// <param name="header">추가할 헤더 정보 (null 가능)</param>
|
|
/// <param name="useAuth">인증 토큰 사용 여부</param>
|
|
/// <returns>지정된 타입으로 변환된 응답 데이터</returns>
|
|
/// <example>
|
|
/// <code>
|
|
/// // DELETE 요청 예제
|
|
/// public async UniTask<bool> DeleteResourceAsync(string resourceId)
|
|
/// {
|
|
/// string jsonBody = $"{{\"Id\": \"{resourceId}\"}}";
|
|
///
|
|
/// return await HttpRequester.Request<bool>("/api/resources", "delete", jsonBody);
|
|
/// }
|
|
/// </code>
|
|
/// </example>
|
|
public static async UniTask<T> Request<T>(string url, string method, string body = null, Dictionary<string, string> header = null, bool useAuth = false)
|
|
{
|
|
return await Request_<T>(url, body, method, header, useAuth);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Dictionary를 JSON body로 변환하여 POST 요청을 수행합니다.
|
|
/// </summary>
|
|
/// <typeparam name="T">응답 데이터를 변환할 타입</typeparam>
|
|
/// <param name="url">요청할 URL (http가 포함되지 않은 경우 Domain과 결합)</param>
|
|
/// <param name="body">요청 본문으로 전송할 Dictionary 데이터 (null 가능)</param>
|
|
/// <param name="header">추가할 헤더 정보 (null 가능)</param>
|
|
/// <param name="useAuth">인증 토큰 사용 여부</param>
|
|
/// <returns>지정된 타입으로 변환된 응답 데이터</returns>
|
|
/// <example>
|
|
/// <code>
|
|
/// // 사용자 로그인 요청 예제
|
|
/// public async UniTask<LoginResponse> LoginAsync(string username, string password)
|
|
/// {
|
|
/// var body = new Dictionary<string, object>
|
|
/// {
|
|
/// { "username", username },
|
|
/// { "password", password }
|
|
/// };
|
|
///
|
|
/// return await HttpRequester.RequestPost<LoginResponse>("/api/login", body);
|
|
/// }
|
|
///
|
|
/// // LoginResponse 클래스 예제
|
|
/// [Serializable]
|
|
/// public class LoginResponse
|
|
/// {
|
|
/// public string token;
|
|
/// public UserInfo userInfo;
|
|
/// }
|
|
/// </code>
|
|
/// </example>
|
|
public static async UniTask<T> RequestPost<T>(string url, Dictionary<string, object> body = null, Dictionary<string, string> header = null, bool useAuth = false)
|
|
{
|
|
return await Request_<T>(url, body == null ? null : JsonHelper.ToJson(body), "post", header, useAuth);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 문자열 형태의 JSON을 body로 사용하여 POST 요청을 수행합니다.
|
|
/// </summary>
|
|
/// <typeparam name="T">응답 데이터를 변환할 타입</typeparam>
|
|
/// <param name="url">요청할 URL (http가 포함되지 않은 경우 Domain과 결합)</param>
|
|
/// <param name="body">요청 본문으로 전송할 JSON 문자열</param>
|
|
/// <param name="header">추가할 헤더 정보 (null 가능)</param>
|
|
/// <param name="useAuth">인증 토큰 사용 여부</param>
|
|
/// <returns>지정된 타입으로 변환된 응답 데이터</returns>
|
|
/// <example>
|
|
/// <code>
|
|
/// // 사용자 정의 JSON 문자열로 요청하는 예제
|
|
/// public async UniTask<ProductResponse> UpdateProductAsync(int productId, string productName)
|
|
/// {
|
|
/// string jsonBody = $"{{\"Id\": {productId}, \"name\": \"{productName}\"}}";
|
|
///
|
|
/// return await HttpRequester.RequestPost<ProductResponse>("/api/products/update", jsonBody);
|
|
/// }
|
|
/// </code>
|
|
/// </example>
|
|
public static async UniTask<T> RequestPost<T>(string url, string body, Dictionary<string, string> header = null, bool useAuth = false)
|
|
{
|
|
return await Request_<T>(url, body, "post", header, useAuth);
|
|
}
|
|
|
|
/// <summary>
|
|
/// GET 방식으로 API를 호출합니다.
|
|
/// </summary>
|
|
/// <typeparam name="T">응답 데이터를 변환할 타입</typeparam>
|
|
/// <param name="url">요청할 URL (http가 포함되지 않은 경우 Domain과 결합)</param>
|
|
/// <param name="body">요청에 포함할 파라미터 (URL 쿼리스트링으로 변환)</param>
|
|
/// <param name="header">추가할 헤더 정보 (null 가능)</param>
|
|
/// <param name="useAuth">인증 토큰 사용 여부</param>
|
|
/// <returns>지정된 타입으로 변환된 응답 데이터</returns>
|
|
/// <example>
|
|
/// <code>
|
|
/// // 상품 목록을 가져오는 예제
|
|
/// public async UniTask<List<Product>> GetProductsAsync(int page = 1, int pageSize = 20)
|
|
/// {
|
|
/// var queryParams = new Dictionary<string, object>
|
|
/// {
|
|
/// { "page", page },
|
|
/// { "pageSize", pageSize }
|
|
/// };
|
|
///
|
|
/// return await HttpRequester.RequestGet<List<Product>>("/api/products", queryParams);
|
|
/// }
|
|
///
|
|
/// // Product 클래스 예제
|
|
/// [Serializable]
|
|
/// public class Product
|
|
/// {
|
|
/// public int Id;
|
|
/// public string name;
|
|
/// public float price;
|
|
/// }
|
|
/// </code>
|
|
/// </example>
|
|
public static async UniTask<T> RequestGet<T>(string url, Dictionary<string, object> body = null, Dictionary<string, string> header = null, bool useAuth = false)
|
|
{
|
|
return await Request_<T>(url, body == null ? null : JsonHelper.ToJson(body), "get", header, useAuth);
|
|
}
|
|
|
|
/// <summary>
|
|
/// HTTP 요청을 처리하는 내부 메소드
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 이 메소드는 모든 HTTP 요청의 공통 로직을 처리합니다:
|
|
/// - URL 구성 (Domain과 상대 경로 결합)
|
|
/// - HTTP 메소드 설정
|
|
/// - 헤더 설정
|
|
/// - 요청 로깅
|
|
/// - 응답 처리 및 역직렬화
|
|
/// </remarks>
|
|
/// <typeparam name="T">응답 데이터를 변환할 타입</typeparam>
|
|
/// <param name="url">요청할 URL</param>
|
|
/// <param name="body">요청 본문 (JSON 문자열)</param>
|
|
/// <param name="methodString">HTTP 메소드 문자열</param>
|
|
/// <param name="header">추가할 헤더 정보</param>
|
|
/// <param name="useAuth">인증 토큰 사용 여부</param>
|
|
/// <returns>지정된 타입으로 변환된 응답 데이터</returns>
|
|
private static async UniTask<T> Request_<T>(string url, string body = null, string methodString = "post", Dictionary<string, string> header = null, bool useAuth = false)
|
|
{
|
|
|
|
HTTPMethods method = StringToMethod(methodString);
|
|
|
|
if (!url.Contains("http")) url = $"{Domain}{url}";
|
|
|
|
var request = SelectHTTPRequest(method, url);
|
|
|
|
request.DownloadSettings = new Best.HTTP.Request.Settings.DownloadSettings() { ContentStreamMaxBuffered = 1024 * 1024 * 200 };
|
|
request.MethodType = method;
|
|
request.SetHeader("Content-Type", "application/json; charset=utf-8");
|
|
//if (useAuth) request.SetHeader("access-token", AuthService.Instance.Entiti.accessToken);
|
|
JObject headerObject = new JObject();
|
|
headerObject.Add("Content-Type", "application/json; charset=utf-8");
|
|
|
|
if (header != null)
|
|
{
|
|
foreach (var kvp in header)
|
|
{
|
|
request.SetHeader(kvp.Key, kvp.Value);
|
|
headerObject.Add(kvp.Key, kvp.Value);
|
|
}
|
|
}
|
|
|
|
if (body != null)
|
|
{
|
|
request.UploadSettings.UploadStream = new MemoryStream(Encoding.UTF8.GetBytes(body));
|
|
}
|
|
|
|
#if !UNITY_WEBGL || UNITY_EDITOR
|
|
HttpLogEntry log = ServerLog.LogHttpRequest(url, methodString, headerObject.ToString(Formatting.None), body, DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"));
|
|
//Debug.Log($"Request APIToken :{AuthService.Instance.Entiti.accessToken}");
|
|
bool isMainThread = PlayerLoopHelper.IsMainThread;
|
|
#endif
|
|
//var response = await request.GetFromJsonResultAsync<T>();
|
|
var response = await request.GetAsStringAsync();
|
|
|
|
#if !UNITY_WEBGL || UNITY_EDITOR
|
|
if (!isMainThread) await UniTask.SwitchToThreadPool();
|
|
log.ResponseData = response;
|
|
log.ResponseDate = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
|
|
ServerLog.LogHttpResponse(log);
|
|
#endif
|
|
//T가 string이면
|
|
if (typeof(T) == typeof(string))
|
|
{
|
|
return (T)(object)response;
|
|
}
|
|
else
|
|
{
|
|
return JsonHelper.FromJson<T>(response);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dictionary를 JSON body로 변환하여 POST 요청을 수행하고 처리 시간도 함께 반환합니다.
|
|
/// </summary>
|
|
/// <typeparam name="T">응답 데이터를 변환할 타입</typeparam>
|
|
/// <param name="url">요청할 URL (http가 포함되지 않은 경우 Domain과 결합)</param>
|
|
/// <param name="body">요청 본문으로 전송할 Dictionary 데이터 (null 가능)</param>
|
|
/// <param name="header">추가할 헤더 정보 (null 가능)</param>
|
|
/// <param name="useAuth">인증 토큰 사용 여부</param>
|
|
/// <returns>지정된 타입으로 변환된 응답 데이터와 요청 처리 시간</returns>
|
|
/// <example>
|
|
/// <code>
|
|
/// // 서버 응답 시간을 측정하는 예제
|
|
/// public async UniTask<(UserData, TimeSpan)> GetUserDataWithPerformanceCheck(int userId)
|
|
/// {
|
|
/// var body = new Dictionary<string, object>
|
|
/// {
|
|
/// { "userId", userId }
|
|
/// };
|
|
///
|
|
/// var (userData, duration) = await HttpRequester.RequestPostWithDuration<UserData>("/api/users/details", body);
|
|
///
|
|
/// Debug.Log($"API 응답 시간: {duration.TotalMilliseconds}ms");
|
|
/// return (userData, duration);
|
|
/// }
|
|
/// </code>
|
|
/// </example>
|
|
public static async UniTask<(T, TimeSpan)> RequestPostWithDuration<T>(string url, Dictionary<string, object> body = null, Dictionary<string, string> header = null, bool useAuth = true)
|
|
{
|
|
return await RequestWithDuration<T>(url, body == null ? null : JsonHelper.ToJson(body), "post", header, useAuth);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 문자열 형태의 JSON을 body로 사용하여 POST 요청을 수행하고 처리 시간도 함께 반환합니다.
|
|
/// </summary>
|
|
/// <typeparam name="T">응답 데이터를 변환할 타입</typeparam>
|
|
/// <param name="url">요청할 URL (http가 포함되지 않은 경우 Domain과 결합)</param>
|
|
/// <param name="body">요청 본문으로 전송할 JSON 문자열</param>
|
|
/// <param name="header">추가할 헤더 정보 (null 가능)</param>
|
|
/// <param name="useAuth">인증 토큰 사용 여부</param>
|
|
/// <returns>지정된 타입으로 변환된 응답 데이터와 요청 처리 시간</returns>
|
|
/// <example>
|
|
/// <code>
|
|
/// // 커스텀 JSON으로 요청하고 응답 시간을 측정하는 예제
|
|
/// public async UniTask<(APIResponse, TimeSpan)> SendComplexDataWithTiming(string jsonData)
|
|
/// {
|
|
/// // jsonData는 이미 직렬화된 JSON 문자열
|
|
///
|
|
/// var (response, duration) = await HttpRequester.RequestPostWithDuration<APIResponse>("/api/complex-operation", jsonData);
|
|
///
|
|
/// if (duration.TotalSeconds > 1.0)
|
|
/// {
|
|
/// Debug.LogWarning($"API 응답이 느립니다: {duration.TotalSeconds}초");
|
|
/// }
|
|
///
|
|
/// return (response, duration);
|
|
/// }
|
|
/// </code>
|
|
/// </example>
|
|
public static async UniTask<(T, TimeSpan)> RequestPostWithDuration<T>(string url, string body, Dictionary<string, string> header = null, bool useAuth = true)
|
|
{
|
|
return await RequestWithDuration<T>(url, body, "post", header, useAuth);
|
|
}
|
|
|
|
/// <summary>
|
|
/// GET 방식으로 API를 호출하고 처리 시간도 함께 반환합니다.
|
|
/// </summary>
|
|
/// <typeparam name="T">응답 데이터를 변환할 타입</typeparam>
|
|
/// <param name="url">요청할 URL (http가 포함되지 않은 경우 Domain과 결합)</param>
|
|
/// <param name="body">요청에 포함할 파라미터 (URL 쿼리스트링으로 변환)</param>
|
|
/// <param name="header">추가할 헤더 정보 (null 가능)</param>
|
|
/// <param name="useAuth">인증 토큰 사용 여부</param>
|
|
/// <returns>지정된 타입으로 변환된 응답 데이터와 요청 처리 시간</returns>
|
|
/// <example>
|
|
/// <code>
|
|
/// // 서버 상태 확인 및 응답 시간 측정 예제
|
|
/// public async UniTask<(ServerStatus, TimeSpan)> CheckServerStatus()
|
|
/// {
|
|
/// var (status, duration) = await HttpRequester.RequestGetWithDuration<ServerStatus>("/api/server/status");
|
|
///
|
|
/// // 응답 시간에 따른 서버 상태 평가
|
|
/// if (duration.TotalMilliseconds < 100)
|
|
/// {
|
|
/// Debug.Log("서버 응답 상태: 매우 좋음");
|
|
/// }
|
|
/// else if (duration.TotalMilliseconds < 500)
|
|
/// {
|
|
/// Debug.Log("서버 응답 상태: 양호");
|
|
/// }
|
|
/// else
|
|
/// {
|
|
/// Debug.Log("서버 응답 상태: 지연됨");
|
|
/// }
|
|
///
|
|
/// return (status, duration);
|
|
/// }
|
|
/// </code>
|
|
/// </example>
|
|
public static async UniTask<(T, TimeSpan)> RequestGetWithDuration<T>(string url, Dictionary<string, object> body = null, Dictionary<string, string> header = null, bool useAuth = true)
|
|
{
|
|
return await RequestWithDuration<T>(url, body == null ? null : JsonHelper.ToJson(body), "get", header, useAuth);
|
|
}
|
|
|
|
/// <summary>
|
|
/// HTTP 요청을 처리하고 처리 시간을 측정하는 내부 메소드
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 이 메소드는 모든 성능 측정이 필요한 HTTP 요청의 공통 로직을 처리합니다:
|
|
/// - 요청 처리 시간 측정
|
|
/// - URL 구성
|
|
/// - HTTP 메소드 설정
|
|
/// - 헤더 설정
|
|
/// - 요청 로깅
|
|
/// - 응답 처리 및 역직렬화
|
|
/// </remarks>
|
|
/// <typeparam name="T">응답 데이터를 변환할 타입</typeparam>
|
|
/// <param name="url">요청할 URL</param>
|
|
/// <param name="body">요청 본문 (JSON 문자열)</param>
|
|
/// <param name="methodString">HTTP 메소드 문자열</param>
|
|
/// <param name="header">추가할 헤더 정보</param>
|
|
/// <param name="useAuth">인증 토큰 사용 여부</param>
|
|
/// <returns>지정된 타입으로 변환된 응답 데이터와 요청 처리 시간의 튜플</returns>
|
|
private static async UniTask<(T, TimeSpan)> RequestWithDuration<T>(string url, string body = null, string methodString = "", Dictionary<string, string> header = null, bool useAuth = true)
|
|
{
|
|
HTTPMethods method = StringToMethod(methodString);
|
|
if (!url.Contains("http")) url = $"{Domain}{url}";
|
|
var request = SelectHTTPRequest(method, url);
|
|
request.DownloadSettings = new Best.HTTP.Request.Settings.DownloadSettings() { ContentStreamMaxBuffered = 1024 * 1024 * 200 };
|
|
request.MethodType = method;
|
|
request.SetHeader("Content-Type", "application/json; charset=utf-8");
|
|
JObject headerObject = new JObject();
|
|
headerObject.Add("Content-Type", "application/json; charset=utf-8");
|
|
if (header != null)
|
|
{
|
|
foreach (var kvp in header)
|
|
{
|
|
request.SetHeader(kvp.Key, kvp.Value);
|
|
headerObject.Add(kvp.Key, kvp.Value);
|
|
}
|
|
}
|
|
//ULog.Debug($"RequestWithDuration APIToken :{AuthService.Instance.Entiti.accessToken}");
|
|
//if (useAuth) request.SetHeader("access-token", AuthService.Instance.Entiti.accessToken);
|
|
if (body != null)
|
|
{
|
|
request.UploadSettings.UploadStream =
|
|
new MemoryStream(Encoding.UTF8.GetBytes(body));
|
|
}
|
|
|
|
HttpLogEntry log = ServerLog.LogHttpRequest(url, methodString, headerObject.ToString(Formatting.None), body, DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"));
|
|
//var response = await request.GetFromJsonResultAsync<T>();
|
|
var now = DateTime.UtcNow;
|
|
bool isMainThread = PlayerLoopHelper.IsMainThread;
|
|
var response = await request.GetAsStringAsync();
|
|
#if !UNITY_WEBGL || UNITY_EDITOR
|
|
if (!isMainThread) await UniTask.SwitchToThreadPool();
|
|
#endif
|
|
var diff = DateTime.UtcNow - now;
|
|
log.ResponseData = response;
|
|
log.ResponseDate = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
|
|
ServerLog.LogHttpResponse(log);
|
|
if (typeof(T) == typeof(string))
|
|
{
|
|
return ((T)(object)response, diff);
|
|
}
|
|
else
|
|
{
|
|
return (JsonHelper.FromJson<T>(response), diff);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 원격 서버에서 파일을 다운로드합니다.
|
|
/// </summary>
|
|
/// <param name="url">다운로드할 파일의 URL</param>
|
|
/// <param name="savePath">파일을 저장할 로컬 경로</param>
|
|
/// <param name="OnComplete">다운로드 완료시 호출될 콜백</param>
|
|
/// <param name="OnProgress">다운로드 진행 상태 업데이트 콜백 (progress: 다운로드된 바이트 수, length: 전체 파일 크기)</param>
|
|
/// <param name="OnError">오류 발생시 호출될 콜백</param>
|
|
/// <returns>발행된 HTTP 요청 객체 (파일이 이미 존재하는 경우 null)</returns>
|
|
/// <remarks>
|
|
/// 지정된 경로에 파일이 이미 존재하는 경우 다운로드를 수행하지 않고 바로 OnComplete 콜백을 호출합니다.
|
|
/// </remarks>
|
|
/// <example>
|
|
/// <code>
|
|
/// // 이미지 파일 다운로드 예제
|
|
/// public void DownloadImage(string imageUrl, string fileName)
|
|
/// {
|
|
/// string savePath = Path.Combine(Application.persistentDataPath, fileName);
|
|
///
|
|
/// HttpRequester.Download(
|
|
/// imageUrl,
|
|
/// savePath,
|
|
/// () => {
|
|
/// Debug.Log($"이미지가 성공적으로 다운로드되었습니다: {savePath}");
|
|
/// // 다운로드된 이미지 사용 코드...
|
|
/// },
|
|
/// (progress, length) => {
|
|
/// float percentage = (float)progress / length * 100f;
|
|
/// Debug.Log($"다운로드 진행률: {percentage:F1}%");
|
|
/// },
|
|
/// (error) => {
|
|
/// Debug.LogError($"다운로드 실패: {error}");
|
|
/// }
|
|
/// );
|
|
/// }
|
|
/// </code>
|
|
/// </example>
|
|
public static HTTPRequest Download(string url, string savePath, Action OnComplete, Action<long, long> OnProgress = null, Action<string> OnError = null)
|
|
{
|
|
ULog.Debug($"Download {url}");
|
|
if (File.Exists(savePath))
|
|
{
|
|
OnComplete.Invoke();
|
|
return null;
|
|
}
|
|
|
|
OnRequestFinishedDelegate onRequest = (HTTPRequest req, HTTPResponse resp) =>
|
|
{
|
|
req.DownloadSettings.OnDownloadStarted = null;
|
|
req.DownloadSettings.OnDownloadProgress = null;
|
|
req.DownloadSettings.DownloadStreamFactory = null;
|
|
|
|
switch (req.State)
|
|
{
|
|
case HTTPRequestStates.Finished:
|
|
if (resp.IsSuccess)
|
|
{
|
|
#if UNITY_WEBGL && !UNITY_EDITOR
|
|
// WebGL: 백그라운드 쓰레드 불가 -> 완료 시 메모리 데이터로 파일 저장
|
|
try
|
|
{
|
|
System.IO.File.WriteAllBytes(savePath, resp.Data);
|
|
OnComplete?.Invoke();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ULog.Error($"Failed to write file on WebGL: {ex.Message}", ex);
|
|
OnError?.Invoke($"Failed to write file: {ex.Message}");
|
|
}
|
|
#else
|
|
//스트리밍 파일 기록은 OnDownloadStarted에서 처리됨
|
|
OnComplete.Invoke();
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
ULog.Warning($"Server error({resp.StatusCode} - {resp.Message})! ");
|
|
OnError?.Invoke($"Server error({resp.StatusCode} - {resp.Message})!");
|
|
}
|
|
break;
|
|
default:
|
|
string detailedError = req.Exception != null ? req.Exception.ToString() : (resp != null ? resp.Message : "Unknown error");
|
|
string errorMsgDefault = $"Request failed! State: {req.State}, URL: {req.CurrentUri}, Error: {detailedError}";
|
|
ULog.Error(errorMsgDefault, req.Exception);
|
|
//ULog.Error(req.State.ToString(), req.Exception != null ? req.Exception: new Exception(resp?.Message));
|
|
OnError?.Invoke(errorMsgDefault);
|
|
break;
|
|
}
|
|
};
|
|
|
|
var request = SelectHTTPRequest(HTTPMethods.Get, url, onRequest);
|
|
#if UNITY_WEBGL && !UNITY_EDITOR
|
|
// WebGL: 쓰레드 기반 스트리밍 미사용. 진행률 콜백만 연결.
|
|
request.DownloadSettings.OnDownloadProgress += (HTTPRequest req, long progress, long length) =>
|
|
{
|
|
ULog.Debug($"Download Progress! progress:{progress} length:{length}");
|
|
OnProgress?.Invoke(progress, length);
|
|
};
|
|
// DownloadStreamFactory/BlockingDownloadContentStream 사용 금지
|
|
#else
|
|
|
|
request.DownloadSettings.OnDownloadStarted += async (HTTPRequest req, HTTPResponse resp, DownloadContentStream stream) =>
|
|
{
|
|
ULog.Debug("Download Started");
|
|
|
|
long len = await UniTask.RunOnThreadPool<long>(() => ConsumeDownloadStream(savePath, stream as BlockingDownloadContentStream));
|
|
if (len == 0)
|
|
OnError?.Invoke($"Download Fail!");
|
|
|
|
ULog.Debug($"Download finished!");
|
|
};
|
|
request.DownloadSettings.OnDownloadProgress += (HTTPRequest req, long progress, long length) =>
|
|
{
|
|
ULog.Debug($"Download Progress! progress:{progress} length:{length}");
|
|
OnProgress?.Invoke(progress, length);
|
|
};
|
|
request.DownloadSettings.DownloadStreamFactory = (req, resp, bufferAvailableHandler)
|
|
=> new BlockingDownloadContentStream(resp, req.DownloadSettings.ContentStreamMaxBuffered, bufferAvailableHandler);
|
|
#endif
|
|
return request.Send();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 다운로드 스트림을 처리하여 파일로 저장합니다.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 이 메소드는 스트림 데이터를 지정된 파일로 저장하며, 다운로드가 완료되거나 중단될 때까지 블로킹 방식으로 데이터를 처리합니다.
|
|
/// 다운로드에 실패한 경우 (길이가 0인 경우) 빈 파일은 삭제됩니다.
|
|
/// </remarks>
|
|
/// <param name="savePath">다운로드한 데이터를 저장할 파일 경로</param>
|
|
/// <param name="blockingStream">다운로드 콘텐츠 스트림</param>
|
|
/// <returns>저장된 파일의 크기(바이트)</returns>
|
|
/// <example>
|
|
/// <code>
|
|
/// // 내부적으로 Download 메소드에서 사용됩니다
|
|
/// long fileSize = await UniTask.RunOnThreadPool<long>(() =>
|
|
/// ConsumeDownloadStream(savePath, downloadStream as BlockingDownloadContentStream));
|
|
/// </code>
|
|
/// </example>
|
|
private static long ConsumeDownloadStream(string savePath, BlockingDownloadContentStream blockingStream)
|
|
{
|
|
long len = 0;
|
|
using (FileStream fs = new FileStream(savePath, System.IO.FileMode.Create))
|
|
{
|
|
try
|
|
{
|
|
//ULog.Debug($"ConsumeDownloadStream blockingStream:{blockingStream == null}");
|
|
while (blockingStream != null && !blockingStream.IsCompleted)
|
|
{
|
|
if (blockingStream.TryTake(out var buffer))
|
|
{
|
|
try
|
|
{
|
|
//ULog.Debug($"ConsumeDownloadStream buffer.Data:{buffer.Data.Length}, Offset:{buffer.Offset}, Count:{buffer.Count}");
|
|
fs.Write(buffer.Data, buffer.Offset, buffer.Count);
|
|
}
|
|
finally
|
|
{
|
|
BufferPool.Release(buffer);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (blockingStream != null) blockingStream.Dispose();
|
|
|
|
len = fs.Length;
|
|
}
|
|
}
|
|
if (len == 0) File.Delete(savePath);
|
|
return len;
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// HTTP 메소드와 URL을 기반으로 적절한 HTTPRequest 객체를 생성합니다.
|
|
/// </summary>
|
|
/// <param name="methods">요청에 사용할 HTTP 메소드</param>
|
|
/// <param name="url">요청할 URL</param>
|
|
/// <param name="onRequest">요청 완료 시 호출될 콜백 (선택 사항)</param>
|
|
/// <returns>생성된 HTTPRequest 객체</returns>
|
|
/// <example>
|
|
/// <code>
|
|
/// // 내부 사용 예제 (직접 사용하지 않음)
|
|
/// HTTPRequest request = SelectHTTPRequest(HTTPMethods.Get, "https://api.example.com/data");
|
|
/// request.SetHeader("Custom-Header", "HeaderValue");
|
|
/// request.Send();
|
|
/// </code>
|
|
/// </example>
|
|
private static HTTPRequest SelectHTTPRequest(HTTPMethods methods, string url, OnRequestFinishedDelegate onRequest = null)
|
|
{
|
|
switch (methods)
|
|
{
|
|
case HTTPMethods.Get:
|
|
|
|
if (onRequest != null)
|
|
return HTTPRequest.CreateGet(url, onRequest);
|
|
else
|
|
return HTTPRequest.CreateGet(url);
|
|
case HTTPMethods.Post:
|
|
return HTTPRequest.CreatePost(url);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 문자열 형태의 HTTP 메소드를 Best.HTTP.HTTPMethods 열거형으로 변환합니다.
|
|
/// </summary>
|
|
/// <param name="method">변환할 HTTP 메소드 문자열 ("get", "post" 등)</param>
|
|
/// <returns>해당하는 HTTPMethods 열거형 값</returns>
|
|
/// <exception cref="ArgumentException">지원하지 않는 HTTP 메소드인 경우 발생</exception>
|
|
private static HTTPMethods StringToMethod(string method)
|
|
{
|
|
return method.ToLower() switch
|
|
{
|
|
"get" => HTTPMethods.Get,
|
|
"post" => HTTPMethods.Post,
|
|
_ => throw new ArgumentException($"Unsupported HTTP method: {method}"),
|
|
};
|
|
}
|
|
}
|
|
|
|
}
|