From 38d5daee5968cffbf51e8e5d3e353a9aaccfd697 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sun, 13 Oct 2024 12:51:29 -0400 Subject: [PATCH] com.utilities.websockets 1.0.0 (#3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Initial Release! 🎉🚀 --- Documentation~/README.md | 3 +++ Runtime/IWebSocket.cs | 5 +++++ Runtime/Plugins/WebSocket.jslib | 15 +++++++++++---- Runtime/WebSocket.cs | 30 +++++++++++++++++++----------- Runtime/WebSocket_WebGL.cs | 25 ++++++++++++++++++++----- package.json | 5 +++-- 6 files changed, 61 insertions(+), 22 deletions(-) diff --git a/Documentation~/README.md b/Documentation~/README.md index 30c1854..c18ce97 100644 --- a/Documentation~/README.md +++ b/Documentation~/README.md @@ -74,6 +74,9 @@ socket.OnClose += (code, reason) => Debug.Log($"Connection Closed: {code} {reaso socket.Connect(); ``` +> [!NOTE] +> `socket.ConnectAsync()` is blocking until the connection is closed. + ### Handling Events You can subscribe to the `OnOpen`, `OnMessage`, `OnError`, and `OnClose` events to handle respective situations: diff --git a/Runtime/IWebSocket.cs b/Runtime/IWebSocket.cs index ccb7dd3..e5bb730 100644 --- a/Runtime/IWebSocket.cs +++ b/Runtime/IWebSocket.cs @@ -34,6 +34,11 @@ public interface IWebSocket : IDisposable /// Uri Address { get; } + /// + /// The request headers used by the . + /// + IReadOnlyDictionary RequestHeaders { get; } + /// /// The sub-protocols used by the . /// diff --git a/Runtime/Plugins/WebSocket.jslib b/Runtime/Plugins/WebSocket.jslib index d1d39ce..d145301 100644 --- a/Runtime/Plugins/WebSocket.jslib +++ b/Runtime/Plugins/WebSocket.jslib @@ -10,7 +10,7 @@ var UnityWebSocketLibrary = { /** * Create a new WebSocket instance and adds it to the $webSockets array. * @param {string} url - The URL to which to connect. - * @param {string[]} subProtocols - An array of strings that indicate the sub-protocols the client is willing to speak. + * @param {string[]} subProtocols - An json array of strings that indicate the sub-protocols the client is willing to speak. * @returns {number} - A pointer to the WebSocket instance. * @param {function} onOpenCallback - The callback function. WebSocket_OnOpenDelegate(IntPtr websocketPtr) in C#. * @param {function} onMessageCallback - The callback function. WebSocket_OnMessageDelegate(IntPtr websocketPtr, IntPtr data, int length, int type) in C#. @@ -22,7 +22,7 @@ var UnityWebSocketLibrary = { try { var subProtocolsStr = UTF8ToString(subProtocols); - var subProtocolsArr = subProtocolsStr ? subProtocolsStr.split(',') : undefined; + var subProtocolsArr = subProtocolsStr ? JSON.parse(subProtocolsStr) : undefined; for (var i = 0; i < webSockets.length; i++) { var instance = webSockets[i]; @@ -43,11 +43,13 @@ var UnityWebSocketLibrary = { onCloseCallback: onCloseCallback }; - if (subProtocolsArr) { + if (subProtocolsArr && Array.isArray(subProtocolsArr)) { webSockets[socketPtr].subProtocols = subProtocolsArr; + } else { + console.error('subProtocols is not an array'); } - // console.log('Created WebSocket object with websocketPtr: ', socketPtr, ' for URL: ', urlStr, ' and sub-protocols: ', subProtocolsArr) + // console.log(`Created WebSocket object with websocketPtr: ${socketPtr} for URL: ${urlStr}, sub-protocols: ${subProtocolsArr}`); return socketPtr; } catch (error) { console.error('Error creating WebSocket object for URL: ', urlStr, ' Error: ', error); @@ -81,6 +83,11 @@ var UnityWebSocketLibrary = { try { var instance = webSockets[socketPtr]; + if (!instance) { + console.error('WebSocket instance not found for websocketPtr: ', socketPtr); + return; + } + if (!instance.subProtocols || instance.subProtocols.length === 0) { instance.socket = new WebSocket(instance.url); } else { diff --git a/Runtime/WebSocket.cs b/Runtime/WebSocket.cs index c294701..f3d37fd 100644 --- a/Runtime/WebSocket.cs +++ b/Runtime/WebSocket.cs @@ -17,12 +17,12 @@ namespace Utilities.WebSockets { public class WebSocket : IWebSocket { - public WebSocket(string url, IReadOnlyList subProtocols = null) - : this(new Uri(url), subProtocols) + public WebSocket(string url, IReadOnlyDictionary requestHeaders = null, IReadOnlyList subProtocols = null) + : this(new Uri(url), requestHeaders, subProtocols) { } - public WebSocket(Uri uri, IReadOnlyList subProtocols = null) + public WebSocket(Uri uri, IReadOnlyDictionary requestHeaders = null, IReadOnlyList subProtocols = null) { var protocol = uri.Scheme; @@ -32,6 +32,7 @@ public WebSocket(Uri uri, IReadOnlyList subProtocols = null) } Address = uri; + RequestHeaders = requestHeaders ?? new Dictionary(); SubProtocols = subProtocols ?? new List(); _socket = new ClientWebSocket(); RunMessageQueue(); @@ -59,10 +60,7 @@ private async void RunMessageQueue() } } - ~WebSocket() - { - Dispose(false); - } + ~WebSocket() => Dispose(false); #region IDisposable @@ -114,6 +112,9 @@ public void Dispose() /// public Uri Address { get; } + /// + public IReadOnlyDictionary RequestHeaders { get; } + /// public IReadOnlyList SubProtocols { get; } @@ -126,7 +127,7 @@ public void Dispose() _ => State.Closed }; - private object _lock = new(); + private readonly object _lock = new(); private ClientWebSocket _socket; private SemaphoreSlim _semaphore = new(1, 1); private CancellationTokenSource _lifetimeCts; @@ -151,13 +152,19 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default) _lifetimeCts?.Dispose(); _lifetimeCts = new CancellationTokenSource(); using var cts = CancellationTokenSource.CreateLinkedTokenSource(_lifetimeCts.Token, cancellationToken); + cancellationToken = cts.Token; + + foreach (var requestHeader in RequestHeaders) + { + _socket.Options.SetRequestHeader(requestHeader.Key, requestHeader.Value); + } foreach (var subProtocol in SubProtocols) { _socket.Options.AddSubProtocol(subProtocol); } - await _socket.ConnectAsync(Address, cts.Token).ConfigureAwait(false); + await _socket.ConnectAsync(Address, cancellationToken).ConfigureAwait(false); _events.Enqueue(() => OnOpen?.Invoke()); var buffer = new Memory(new byte[8192]); @@ -168,11 +175,12 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default) do { - result = await _socket.ReceiveAsync(buffer, cts.Token).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + result = await _socket.ReceiveAsync(buffer, cancellationToken).ConfigureAwait(false); stream.Write(buffer.Span[..result.Count]); } while (!result.EndOfMessage); - await stream.FlushAsync(cts.Token).ConfigureAwait(false); + await stream.FlushAsync(cancellationToken).ConfigureAwait(false); var memory = new ReadOnlyMemory(stream.GetBuffer(), 0, (int)stream.Length); if (result.MessageType != WebSocketMessageType.Close) diff --git a/Runtime/WebSocket_WebGL.cs b/Runtime/WebSocket_WebGL.cs index 18e3717..c6b7edb 100644 --- a/Runtime/WebSocket_WebGL.cs +++ b/Runtime/WebSocket_WebGL.cs @@ -11,17 +11,18 @@ using System.Threading.Tasks; using UnityEngine; using Utilities.Async; +using Newtonsoft.Json; namespace Utilities.WebSockets { public class WebSocket : IWebSocket { - public WebSocket(string url, IReadOnlyList subProtocols = null) - : this(new Uri(url), subProtocols) + public WebSocket(string url, IReadOnlyDictionary requestHeaders = null, IReadOnlyList subProtocols = null) + : this(new Uri(url), requestHeaders, subProtocols) { } - public WebSocket(Uri uri, IReadOnlyList subProtocols = null) + public WebSocket(Uri uri, IReadOnlyDictionary requestHeaders = null, IReadOnlyList subProtocols = null) { var protocol = uri.Scheme; @@ -30,9 +31,21 @@ public WebSocket(Uri uri, IReadOnlyList subProtocols = null) throw new ArgumentException($"Unsupported protocol: {protocol}"); } + if (requestHeaders is { Count: > 0 }) + { + Debug.LogWarning("Request Headers are not supported in WebGL and will be ignored."); + } + Address = uri; SubProtocols = subProtocols ?? new List(); - _socket = WebSocket_Create(uri.ToString(), string.Join(',', SubProtocols), WebSocket_OnOpen, WebSocket_OnMessage, WebSocket_OnError, WebSocket_OnClose); + RequestHeaders = requestHeaders ?? new Dictionary(); + _socket = WebSocket_Create( + uri.ToString(), + JsonConvert.SerializeObject(subProtocols), + WebSocket_OnOpen, + WebSocket_OnMessage, + WebSocket_OnError, + WebSocket_OnClose); if (_socket == IntPtr.Zero || !_sockets.TryAdd(_socket, this)) { @@ -210,6 +223,8 @@ private static void WebSocket_OnClose(IntPtr websocketPtr, CloseStatusCode code, /// public Uri Address { get; } + public IReadOnlyDictionary RequestHeaders { get; } + /// public IReadOnlyList SubProtocols { get; } @@ -218,7 +233,7 @@ private static void WebSocket_OnClose(IntPtr websocketPtr, CloseStatusCode code, ? (State)WebSocket_GetState(_socket) : State.Closed; - private object _lock = new(); + private readonly object _lock = new(); private IntPtr _socket; private SemaphoreSlim _semaphore = new(1, 1); private CancellationTokenSource _lifetimeCts; diff --git a/package.json b/package.json index 04408ad..5f22bd6 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "Utilities.WebSockets", "description": "A simple websocket package for Unity (UPM)", "keywords": [], - "version": "1.0.0-preview.2", + "version": "1.0.0", "unity": "2021.3", "documentationUrl": "https://github.com/RageAgainstThePixel/com.utilities.websockets#documentation", "changelogUrl": "https://github.com/RageAgainstThePixel/com.utilities.websockets/releases", @@ -17,7 +17,8 @@ "url": "https://github.com/StephenHodgson" }, "dependencies": { - "com.utilities.async": "2.1.7" + "com.utilities.async": "2.1.7", + "com.unity.nuget.newtonsoft-json": "3.2.1" }, "samples": [ {