using System; using System.Collections.Generic; using System.Net; using System.Text; using System.Threading; using UnityEngine; /// /// HTTP 서버 - Streamingle 웹 대시보드 UI를 제공합니다. /// Resources/StreamingleDashboard/ 폴더에서 HTML, CSS, JS를 로드합니다. /// RetargetingHTTPServer 패턴을 따릅니다. /// public class StreamingleDashboardServer { private HttpListener listener; private Thread listenerThread; private bool isRunning = false; private int httpPort; private int wsPort; private int retargetingWsPort; private string cachedCSS = ""; private string cachedTemplate = ""; private string cachedJS = ""; private List boundAddresses = new List(); public bool IsRunning => isRunning; public IReadOnlyList BoundAddresses => boundAddresses; public StreamingleDashboardServer(int httpPort, int wsPort, int retargetingWsPort = 0) { this.httpPort = httpPort; this.wsPort = wsPort; this.retargetingWsPort = retargetingWsPort; } public void Start() { if (isRunning) return; LoadResources(); try { listener = new HttpListener(); boundAddresses.Clear(); // http://+: 로 모든 인터페이스 바인딩 시도 (관리자 권한 또는 URL ACL 필요) bool wildcardBound = false; try { listener.Prefixes.Add($"http://+:{httpPort}/"); listener.Start(); wildcardBound = true; boundAddresses.Add($"http://localhost:{httpPort}"); string localIP = GetLocalIPAddress(); if (!string.IsNullOrEmpty(localIP)) { boundAddresses.Add($"http://{localIP}:{httpPort}"); } Debug.Log("[StreamingleDashboard] 모든 인터페이스 바인딩 성공 (LAN 접속 가능)"); } catch (Exception) { // 와일드카드 실패 시 리스너 재생성 try { listener.Close(); } catch { } listener = new HttpListener(); // LAN IP 직접 바인딩 시도 string localIP = GetLocalIPAddress(); bool lanBound = false; listener.Prefixes.Add($"http://localhost:{httpPort}/"); boundAddresses.Add($"http://localhost:{httpPort}"); listener.Prefixes.Add($"http://127.0.0.1:{httpPort}/"); boundAddresses.Add($"http://127.0.0.1:{httpPort}"); if (!string.IsNullOrEmpty(localIP)) { try { listener.Prefixes.Add($"http://{localIP}:{httpPort}/"); boundAddresses.Add($"http://{localIP}:{httpPort}"); lanBound = true; } catch (Exception) { // LAN IP 바인딩도 실패 } } listener.Start(); if (lanBound) Debug.Log($"[StreamingleDashboard] LAN IP 바인딩 성공 ({localIP})"); else Debug.LogWarning("[StreamingleDashboard] LAN 바인딩 실패. localhost만 접속 가능. 관리자 권한으로 실행하거나 다음 명령어를 실행하세요:\n" + $" netsh http add urlacl url=http://+:{httpPort}/ user=Everyone"); } isRunning = true; listenerThread = new Thread(HandleRequests); listenerThread.IsBackground = true; listenerThread.Start(); Debug.Log("[StreamingleDashboard] HTTP 서버 시작됨"); foreach (var addr in boundAddresses) { Debug.Log($"[StreamingleDashboard] 대시보드 접속: {addr}"); } } catch (Exception ex) { Debug.LogError($"[StreamingleDashboard] 서버 시작 실패: {ex.Message}"); } } public void Stop() { if (!isRunning) return; isRunning = false; try { listener?.Stop(); listener?.Close(); } catch (Exception) { } Debug.Log("[StreamingleDashboard] HTTP 서버 중지됨"); } private void LoadResources() { TextAsset cssAsset = Resources.Load("StreamingleDashboard/dashboard_style"); TextAsset templateAsset = Resources.Load("StreamingleDashboard/dashboard_template"); TextAsset jsAsset = Resources.Load("StreamingleDashboard/dashboard_script"); cachedCSS = cssAsset != null ? cssAsset.text : GetFallbackCSS(); cachedTemplate = templateAsset != null ? templateAsset.text : GetFallbackTemplate(); cachedJS = jsAsset != null ? jsAsset.text : GetFallbackJS(); if (cssAsset == null || templateAsset == null || jsAsset == null) { Debug.LogWarning("[StreamingleDashboard] 일부 리소스를 로드할 수 없습니다. Fallback 사용 중."); } } private void HandleRequests() { while (isRunning) { try { HttpListenerContext context = listener.GetContext(); ProcessRequest(context); } catch (HttpListenerException) { // 서버 종료 시 발생 } catch (Exception ex) { if (isRunning) { Debug.LogError($"[StreamingleDashboard] 요청 처리 오류: {ex.Message}"); } } } } private void ProcessRequest(HttpListenerContext context) { try { string path = context.Request.Url.AbsolutePath; string responseContent; string contentType; if (path == "/" || path == "/index.html") { responseContent = GenerateHTML(); contentType = "text/html; charset=utf-8"; } else if (path == "/style.css") { responseContent = cachedCSS; contentType = "text/css; charset=utf-8"; } else if (path == "/script.js") { responseContent = cachedJS .Replace("{{WS_PORT}}", wsPort.ToString()) .Replace("{{RETARGETING_WS_PORT}}", retargetingWsPort.ToString()); contentType = "application/javascript; charset=utf-8"; } else { context.Response.StatusCode = 404; responseContent = "Not Found"; contentType = "text/plain"; } byte[] buffer = Encoding.UTF8.GetBytes(responseContent); context.Response.ContentType = contentType; context.Response.ContentLength64 = buffer.Length; context.Response.AddHeader("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0"); context.Response.OutputStream.Write(buffer, 0, buffer.Length); context.Response.Close(); } catch (Exception ex) { Debug.LogError($"[StreamingleDashboard] 응답 처리 오류: {ex.Message}"); } } private string GenerateHTML() { string html = cachedTemplate; html = html.Replace("{{CSS}}", cachedCSS); html = html.Replace("{{JS}}", cachedJS .Replace("{{WS_PORT}}", wsPort.ToString()) .Replace("{{RETARGETING_WS_PORT}}", retargetingWsPort.ToString())); return html; } private string GetLocalIPAddress() { try { var host = Dns.GetHostEntry(Dns.GetHostName()); foreach (var ip in host.AddressList) { if (ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) { return ip.ToString(); } } } catch (Exception) { } return ""; } private string GetFallbackCSS() { return @" body { font-family: sans-serif; background: #0f172a; color: #f1f5f9; padding: 20px; } .error { color: #ef4444; padding: 20px; text-align: center; } "; } private string GetFallbackTemplate() { return @" Streamingle Dashboard
리소스 파일을 찾을 수 없습니다.
Assets/Resources/StreamingleDashboard/ 폴더에 파일이 있는지 확인해주세요.
"; } private string GetFallbackJS() { return "console.error('JavaScript resource not found');"; } }