Direct Messaging
Direct messaging opens peer-to-peer UDP connections between the clients in a room, so latency-critical payloads travel directly between players instead of taking the round trip through the server. Typical uses are position streams and voice data. A JavaScript client has no peer-to-peer transport and sends and receives direct messages through the server relay only, see Direct Messaging in JavaScript.
Direct messaging is only available in Realtime Core and is not used by Fusion, Quantum or any other Photon product. It is an advanced option for very specific bespoke applications built directly on Realtime Core. Direct connections expose the IP addresses of players to each other and are therefore considered a security risk; avoid them unless your use case specifically requires them.
How Direct Connections Are Established
The direct links are plain UDP sockets between the peers, established with NAT punch-through. Each client queries STUN servers to discover its own public, server-reflexive address, shares that address with the other clients through the Photon server and then both sides punch a hole through their NATs by sending UDP packets to each other's reflexive address. The client uses a built-in list of public STUN servers to obtain the reflexive addresses; this list is not configurable. The mechanism is barebones and UDP only. Punch-through does not succeed for every NAT and firewall combination, which is why the relay fallback exists.
Enabling Direct Messaging
Direct messaging is enabled per room at creation time through the direct mode of the create options.
The direct mode defines which player establishes a direct connection to which other player:
| C++ | JavaScript | Meaning |
|---|---|---|
DirectMode::None |
'none' |
The default. No direct connections; direct messages cannot be sent in the room. |
DirectMode::AllToOthers |
Not available | Each client establishes a direct connection with every other client inside the room. |
DirectMode::AllToAll |
'allToAll' |
Each client establishes a direct connection with every client inside the room, including itself. |
DirectMode::MasterToOthers |
Not available | The master client establishes a direct connection with every other client inside the room. All other clients only establish a direct connection with the master client but not with each other. |
DirectMode::MasterToAll |
'masterToAll' |
The master client establishes a direct connection with every client inside the room, including itself. All other clients only establish a direct connection with the master client but not with each other. |
Everything on this page assumes a room created with a direct mode:
C++
Task<Result<Room>> CreateP2PRoom(RealtimeClient& client)
{
CreateRoomOptions options;
options.MaxPlayers = 8;
options.DirectMessaging = DirectMode::AllToAll;
co_return co_await client.CreateRoom(PHOTON_STR("p2p-match"), options);
}
Creating a room with a direct mode is not available over the C API.
realtime_create_room() has no directMessaging parameter and a room a C client creates always has DirectMode::None and direct messaging stays off in it.
JavaScript
const room = await client.createRoom('p2p-match', { maxPlayers: 8, directMessaging: 'allToAll' });
A JavaScript client opens no direct connections in either mode; the mode still enables relayed direct messages in the room.
Sending Direct Messages
A direct message is a payload of raw bytes, sent with a set of send options.
In C++, an overload also sends any trivially copyable struct byte for byte; in JavaScript, the payload is a Uint8Array.
Sending requires the client to be in a room with direct messaging enabled, also when the message takes the relay.
| C++ | C | JavaScript |
|---|---|---|
SendDirect() |
realtime_send_direct() |
sendDirect() |
The return value is the number of target players for which the message could successfully be handed off for sending.
A positive count does not guarantee that the message will be received; 0 means there was no eligible target and -1 means the client is not in a room.
When the relay fallback is enabled, this number includes the recipients for which the fallback relay has been used.
A payload larger than 1199 bytes is not sent at all and counts no recipients.
| C++ | C | JavaScript | Default | Description |
|---|---|---|---|---|
TargetPlayers |
targetPlayers, targetCount |
targetPlayers |
empty | Explicit recipient player numbers. Overrides the target group when non-empty. |
TargetGroup |
receiverGroup |
receivers |
Others | Which players receive the message: the others, all players or the master client. |
FallbackRelay |
fallbackRelay |
fallbackRelay |
false |
Routes the message through the server when no direct link to a target exists. |
Streaming a position struct to every other player each frame:
C++
struct PositionUpdate
{
float X;
float Y;
float Z;
};
void BroadcastPosition(RealtimeClient& client, const PositionUpdate& position)
{
DirectMessageOptions options;
options.FallbackRelay = true; // Use the server if no P2P link exists
client.SendDirect(position, options);
}
C
typedef struct
{
float x;
float y;
float z;
} PositionUpdate;
/* Returns a recipient count, not a success flag. */
static int32_t BroadcastPosition(RealtimeHandle client, const PositionUpdate* position)
{
int32_t recipients = realtime_send_direct(client,
(const uint8_t*)position, (int32_t)sizeof(*position),
NULL, 0, /* no explicit targets */
0, /* receiverGroup: Others */
1); /* fallbackRelay: use the server */
/* Test for the two failures explicitly: every positive value is a count of
recipients, so 1 means one recipient rather than "accepted". */
if (recipients < 0)
{
/* Not in a room. */
}
else if (recipients == 0)
{
/* In a room, but no eligible target. */
}
return recipients;
}
/* The return value has the same meaning as in C++: the number of targets the
message was handed off for, and a positive count still does not guarantee
delivery. Unlike realtime_send_event, which returns non-zero on success,
this is a count, so do not compare it against 1. Explicit recipients are an
int32_t array with its count, and they override receiverGroup when the array
is not NULL. */
JavaScript
const position = { x: 1, y: 2.5, z: -3 };
// Three little-endian 32-bit floats, the byte layout of the C++ PositionUpdate struct.
const data = new Uint8Array(12);
const view = new DataView(data.buffer);
view.setFloat32(0, position.x, true);
view.setFloat32(4, position.y, true);
view.setFloat32(8, position.z, true);
// A web client has no peer-to-peer links: without fallbackRelay nothing is sent.
const recipients = client.sendDirect(data, { fallbackRelay: true });
Receiving Direct Messages
Incoming messages arrive through a callback that carries the sender's player number, the payload bytes and a relay flag.
The relay flag is false when the message arrived over the direct link and true when it took the server relay.
| C++ | C | JavaScript |
|---|---|---|
OnDirectMessage |
RealtimeEventType_DirectMessage |
directMessage event |
In C++, the payload span is only valid during the callback, so copy the payload out if you keep it.
In JavaScript, the payload is a Uint8Array the handler may keep.
A receive handler that mirrors the position stream above:
C++
RealtimeCore::Common::ScopedSubscription directMessages = client.OnDirectMessage.Subscribe(
[](int senderId, std::span<const uint8_t> data, bool isRelay) {
if (data.size() != sizeof(PositionUpdate))
{
return;
}
PositionUpdate position;
std::memcpy(&position, data.data(), sizeof(position));
if (isRelay)
{
// Took the server detour: expect higher latency for this sender.
}
});
C
/* DirectMessage (109): `origin` is the sender, `counter` is 1 when the message
took the server relay, and the blob is the raw payload. */
if (e->type == RealtimeEventType_DirectMessage &&
e->blobOffset >= 0 &&
e->blobLength == (int32_t)sizeof(PositionUpdate))
{
PositionUpdate position;
memcpy(&position, blob + e->blobOffset, sizeof(position));
if (e->counter != 0)
{
/* Took the server detour: expect higher latency for this sender. */
}
}
/* The payload lives in the queue blob rather than in a callback span, so
it stays readable for the whole batch and is invalidated by the next
realtime_event_queue_flush instead of by the return from a handler. */
JavaScript
client.on('directMessage', ({ senderNumber, data, isRelay }) => {
if (data.byteLength !== 12) {
return;
}
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
const position = { x: view.getFloat32(0, true), y: view.getFloat32(4, true), z: view.getFloat32(8, true) };
// A relayed message took the server detour: expect higher latency for this sender.
// Always the case in a web client, which has no direct links.
console.log(`#${senderNumber} is at`, position, isRelay ? 'via the server' : 'directly');
});
Direct Connection Callbacks
The per-peer link status is reported through two callbacks, each carrying the remote player's number: one fires when a direct connection to a player comes up, and the other fires when an attempt does not succeed.
| C++ | C | JavaScript |
|---|---|---|
OnDirectConnectionEstablished |
RealtimeEventType_DirectConnectionEstablished |
directConnectionEstablished event |
OnDirectConnectionFailed |
RealtimeEventType_DirectConnectionFailed |
directConnectionFailed event |
Tracking which peers have a live P2P link:
C++
std::set<int> directPeers;
RealtimeCore::Common::SubscriptionBag subscriptions;
subscriptions += client.OnDirectConnectionEstablished.Subscribe(
[&directPeers](int remotePlayerId) { directPeers.insert(remotePlayerId); });
subscriptions += client.OnDirectConnectionFailed.Subscribe(
[&directPeers](int remotePlayerId) { directPeers.erase(remotePlayerId); });
C
/* Both events put the remote player number in `origin`. */
switch (e->type)
{
case RealtimeEventType_DirectConnectionEstablished:
DirectPeersInsert(e->origin);
break;
case RealtimeEventType_DirectConnectionFailed:
DirectPeersErase(e->origin);
break;
default:
break;
}
/* Neither event carries a blob, so the player number in origin is the
whole payload. */
A JavaScript client never opens or attempts a direct connection, so neither directConnectionEstablished nor directConnectionFailed fires and there are no live links to track.
Treat every peer as relayed; see Direct Messaging in JavaScript.
Relay Fallback
Pure peer-to-peer is unreliable on the open internet: NAT punch-through fails for many player pairs because symmetric NATs, strict firewalls and mobile carriers block the direct link. The relay fallback option covers these cases by routing the message through the server whenever the direct link to a target is unavailable; such messages arrive with the relay flag set.
Enable the fallback unless losing the payload entirely is acceptable for the affected peers. Treat a failed direct connection as the signal that a peer will stay on relay, and budget that peer's latency accordingly.
Direct Messaging in JavaScript
A JavaScript client speaks WebSocket only and has no peer-to-peer transport, in the browser and in Node.js alike. It takes part in direct messaging through the server relay:
- It creates and joins rooms with a direct mode, sends with
sendDirect()and receives through thedirectMessageevent. - It sends nothing unless
fallbackRelayistrue: without the fallback,sendDirect()returns0. - Every message it receives arrives with
isRelayset totrue. - It never receives
directConnectionEstablishedordirectConnectionFailed. - Native clients in the room never get a direct connection to it either, so their messages reach it only when they send with the relay fallback enabled.
'allToAll'and'masterToAll'behave the same for it: the mode only decides which direct connections the native clients in the room attempt.DirectMode::AllToOthersandDirectMode::MasterToOthersare not available in JavaScript; a room created with one of them by a native client reads as'none'inroom.directModeand in room listings.
Last updated on
Back to top