Custom Relay

Custom relay (Advanced)

Similar to Steam, Epic and PlayFab relays, you can create your own custom relay implementation and route traffic via any networking service. The relay implementation consists of three parts, each class implementing one of three interfaces.

  • ITransport (Client) - Outgoing connection. Passes messages between the client and the networking service.

  • IRelay (Host) - Listens for incoming connections and instantiates IRelayConnections.

  • IRelayConnection (Host) - Incoming connection. Passes messages between the Replication Server and the networking service.

Let's say we want to implement a custom relay that uses an API called FoobarNetworkingService. The code here outlines the main points to implement for routing network traffic.

First, we'll create a CustomTransport class to manage the outgoing connection from the client to the host. CustomTransport implements the ITransport interface that provides a few important methods. The Open and Close methods are used to connect and disconnect to/from the networking service. The Send and Receive methods are used to send and receive messages to/from the networking service.

using System;
using System.Collections.Generic;
using System.Net;
using Coherence.Brook;
using Coherence.Common;
using Coherence.Connection;
using Coherence.Transport;

public class CustomTransport : ITransport
{
    // Members required by ITransport
    public event Action OnOpen;
    public event Action<ConnectionException> OnError;
    public TransportState State { get; private set; }
    public bool IsReliable => false;
    public bool CanSend => true;
    public int HeaderSize => 0;
    public string Description => "Custom";

    // In this example, data is routed over a FoobarConnection that is provided by the FoobarNetworkingService
    private FoobarConnection foobarConnection;
        
    // The networking service normally requires some way to identify the host, for example a string or IP-address
    private string hostEndpoint;

    // Instantiates the CustomTransport configured to connect to a specific remote host endpoint
    public CustomTransport(string hostEndpoint)
    {
        this.hostEndpoint = hostEndpoint;
    }

    // This method is called when the client calls CoherenceBridge.Connect
    public void Open(EndpointData _, ConnectionSettings __)
    {
        // Initialize the networking service
        FoobarNetworkingService.Init();

        // Connect to the FoobarNetworkingService
        foobarConnection = FoobarNetworkingService.OpenNewOutgoingConnection(hostEndpoint);

        // Mark the transport as open, i.e. ready to send and receive messages
        State = TransportState.Open;

        // Notify the client that the connection has been opened
        OnOpen?.Invoke();
    }

    // This method is called when the CoherenceBridge disconnects
    public void Close()
    {
        // Mark the transport as closed, i.e., no longer able to send or receive messages
        State = TransportState.Closed;

        // Disconnect from the FoobarNetworkingService
        foobarConnection.Close();

        // Dispose the networking service
        FoobarNetworkingService.Shutdown();
    }

    // This method is not mandatory but can be used e.g., to send a final disconnect message before the connection is closed
    public void PrepareDisconnect() { }

    // This method is called each frame. The buffer should be populated with incoming messages
    public void Receive(List<(IInOctetStream, IPEndPoint)> buffer)
    {
        // If there are any incoming messages, push them to the return buffer
        while (foobarConnection.TryReceiveMessage(out var data))
        {
            buffer.Add((new InOctetStream(data), default));
        }
    }

    // This method is called for each packet that the client wants to send to the Replication Server
    public void Send(IOutOctetStream stream)
    {
        // Convert the stream to a byte-array and send it over the foobarConnection
        foobarConnection.SendMessage(stream.Close().ToArray());
    }

    public void SendTo(IOutOctetStream stream, IPEndPoint endpoint, SessionID sessionID)
    {
        throw new Exception("SendTo not supported on this transport.");
    }
}

The CustomTransport will be instantiated when the client attempts to connect to the host, usually as a result of calling CoherenceBridge.Connect. You can control how the transport is instantiated by implementing an ITransportFactory.

using Coherence.Log;
using Coherence.Stats;
using Coherence.Transport;

public class CustomTransportFactory : ITransportFactory
{
    // The transport factory is a good place to inject the host identifier or endpoint.
    private string hostIdentifier;

    public CustomTransportFactory(string hostIdentifier)
    {
        this.hostIdentifier = hostIdentifier;
    }

    public ITransport Create(IStats stats, Logger logger)
    {
        return new CustomTransport(hostIdentifier);
    }
}

Finally, to configure the client to actually use the CustomTransport, just set the transport factory on CoherenceBridge.

// This is called on the client
CoherenceBridge.SetTransportFactory(new CustomTransportFactory("some host identifier"));

This is everything needed on the client-side.

