초기 커밋.
This commit is contained in:
522
Assets/Scripts/UVC/Network/HttpRequester.cs
Normal file
522
Assets/Scripts/UVC/Network/HttpRequester.cs
Normal file
@@ -0,0 +1,522 @@
|
||||
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)로 처리됩니다.
|
||||
/// </remarks>
|
||||
public static class HttpRequester
|
||||
{
|
||||
/// <summary>
|
||||
/// 기본 도메인 주소입니다.
|
||||
/// </summary>
|
||||
public static string Domain {get; set; } = ""; // Replace with your actual API domain
|
||||
|
||||
/// <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, HTTPMethods.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, HTTPMethods.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, HTTPMethods.Get, header, useAuth);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HTTP 요청을 처리하는 내부 메소드
|
||||
/// </summary>
|
||||
private static async UniTask<T> Request<T>(string url, object body = null, HTTPMethods method = HTTPMethods.Post, Dictionary<string, string> header = null, bool useAuth = false)
|
||||
{
|
||||
string bodyString = "";
|
||||
if (body != null)
|
||||
{
|
||||
if (body is string)
|
||||
{
|
||||
bodyString = body.ToString();
|
||||
}
|
||||
else if (body is Dictionary<string, object>)
|
||||
{
|
||||
bodyString = JsonHelper.ToJson(body);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
HttpLogEntry log = ServerLog.LogHttpRequest(url, method.ToString(), headerObject.ToString(Formatting.None), bodyString, DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"));
|
||||
//Debug.Log($"Request APIToken :{AuthService.Instance.Entiti.accessToken}");
|
||||
if (body != null)
|
||||
{
|
||||
if (body is string)
|
||||
{
|
||||
request.UploadSettings.UploadStream =
|
||||
new MemoryStream(Encoding.UTF8.GetBytes(body as string));
|
||||
}
|
||||
else if (body is Dictionary<string, object>)
|
||||
{
|
||||
request.UploadSettings.UploadStream =
|
||||
new MemoryStream(Encoding.UTF8.GetBytes(JsonHelper.ToJson(body)));
|
||||
}
|
||||
}
|
||||
//var response = await request.GetFromJsonResultAsync<T>();
|
||||
var response = await request.GetAsStringAsync();
|
||||
log.ResponseData = response;
|
||||
log.ResponseDate = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
|
||||
ServerLog.LogHttpResponse(log);
|
||||
//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, HTTPMethods.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, HTTPMethods.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, HTTPMethods.Get, header, useAuth);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HTTP 요청을 처리하고 처리 시간을 측정하는 내부 메소드
|
||||
/// </summary>
|
||||
private static async UniTask<(T, TimeSpan)> RequestWithDuration<T>(string url, object body = null, HTTPMethods method = HTTPMethods.Post, Dictionary<string, string> header = null, bool useAuth = true)
|
||||
{
|
||||
string bodyString = "";
|
||||
if (body != null)
|
||||
{
|
||||
if (body is string)
|
||||
{
|
||||
bodyString = body.ToString();
|
||||
}
|
||||
else if (body is Dictionary<string, object>)
|
||||
{
|
||||
bodyString = JsonHelper.ToJson(body);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
//Debug.Log($"RequestWithDuration APIToken :{AuthService.Instance.Entiti.accessToken}");
|
||||
//if (useAuth) request.SetHeader("access-token", AuthService.Instance.Entiti.accessToken);
|
||||
if (body != null)
|
||||
{
|
||||
if (body is string)
|
||||
{
|
||||
request.UploadSettings.UploadStream =
|
||||
new MemoryStream(Encoding.UTF8.GetBytes(body as string));
|
||||
}
|
||||
else if (body is Dictionary<string, object>)
|
||||
{
|
||||
request.UploadSettings.UploadStream =
|
||||
new MemoryStream(Encoding.UTF8.GetBytes(JsonHelper.ToJson(body)));
|
||||
}
|
||||
}
|
||||
|
||||
HttpLogEntry log = ServerLog.LogHttpRequest(url, method.ToString(), headerObject.ToString(Formatting.None), bodyString, DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"));
|
||||
//var response = await request.GetFromJsonResultAsync<T>();
|
||||
var now = DateTime.UtcNow;
|
||||
var response = await request.GetAsStringAsync();
|
||||
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)
|
||||
{
|
||||
Debug.Log($"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)
|
||||
{
|
||||
//System.IO.File.WriteAllBytes(savePath, resp.Data);
|
||||
OnComplete.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"Server error({resp.StatusCode} - {resp.Message})! " + new Exception(resp.Message));
|
||||
OnError?.Invoke($"Server error({resp.StatusCode} - {resp.Message})!");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
Debug.Log(req.State.ToString() + ", " + new Exception(resp.Message));
|
||||
OnError?.Invoke(req.State.ToString());
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
var request = SelectHTTPRequest(HTTPMethods.Get, url, onRequest);
|
||||
|
||||
request.DownloadSettings.OnDownloadStarted += async (HTTPRequest req, HTTPResponse resp, DownloadContentStream stream) =>
|
||||
{
|
||||
Debug.Log("Download Started");
|
||||
|
||||
long len = await UniTask.RunOnThreadPool<long>(() => ConsumeDownloadStream(savePath, stream as BlockingDownloadContentStream));
|
||||
if (len == 0)
|
||||
OnError?.Invoke($"Download Fail!");
|
||||
|
||||
Debug.Log($"Download finished!");
|
||||
};
|
||||
request.DownloadSettings.OnDownloadProgress += (HTTPRequest req, long progress, long length) =>
|
||||
{
|
||||
Debug.Log($"Download Progress! progress:{progress} length:{length}");
|
||||
OnProgress?.Invoke(progress, length);
|
||||
};
|
||||
request.DownloadSettings.DownloadStreamFactory = (req, resp, bufferAvailableHandler)
|
||||
=> new BlockingDownloadContentStream(resp, req.DownloadSettings.ContentStreamMaxBuffered, bufferAvailableHandler);
|
||||
|
||||
return request.Send();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 다운로드 스트림을 처리하여 파일로 저장합니다.
|
||||
/// </summary>
|
||||
private static long ConsumeDownloadStream(string savePath, BlockingDownloadContentStream blockingStream)
|
||||
{
|
||||
long len = 0;
|
||||
using (FileStream fs = new FileStream(savePath, System.IO.FileMode.Create))
|
||||
{
|
||||
try
|
||||
{
|
||||
//Debug.Log($"ConsumeDownloadStream blockingStream:{blockingStream == null}");
|
||||
while (blockingStream != null && !blockingStream.IsCompleted)
|
||||
{
|
||||
if (blockingStream.TryTake(out var buffer))
|
||||
{
|
||||
try
|
||||
{
|
||||
//Debug.Log($"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>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/Network/HttpRequester.cs.meta
Normal file
2
Assets/Scripts/UVC/Network/HttpRequester.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6d0b4948de15d4741ac98029df843f34
|
||||
367
Assets/Scripts/UVC/Network/MQTTService.cs
Normal file
367
Assets/Scripts/UVC/Network/MQTTService.cs
Normal file
@@ -0,0 +1,367 @@
|
||||
using Best.MQTT;
|
||||
using Best.MQTT.Packets.Builders;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UVC.Log;
|
||||
|
||||
namespace UVC.network
|
||||
{
|
||||
/// <summary>
|
||||
/// MQTT 클라이언트를 관리하고 메시지 송수신을 처리하는 서비스 클래스입니다.
|
||||
/// 이 클래스는 MQTT 브로커와의 연결 관리, 토픽 구독, 메시지 발행 등의 기능을 제공합니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 이 클래스는 스레드 안전한 방식으로 토픽 핸들러를 관리하며, 연결 끊김 시 자동 재연결 기능을 제공합니다.
|
||||
/// 내부적으로 Best.MQTT 라이브러리를 사용하여 MQTT 프로토콜 통신을 구현합니다.
|
||||
/// </remarks>
|
||||
public class MQTTService
|
||||
{
|
||||
private string MQTTDomain = "localhost";
|
||||
private int MQTTPort = 1883;
|
||||
|
||||
private MQTTClient client;
|
||||
|
||||
private bool autoReconnect = true; // 자동 재연결 여부
|
||||
private int reconnectDelay = 1000; // 재연결 시도 간격 (ms)
|
||||
|
||||
private ConcurrentDictionary<string, Action<string, string>> topicHandler;
|
||||
|
||||
/// <summary>
|
||||
/// MQTT 브로커와의 연결 상태를 확인합니다.
|
||||
/// </summary>
|
||||
/// <value>클라이언트가 초기화되고 브로커에 연결된 경우 <c>true</c>를 반환합니다.</value>
|
||||
public bool IsConnected => client != null && client.State == ClientStates.Connected;
|
||||
|
||||
private Action<string, string> onMessageReceived;
|
||||
|
||||
/// <summary>
|
||||
/// MQTTService 인스턴스를 생성합니다.
|
||||
/// </summary>
|
||||
/// <param name="domain">MQTT 브로커의 호스트명 또는 IP 주소입니다.</param>
|
||||
/// <param name="port">MQTT 브로커의 포트 번호입니다.</param>
|
||||
/// <param name="autoReconnect">연결이 끊겼을 때 자동으로 재연결을 시도할지 여부입니다.</param>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // localhost의 기본 MQTT 포트(1883)에 연결하는 서비스 생성
|
||||
/// var mqttService = new MQTTService("localhost", 1883);
|
||||
///
|
||||
/// // 자동 재연결 비활성화
|
||||
/// var mqttService = new MQTTService("mqtt.example.com", 8883, false);
|
||||
/// </code>
|
||||
/// </example>
|
||||
public MQTTService(string domain, int port = 1883, bool autoReconnect = true)
|
||||
{
|
||||
topicHandler = new ConcurrentDictionary<string, Action<string, string>>();
|
||||
MQTTDomain = domain;
|
||||
MQTTPort = port;
|
||||
this.autoReconnect = autoReconnect;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MQTT 클라이언트를 생성하고 이벤트 핸들러를 등록합니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 이 메서드는 클라이언트가 이미 존재하는 경우 기존 연결을 종료한 후 새로운 클라이언트를 생성합니다.
|
||||
/// </remarks>
|
||||
private void CreateMQTTClient()
|
||||
{
|
||||
Debug.Log($"MQTT Domain:{MQTTDomain} , MQTTPORT:{MQTTPort}");
|
||||
var options = new ConnectionOptionsBuilder()
|
||||
.WithTCP(MQTTDomain, MQTTPort)
|
||||
.Build();
|
||||
|
||||
if (client != null) Disconnect();
|
||||
|
||||
client = new MQTTClient(options);
|
||||
client.OnConnected += OnConnectedMQTT;
|
||||
client.OnStateChanged += OnStateChangedMQTT;
|
||||
client.OnDisconnect += OnDisconnectedMQTT;
|
||||
client.OnError += OnErrorMQTT;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 MQTT 토픽에 대한 메시지 핸들러를 추가합니다.
|
||||
/// </summary>
|
||||
/// <param name="topic">구독할 MQTT 토픽입니다.</param>
|
||||
/// <param name="handler">토픽에 메시지가 수신되면 호출될 핸들러입니다.
|
||||
/// 첫 번째 매개변수는 토픽이고 두 번째 매개변수는 메시지 내용입니다.</param>
|
||||
/// <remarks>
|
||||
/// 하나의 토픽에 여러 핸들러를 등록할 수 있으며, 메시지 수신 시 모든 핸들러가 호출됩니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // MQTT 서비스 인스턴스 생성
|
||||
/// var mqtt = new MQTTService("localhost", 1883);
|
||||
///
|
||||
/// // 핸들러 등록 및 연결
|
||||
/// mqtt.AddTopicHandler("sensor/temperature", (topic, message) => {
|
||||
/// Debug.Log($"온도 데이터 수신: {message}");
|
||||
/// });
|
||||
/// mqtt.Connect();
|
||||
/// </code>
|
||||
/// </example>
|
||||
public void AddTopicHandler(string topic, Action<string, string> handler)
|
||||
{
|
||||
topicHandler.AddOrUpdate(
|
||||
topic,
|
||||
// 키가 없을 때 새로운 핸들러 추가
|
||||
handler,
|
||||
// 키가 이미 있을 때 기존 핸들러에 추가
|
||||
(_, existingHandler) => existingHandler + handler
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 MQTT 토픽에서 메시지 핸들러를 제거합니다.
|
||||
/// </summary>
|
||||
/// <param name="topic">핸들러를 제거할 MQTT 토픽입니다.</param>
|
||||
/// <param name="handler">제거할 메시지 핸들러입니다.</param>
|
||||
/// <remarks>
|
||||
/// 지정된 토픽에 대한 모든 핸들러가 제거되면 해당 토픽에 대한 키도 제거됩니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // 핸들러 정의
|
||||
/// Action<string, string> temperatureHandler = (topic, message) => {
|
||||
/// Debug.Log($"온도 데이터 수신: {message}");
|
||||
/// };
|
||||
///
|
||||
/// // 핸들러 등록
|
||||
/// mqtt.AddTopicHandler("sensor/temperature", temperatureHandler);
|
||||
///
|
||||
/// // 나중에 핸들러 제거
|
||||
/// mqtt.RemoveTopicHandler("sensor/temperature", temperatureHandler);
|
||||
/// </code>
|
||||
/// </example>
|
||||
public void RemoveTopicHandler(string topic, Action<string, string> handler)
|
||||
{
|
||||
topicHandler.AddOrUpdate(
|
||||
topic,
|
||||
// 키가 없는 경우 - 여기서는 발생하면 안 됨
|
||||
_ => null,
|
||||
// 기존 핸들러에서 제거
|
||||
(_, existingHandler) =>
|
||||
{
|
||||
var updatedHandler = existingHandler - handler;
|
||||
return updatedHandler;
|
||||
}
|
||||
);
|
||||
|
||||
// 핸들러가 null이면 키 자체를 제거
|
||||
if (topicHandler.TryGetValue(topic, out var currentHandler) && currentHandler == null)
|
||||
{
|
||||
topicHandler.TryRemove(topic, out _);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 토픽 핸들러를 제거합니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 이 메서드는 모든 토픽 구독을 효과적으로 취소합니다. 다음에 연결할 때
|
||||
/// 새로운 핸들러를 추가해야 합니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // 모든 핸들러 제거
|
||||
/// mqtt.ClearTopicHandlers();
|
||||
/// </code>
|
||||
/// </example>
|
||||
public void ClearTopicHandlers()
|
||||
{
|
||||
topicHandler.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MQTT 브로커에 연결합니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 이 메서드는 새 MQTT 클라이언트를 생성하고 브로커에 연결을 시작합니다.
|
||||
/// 연결 성공 시 OnConnectedMQTT 이벤트 핸들러가 호출되며, 이때 등록된 모든 토픽을 구독합니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // MQTT 서비스 인스턴스 생성
|
||||
/// var mqtt = new MQTTService("test.mosquitto.org", 1883);
|
||||
///
|
||||
/// // 토픽 핸들러 추가 후 연결
|
||||
/// mqtt.AddTopicHandler("test/topic", (topic, message) => {
|
||||
/// Debug.Log($"메시지 수신: {message}");
|
||||
/// });
|
||||
/// mqtt.Connect();
|
||||
/// </code>
|
||||
/// </example>
|
||||
public void Connect()
|
||||
{
|
||||
CreateMQTTClient();
|
||||
client.BeginConnect(ConnectPacketBuilderCallback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MQTT 브로커와의 연결을 종료합니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 이 메서드는 모든 이벤트 핸들러를 제거하고 연결된 경우 MQTT 브로커로 정상 연결 종료 패킷을 전송합니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // 사용이 끝난 후 연결 종료
|
||||
/// mqtt.Disconnect();
|
||||
/// </code>
|
||||
/// </example>
|
||||
public void Disconnect()
|
||||
{
|
||||
if (client != null)
|
||||
{
|
||||
// 이벤트 핸들러 제거
|
||||
client.OnConnected -= OnConnectedMQTT;
|
||||
client.OnStateChanged -= OnStateChangedMQTT;
|
||||
client.OnDisconnect -= OnDisconnectedMQTT;
|
||||
client.OnError -= OnErrorMQTT;
|
||||
if (IsConnected)
|
||||
{
|
||||
client.CreateDisconnectPacketBuilder()
|
||||
.WithReasonCode(DisconnectReasonCodes.NormalDisconnection)
|
||||
.WithReasonString("Bye")
|
||||
.BeginDisconnect();
|
||||
}
|
||||
client = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 연결 패킷 빌더를 구성하는 콜백 메서드입니다.
|
||||
/// </summary>
|
||||
/// <param name="client">MQTT 클라이언트 인스턴스</param>
|
||||
/// <param name="builder">연결 패킷 빌더</param>
|
||||
/// <returns>구성된 연결 패킷 빌더</returns>
|
||||
private ConnectPacketBuilder ConnectPacketBuilderCallback(MQTTClient client, ConnectPacketBuilder builder)
|
||||
{
|
||||
return builder.WithKeepAlive(60 * 60);//keep alive 1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MQTT 브로커에 연결되었을 때 호출되는 이벤트 핸들러입니다.
|
||||
/// </summary>
|
||||
/// <param name="client">연결된 MQTT 클라이언트</param>
|
||||
/// <remarks>
|
||||
/// 이 메서드는 연결 성공 시 등록된 모든 토픽에 대한 구독을 시작합니다.
|
||||
/// </remarks>
|
||||
private void OnConnectedMQTT(MQTTClient client)
|
||||
{
|
||||
Debug.Log($"MQTT OnConnected");
|
||||
BulkSubscribePacketBuilder builder = client.CreateBulkSubscriptionBuilder();
|
||||
foreach (var topic in topicHandler.Keys)
|
||||
{
|
||||
builder.WithTopic(new SubscribeTopicBuilder(topic).WithMessageCallback(OnTopic));
|
||||
}
|
||||
builder.BeginSubscribe();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MQTT 클라이언트의 상태가 변경되었을 때 호출되는 이벤트 핸들러입니다.
|
||||
/// </summary>
|
||||
/// <param name="client">MQTT 클라이언트</param>
|
||||
/// <param name="oldState">이전 상태</param>
|
||||
/// <param name="newState">새 상태</param>
|
||||
private void OnStateChangedMQTT(MQTTClient client, ClientStates oldState, ClientStates newState)
|
||||
{
|
||||
Debug.Log($"MQTT OnStateChanged {oldState} => {newState}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MQTT 브로커와의 연결이 종료되었을 때 호출되는 이벤트 핸들러입니다.
|
||||
/// </summary>
|
||||
/// <param name="client">MQTT 클라이언트</param>
|
||||
/// <param name="code">연결 종료 이유 코드</param>
|
||||
/// <param name="reason">연결 종료 이유 문자열</param>
|
||||
/// <remarks>
|
||||
/// 자동 재연결이 활성화된 경우 지정된 지연 시간 후에 재연결을 시도합니다.
|
||||
/// </remarks>
|
||||
private void OnDisconnectedMQTT(MQTTClient client, DisconnectReasonCodes code, string reason)
|
||||
{
|
||||
//ULog.Info($"MQTT Disconnected - code: {code}, reason: '{reason}'");
|
||||
Debug.Log($"MQTTDisconnected pcTime={DateTime.Now}");
|
||||
// 지연 후에만 한 번 연결
|
||||
if (Application.isPlaying && autoReconnect)
|
||||
{
|
||||
UniTask.Delay(reconnectDelay).ContinueWith(() =>
|
||||
{
|
||||
Connect();
|
||||
}).Forget();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MQTT 클라이언트에서 오류가 발생했을 때 호출되는 이벤트 핸들러입니다.
|
||||
/// </summary>
|
||||
/// <param name="client">MQTT 클라이언트</param>
|
||||
/// <param name="reason">오류 발생 이유</param>
|
||||
/// <remarks>
|
||||
/// 이 이벤트 후에는 OnDisconnectedMQTT 이벤트도 발생합니다.
|
||||
/// </remarks>
|
||||
private void OnErrorMQTT(MQTTClient client, string reason)
|
||||
{
|
||||
Debug.LogError($"MQTT OnError reason: '{reason}'");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 구독한 토픽으로 메시지가 수신되었을 때 호출되는 콜백입니다.
|
||||
/// </summary>
|
||||
/// <param name="client">MQTT 클라이언트</param>
|
||||
/// <param name="topic">구독 토픽 정보</param>
|
||||
/// <param name="topicName">수신된 메시지의 토픽 이름</param>
|
||||
/// <param name="message">수신된 메시지</param>
|
||||
/// <remarks>
|
||||
/// 이 메서드는 메시지를 로깅하고 해당 토픽에 등록된 모든 핸들러를 호출합니다.
|
||||
/// </remarks>
|
||||
private void OnTopic(MQTTClient client, SubscriptionTopic topic, string topicName, ApplicationMessage message)
|
||||
{
|
||||
string payload = Encoding.UTF8.GetString(message.Payload.Data, message.Payload.Offset, message.Payload.Count);
|
||||
Debug.Log($"MQTT OnTopic {topic.Filter.OriginalFilter} => {payload}");
|
||||
ServerLog.LogMqtt(MQTTDomain, MQTTPort.ToString(), topic.Filter.OriginalFilter, payload, DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"));
|
||||
if (onMessageReceived != null)
|
||||
{
|
||||
onMessageReceived.Invoke(topic.Filter.OriginalFilter, payload);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 토픽으로 메시지를 발행(publish)합니다.
|
||||
/// </summary>
|
||||
/// <param name="topic">메시지를 발행할 토픽입니다.</param>
|
||||
/// <param name="message">발행할 메시지 내용입니다.</param>
|
||||
/// <remarks>
|
||||
/// 이 메서드는 클라이언트가 연결되어 있지 않으면 오류 로그를 기록하고 메시지를 발행하지 않습니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // MQTT 서비스 인스턴스 생성 및 연결
|
||||
/// var mqtt = new MQTTService("localhost", 1883);
|
||||
/// mqtt.Connect();
|
||||
///
|
||||
/// // 메시지 발행
|
||||
/// mqtt.Publish("device/status", "online");
|
||||
///
|
||||
/// // JSON 형식 메시지 발행
|
||||
/// string jsonMessage = JsonUtility.ToJson(new DeviceStatus { status = "online", battery = 95 });
|
||||
/// mqtt.Publish("device/status", jsonMessage);
|
||||
/// </code>
|
||||
/// </example>
|
||||
public void Publish(string topic, string message)
|
||||
{
|
||||
if (client == null || client.State != ClientStates.Connected)
|
||||
{
|
||||
Debug.LogError("MQTT client is not connected. Cannot publish message.");
|
||||
return;
|
||||
}
|
||||
client.CreateApplicationMessageBuilder(topic)
|
||||
.WithPayload(message)
|
||||
.BeginPublish();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/Network/MQTTService.cs.meta
Normal file
2
Assets/Scripts/UVC/Network/MQTTService.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f998dc656acc3d7499dbf134df597fc0
|
||||
Reference in New Issue
Block a user