Guidance for Unity Transport

Im trying to use the Unity Transport package and have unity as a server, and connect to it with a JS client.

So far the JS client connects successfully to the server but the server itself does not show any new connections.

I wondered if there was any way I could actually show the JS client as a connection, seeing as they are actually connected to the websocket driver.

My ultimate goal is to be able to create a Jobified Server like the one below:

https://docs-multiplayer.unity3d.com/transport/current/samples/jobifiedserverbehaviour/index.html

Would creating a custom WebSocketInterface help?

#if !UNITY_WEBGL || UNITY_EDITOR

using Unity.Burst;
using Unity.Jobs;

namespace Unity.Networking.Transport
{
    [BurstCompile]
    public struct WebSocketNetworkInterface : INetworkInterface
    {
        // In all platforms but WebGL this network interface is just a TCPNetworkInterface in disguise.
        // The websocket protocol is in fact implemented in the WebSocketLayer. For WebGL this interface is
        // implemented in terms of javascript bindings, it does not support Bind()/Listen() and the websocket protocol
        // is implemented by the browser.
        TCPNetworkInterface tcp;

        public NetworkEndpoint LocalEndpoint => tcp.LocalEndpoint;

        internal ConnectionList CreateConnectionList() => tcp.CreateConnectionList();
        public int Initialize(ref NetworkSettings settings, ref int packetPadding) => tcp.Initialize(ref settings, ref packetPadding);
        public int Bind(NetworkEndpoint endpoint) => tcp.Bind(endpoint);
        public int Listen() => tcp.Listen();
        public void Dispose() => tcp.Dispose();

        public JobHandle ScheduleReceive(ref ReceiveJobArguments arguments, JobHandle dep) => tcp.ScheduleReceive(ref arguments, dep);
        public JobHandle ScheduleSend(ref SendJobArguments arguments, JobHandle dep) => tcp.ScheduleSend(ref arguments, dep);
    }
}

#else

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;

using Unity.Collections;
using Unity.Jobs;
using Unity.Networking.Transport.Logging;
using Unity.Networking.Transport.Relay;

namespace Unity.Networking.Transport
{
    public struct WebSocketNetworkInterface : INetworkInterface
    {
        private const string DLL = "__Internal";

        static class WebSocket
        {
            public static int s_NextSocketId = 0;

            [DllImport(DLL, EntryPoint = "js_html_utpWebSocketCreate")]
            public static extern void Create(int sockId, IntPtr addrData, int addrSize);

            [DllImport(DLL, EntryPoint = "js_html_utpWebSocketDestroy")]
            public static extern void Destroy(int sockId);

            [DllImport(DLL, EntryPoint = "js_html_utpWebSocketSend")]
            public static extern int Send(int sockId, IntPtr data, int size);

            [DllImport(DLL, EntryPoint = "js_html_utpWebSocketRecv")]
            public static extern int Recv(int sockId, IntPtr data, int size);

            [DllImport(DLL, EntryPoint = "js_html_utpWebSocketIsConnected")]
            public static extern int IsConnectionReady(int sockId);
        }

        unsafe struct InternalData
        {
            public NetworkEndpoint ListenEndpoint;
            public int ConnectTimeoutMS; // maximum time to wait for a connection to complete

            // If non-empty, will connect to this hostname with the wss:// protocol. Otherwise the
            // IP address of the endpoint is used to connect with the ws:// protocol.
            public FixedString512Bytes SecureHostname;
        }

        unsafe struct ConnectionData
        {
            public int Socket;
            public long ConnectStartTime;
        }

        private NativeReference<InternalData> m_InternalData;

        // Maps a connection id from the connection list to its connection data.
        private ConnectionDataMap<ConnectionData> m_ConnectionMap;

        // List of connection information carried over to the layer above
        private ConnectionList m_ConnectionList;

        internal ConnectionList CreateConnectionList()
        {
            m_ConnectionList = ConnectionList.Create();
            return m_ConnectionList;
        }

        public unsafe NetworkEndpoint LocalEndpoint => m_InternalData.Value.ListenEndpoint;

        public bool IsCreated => m_InternalData.IsCreated;