On the host-side, we need a CustomRelayConnection class to manage the incoming connection. This class implements IRelayConnection and is a mirror image of the CustomTransport. The OnConnectionOpened and OnConnectionClosed methods are called in response to CustomTransport.Open and CustomTransport.Close.

The SendMessageToClient and ReceiveMessagesFromClient methods are responsible for sending and receiving messages over the networking services, similar to CustomTransport.Send and CustomTransport.Receive.

using Coherence.Toolkit.Relay;

public class CustomRelayConnection : IRelayConnection
{
    // In this example, data is routed over a FoobarConnection that is provided by the FoobarNetworkingService
    private FoobarConnection foobarConnection;

    public CustomRelayConnection(FoobarConnection foobarConnection)
    {
        this.foobarConnection = foobarConnection;
    }

    // This is a good place to initialize an opened connection
    public void OnConnectionOpened() { }

    // This is a good place to dispose a closed connection
    public void OnConnectionClosed() { }

    // This method is called each frame. The buffer should be populated with incoming messages.
    public void ReceiveMessagesFromClient(List<ArraySegment<byte>> buffer)
    {
        // If there are any incoming messages, push them to the return buffer
        while (foobarConnection.TryReceiveMessage(out var data))
        {
            buffer.Add(new ArraySegment<byte>(data));
        }
    }

    // This method is called for each packet that the Replication Server wants to send to the client
    public void SendMessageToClient(ArraySegment<byte> packetData)
    {
        // Convert the stream to a byte-array and send it over the foobarConnection
        foobarConnection.SendMessage(packetData.ToArray());
    }
}

Now we just need a CustomRelay class that listens for incoming FoobarConnections and maps them to a corresponding CustomRelayConnection.

using System;
using Coherence.Toolkit.Relay;
using System.Collections.Generic;
using Coherence.Connection;

public class CustomRelay : IRelay
{
    public event Action<ConnectionException> OnError;

    // This property is populated by the CoherenceBridge and provides access to the CoherenceRelayManager.
    public CoherenceRelayManager RelayManager { get; set; }

    // A map to keep track of all existing relay connections
    private Dictionary<FoobarConnection, IRelayConnection> connectionMap = new Dictionary<FoobarConnection, IRelayConnection>();

    // This method is called when the host has connected to the replication server.
    public void Open()
    {
        // Initialize the networking service
        FoobarNetworkingService.Init();

        // Listen for connection events
        FoobarNetworkingService.OnConnectionOpened += HandleFoobarConnectionOpened;
        FoobarNetworkingService.OnConnectionClosed += HandleFoobarConnectionClosed;
        FoobarNetworkingService.OnConnectionError += HandleFoobarConnectionError;
    }

    private void HandleFoobarConnectionError(string errorMessage)
    {
        OnError?.Invoke(new ConnectionException(errorMessage));
    }

    private void HandleFoobarConnectionOpened(FoobarConnection foobarConnection)
    {
        // Create a new relay connection
        var relayConnection = new CustomRelayConnection(foobarConnection);

        // Map the connection for easy access later
        connectionMap[foobarConnection] = relayConnection;

        // Register the new relay connection
        RelayManager.OpenRelayConnection(relayConnection);
    }

    private void HandleFoobarConnectionClosed(FoobarConnection foobarConnection)
    {
        // Use the map to find the corresponding relay connection
        var relayConnection = connectionMap[foobarConnection];

        // Unregister the relay connection
        RelayManager.CloseAndRemoveRelayConnection(relayConnection);

        // Remove the connection from the map
        connectionMap.Remove(foobarConnection);
    }

    // This method is called when the host has disconnected from the replication server.
    public void Close()
    {
        // Dispose the networking service
        FoobarNetworkingService.Shutdown();

        // Unsubscribe event listeners
        FoobarNetworkingService.OnConnectionOpened -= HandleFoobarConnectionOpened;
        FoobarNetworkingService.OnConnectionClosed -= HandleFoobarConnectionClosed;
        FoobarNetworkingService.OnConnectionError -= HandleFoobarConnectionError;
    }

    // This method is called every frame before packets are processed.
    public void Update()
    {
        // Tick the networking service
        FoobarNetworkingService.Update();
    }

    // This method is called every frame after packets have been processed.
    public void Flush()
    {
        // If the networking service needs to flush anything, this is the place to do it.
        FoobarNetworkingService.Flush();
    }
}

Finally, to configure the host to actually use the CustomRelay, simply set the relay on the CoherenceBridge:

// This is called on the host
CoherenceBridge.SetRelay(new CustomRelay());

These are all the necessary steps required to configure a custom relay.

For a complete relay code example, please review the Steam Relay source code.

Last updated

Was this helpful?