From b33069fc9e4f5c728218e966d60a5a752bf2799b Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Tue, 5 Aug 2025 04:26:30 +0900 Subject: [PATCH 1/2] Update WebSocketManager.cs --- .../WelsonJS.Launcher/WebSocketManager.cs | 92 +++++++++---------- 1 file changed, 45 insertions(+), 47 deletions(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManager.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManager.cs index 43c8e6f..7b30358 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManager.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManager.cs @@ -5,12 +5,11 @@ // using System; using System.Collections.Concurrent; -using System.IO; using System.Net.WebSockets; -using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; +using System.Security.Cryptography; namespace WelsonJS.Launcher { @@ -28,12 +27,11 @@ namespace WelsonJS.Launcher private string MakeKey(string host, int port, string path) { - // To create a unique key for the WebSocket connection - string input = host + ":" + port + "/" + path; + string raw = host + ":" + port + "/" + path; using (var md5 = MD5.Create()) { - byte[] hash = md5.ComputeHash(Encoding.UTF8.GetBytes(input)); - return BitConverter.ToString(hash).Replace("-", "").ToLower(); + byte[] hash = md5.ComputeHash(Encoding.UTF8.GetBytes(raw)); + return BitConverter.ToString(hash).Replace("-", "").ToLower(); // 32자 } } @@ -41,34 +39,39 @@ namespace WelsonJS.Launcher { string key = MakeKey(host, port, path); - if (_wsPool.TryGetValue(key, out var entry) && entry.Socket?.State == WebSocketState.Open) - return entry.Socket; - - // 재연결 필요 - if (entry != null) + if (_wsPool.TryGetValue(key, out var entry)) { - _wsPool.TryRemove(key, out _); - entry.Socket?.Dispose(); + var socket = entry.Socket; + + if (socket != null) + { + if (socket.State == WebSocketState.Open) + return socket; + + Remove(host, port, path); + } } - var ws = new ClientWebSocket(); - Uri uri = new Uri($"ws://{host}:{port}/{path}"); + var newSocket = new ClientWebSocket(); + var uri = new Uri($"ws://{host}:{port}/{path}"); try { - await ws.ConnectAsync(uri, CancellationToken.None); + await newSocket.ConnectAsync(uri, CancellationToken.None); + _wsPool[key] = new WebSocketEntry { - Socket = ws, + Socket = newSocket, Host = host, Port = port, Path = path }; - return ws; + + return newSocket; } catch { - ws.Dispose(); + newSocket.Dispose(); throw; } } @@ -78,53 +81,48 @@ namespace WelsonJS.Launcher string key = MakeKey(host, port, path); if (_wsPool.TryRemove(key, out var entry)) { - entry.Socket?.Abort(); - entry.Socket?.Dispose(); + try + { + entry.Socket?.Abort(); + entry.Socket?.Dispose(); + } + catch { /* ignore */ } } } - public async Task SendWithReconnectAsync(string host, int port, string path, byte[] message, CancellationToken token) + public async Task SendAndReceiveAsync(string host, int port, string path, string message, int timeoutSeconds) { - ClientWebSocket ws; + var buffer = Encoding.UTF8.GetBytes(message); + var cts = timeoutSeconds > 0 ? new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)) : new CancellationTokenSource(); try { - ws = await GetOrCreateAsync(host, port, path); - await ws.SendAsync(new ArraySegment(message), WebSocketMessageType.Text, true, token); - return true; + var socket = await GetOrCreateAsync(host, port, path); + await socket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, cts.Token); + + var recvBuffer = new byte[4096]; + var result = await socket.ReceiveAsync(new ArraySegment(recvBuffer), cts.Token); + return Encoding.UTF8.GetString(recvBuffer, 0, result.Count); } catch { Remove(host, port, path); + try { - ws = await GetOrCreateAsync(host, port, path); - await ws.SendAsync(new ArraySegment(message), WebSocketMessageType.Text, true, token); - return true; + var socket = await GetOrCreateAsync(host, port, path); + await socket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, cts.Token); + + var recvBuffer = new byte[4096]; + var result = await socket.ReceiveAsync(new ArraySegment(recvBuffer), cts.Token); + return Encoding.UTF8.GetString(recvBuffer, 0, result.Count); } catch { Remove(host, port, path); - return false; + throw; } } } - - public async Task SendAndReceiveAsync(string host, int port, string path, string message, int timeoutSeconds, int bufferSize = 65536) - { - var buffer = Encoding.UTF8.GetBytes(message); - CancellationTokenSource cts = timeoutSeconds > 0 - ? new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)) - : new CancellationTokenSource(); - - if (!await SendWithReconnectAsync(host, port, path, buffer, cts.Token)) - throw new IOException("Failed to send after reconnect"); - - ClientWebSocket ws = await GetOrCreateAsync(host, port, path); - - byte[] recvBuffer = new byte[bufferSize]; - WebSocketReceiveResult result = await ws.ReceiveAsync(new ArraySegment(recvBuffer), cts.Token); - return Encoding.UTF8.GetString(recvBuffer, 0, result.Count); - } } } From 57fae72f42ddbe3fd257985965bdb60176d6de52 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Tue, 5 Aug 2025 04:33:18 +0900 Subject: [PATCH 2/2] Refactor WebSocketManager for improved clarity and reliability Renamed internal classes and fields for clarity, added comments, and refactored SendAndReceiveAsync to use a helper method and a retry mechanism. Improved resource cleanup and error handling for stale sockets. These changes enhance code readability and robustness in WebSocket connection management. --- .../WelsonJS.Launcher/WebSocketManager.cs | 97 +++++++++---------- 1 file changed, 48 insertions(+), 49 deletions(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManager.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManager.cs index 7b30358..16f5471 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManager.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManager.cs @@ -6,123 +6,122 @@ using System; using System.Collections.Concurrent; using System.Net.WebSockets; +using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; -using System.Security.Cryptography; namespace WelsonJS.Launcher { public class WebSocketManager { - private class WebSocketEntry + private class Entry { - public ClientWebSocket Socket { get; set; } - public string Host { get; set; } - public int Port { get; set; } - public string Path { get; set; } + public ClientWebSocket Socket; + public string Host; + public int Port; + public string Path; } - private readonly ConcurrentDictionary _wsPool = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _pool = new ConcurrentDictionary(); + // Create a unique cache key based on host, port, and path using MD5 private string MakeKey(string host, int port, string path) { string raw = host + ":" + port + "/" + path; using (var md5 = MD5.Create()) { byte[] hash = md5.ComputeHash(Encoding.UTF8.GetBytes(raw)); - return BitConverter.ToString(hash).Replace("-", "").ToLower(); // 32자 + return BitConverter.ToString(hash).Replace("-", "").ToLower(); } } + // Get existing WebSocket if valid, otherwise connect and store a new one public async Task GetOrCreateAsync(string host, int port, string path) { string key = MakeKey(host, port, path); - if (_wsPool.TryGetValue(key, out var entry)) + if (_pool.TryGetValue(key, out var entry)) { - var socket = entry.Socket; + var sock = entry.Socket; + if (sock != null && sock.State == WebSocketState.Open) + return sock; - if (socket != null) - { - if (socket.State == WebSocketState.Open) - return socket; - - Remove(host, port, path); - } + // Remove stale or broken socket + Remove(host, port, path); } - var newSocket = new ClientWebSocket(); + var newSock = new ClientWebSocket(); var uri = new Uri($"ws://{host}:{port}/{path}"); try { - await newSocket.ConnectAsync(uri, CancellationToken.None); - - _wsPool[key] = new WebSocketEntry + await newSock.ConnectAsync(uri, CancellationToken.None); + _pool[key] = new Entry { - Socket = newSocket, + Socket = newSock, Host = host, Port = port, Path = path }; - - return newSocket; + return newSock; } catch { - newSocket.Dispose(); + newSock.Dispose(); throw; } } + // Remove WebSocket from the pool and dispose public void Remove(string host, int port, string path) { string key = MakeKey(host, port, path); - if (_wsPool.TryRemove(key, out var entry)) + if (_pool.TryRemove(key, out var entry)) { try { entry.Socket?.Abort(); entry.Socket?.Dispose(); } - catch { /* ignore */ } + catch { /* Ignore errors */ } } } - public async Task SendAndReceiveAsync(string host, int port, string path, string message, int timeoutSeconds) + // Send message and receive response with 1 retry on failure + public async Task SendAndReceiveAsync(string host, int port, string path, string message, int timeoutSec) { - var buffer = Encoding.UTF8.GetBytes(message); - var cts = timeoutSeconds > 0 ? new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)) : new CancellationTokenSource(); + byte[] buf = Encoding.UTF8.GetBytes(message); + var cts = timeoutSec > 0 + ? new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSec)) + : new CancellationTokenSource(); - try + for (int attempt = 0; attempt < 2; attempt++) { - var socket = await GetOrCreateAsync(host, port, path); - await socket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, cts.Token); - - var recvBuffer = new byte[4096]; - var result = await socket.ReceiveAsync(new ArraySegment(recvBuffer), cts.Token); - return Encoding.UTF8.GetString(recvBuffer, 0, result.Count); - } - catch - { - Remove(host, port, path); - try { - var socket = await GetOrCreateAsync(host, port, path); - await socket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, cts.Token); - - var recvBuffer = new byte[4096]; - var result = await socket.ReceiveAsync(new ArraySegment(recvBuffer), cts.Token); - return Encoding.UTF8.GetString(recvBuffer, 0, result.Count); + return await TrySendAndReceiveAsync(host, port, path, buf, cts.Token); } catch { Remove(host, port, path); - throw; + if (attempt == 1) throw; } } + + throw new InvalidOperationException("Unreachable"); + } + + // Internal helper for sending and receiving data + private async Task TrySendAndReceiveAsync(string host, int port, string path, byte[] buf, CancellationToken token) + { + var sock = await GetOrCreateAsync(host, port, path); + await sock.SendAsync(new ArraySegment(buf), WebSocketMessageType.Text, true, token); + + byte[] recv = new byte[4096]; + var result = await sock.ReceiveAsync(new ArraySegment(recv), token); + + return Encoding.UTF8.GetString(recv, 0, result.Count); } } }