endel / NativeWebSocket

🔌 WebSocket client for Unity - with no external dependencies (WebGL, Native, Android, iOS, UWP)
Other
1.13k stars 155 forks source link

Why is Connect method blocking #45

Open angelhodar opened 3 years ago

angelhodar commented 3 years ago

Hey @endel, I just noticed that the Connect method is blocking because execution doesnt continue after calling method from another script, is this intentional? Also, can this be created outside a MonoBehaviour class? I say it because of the code inside the Update method for editor and webgl.

Thanks anyway for such a simple and amazing package!

divillysausages commented 2 years ago

This is because at the end of the Connect() method, you have await Receive();, so Connect doesn't actually return until after you've disconnected.

Personally, I wasn't a big fan of this, so I always make the following changes in my projects:

I split Connect() into separate Connect and Listen methods:

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

        await m_Socket.ConnectAsync(uri, m_CancellationToken);
        OnOpen?.Invoke();
    }
    catch (Exception ex)
    {
        OnError?.Invoke(ex.Message);
        OnClose?.Invoke(WebSocketCloseCode.Abnormal);
    }
}

public async void Listen()
{
    try
    {
        await Receive();
    }
    catch (Exception ex)
    {
        OnError?.Invoke(ex.Message);
        OnClose?.Invoke(WebSocketCloseCode.Abnormal);
    }
    finally
    {
        if (m_Socket != null)
        {
            m_TokenSource.Cancel();
            m_Socket.Dispose();
        }
    }
}

On my implementation layer, this means that my Connect() method looks something like this:

public async Task Connect( string uri )
{
    if( this.isOpen )
        return; // we're already connected

    this.m_socket = new WebSocket( uri );

    // add our event listeners
    this.m_socket.OnOpen += this._onWebSocketOpen;
    ...

    // launch our connection
    await this.m_socket.Connect();

    // wait until the socket is actually open
    await new WaitUntil( () => this.isOpen );
}

public void Update()
{
#if !UNITY_WEBGL || UNITY_EDITOR
    if( this.isOpen )
        this.m_socket.DispatchMessageQueue(); // get any messages
#endif
}

// called when our socket is open and ready for business
private void _onWebSocketOpen()
{
    // start listening on the socket
    this.m_socket.Listen();
}

This then lets me do something like the following any time I actually need to connect:

if( !this.m_networkService.isConnected )
    await this.m_networkService.Connect( websocketServerURI );
this.m_networkService.DoSomeNetworkCall();
divillysausages commented 2 years ago

You'll also need the following to allow you to await built-in Unity calls like WaitUntil:

Extension methods to expose a GetAwaiter() method:

using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using UnityEngine;

public static class ExtensionMethods
{
    // Allows the use of async/await (instead of yield) with any Unity AsyncOperation (e.g. UnityWebRequest)
    // from https://gist.github.com/mattyellen/d63f1f557d08f7254345bff77bfdc8b3
    public static TaskAwaiter GetAwaiter( this AsyncOperation asyncOp )
    {
        var tcs = new TaskCompletionSource<object>();
        asyncOp.completed += obj => { tcs.SetResult( null ); };
        return ( (Task)tcs.Task ).GetAwaiter();
    }

    // Allows the use of async/await for a WaitUntil coroutine
    public static AsyncCoroutineAwaiter GetAwaiter( this WaitUntil wait )
    {
        AsyncCoroutineAwaiter awaiter = new AsyncCoroutineAwaiter();
        AsyncCoroutineRunner.instance.StartCoroutine( wait, awaiter );
        return awaiter;
    }
}

The awaiter wrapper class:

using System.Runtime.CompilerServices;

/**
 * A custom awaiter used when moving from coroutines to async/await
 */
public class AsyncCoroutineAwaiter : INotifyCompletion
{
    private System.Action m_continuation = null; // an action invoked when the task is done

    public bool IsCompleted { get; private set; }

    public void GetResult() { }

    public void Complete()
    {
        this.IsCompleted = true;
        this.m_continuation?.Invoke();
    }

    void INotifyCompletion.OnCompleted( System.Action continuation )
    {
        this.m_continuation = continuation;
    }
}

Class to actually run the coroutines on:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/**
 * Helper class for running coroutines using the new async/await flow
 */
public class AsyncCoroutineRunner : MonoBehaviour
{
    private static AsyncCoroutineRunner m_instance = null;

    public static AsyncCoroutineRunner instance
    {
        get
        {
            if( AsyncCoroutineRunner.m_instance == null )
            {
                GameObject gObj = new GameObject( "AsyncCoroutineRunner" );
                AsyncCoroutineRunner.m_instance = gObj.AddComponent<AsyncCoroutineRunner>();
                gObj.hideFlags = HideFlags.HideAndDontSave;
                DontDestroyOnLoad( gObj );
            }
            return AsyncCoroutineRunner.m_instance;
        }
    }

    void OnDestroy()
    {
        this.StopAllCoroutines();
    }

    public Coroutine StartCoroutine( object instruction, AsyncCoroutineAwaiter awaiter )
    {
        return this.StartCoroutine( this._wrap( instruction, awaiter ) );
    }

    private IEnumerator _wrap( object instruction, AsyncCoroutineAwaiter awaiter )
    {
        yield return instruction;
        awaiter.Complete();
    }
}

Then, you can add new extension methods as you see fit to the ExtensionMethods static class. For example, if you wanted to await a WaitForSeconds() call:

// Allows the use of async/await for a WaitForSeconds coroutine
public static AsyncCoroutineAwaiter GetAwaiter( this WaitForSeconds wait )
{
    AsyncCoroutineAwaiter awaiter = new AsyncCoroutineAwaiter();
    AsyncCoroutineRunner.instance.StartCoroutine( wait, awaiter );
    return awaiter;
}

// in your code elsewhere
await new WaitForSeconds( 1.5f );