        public unsafe int Initialize(ref NetworkSettings settings, ref int packetPadding)
        {
            var networkConfiguration = settings.GetNetworkConfigParameters();

            // This needs to match the value of Unity.Networking.Transport.WebSocket.MaxPayloadSize
            packetPadding += 14;

            var secureHostname = new FixedString512Bytes();
            if (settings.TryGet<RelayNetworkParameter>(out var relayParams) && relayParams.ServerData.IsSecure != 0)
                secureHostname.CopyFrom(relayParams.ServerData.HostString);

#if ENABLE_MANAGED_UNITYTLS
            // Shouldn't be required for normal use cases but is provided as an out in case the user
            // wants to override the hostname (useful if say the user ended up resolving the Relay's
            // hostname on their own instead of providing it directly in the Relay parameters).
            if (settings.TryGet<TLS.SecureNetworkProtocolParameter>(out var secureParams))
                secureHostname.CopyFrom(secureParams.Hostname);
#endif

            var state = new InternalData
            {
                ListenEndpoint = NetworkEndpoint.AnyIpv4,
                ConnectTimeoutMS = networkConfiguration.connectTimeoutMS * networkConfiguration.maxConnectAttempts,
                SecureHostname = secureHostname,
            };
            m_InternalData = new NativeReference<InternalData>(state, Allocator.Persistent);

            m_ConnectionMap = new ConnectionDataMap<ConnectionData>(1, default, Allocator.Persistent);
            return 0;
        }

        public unsafe int Bind(NetworkEndpoint endpoint)
        {
            var state = m_InternalData.Value;
            state.ListenEndpoint = endpoint;
            m_InternalData.Value = state;

            return 0;
        }

        public unsafe int Listen()
        {
            return 0;
        }

        public unsafe void Dispose()
        {
            m_InternalData.Dispose();

            for (int i = 0; i < m_ConnectionMap.Length; ++i)
            {
                WebSocket.Destroy(m_ConnectionMap.DataAt(i).Socket);
            }

            m_ConnectionMap.Dispose();
            m_ConnectionList.Dispose();
        }

        public JobHandle ScheduleReceive(ref ReceiveJobArguments arguments, JobHandle dep)
        {
            return new ReceiveJob
            {
                ReceiveQueue = arguments.ReceiveQueue,
                InternalData = m_InternalData,
                ConnectionList = m_ConnectionList,
                ConnectionMap = m_ConnectionMap,
                Time = arguments.Time,
            }.Schedule(dep);
        }

        struct ReceiveJob : IJob
        {
            public PacketsQueue ReceiveQueue;
            public NativeReference<InternalData> InternalData;
            public ConnectionList ConnectionList;
            public ConnectionDataMap<ConnectionData> ConnectionMap;
            public long Time;

            private void Abort(ref ConnectionId connectionId, ref ConnectionData connectionData, Error.DisconnectReason reason = default)
            {
                ConnectionList.FinishDisconnecting(ref connectionId, reason);
                ConnectionMap.ClearData(ref connectionId);
                WebSocket.Destroy(connectionData.Socket);
            }

