초기 커밋.

This commit is contained in:
김형인
2025-06-04 23:10:11 +09:00
parent 52d1b89070
commit 0a489ab39b
5035 changed files with 517142 additions and 0 deletions

View 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;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6d0b4948de15d4741ac98029df843f34

View 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&lt;string, string&gt; 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();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f998dc656acc3d7499dbf134df597fc0