-
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 | — |
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.
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 — 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.