            public unsafe void Execute()
            {
                // Update each connection from the connection list
                var count = ConnectionList.Count;
                for (int i = 0; i < count; i++)
                {
                    var connectionId = ConnectionList.ConnectionAt(i);
                    var connectionState = ConnectionList.GetConnectionState(connectionId);

                    if (connectionState == NetworkConnection.State.Disconnected)
                        continue;

                    var connectionData = ConnectionMap[connectionId];

                    // Detect if the upper layer is requesting to connect.
                    if (connectionState == NetworkConnection.State.Connecting)
                    {
                        // The time here is a signed 64bit and we're never going to run at time 0 so if the connection
                        // has ConnectStartTime == 0 it's the creation of this connection data.
                        if (connectionData.ConnectStartTime == 0)
                        {
                            var socket = ++WebSocket.s_NextSocketId;
                            GetServerAddress(connectionId, out var address);
                            WebSocket.Create(socket, (IntPtr)address.GetUnsafePtr(), address.Length);

                            connectionData.ConnectStartTime = Time;
                            connectionData.Socket = socket;
                        }

                        // Check if the WebSocket connection is established.
                        var status = WebSocket.IsConnectionReady(connectionData.Socket);
                        if (status > 0)
                        {
                            ConnectionList.FinishConnectingFromLocal(ref connectionId);
                        }
                        else if (status < 0)
                        {
                            ConnectionList.StartDisconnecting(ref connectionId);
                            Abort(ref connectionId, ref connectionData, Error.DisconnectReason.MaxConnectionAttempts);
                            continue;
                        }

                        // Disconnect if we've reached the maximum connection timeout.
                        if (Time - connectionData.ConnectStartTime >= InternalData.Value.ConnectTimeoutMS)
                        {
                            ConnectionList.StartDisconnecting(ref connectionId);
                            Abort(ref connectionId, ref connectionData, Error.DisconnectReason.MaxConnectionAttempts);
                            continue;
                        }

                        ConnectionMap[connectionId] = connectionData;
                        continue;
                    }

                    // Detect if the upper layer is requesting to disconnect.
                    if (connectionState == NetworkConnection.State.Disconnecting)
                    {
                        Abort(ref connectionId, ref connectionData);
                        continue;
                    }

                    // Read data from the connection if we can. Receive should return chunks of up to MTU.
                    // Close the connection in case of a receive error.
                    var endpoint = ConnectionList.GetConnectionEndpoint(connectionId);
                    var nbytes = 0;
                    while (true)
                    {
                        // No need to disconnect in case the receive queue becomes full just let the TCP socket buffer
                        // the incoming data.
                        if (!ReceiveQueue.EnqueuePacket(out var packetProcessor))
                            break;

                        nbytes = WebSocket.Recv(connectionData.Socket, (IntPtr)(byte*)packetProcessor.GetUnsafePayloadPtr() + packetProcessor.Offset, packetProcessor.BytesAvailableAtEnd);
                        if (nbytes > 0)
                        {
                            packetProcessor.ConnectionRef = connectionId;
                            packetProcessor.EndpointRef = endpoint;
                            packetProcessor.SetUnsafeMetadata(nbytes, packetProcessor.Offset);
                        }
                        else
                        {
                            packetProcessor.Drop();
                            break;
                        }
                    }

                    if (nbytes < 0)
                    {
                        // Disconnect
                        ConnectionList.StartDisconnecting(ref connectionId);
                        Abort(ref connectionId, ref connectionData, Error.DisconnectReason.ClosedByRemote);
                        continue;
                    }

                    // Update the connection data
                    ConnectionMap[connectionId] = connectionData;
                }
            }

            // Get the address to connect to for the given connection. If not using TLS, then this
            // is just "ws://{address}:{port}" where address/port are taken from the connection's
            // endpoint in the connection list. But if using TLS, then the hostname provided in the
            // secure parameters overrides the address, and we connect to "wss://{hostname}:{port}"
            // (with the port still taken from the connection's endpoint in the connection list).
            private void GetServerAddress(ConnectionId connection, out FixedString512Bytes address)
            {
                var endpoint = ConnectionList.GetConnectionEndpoint(connection);
                var secureHostname = InternalData.Value.SecureHostname;

                if (secureHostname.IsEmpty)
                    address = FixedString.Format("ws://{0}", endpoint.ToFixedString());
                else
                    address = FixedString.Format("wss://{0}:{1}", secureHostname, endpoint.Port);
            }
        }

        public JobHandle ScheduleSend(ref SendJobArguments arguments, JobHandle dep)
        {
            return new SendJob
            {
                SendQueue = arguments.SendQueue,
                ConnectionList = m_ConnectionList,
                ConnectionMap = m_ConnectionMap,
            }.Schedule(dep);
        }

        unsafe struct SendJob : IJob
        {
            public PacketsQueue SendQueue;
            public ConnectionList ConnectionList;
            public ConnectionDataMap<ConnectionData> ConnectionMap;

