#if !NET5_0_OR_GREATER using System; using System.Collections.Generic; using System.IO; using System.Net.WebSockets; using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Tasks; using AOT; using System.Runtime.InteropServices; using UnityEngine; using System.Collections; public class MainThreadUtil : MonoBehaviour { public static MainThreadUtil Instance { get; private set; } public static SynchronizationContext synchronizationContext { get; private set; } [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] public static void Setup() { Instance = new GameObject("MainThreadUtil") .AddComponent(); synchronizationContext = SynchronizationContext.Current; } public static void Run(IEnumerator waitForUpdate) { synchronizationContext.Post(_ => Instance.StartCoroutine( waitForUpdate), null); } void Awake() { gameObject.hideFlags = HideFlags.HideAndDontSave; DontDestroyOnLoad(gameObject); } } public class WaitForUpdate : CustomYieldInstruction { public override bool keepWaiting { get { return false; } } public MainThreadAwaiter GetAwaiter() { var awaiter = new MainThreadAwaiter(); MainThreadUtil.Run(CoroutineWrapper(this, awaiter)); return awaiter; } public class MainThreadAwaiter : INotifyCompletion { Action continuation; public bool IsCompleted { get; set; } public void GetResult() { } public void Complete() { IsCompleted = true; continuation?.Invoke(); } void INotifyCompletion.OnCompleted(Action continuation) { this.continuation = continuation; } } public static IEnumerator CoroutineWrapper(IEnumerator theWorker, MainThreadAwaiter awaiter) { yield return theWorker; awaiter.Complete(); } } namespace NativeWebSocket { public delegate void WebSocketOpenEventHandler(); public delegate void WebSocketMessageEventHandler(byte[] data); public delegate void WebSocketErrorEventHandler(string errorMsg); public delegate void WebSocketCloseEventHandler(WebSocketCloseCode closeCode); public enum WebSocketCloseCode { /* Do NOT use NotSet - it's only purpose is to indicate that the close code cannot be parsed. */ NotSet = 0, Normal = 1000, Away = 1001, ProtocolError = 1002, UnsupportedData = 1003, Undefined = 1004, NoStatus = 1005, Abnormal = 1006, InvalidData = 1007, PolicyViolation = 1008, TooBig = 1009, MandatoryExtension = 1010, ServerError = 1011, TlsHandshakeFailure = 1015 } public enum WebSocketState { Connecting, Open, Closing, Closed } public interface IWebSocket { event WebSocketOpenEventHandler OnOpen; event WebSocketMessageEventHandler OnMessage; event WebSocketErrorEventHandler OnError; event WebSocketCloseEventHandler OnClose; WebSocketState State { get; } } public static class WebSocketHelpers { public static WebSocketCloseCode ParseCloseCodeEnum(int closeCode) { if (WebSocketCloseCode.IsDefined(typeof(WebSocketCloseCode), closeCode)) { return (WebSocketCloseCode)closeCode; } else { return WebSocketCloseCode.Undefined; } } public static WebSocketException GetErrorMessageFromCode(int errorCode, Exception inner) { switch (errorCode) { case -1: return new WebSocketUnexpectedException("WebSocket instance not found.", inner); case -2: return new WebSocketInvalidStateException("WebSocket is already connected or in connecting state.", inner); case -3: return new WebSocketInvalidStateException("WebSocket is not connected.", inner); case -4: return new WebSocketInvalidStateException("WebSocket is already closing.", inner); case -5: return new WebSocketInvalidStateException("WebSocket is already closed.", inner); case -6: return new WebSocketInvalidStateException("WebSocket is not in open state.", inner); case -7: return new WebSocketInvalidArgumentException("Cannot close WebSocket. An invalid code was specified or reason is too long.", inner); default: return new WebSocketUnexpectedException("Unknown error.", inner); } } } public class WebSocketException : Exception { public WebSocketException() { } public WebSocketException(string message) : base(message) { } public WebSocketException(string message, Exception inner) : base(message, inner) { } } public class WebSocketUnexpectedException : WebSocketException { public WebSocketUnexpectedException() { } public WebSocketUnexpectedException(string message) : base(message) { } public WebSocketUnexpectedException(string message, Exception inner) : base(message, inner) { } } public class WebSocketInvalidArgumentException : WebSocketException { public WebSocketInvalidArgumentException() { } public WebSocketInvalidArgumentException(string message) : base(message) { } public WebSocketInvalidArgumentException(string message, Exception inner) : base(message, inner) { } } public class WebSocketInvalidStateException : WebSocketException { public WebSocketInvalidStateException() { } public WebSocketInvalidStateException(string message) : base(message) { } public WebSocketInvalidStateException(string message, Exception inner) : base(message, inner) { } } public class WaitForBackgroundThread { public ConfiguredTaskAwaitable.ConfiguredTaskAwaiter GetAwaiter() { return Task.Run(() => { }).ConfigureAwait(false).GetAwaiter(); } } #if UNITY_WEBGL && !UNITY_EDITOR /// /// WebSocket class bound to JSLIB. /// public class WebSocket : IWebSocket { /* WebSocket JSLIB functions */ [DllImport ("__Internal")] public static extern int WebSocketConnect (int instanceId); [DllImport ("__Internal")] public static extern int WebSocketClose (int instanceId, int code, string reason); [DllImport ("__Internal")] public static extern int WebSocketSend (int instanceId, byte[] dataPtr, int dataLength); [DllImport ("__Internal")] public static extern int WebSocketSendText (int instanceId, string message); [DllImport ("__Internal")] public static extern int WebSocketGetState (int instanceId); protected int instanceId; public event WebSocketOpenEventHandler OnOpen; public event WebSocketMessageEventHandler OnMessage; public event WebSocketErrorEventHandler OnError; public event WebSocketCloseEventHandler OnClose; public WebSocket (string url, Dictionary headers = null) { if (!WebSocketFactory.isInitialized) { WebSocketFactory.Initialize (); } int instanceId = WebSocketFactory.WebSocketAllocate (url); WebSocketFactory.instances.Add (instanceId, this); this.instanceId = instanceId; } public WebSocket (string url, string subprotocol, Dictionary headers = null) { if (!WebSocketFactory.isInitialized) { WebSocketFactory.Initialize (); } int instanceId = WebSocketFactory.WebSocketAllocate (url); WebSocketFactory.instances.Add (instanceId, this); WebSocketFactory.WebSocketAddSubProtocol(instanceId, subprotocol); this.instanceId = instanceId; } public WebSocket (string url, List subprotocols, Dictionary headers = null) { if (!WebSocketFactory.isInitialized) { WebSocketFactory.Initialize (); } int instanceId = WebSocketFactory.WebSocketAllocate (url); WebSocketFactory.instances.Add (instanceId, this); foreach (string subprotocol in subprotocols) { WebSocketFactory.WebSocketAddSubProtocol(instanceId, subprotocol); } this.instanceId = instanceId; } ~WebSocket () { WebSocketFactory.HandleInstanceDestroy (this.instanceId); } public int GetInstanceId () { return this.instanceId; } public Task Connect () { int ret = WebSocketConnect (this.instanceId); if (ret < 0) throw WebSocketHelpers.GetErrorMessageFromCode (ret, null); return Task.CompletedTask; } public void CancelConnection () { if (State == WebSocketState.Open) Close (WebSocketCloseCode.Abnormal); } public Task Close (WebSocketCloseCode code = WebSocketCloseCode.Normal, string reason = null) { int ret = WebSocketClose (this.instanceId, (int) code, reason); if (ret < 0) throw WebSocketHelpers.GetErrorMessageFromCode (ret, null); return Task.CompletedTask; } public Task Send (byte[] data) { int ret = WebSocketSend (this.instanceId, data, data.Length); if (ret < 0) throw WebSocketHelpers.GetErrorMessageFromCode (ret, null); return Task.CompletedTask; } public Task SendText (string message) { int ret = WebSocketSendText (this.instanceId, message); if (ret < 0) throw WebSocketHelpers.GetErrorMessageFromCode (ret, null); return Task.CompletedTask; } public WebSocketState State { get { int state = WebSocketGetState (this.instanceId); if (state < 0) throw WebSocketHelpers.GetErrorMessageFromCode (state, null); switch (state) { case 0: return WebSocketState.Connecting; case 1: return WebSocketState.Open; case 2: return WebSocketState.Closing; case 3: return WebSocketState.Closed; default: return WebSocketState.Closed; } } } public void DelegateOnOpenEvent () { this.OnOpen?.Invoke (); } public void DelegateOnMessageEvent (byte[] data) { this.OnMessage?.Invoke (data); } public void DelegateOnErrorEvent (string errorMsg) { this.OnError?.Invoke (errorMsg); } public void DelegateOnCloseEvent (int closeCode) { this.OnClose?.Invoke (WebSocketHelpers.ParseCloseCodeEnum (closeCode)); } } #else public class WebSocket : IWebSocket { public event WebSocketOpenEventHandler OnOpen; public event WebSocketMessageEventHandler OnMessage; public event WebSocketErrorEventHandler OnError; public event WebSocketCloseEventHandler OnClose; private Uri uri; private Dictionary headers; private List subprotocols; private ClientWebSocket m_Socket = new ClientWebSocket(); private CancellationTokenSource m_TokenSource; private CancellationToken m_CancellationToken; private readonly object OutgoingMessageLock = new object(); private readonly object IncomingMessageLock = new object(); private bool isSending = false; private List> sendBytesQueue = new List>(); private List> sendTextQueue = new List>(); public WebSocket(string url, Dictionary headers = null) { uri = new Uri(url); if (headers == null) { this.headers = new Dictionary(); } else { this.headers = headers; } subprotocols = new List(); string protocol = uri.Scheme; if (!protocol.Equals("ws") && !protocol.Equals("wss")) throw new ArgumentException("Unsupported protocol: " + protocol); } public WebSocket(string url, string subprotocol, Dictionary headers = null) { uri = new Uri(url); if (headers == null) { this.headers = new Dictionary(); } else { this.headers = headers; } subprotocols = new List {subprotocol}; string protocol = uri.Scheme; if (!protocol.Equals("ws") && !protocol.Equals("wss")) throw new ArgumentException("Unsupported protocol: " + protocol); } public WebSocket(string url, List subprotocols, Dictionary headers = null) { uri = new Uri(url); if (headers == null) { this.headers = new Dictionary(); } else { this.headers = headers; } this.subprotocols = subprotocols; string protocol = uri.Scheme; if (!protocol.Equals("ws") && !protocol.Equals("wss")) throw new ArgumentException("Unsupported protocol: " + protocol); } public void CancelConnection() { m_TokenSource?.Cancel(); } public async Task Connect() { try { m_TokenSource = new CancellationTokenSource(); m_CancellationToken = m_TokenSource.Token; m_Socket = new ClientWebSocket(); foreach (var header in headers) { m_Socket.Options.SetRequestHeader(header.Key, header.Value); } foreach (string subprotocol in subprotocols) { m_Socket.Options.AddSubProtocol(subprotocol); } await m_Socket.ConnectAsync(uri, m_CancellationToken); OnOpen?.Invoke(); await Receive(); } catch (Exception ex) { OnError?.Invoke(ex.Message); OnClose?.Invoke(WebSocketCloseCode.Abnormal); } finally { if (m_Socket != null) { m_TokenSource.Cancel(); m_Socket.Dispose(); } } } public WebSocketState State { get { switch (m_Socket.State) { case System.Net.WebSockets.WebSocketState.Connecting: return WebSocketState.Connecting; case System.Net.WebSockets.WebSocketState.Open: return WebSocketState.Open; case System.Net.WebSockets.WebSocketState.CloseSent: case System.Net.WebSockets.WebSocketState.CloseReceived: return WebSocketState.Closing; case System.Net.WebSockets.WebSocketState.Closed: return WebSocketState.Closed; default: return WebSocketState.Closed; } } } public Task Send(byte[] bytes) { // return m_Socket.SendAsync(buffer, WebSocketMessageType.Binary, true, CancellationToken.None); return SendMessage(sendBytesQueue, WebSocketMessageType.Binary, new ArraySegment(bytes)); } public Task SendText(string message) { var encoded = Encoding.UTF8.GetBytes(message); // m_Socket.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None); return SendMessage(sendTextQueue, WebSocketMessageType.Text, new ArraySegment(encoded, 0, encoded.Length)); } private async Task SendMessage(List> queue, WebSocketMessageType messageType, ArraySegment buffer) { // Return control to the calling method immediately. // await Task.Yield (); // Make sure we have data. if (buffer.Count == 0) { return; } // The state of the connection is contained in the context Items dictionary. bool sending; lock (OutgoingMessageLock) { sending = isSending; // If not, we are now. if (!isSending) { isSending = true; } } if (!sending) { // Lock with a timeout, just in case. if (!Monitor.TryEnter(m_Socket, 1000)) { // If we couldn't obtain exclusive access to the socket in one second, something is wrong. await m_Socket.CloseAsync(WebSocketCloseStatus.InternalServerError, string.Empty, m_CancellationToken); return; } try { // Send the message synchronously. var t = m_Socket.SendAsync(buffer, messageType, true, m_CancellationToken); t.Wait(m_CancellationToken); } finally { Monitor.Exit(m_Socket); } // Note that we've finished sending. lock (OutgoingMessageLock) { isSending = false; } // Handle any queued messages. await HandleQueue(queue, messageType); } else { // Add the message to the queue. lock (OutgoingMessageLock) { queue.Add(buffer); } } } private async Task HandleQueue(List> queue, WebSocketMessageType messageType) { var buffer = new ArraySegment(); lock (OutgoingMessageLock) { // Check for an item in the queue. if (queue.Count > 0) { // Pull it off the top. buffer = queue[0]; queue.RemoveAt(0); } } // Send that message. if (buffer.Count > 0) { await SendMessage(queue, messageType, buffer); } } private List m_MessageList = new List(); // simple dispatcher for queued messages. public void DispatchMessageQueue() { if (m_MessageList.Count == 0) { return; } List messageListCopy; lock (IncomingMessageLock) { messageListCopy = new List(m_MessageList); m_MessageList.Clear(); } var len = messageListCopy.Count; for (int i = 0; i < len; i++) { OnMessage?.Invoke(messageListCopy[i]); } } public async Task Receive() { WebSocketCloseCode closeCode = WebSocketCloseCode.Abnormal; await new WaitForBackgroundThread(); ArraySegment buffer = new ArraySegment(new byte[8192]); try { while (m_Socket.State == System.Net.WebSockets.WebSocketState.Open) { WebSocketReceiveResult result = null; using (var ms = new MemoryStream()) { do { result = await m_Socket.ReceiveAsync(buffer, m_CancellationToken); ms.Write(buffer.Array, buffer.Offset, result.Count); } while (!result.EndOfMessage); ms.Seek(0, SeekOrigin.Begin); if (result.MessageType == WebSocketMessageType.Text) { lock (IncomingMessageLock) { m_MessageList.Add(ms.ToArray()); } //using (var reader = new StreamReader(ms, Encoding.UTF8)) //{ // string message = reader.ReadToEnd(); // OnMessage?.Invoke(this, new MessageEventArgs(message)); //} } else if (result.MessageType == WebSocketMessageType.Binary) { lock (IncomingMessageLock) { m_MessageList.Add(ms.ToArray()); } } else if (result.MessageType == WebSocketMessageType.Close) { await Close(); closeCode = WebSocketHelpers.ParseCloseCodeEnum((int)result.CloseStatus); break; } } } } catch (Exception) { m_TokenSource.Cancel(); } finally { await new WaitForUpdate(); OnClose?.Invoke(closeCode); } } public async Task Close() { if (State == WebSocketState.Open) { await m_Socket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, m_CancellationToken); } } } #endif /// /// Factory /// /// /// Class providing static access methods to work with JSLIB WebSocket or WebSocketSharp interface /// public static class WebSocketFactory { #if UNITY_WEBGL && !UNITY_EDITOR /* Map of websocket instances */ public static Dictionary instances = new Dictionary (); /* Delegates */ public delegate void OnOpenCallback (int instanceId); public delegate void OnMessageCallback (int instanceId, System.IntPtr msgPtr, int msgSize); public delegate void OnErrorCallback (int instanceId, System.IntPtr errorPtr); public delegate void OnCloseCallback (int instanceId, int closeCode); /* WebSocket JSLIB callback setters and other functions */ [DllImport ("__Internal")] public static extern int WebSocketAllocate (string url); [DllImport ("__Internal")] public static extern int WebSocketAddSubProtocol (int instanceId, string subprotocol); [DllImport ("__Internal")] public static extern void WebSocketFree (int instanceId); [DllImport ("__Internal")] public static extern void WebSocketSetOnOpen (OnOpenCallback callback); [DllImport ("__Internal")] public static extern void WebSocketSetOnMessage (OnMessageCallback callback); [DllImport ("__Internal")] public static extern void WebSocketSetOnError (OnErrorCallback callback); [DllImport ("__Internal")] public static extern void WebSocketSetOnClose (OnCloseCallback callback); /* If callbacks was initialized and set */ public static bool isInitialized = false; /* * Initialize WebSocket callbacks to JSLIB */ public static void Initialize () { WebSocketSetOnOpen (DelegateOnOpenEvent); WebSocketSetOnMessage (DelegateOnMessageEvent); WebSocketSetOnError (DelegateOnErrorEvent); WebSocketSetOnClose (DelegateOnCloseEvent); isInitialized = true; } /// /// Called when instance is destroyed (by destructor) /// Method removes instance from map and free it in JSLIB implementation /// /// Instance identifier. public static void HandleInstanceDestroy (int instanceId) { instances.Remove (instanceId); WebSocketFree (instanceId); } [MonoPInvokeCallback (typeof (OnOpenCallback))] public static void DelegateOnOpenEvent (int instanceId) { WebSocket instanceRef; if (instances.TryGetValue (instanceId, out instanceRef)) { instanceRef.DelegateOnOpenEvent (); } } [MonoPInvokeCallback (typeof (OnMessageCallback))] public static void DelegateOnMessageEvent (int instanceId, System.IntPtr msgPtr, int msgSize) { WebSocket instanceRef; if (instances.TryGetValue (instanceId, out instanceRef)) { byte[] msg = new byte[msgSize]; Marshal.Copy (msgPtr, msg, 0, msgSize); instanceRef.DelegateOnMessageEvent (msg); } } [MonoPInvokeCallback (typeof (OnErrorCallback))] public static void DelegateOnErrorEvent (int instanceId, System.IntPtr errorPtr) { WebSocket instanceRef; if (instances.TryGetValue (instanceId, out instanceRef)) { string errorMsg = Marshal.PtrToStringAuto (errorPtr); instanceRef.DelegateOnErrorEvent (errorMsg); } } [MonoPInvokeCallback (typeof (OnCloseCallback))] public static void DelegateOnCloseEvent (int instanceId, int closeCode) { WebSocket instanceRef; if (instances.TryGetValue (instanceId, out instanceRef)) { instanceRef.DelegateOnCloseEvent (closeCode); } } #endif /// /// Create WebSocket client instance /// /// The WebSocket instance. /// WebSocket valid URL. public static WebSocket CreateInstance(string url) { return new WebSocket(url); } } } #endif