Search Unity

Question NetCode for ECS: dynamic port listening / relay

Discussion in 'NetCode for ECS' started by Deathwing, Dec 13, 2022.

  1. Deathwing

    Deathwing

    Joined:
    May 8, 2013
    Posts:
    25
    Hey guys,

    I have already made a good amount of progress on working with NetCode for ECS; in general, I love it.
    But it also makes my life harder than it should be :)

    I have 2 problems, and whenever I almost found a solution, I ran into an internal code block inside the NetCode package, which just makes me cry :D

    My first issue:
    I would like to support dynamic port listening Of course, the whole system behind it works already pretty well, call bind/listen with port 0, and it will automatically grab a free port. Awesome, but now comes the tricky part, I would like to KNOW which port it is listening on...

    Technically this is quite easy, as the INetworkInterface contains GetLocalEndpoint, which knows the port... But I can't find ANY way to access it, nor the NetworkDriver or the NetworkDriverStore.
    The only thing I can access is the NetworkStreamDriver, and oh boy, that guy is not very public-friendly :D
    And as most of the internal stuff uses refs, I can't even try to get it working with reflection :(

    Does anyone have an idea how to access it?

    My second issue:
    I guess it's connected to the sheer protectivity with internals:
    I would like to inject RelayServerData into the NetworkSettings of the used drivers, in order to support a possible relay connection, but again, I don't see any way how to achieve that, as I can of course create new NetworkDrivers with ease, but that doesn't help at all.

    It's not that I would like to do any edge-case stuff here, therefore I think I just miss something obvious, so I really would appreciate any help someone can offer!

    Cheers
     
    bb8_1 likes this.
  2. miniwolf_unity

    miniwolf_unity

    Unity Technologies

    Joined:
    Apr 10, 2018
    Posts:
    136
    For the first point you can do this inside of a system, will this be what you are looking for?:

    var connectionEntity SystemAPI.GetSingletonEntity<NetworkStreamConnection>()
    var connection = EntityManager.GetComponentData<NetworkStreamConnection>(connectionEntity);
    var port = SystemAPI.GetSingletonRW<NetworkStreamDriver>().ValueRO.GetRemoteEndPoint(connection).Port;


    The second point regarding passing the RelayServerData to the NetworkSettings, you can see an example of this in the DefaultDriverConstructor.RegisterClientDriver here we have an override that allows passing the RelayServerData to the driver together with some default NetworkSettings.

    To do this you can make a simple implementation of INetworkStreamDriverConstructor:
    Code (CSharp):
    1. public struct SecureDriverConstructor : INetworkStreamDriverConstructor
    2. {
    3.     public void CreateClientDriver(World world, ref NetworkDriverStore driverStore, NetDebug netDebug)
    4.     {
    5.         var yourRelayServerData = // code for getting your data for the client;
    6.         DefaultDriverBuilder.RegisterClientDriver(
    7.                 world, ref driverStore, netDebug, yourRelayServerData;
    8.     }
    9.  
    10.     public void CreateServerDriver(World world, ref NetworkDriverStore driverStore, NetDebug netDebug)
    11.     {
    12.         var yourRelayServerData = // code for getting your data for the server;
    13.         DefaultDriverBuilder.RegisterServerDriver(
    14.                 world, ref driverStore, netDebug, yourRelayServerData;
    15.     }
    16. }
    A similar question was answered here: https://forum.unity.com/threads/rel...to-connect-local-client.1365681/#post-8647998
     
    Occuros likes this.
  3. Deathwing

    Deathwing

    Joined:
    May 8, 2013
    Posts:
    25
    Hey miniwolf_unity,

    thanks for the reply!

    Unfortunately, for the first point, it would only work on the client, once he is connected, that is why I wanted a way to receive on the server his listening port, which I think can be technically accessed from the GetLocalEndpoint.

    For the second point, I will definitely check it out, my only concern about that way was, I have to tell right from the beginning if I want to have a relay in or not, as basically the network setting is baked into the drivers, I would like a more modular approach, where I can change the network settings as I want on the fly.

    Btw, I found a bug (exception) inside the NetworkStreamListenSystem, it is never initializing
    Code (CSharp):
    1. private ComponentLookup<ConnectionState> m_ConnectionStateFromEntity;
    And therefore the OnUpdate of it will throw a null ref exception here:
    Code (CSharp):
    1. public void OnUpdate(ref SystemState systemState)
    2.         {
    3.             var netDebug = SystemAPI.GetSingleton<NetDebug>();
    4.             ref var networkStreamDriver = ref SystemAPI.GetSingletonRW<NetworkStreamDriver>().ValueRW;
    5.  
    6.             var ents = m_ConnectionRequestListenQuery.ToEntityArray(Allocator.Temp);
    7.             m_NetworkStreamRequestListenFromEntity.Update(ref systemState); <--- HERE IT WILL THROW
    8.             var requestFromEntity = m_NetworkStreamRequestListenFromEntity;
    9. ...
    10. }
    Cheers

    Edit:

    I dug a bit deeper into your suggestion for the relay issue, but I see a problem here.
    Of course, I can initialize the Drivers with my relay setting, but I can never change it, and that is a big problem, as basically every host/join attempt could require new relay data, which I can not pass anymore, as the drivers and their network settings have been initialized already and I cant access it.

    Here is also my custom Bootstrap if anyone is interested:
    Code (CSharp):
    1. using Unity.Entities;
    2. using Unity.NetCode;
    3. using Unity.Networking.Transport.Relay;
    4.  
    5. using UnityEngine.Scripting;
    6.  
    7. public enum ProtocolType
    8. {
    9.     UnityTransport,
    10.     RelayUnityTransport,
    11. }
    12.  
    13. [Preserve]
    14. public class GameBootstrap : ClientServerBootstrap
    15. {
    16.     struct RelayIPCAndSocketDriverConstructor : INetworkStreamDriverConstructor
    17.     {
    18.         public static RelayServerData relayData;
    19.  
    20.         public void CreateClientDriver(World world, ref NetworkDriverStore driverStore, NetDebug netDebug) => DefaultDriverBuilder.RegisterClientDriver(world, ref driverStore, netDebug, ref relayData);
    21.  
    22.         public void CreateServerDriver(World world, ref NetworkDriverStore driverStore, NetDebug netDebug) => DefaultDriverBuilder.RegisterServerDriver(world, ref driverStore, netDebug, ref relayData);
    23.     }
    24.  
    25.     static GameBootstrap instance;
    26.  
    27.     bool initialised;
    28.     ProtocolType protocol;
    29.  
    30.     public static ProtocolType Protocol => instance.protocol;
    31.  
    32.     public static void InitializeNetCode(RelayServerData? relayServerData) => instance.Initialize(relayServerData);
    33.  
    34.     public override bool Initialize(string defaultWorldName)
    35.     {
    36.         instance = this;
    37.         return false;
    38.     }
    39.  
    40.     void Initialize(RelayServerData? relayServerData)
    41.     {
    42.         if (initialised)
    43.             return;
    44.  
    45.         if (relayServerData.HasValue)
    46.         {
    47.             RelayIPCAndSocketDriverConstructor.relayData = relayServerData.Value;
    48.             NetworkStreamReceiveSystem.DriverConstructor = new RelayIPCAndSocketDriverConstructor();
    49.             protocol = ProtocolType.RelayUnityTransport;
    50.         }
    51.         else
    52.             protocol = ProtocolType.UnityTransport;
    53.  
    54.         initialised = true;
    55.  
    56.         base.Initialize(null);
    57.     }
    58. }
    Edit 2:
    Another issue found, your RegisterServerDriver method calls RegisterClientDriver...
    Code (CSharp):
    1.         public static void RegisterServerDriver(World world, ref NetworkDriverStore driverStore, NetDebug netDebug, ref RelayServerData relayData, int playerCount = 0)
    2.         {
    3.             var settings = GetNetworkServerSettings(playerCount: playerCount);
    4.             settings = settings.WithRelayParameters(ref relayData);
    5.             RegisterClientDriver(world, ref driverStore, netDebug, settings);
    6.         }
    Edit 3:
    Would be great when we use the CreateServerDriver/CreateClientDriver with the relay setting, that it would skip the IPC transport layer, as it throws an exception if the relay is encountered :)

    Edit 4:
    I finally also succeeded to have a local server&client using relay. So I start a server listening on relay, while also connecting to it (but I can only get it working by also using the relay service, to retrieve the relay data.

    In general that has multiple negative impacts, 1. the players own connection is based on the relay service, 2. the player is having a ping (like 40 for me) to his own game xD)
    Therefore:
    How can I achieve a local connection to my server, which is hosting utilizing the relay server, without also going through the relay server? How can I 'host' a game, as a server, and have network components as a 'client' without establishing a real connection, that would solve kind of all my problems
     
    Last edited: Dec 14, 2022
  4. miniwolf_unity

    miniwolf_unity

    Unity Technologies

    Joined:
    Apr 10, 2018
    Posts:
    136
    Ahh, yes. I see your problem. I do not believe we considered this as a use case. In the average case you would either use AutoConnect port, or you would pass a well known port yourself when calling Bind/Listen.

    But I plan to expose two methods in the NetworkStreamDriver, would that solve your use case?

    Code (CSharp):
    1.         /// <summary>
    2.         /// Get the local endpoint (the endpoint remote peers will use to reach this driver) used by the first driver inside <see cref="NetworkDriverStore"/>.
    3.         /// This is similar to calling <see cref="GetLocalEndPoint(int)"/> with NetworkDriverStore.FirstDriverId as argument.
    4.         /// </summary>
    5.         /// <returns>The local endpoint of the driver.</returns>
    6.         public NetworkEndpoint GetLocalEndPoint()
    7.         {
    8.             return GetLocalEndPoint(NetworkDriverStore.FirstDriverId);
    9.         }
    10.  
    11.         /// <summary>
    12.         /// Get the local endpoint used by the driver (the endpoint remote peers will use to reach this driver).
    13.         /// <br/>
    14.         /// When multiple drivers exist, e.g. when using both IPC and Socket connection, multiple drivers will be available
    15.         /// in the <see cref="NetworkDriverStore"/>.
    16.         /// </summary>
    17.         /// <returns>The local endpoint of the driver.</returns>
    18.         public NetworkEndpoint GetLocalEndPoint(int driverId)
    19.         {
    20.             return DriverStore.GetNetworkDriver(driverId).GetLocalEndpoint();
    21.         }
     
  5. miniwolf_unity

    miniwolf_unity

    Unity Technologies

    Joined:
    Apr 10, 2018
    Posts:
    136
    Thank you for catching this, I can see that this was also found by my team, and it will be shipped with one of the following prereleases. If you need me to find out which I can look into the release schedule for you.
     
  6. miniwolf_unity

    miniwolf_unity

    Unity Technologies

    Joined:
    Apr 10, 2018
    Posts:
    136
    What is the use case for updating the relay server data? Maybe I do not understand your use case. How I think this would work:

    On the server, you would establish the relay connection, maybe during a bootstrap world, and then host the game. If you need to close the game and host a new game, you would tear down this connection and set up a new connection with the same relay server data.

    For a client to connect they would need to pass in the join code and then establish the relay connection. The result of this connection is a new relay server data struct which is not the same struct as the one created on the server.
    Again here you could tear down the connection when exiting the level and create a new one when using the new join code.
     
  7. miniwolf_unity

    miniwolf_unity

    Unity Technologies

    Joined:
    Apr 10, 2018
    Posts:
    136
    Yes that is a bug, I will get that fixed
     
    bb8_1 likes this.
  8. miniwolf_unity

    miniwolf_unity

    Unity Technologies

    Joined:
    Apr 10, 2018
    Posts:
    136
    You are absolutely right, I have a PR fixing this problem, that has not landed yet internally.

    I have the changes at hand if you need them, but they will land soon, and then be released.
     
    bb8_1 likes this.
  9. miniwolf_unity

    miniwolf_unity

    Unity Technologies

    Joined:
    Apr 10, 2018
    Posts:
    136
    In this case, you would need to register two drivers, one for IPC (this is the one that the host would use to connect as a client and one using Socket that passes in the relay server data.
    That should give you the scenario you are looking for.
    Then when connecting the client, you will determine whether it is the hosting client and therefor use the IPC or whether it should set up the relay connection.
     
    bb8_1 likes this.
  10. Deathwing

    Deathwing

    Joined:
    May 8, 2013
    Posts:
    25
    Yes, that would be really helpful!

    Nah, no worries, I just changed my custom implementation (because I needed it anyway because of the error throwing) and it is just a copy of the unity constructor code with smaller adjustments, so I can wait.

    That is also exactly how I ended up :) Having the client/server world with the drivers always created when I really wanna go into a multiplayer scenario.

    Cool, I will check that out, but in general, the client is still connected via a regular socket/IPC/etc connection to themself, while if I compare it to the StartHost approach of the NetCode for GameObject approach, using the hosting mode, the server is not really using a connection to have its client state available, just wondering if that is also possible here, because when I look at the Multiplayer PlayMode Tools (which are really nice btw) I can see the latency, which ideally should be something close to 0 :) (see attachment)

    Cheers
     

    Attached Files:

    Last edited: Dec 14, 2022
    bb8_1 likes this.
  11. Deathwing

    Deathwing

    Joined:
    May 8, 2013
    Posts:
    25
    So I used your proposed way, and it works great! A latency of 12+-0, (not sure if that is correct so) for hosting with relay, but connecting via an IPC driver.

    For anyone interested in the issue, and also needs maybe a bit of help, I just uploaded my current scripts on gist GitHub, here you go:
    https://gist.github.com/Deathwing/8c8a5c1ed68c87e6ee31b67fd8aa8db0
     
    kynphlee, BHostyle, PolarTron and 3 others like this.
  12. BHostyle

    BHostyle

    Joined:
    Jul 25, 2018
    Posts:
    4
    @Deathwing Thanks for sharing that gist. It has definitely helped me out. I am running into an issue where a NetworkIdComponent never gets created.

    In the console I show that I am listening on "0.0.0.0:58056" and Connecting on "127.0.0.1:58056"

    The ServerListenSystem and the ClientConnectSystem both run and tell the NetworkStreamDriver to Listen and Connect. No errors are thrown. But, after that my initial systems don't seem to run because there is no Entity with a NetworkIdComponent found.

    Any help you or anyone else can give I'd really appreciate it.
     
  13. miniwolf_unity

    miniwolf_unity

    Unity Technologies

    Joined:
    Apr 10, 2018
    Posts:
    136
    If you look inside NetworkStreamReceiveSystem.OnUpdate we spawn a ConnectionAccceptJob. This is the one that assigns the NetworkIdComponent to the entity. Using that could give you some information on why you cannot find the entity. Putting a breakpoint in this code and looking at the state of things would be my approach to investigate your issue.
     
  14. BHostyle

    BHostyle

    Joined:
    Jul 25, 2018
    Posts:
    4
    @miniwolf_unity Thank you for pointing me in the right direction. I found that the drivers that were being stored in the driverStore were equal to default(NetworkConnection) which was causing the ConnectionAcceptJob to be useless.

    That led me to look at where I'm creating the driver. I call a static method GetNetworkServerSettings on DefaultDriverBuilder. In the summary it mentions that it will use the NetworkSimulator parameters set by Multiplayer PlayMode Tools.

    Then when I looked at my PlayMode tools, hovering over "Disabled" tells me that "Disabling the simulator will also allow in-proc servers to use IPC connections." That was the key. Disabling the PlayMode Tools Simulator fixed my problem. Ultimately no bug or code issue that I could find, just that I needed to turn off the PlayMode Tools Simulator.

    Thank you.
     
  15. miniwolf_unity

    miniwolf_unity

    Unity Technologies

    Joined:
    Apr 10, 2018
    Posts:
    136
    That is awesome @BHostyle. I am happy you were able to resolve your issue.
     
  16. BHostyle

    BHostyle

    Joined:
    Jul 25, 2018
    Posts:
    4
    After testing a Client connection tonight I'm now not so certain that the Relay connection is actually active. When I used NGO I would set the Host or Client Relay Data on the NetworkManager.Singleton UnityTransport component and then call StartHost or StartClient.

    Perhaps I'm missing something with Netcodefor for Entities, but based off of the gist Deathwing shared I don't see anything specifically like that.

    The issue I'm running into is that when I try to connect a client the ClientServer receives my GoInGameRequest, spawns the GhostPrefab then it disappears immediately on the ClientServer. I also get an error on the ClientServer that says 'ERROR | Received error message from Relay: not connected.' I'm at a loss right now of what I should dig into next to resolve the issue.

    I'm also seeing this warning popup on my tests.
    [Worker0] 1 entities in the scene 'GameSubScene01' had no SceneSection and as a result were not serialized at all.


    Also, any update on a Unity provided demo project showing how to utilize Relay and Netcode for Entities?