Search Unity

Discussion Guidance for Unity Transport

Discussion in 'Multiplayer' started by bakhtrian, Feb 14, 2023.

  1. bakhtrian

    bakhtrian

    Joined:
    Jan 24, 2023
    Posts:
    5
    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?


    Code (CSharp):
    1. #if !UNITY_WEBGL || UNITY_EDITOR
    2.  
    3. using Unity.Burst;
    4. using Unity.Jobs;
    5.  
    6. namespace Unity.Networking.Transport
    7. {
    8.     [BurstCompile]
    9.     public struct WebSocketNetworkInterface : INetworkInterface
    10.     {
    11.         // In all platforms but WebGL this network interface is just a TCPNetworkInterface in disguise.
    12.         // The websocket protocol is in fact implemented in the WebSocketLayer. For WebGL this interface is
    13.         // implemented in terms of javascript bindings, it does not support Bind()/Listen() and the websocket protocol
    14.         // is implemented by the browser.
    15.         TCPNetworkInterface tcp;
    16.  
    17.         public NetworkEndpoint LocalEndpoint => tcp.LocalEndpoint;
    18.  
    19.         internal ConnectionList CreateConnectionList() => tcp.CreateConnectionList();
    20.         public int Initialize(ref NetworkSettings settings, ref int packetPadding) => tcp.Initialize(ref settings, ref packetPadding);
    21.         public int Bind(NetworkEndpoint endpoint) => tcp.Bind(endpoint);
    22.         public int Listen() => tcp.Listen();
    23.         public void Dispose() => tcp.Dispose();
    24.  
    25.         public JobHandle ScheduleReceive(ref ReceiveJobArguments arguments, JobHandle dep) => tcp.ScheduleReceive(ref arguments, dep);
    26.         public JobHandle ScheduleSend(ref SendJobArguments arguments, JobHandle dep) => tcp.ScheduleSend(ref arguments, dep);
    27.     }
    28. }
    29.  
    30. #else
    31.  
    32. using System;
    33. using System.Diagnostics;
    34. using System.Runtime.InteropServices;
    35.  
    36. using Unity.Collections;
    37. using Unity.Jobs;
    38. using Unity.Networking.Transport.Logging;
    39. using Unity.Networking.Transport.Relay;
    40.  
    41. namespace Unity.Networking.Transport
    42. {
    43.     public struct WebSocketNetworkInterface : INetworkInterface
    44.     {
    45.         private const string DLL = "__Internal";
    46.  
    47.         static class WebSocket
    48.         {
    49.             public static int s_NextSocketId = 0;
    50.  
    51.             [DllImport(DLL, EntryPoint = "js_html_utpWebSocketCreate")]
    52.             public static extern void Create(int sockId, IntPtr addrData, int addrSize);
    53.  
    54.             [DllImport(DLL, EntryPoint = "js_html_utpWebSocketDestroy")]
    55.             public static extern void Destroy(int sockId);
    56.  
    57.             [DllImport(DLL, EntryPoint = "js_html_utpWebSocketSend")]
    58.             public static extern int Send(int sockId, IntPtr data, int size);
    59.  
    60.             [DllImport(DLL, EntryPoint = "js_html_utpWebSocketRecv")]
    61.             public static extern int Recv(int sockId, IntPtr data, int size);
    62.  
    63.             [DllImport(DLL, EntryPoint = "js_html_utpWebSocketIsConnected")]
    64.             public static extern int IsConnectionReady(int sockId);
    65.         }
    66.  
    67.         unsafe struct InternalData
    68.         {
    69.             public NetworkEndpoint ListenEndpoint;
    70.             public int ConnectTimeoutMS; // maximum time to wait for a connection to complete
    71.  
    72.             // If non-empty, will connect to this hostname with the wss:// protocol. Otherwise the
    73.             // IP address of the endpoint is used to connect with the ws:// protocol.
    74.             public FixedString512Bytes SecureHostname;
    75.         }
    76.  
    77.         unsafe struct ConnectionData
    78.         {
    79.             public int Socket;
    80.             public long ConnectStartTime;
    81.         }
    82.  
    83.         private NativeReference<InternalData> m_InternalData;
    84.  
    85.         // Maps a connection id from the connection list to its connection data.
    86.         private ConnectionDataMap<ConnectionData> m_ConnectionMap;
    87.  
    88.         // List of connection information carried over to the layer above
    89.         private ConnectionList m_ConnectionList;
    90.  
    91.         internal ConnectionList CreateConnectionList()
    92.         {
    93.             m_ConnectionList = ConnectionList.Create();
    94.             return m_ConnectionList;
    95.         }
    96.  
    97.         public unsafe NetworkEndpoint LocalEndpoint => m_InternalData.Value.ListenEndpoint;
    98.  
    99.         public bool IsCreated => m_InternalData.IsCreated;
    100.  
    101.         public unsafe int Initialize(ref NetworkSettings settings, ref int packetPadding)
    102.         {
    103.             var networkConfiguration = settings.GetNetworkConfigParameters();
    104.  
    105.             // This needs to match the value of Unity.Networking.Transport.WebSocket.MaxPayloadSize
    106.             packetPadding += 14;
    107.  
    108.             var secureHostname = new FixedString512Bytes();
    109.             if (settings.TryGet<RelayNetworkParameter>(out var relayParams) && relayParams.ServerData.IsSecure != 0)
    110.                 secureHostname.CopyFrom(relayParams.ServerData.HostString);
    111.  
    112. #if ENABLE_MANAGED_UNITYTLS
    113.             // Shouldn't be required for normal use cases but is provided as an out in case the user
    114.             // wants to override the hostname (useful if say the user ended up resolving the Relay's
    115.             // hostname on their own instead of providing it directly in the Relay parameters).
    116.             if (settings.TryGet<TLS.SecureNetworkProtocolParameter>(out var secureParams))
    117.                 secureHostname.CopyFrom(secureParams.Hostname);
    118. #endif
    119.  
    120.             var state = new InternalData
    121.             {
    122.                 ListenEndpoint = NetworkEndpoint.AnyIpv4,
    123.                 ConnectTimeoutMS = networkConfiguration.connectTimeoutMS * networkConfiguration.maxConnectAttempts,
    124.                 SecureHostname = secureHostname,
    125.             };
    126.             m_InternalData = new NativeReference<InternalData>(state, Allocator.Persistent);
    127.  
    128.             m_ConnectionMap = new ConnectionDataMap<ConnectionData>(1, default, Allocator.Persistent);
    129.             return 0;
    130.         }
    131.  
    132.         public unsafe int Bind(NetworkEndpoint endpoint)
    133.         {
    134.             var state = m_InternalData.Value;
    135.             state.ListenEndpoint = endpoint;
    136.             m_InternalData.Value = state;
    137.  
    138.             return 0;
    139.         }
    140.  
    141.         public unsafe int Listen()
    142.         {
    143.             return 0;
    144.         }
    145.  
    146.         public unsafe void Dispose()
    147.         {
    148.             m_InternalData.Dispose();
    149.  
    150.             for (int i = 0; i < m_ConnectionMap.Length; ++i)
    151.             {
    152.                 WebSocket.Destroy(m_ConnectionMap.DataAt(i).Socket);
    153.             }
    154.  
    155.             m_ConnectionMap.Dispose();
    156.             m_ConnectionList.Dispose();
    157.         }
    158.  
    159.         public JobHandle ScheduleReceive(ref ReceiveJobArguments arguments, JobHandle dep)
    160.         {
    161.             return new ReceiveJob
    162.             {
    163.                 ReceiveQueue = arguments.ReceiveQueue,
    164.                 InternalData = m_InternalData,
    165.                 ConnectionList = m_ConnectionList,
    166.                 ConnectionMap = m_ConnectionMap,
    167.                 Time = arguments.Time,
    168.             }.Schedule(dep);
    169.         }
    170.  
    171.         struct ReceiveJob : IJob
    172.         {
    173.             public PacketsQueue ReceiveQueue;
    174.             public NativeReference<InternalData> InternalData;
    175.             public ConnectionList ConnectionList;
    176.             public ConnectionDataMap<ConnectionData> ConnectionMap;
    177.             public long Time;
    178.  
    179.             private void Abort(ref ConnectionId connectionId, ref ConnectionData connectionData, Error.DisconnectReason reason = default)
    180.             {
    181.                 ConnectionList.FinishDisconnecting(ref connectionId, reason);
    182.                 ConnectionMap.ClearData(ref connectionId);
    183.                 WebSocket.Destroy(connectionData.Socket);
    184.             }
    185.  
    186.             public unsafe void Execute()
    187.             {
    188.                 // Update each connection from the connection list
    189.                 var count = ConnectionList.Count;
    190.                 for (int i = 0; i < count; i++)
    191.                 {
    192.                     var connectionId = ConnectionList.ConnectionAt(i);
    193.                     var connectionState = ConnectionList.GetConnectionState(connectionId);
    194.  
    195.                     if (connectionState == NetworkConnection.State.Disconnected)
    196.                         continue;
    197.  
    198.                     var connectionData = ConnectionMap[connectionId];
    199.  
    200.                     // Detect if the upper layer is requesting to connect.
    201.                     if (connectionState == NetworkConnection.State.Connecting)
    202.                     {
    203.                         // The time here is a signed 64bit and we're never going to run at time 0 so if the connection
    204.                         // has ConnectStartTime == 0 it's the creation of this connection data.
    205.                         if (connectionData.ConnectStartTime == 0)
    206.                         {
    207.                             var socket = ++WebSocket.s_NextSocketId;
    208.                             GetServerAddress(connectionId, out var address);
    209.                             WebSocket.Create(socket, (IntPtr)address.GetUnsafePtr(), address.Length);
    210.  
    211.                             connectionData.ConnectStartTime = Time;
    212.                             connectionData.Socket = socket;
    213.                         }
    214.  
    215.                         // Check if the WebSocket connection is established.
    216.                         var status = WebSocket.IsConnectionReady(connectionData.Socket);
    217.                         if (status > 0)
    218.                         {
    219.                             ConnectionList.FinishConnectingFromLocal(ref connectionId);
    220.                         }
    221.                         else if (status < 0)
    222.                         {
    223.                             ConnectionList.StartDisconnecting(ref connectionId);
    224.                             Abort(ref connectionId, ref connectionData, Error.DisconnectReason.MaxConnectionAttempts);
    225.                             continue;
    226.                         }
    227.  
    228.                         // Disconnect if we've reached the maximum connection timeout.
    229.                         if (Time - connectionData.ConnectStartTime >= InternalData.Value.ConnectTimeoutMS)
    230.                         {
    231.                             ConnectionList.StartDisconnecting(ref connectionId);
    232.                             Abort(ref connectionId, ref connectionData, Error.DisconnectReason.MaxConnectionAttempts);
    233.                             continue;
    234.                         }
    235.  
    236.                         ConnectionMap[connectionId] = connectionData;
    237.                         continue;
    238.                     }
    239.  
    240.                     // Detect if the upper layer is requesting to disconnect.
    241.                     if (connectionState == NetworkConnection.State.Disconnecting)
    242.                     {
    243.                         Abort(ref connectionId, ref connectionData);
    244.                         continue;
    245.                     }
    246.  
    247.                     // Read data from the connection if we can. Receive should return chunks of up to MTU.
    248.                     // Close the connection in case of a receive error.
    249.                     var endpoint = ConnectionList.GetConnectionEndpoint(connectionId);
    250.                     var nbytes = 0;
    251.                     while (true)
    252.                     {
    253.                         // No need to disconnect in case the receive queue becomes full just let the TCP socket buffer
    254.                         // the incoming data.
    255.                         if (!ReceiveQueue.EnqueuePacket(out var packetProcessor))
    256.                             break;
    257.  
    258.                         nbytes = WebSocket.Recv(connectionData.Socket, (IntPtr)(byte*)packetProcessor.GetUnsafePayloadPtr() + packetProcessor.Offset, packetProcessor.BytesAvailableAtEnd);
    259.                         if (nbytes > 0)
    260.                         {
    261.                             packetProcessor.ConnectionRef = connectionId;
    262.                             packetProcessor.EndpointRef = endpoint;
    263.                             packetProcessor.SetUnsafeMetadata(nbytes, packetProcessor.Offset);
    264.                         }
    265.                         else
    266.                         {
    267.                             packetProcessor.Drop();
    268.                             break;
    269.                         }
    270.                     }
    271.  
    272.                     if (nbytes < 0)
    273.                     {
    274.                         // Disconnect
    275.                         ConnectionList.StartDisconnecting(ref connectionId);
    276.                         Abort(ref connectionId, ref connectionData, Error.DisconnectReason.ClosedByRemote);
    277.                         continue;
    278.                     }
    279.  
    280.                     // Update the connection data
    281.                     ConnectionMap[connectionId] = connectionData;
    282.                 }
    283.             }
    284.  
    285.             // Get the address to connect to for the given connection. If not using TLS, then this
    286.             // is just "ws://{address}:{port}" where address/port are taken from the connection's
    287.             // endpoint in the connection list. But if using TLS, then the hostname provided in the
    288.             // secure parameters overrides the address, and we connect to "wss://{hostname}:{port}"
    289.             // (with the port still taken from the connection's endpoint in the connection list).
    290.             private void GetServerAddress(ConnectionId connection, out FixedString512Bytes address)
    291.             {
    292.                 var endpoint = ConnectionList.GetConnectionEndpoint(connection);
    293.                 var secureHostname = InternalData.Value.SecureHostname;
    294.  
    295.                 if (secureHostname.IsEmpty)
    296.                     address = FixedString.Format("ws://{0}", endpoint.ToFixedString());
    297.                 else
    298.                     address = FixedString.Format("wss://{0}:{1}", secureHostname, endpoint.Port);
    299.             }
    300.         }
    301.  
    302.         public JobHandle ScheduleSend(ref SendJobArguments arguments, JobHandle dep)
    303.         {
    304.             return new SendJob
    305.             {
    306.                 SendQueue = arguments.SendQueue,
    307.                 ConnectionList = m_ConnectionList,
    308.                 ConnectionMap = m_ConnectionMap,
    309.             }.Schedule(dep);
    310.         }
    311.  
    312.         unsafe struct SendJob : IJob
    313.         {
    314.             public PacketsQueue SendQueue;
    315.             public ConnectionList ConnectionList;
    316.             public ConnectionDataMap<ConnectionData> ConnectionMap;
    317.  
    318.             private void Abort(ref ConnectionId connectionId, ref ConnectionData connectionData, Error.DisconnectReason reason = default)
    319.             {
    320.                 ConnectionList.FinishDisconnecting(ref connectionId, reason);
    321.                 ConnectionMap.ClearData(ref connectionId);
    322.                 WebSocket.Destroy(connectionData.Socket);
    323.             }
    324.  
    325.             public void Execute()
    326.             {
    327.                 // Each packet is sent individually. The connection is aborted if a packet cannot be transmiited
    328.                 // entirely.
    329.                 var count = SendQueue.Count;
    330.                 for (int i = 0; i < count; i++)
    331.                 {
    332.                     var packetProcessor = SendQueue[i];
    333.                     if (packetProcessor.Length == 0)
    334.                         continue;
    335.  
    336.                     var connectionId = packetProcessor.ConnectionRef;
    337.                     var connectionState = ConnectionList.GetConnectionState(connectionId);
    338.  
    339.                     if (connectionState != NetworkConnection.State.Connected)
    340.                     {
    341.                         packetProcessor.Drop();
    342.                         continue;
    343.                     }
    344.  
    345.                     var connectionData = ConnectionMap[connectionId];
    346.  
    347.                     var nbytes = WebSocket.Send(connectionData.Socket, (IntPtr)(byte*)packetProcessor.GetUnsafePayloadPtr() + packetProcessor.Offset, packetProcessor.Length);
    348.                     if (nbytes != packetProcessor.Length)
    349.                     {
    350.                         // Disconnect
    351.                         ConnectionList.StartDisconnecting(ref connectionId);
    352.                         Abort(ref connectionId, ref connectionData, Error.DisconnectReason.ClosedByRemote);
    353.                         continue;
    354.                     }
    355.  
    356.                     ConnectionMap[connectionId] = connectionData;
    357.                 }
    358.             }
    359.         }
    360.     }
    361. }
    362.  
    363. #endif
    364.  
    Sources
    https://forum.unity.com/threads/is-websocket-server-ready-to-use.1348337/

    https://docs-multiplayer.unity3d.com/transport/2.0.0/minimal-workflow-ws/index.html
     
    Last edited: Feb 14, 2023
  2. simon-lemay-unity

    simon-lemay-unity

    Unity Technologies

    Joined:
    Jul 19, 2021
    Posts:
    441
    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?
     
    bakhtrian likes this.
  3. bakhtrian

    bakhtrian

    Joined:
    Jan 24, 2023
    Posts:
    5
    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.
     
  4. simon-lemay-unity

    simon-lemay-unity

    Unity Technologies

    Joined:
    Jul 19, 2021
    Posts:
    441
    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.
     
    bakhtrian likes this.
  5. bakhtrian

    bakhtrian

    Joined:
    Jan 24, 2023
    Posts:
    5
  6. simon-lemay-unity

    simon-lemay-unity

    Unity Technologies

    Joined:
    Jul 19, 2021
    Posts:
    441
    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).
     
    bakhtrian likes this.
  7. bakhtrian

    bakhtrian

    Joined:
    Jan 24, 2023
    Posts:
    5
    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
     
  8. simon-lemay-unity

    simon-lemay-unity

    Unity Technologies

    Joined:
    Jul 19, 2021
    Posts:
    441
    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.
     
    bakhtrian likes this.
  9. bakhtrian

    bakhtrian

    Joined:
    Jan 24, 2023
    Posts:
    5
    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.
     
  10. wanlwanl

    wanlwanl

    Joined:
    Jul 7, 2022
    Posts:
    7
    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