            private void Abort(ref ConnectionId connectionId, ref ConnectionData connectionData, Error.DisconnectReason reason = default)
            {
                ConnectionList.FinishDisconnecting(ref connectionId, reason);
                ConnectionMap.ClearData(ref connectionId);
                WebSocket.Destroy(connectionData.Socket);
            }

            public void Execute()
            {
                // Each packet is sent individually. The connection is aborted if a packet cannot be transmiited
                // entirely.
                var count = SendQueue.Count;
                for (int i = 0; i < count; i++)
                {
                    var packetProcessor = SendQueue[i];
                    if (packetProcessor.Length == 0)
                        continue;

                    var connectionId = packetProcessor.ConnectionRef;
                    var connectionState = ConnectionList.GetConnectionState(connectionId);

                    if (connectionState != NetworkConnection.State.Connected)
                    {
                        packetProcessor.Drop();
                        continue;
                    }

                    var connectionData = ConnectionMap[connectionId];

                    var nbytes = WebSocket.Send(connectionData.Socket, (IntPtr)(byte*)packetProcessor.GetUnsafePayloadPtr() + packetProcessor.Offset, packetProcessor.Length);
                    if (nbytes != packetProcessor.Length)
                    {
                        // Disconnect
                        ConnectionList.StartDisconnecting(ref connectionId);
                        Abort(ref connectionId, ref connectionData, Error.DisconnectReason.ClosedByRemote);
                        continue;
                    }

                    ConnectionMap[connectionId] = connectionData;
                }
            }
        }
    }
}

#endif

Sources

What do you mean when you say that the JS client connects but the server doesn’t see the connection? Are you using the transport package on your JS client too?

1 Like

Is it possible to use the transport package in javascript on a web client?

Im just connecting to it with websockets in javascript, without anything extra, and it does connect to the server.
If i turn the server off, the js websockets stops connecting. So I know there is an open connection between the two.

In the unity server code however the connections.length shows as 0, even though when i debug I can see a connection being processed and allowed to connect, and when i disconnect from the client, it processes that as well.

There is no way to use the transport package directly from JS in a web client. It can be used through WebGL builds of Unity projects, but not in a standalone manner.

Unfortunately I think what you’re trying to achieve is not supported by the transport package. Its WebSocket implementation is only meant to support connections where both peers are using the transport package. It is not meant as a general purpose WebSocket library. For this use case I’d recommend using a library like websocket-sharp.

1 Like

Nice find. So would it then be possible to create a Jobified version of the websocket-sharp library?

Similar to this one:
https://docs-multiplayer.unity3d.com/transport/current/samples/jobifiedserverbehaviour/index.html

You would need to use the job system of Unity directly with the websocket-sharp library. Although you would probably be limited by only being able to pass blittable types to a job. The example you linked to is for the transport package specifically, and as I mentioned the expectation for that package is that both client and server use it.

Is there any particular reason why you wish to use the job system? In fact, is there any reason why you wish to use the Unity runtime on the server? It seems to me like perhaps you’d be better served by a traditional C# application (which you could make multithreaded if the performance requires it).

1 Like

I wanted to use Unity C# in the backend and utilise the job system, ECS / DOTs and take advantage of things like navmesh pathfinding and other unity features. Then connect to the unity server from a javascript client and allow other javascript clients to receive updates

In this case is there any reason your clients can’t be Unity WebGL builds? You’d have access to the transport package and most of the DOTS ecosystem, which would allow you to share data structures, among other things.

As an alternative, you could still use DOTS on the server and simply not use jobs for the WebSocket part. Compute the relevant updates and statuses using DOTS and jobs, and periodically sync/copy them to storage accessible from the main thread. Then you could use websocket-sharp to serve this information to your JS clients. The key term to search for would be “hybrid workflow” in this case.

1 Like

Mostly because Unity WebGL isn’t as performant as BabylonJS or threejs. Its kind of awkward to use on the web and not as fast. Would also be difficult to utilise web assembly and web workers

I think the hybrid approach is a a good compromise. I will try this and see if further gains are needed.

I’m not sure - if a 3rd party transport based on websocket and all server and client connects to a managed service, and all game instances can talks to each other - will solve your problem? I’d like to recommend something if you want