-
Notifications
You must be signed in to change notification settings - Fork 4
Peer to Peer
Two peers behind NAT cannot dial each other directly. znet's P2P module solves this the usual way: both connect out to a rendezvous server, learn each other's external address, then punch simultaneously so each one's NAT sees the other's traffic as a reply to something it already sent.
The result is an ordinary PeerSession. Once it exists, everything else in this
wiki applies unchanged.
PeerLocator |
Talks to the rendezvous server, exchanges addresses, drives the punch |
p2p::PunchSync |
The punch itself. Called by the locator; usable directly if you have addresses already |
p2p::IsInitiator |
Decides which peer takes the server role |
rendezvous-server |
A ready-to-run rendezvous server in the repo |
// the locator's Wait() returns once punching resolves, so hold the session
// yourself if the connection should outlive it
std::shared_ptr<PeerSession> g_session;
std::unique_ptr<p2p::PeerLocator> g_locator;
bool OnPeerReady(p2p::PeerLocatorReadyEvent& event) {
// registered with the rendezvous server under event.peer_name(); ask for
// whoever you want to reach
g_locator->AskPeer("the-other-peer");
return false;
}
bool OnPeerConnected(p2p::PeerConnectedEvent& event) {
g_session = event.session();
g_session->SetCodec(std::make_shared<Codec>());
// both peers run the same code, so one has to take the server role. this
// derives it from the punch id so the two sides never both pick the same one.
bool acts_as_server = p2p::IsInitiator(event.punch_id(), event.self_peer_name(),
event.target_peer_name());
return false;
}
void OnEvent(Event& event) {
EventDispatcher dispatcher{event};
dispatcher.Dispatch<p2p::PeerLocatorReadyEvent>(ZNET_BIND_GLOBAL_FN(OnPeerReady));
dispatcher.Dispatch<p2p::PeerConnectedEvent>(ZNET_BIND_GLOBAL_FN(OnPeerConnected));
}
int RunPeer() {
p2p::PeerLocatorConfig config{"rendezvous.example.com", 25000};
g_locator.reset(new p2p::PeerLocator(config));
g_locator->SetEventCallback(ZNET_BIND_GLOBAL_FN(OnEvent));
if (g_locator->Connect() != Result::Success) {
return 1;
}
g_locator->Wait(); // returns once punching resolves, either way
return 0;
}| Event | When | Accessors |
|---|---|---|
PeerLocatorReadyEvent |
Registered with the rendezvous server |
peer_name(), endpoint()
|
PeerConnectedEvent |
The punch succeeded and a session exists |
session(), punch_id(), self_peer_name(), target_peer_name()
|
PeerLocatorCloseEvent |
The locator shut down | None |
Wait() returns when punching resolves, not when you are done. The locator's
job ends once it has produced a session. If you let the locator go out of scope
without holding the session, the connection goes with it. Keep the shared_ptr
somewhere that outlives the locator, as the example does.
To reuse a locator for another peer, call Connect() again and handle the events
again.
Somebody has to be the server. Both peers run identical code, so without a
tiebreak both would wait for the other to speak first. p2p::IsInitiator derives
the answer from the punch id and the two names, so the two sides always
disagree: exactly one gets true.
This also decides encryption: the initiator is the accepting side, so its
options are what the session adopts. See
Encryption and Compression.
PeerLocatorConfig::connection_type picks the transport for the punched
connection. ZDT is the better fit: it is UDP underneath, and UDP hole punching
works through more NATs than TCP's does.
Note that this one defaults to ConnectionType::TCP, unlike ClientConfig
and ServerConfig, which default to ZDT. If you want ZDT for a punched
connection, say so:
p2p::PeerLocatorConfig config{"rendezvous.example.com", 25000};
config.connection_type = ConnectionType::ZDT;Hole punching does not always work. Symmetric NATs assign a different external port per destination, so the address learned from the rendezvous server is not the one your peer must send to. There is no fallback relay in znet: if the punch fails, it fails, and your application decides what to do next.
The repo ships one:
cmake --build build --target rendezvous-server
./build/rendezvous-server/rendezvous-serverIt needs a public address both peers can reach. It only brokers introductions, and no traffic flows through it once the punch succeeds.
The P2P module is younger than the rest of the library and less exercised. Known gaps as of this writing:
-
StartPunchRequestPacket::connection_type_is never serialized, so the transport choice does not survive the wire andPunchSyncdispatches on an indeterminate value -
PeerLocator'sis_running_andsession_are read across threads without synchronization -
~PeerLocatordoes not join its task
Treat it as usable but not yet hardened. The p2p example is the working reference.