Skip to content
ReferenceType edited this page May 27, 2024 · 3 revisions

P2P Relay Client/Server

P2P system inherently works with MessageProtocol

This model is what I personally use on my other projects such as P2PVideocall and Multiplayer Starfighter Game. Basically you have a Relay server somewhere in your network, which can act as a local network hub in LAN and/or open to connections from internet if port forwarding is enabled.


Relay clients (Peers) connect to Relay server and get notifications about existence of other peers. Peers can send messages to each other through Relay Server, or directly to each other (Tcp and Udp holepunch).


Relay server

Server is completely passive, namely, its sole responsibility is allowing other peers to discover and send messages to each other. Additionally providing NAT traversal methods such as UDP and TCP holepunching to establish direct sockets allow direct communication via Internet or LAN.

Relay Server Is Serialization Agnostic which means its data agnostic. Any serialized network Peers, (Protobuff, MessagePack, Json etc) can use the same relay server.


To use the Relay server, simply declere your server as:

      var scert = new X509Certificate2("server.pfx", "greenpass");
      var server = new RelayServer(port, scert, "YourServerName");
      server.StartServer();

Relay server is already pre-configured with optimum settings.

Relay Client

Relay client is where your application logic is implemented. You can connect your client applications to discover and talk with each other.

To declere a client:

      var cert = new X509Certificate2("client.pfx", "greenpass");
      var client = new RelayClient(cert);

      client.OnPeerRegistered += (Guid peerId) => ..
      client.OnPeerUnregistered += (Guid peerId) => ..
      client.OnMessageReceived += (MessageEnvelope message) => .. 
      client.OnUdpMessageReceived += (MessageEnvelope message) => ..
      client.OnDisconnected += () => ..

      client.Connect("127.0.0.1", 20010);

Method signatures and callbacks are identical to proto client/server model (also with Payloads). Only difference is you have to specify the destination peer Guid Id.

      client.SendAsyncMessage(destinationPeerId, new MessageEnvelope() { Header = "Hello" });
      client.SendUdpMesssage(destinationPeerId, new MessageEnvelope() { Header = "Hello" });
      // Or with an async reply
      MessageEnvelope response = await client.SendRequestAndWaitResponse(destinationPeerId,
                                            new MessageEnvelope() { Header = "Who Are You?" });

Peer Ids comes from OnPeerRegistered event, whenever a new peer is connected to relay server. Relay Server guaranties synchronization of current peer set with eventual consistency among all peers. So new peers will receive all other connected peers from this event and old peers will receive an update.

Following image shows a peer update diagram on connection to relay server:

Additionally there is a broadcast support for TCP and Udp. This allows a single message to be send to relay server and multiplexed to all reachable peers.

      client.BroadcastMessage(peerId, envelope);
      client.BroadcastUdpMessage(peerId, envelope);

Udp messages can be more than the datagram limit of 65,527 bytes. The system detects large udp messages as Jumbo messages and sends them in chunks. Receiving end with will try to reconstruct the message. if all the parts does not arrive within a timeout message is dropped. In a nutshell it works same as regular udp, but with larger payloads. Max message size for udp is 16,256,000 bytes. This is also applicable to Udp broadcasts.

      client.SendUdpMesssage(destinationPeerId,
             new MessageEnvelope() { Header = "Hello" Payload = new byte[256000]});

We have build in reliable udp protocol implemneted aswell which is TCP implemented over UDP.

      client.SendRudpMessage(peerId, envelope);
      client.SendRudpMessage(peerId, envelope, innerMessage);
      client.SendRudpMessageAndWaitResponse(peerId, envelope, innerMessage);
      client.SendRudpMessageAndWaitResponse(peerId, envelope);

On top of that 3 independent channels are provided for Rudp. Ch1,Ch2 and Realtime. Reason for this is if you send a large message like a file from single channel, it will be blocked until message is completely sent. hence subsequent messages has to wait in buffer. By having independent channels this issue is avoided.

Ch1 and 2 are standard channels with same timeout mechanics of windows TCP implmentation. Realtime channel is more sensitive to timeouts and may resend messages "pre-emtively". But resends are much faster so data is delivered reliably with minimum delay.

      client.SendRudpMessage(peerId, envelope, RudpChannel.Ch1);
      client.SendRudpMessage(peerId, envelope, RudpChannel.Ch2);
      client.SendRudpMessage(peerId, envelope, RudpChannel.Realtime);

Holepunch Support:

      bool result = await client.RequestHolePunchAsync(destinationPeerId, timeOut:10000);

if succesfull, it will allow you to send direct udp messages between current and destination peers for the rest of the udp messages in both directions.