mirror of
https://github.com/logos-co/logos-libp2p-module.git
synced 2026-08-27 21:31:08 +00:00
211 lines
6.6 KiB
C++
211 lines
6.6 KiB
C++
/// # Tutorial 3: Connecting Peers and Exchanging Data
|
|
///
|
|
/// Now that we can create and configure nodes, let's make them talk to
|
|
/// each other! In this tutorial we'll:
|
|
///
|
|
/// - Create two nodes
|
|
/// - Connect one to the other
|
|
/// - Dial a protocol and open a stream
|
|
/// - Exchange data over the stream
|
|
///
|
|
/// ## How libp2p Connections Work
|
|
///
|
|
/// A libp2p connection is established in two steps:
|
|
///
|
|
/// 1. **Connect** — Establish a transport-level connection to a remote peer,
|
|
/// identified by its Peer ID and a multiaddress.
|
|
///
|
|
/// 2. **Dial** — Negotiate a protocol on top of the connection and open a
|
|
/// bidirectional stream. The stream is what you actually read from and
|
|
/// write to.
|
|
///
|
|
/// Streams in `logos-libp2p-module` are identified by a numeric `streamId`
|
|
/// that you get back from `dial()` and pass to read/write functions.
|
|
///
|
|
/// ## Stream Lifecycle
|
|
///
|
|
/// A stream must follow this lifecycle:
|
|
/// 1. `dial()` — Open a stream (returns a `streamId`)
|
|
/// 2. `streamWrite()` / `streamWriteLp()` — Send data
|
|
/// 3. `streamReadLp()` / `streamReadExactly()` — Receive data
|
|
/// 4. `streamClose()` or `streamCloseWithEOF()` — Close gracefully
|
|
/// 5. `streamRelease()` — Free server-side resources
|
|
///
|
|
/// -----------
|
|
|
|
#include <cstdio>
|
|
#include <cstdint>
|
|
#include <string>
|
|
#include <vector>
|
|
#include "plugin.h"
|
|
|
|
int main()
|
|
{
|
|
printf("=== Tutorial 3: Connecting Peers ===\n\n");
|
|
|
|
setLogLevel("fatal");
|
|
|
|
/// ## Step 1: Create and start two nodes
|
|
///
|
|
/// We create two nodes. Node A listens on port 9190, Node B on port 9191.
|
|
/// For simplicity, both mount the built-in `/ipfs/ping/1.0.0` protocol
|
|
/// (enabled by default — no extra config needed).
|
|
Libp2pModuleOptions optsA;
|
|
optsA.addrs = {"/ip4/127.0.0.1/tcp/9190"};
|
|
|
|
Libp2pModuleOptions optsB;
|
|
optsB.addrs = {"/ip4/127.0.0.1/tcp/9191"};
|
|
// Node B needs to know about node A to connect, but we'll pass
|
|
// that info after starting both nodes.
|
|
|
|
Libp2pModuleImpl nodeA(optsA);
|
|
Libp2pModuleImpl nodeB(optsB);
|
|
|
|
StdLogosResult startARes = nodeA.start();
|
|
if (!startARes.success) {
|
|
fprintf(stderr, "Node A failed to start: %s\n",
|
|
startARes.error.c_str());
|
|
return 1;
|
|
}
|
|
printf("Node A started\n");
|
|
|
|
StdLogosResult startBRes = nodeB.start();
|
|
if (!startBRes.success) {
|
|
fprintf(stderr, "Node B failed to start: %s\n",
|
|
startBRes.error.c_str());
|
|
return 1;
|
|
}
|
|
printf("Node B started\n");
|
|
|
|
/// ## Step 2: Get Node A's address info
|
|
///
|
|
/// Node B needs to know where to find Node A. We get Node A's
|
|
/// peer ID and listening addresses from `peerInfo()`.
|
|
StdLogosResult infoA = nodeA.peerInfo();
|
|
if (!infoA.success) {
|
|
fprintf(stderr, "Failed to get node A info: %s\n",
|
|
infoA.error.c_str());
|
|
return 1;
|
|
}
|
|
|
|
std::string peerIdA = infoA.value["peerId"].get<std::string>();
|
|
std::vector<std::string> addrsA;
|
|
for (const auto& a : infoA.value["addrs"]) {
|
|
addrsA.push_back(a.get<std::string>());
|
|
}
|
|
|
|
printf("Node A peer ID: %s\n", peerIdA.c_str());
|
|
printf("Node A addresses:\n");
|
|
for (const auto& a : addrsA) {
|
|
printf(" %s\n", a.c_str());
|
|
}
|
|
|
|
/// ## Step 3: Connect Node B to Node A
|
|
///
|
|
/// `connectPeer()` establishes the transport connection. The timeout
|
|
/// parameter is in milliseconds.
|
|
printf("\nConnecting Node B to Node A...\n");
|
|
StdLogosResult connectRes = nodeB.connectPeer(peerIdA, addrsA, 5000);
|
|
if (!connectRes.success) {
|
|
fprintf(stderr, "Failed to connect: %s\n",
|
|
connectRes.error.c_str());
|
|
return 1;
|
|
}
|
|
printf("Connected!\n");
|
|
|
|
/// ## Step 4: List connected peers
|
|
///
|
|
/// We can verify the connection by listing connected peers on each node.
|
|
///
|
|
/// The direction parameter:
|
|
/// - `PEER_DIRECTION_INBOUND` means "incoming connections" (other peers connected to this node).
|
|
/// - `PEER_DIRECTION_OUTBOUND` means "outgoing connections" (this node connected to other peers).
|
|
StdLogosResult peersA = nodeA.connectedPeers(PEER_DIRECTION_INBOUND);
|
|
if (!peersA.success) {
|
|
fprintf(stderr, "Failed to list Node A peers: %s\n",
|
|
peersA.error.c_str());
|
|
return 1;
|
|
}
|
|
printf("\nNode A's incoming connected peers:\n");
|
|
for (const auto& p : peersA.value) {
|
|
printf(" %s\n", p.get<std::string>().c_str());
|
|
}
|
|
|
|
StdLogosResult peersB = nodeB.connectedPeers(PEER_DIRECTION_OUTBOUND);
|
|
if (!peersB.success) {
|
|
fprintf(stderr, "Failed to list Node B peers: %s\n",
|
|
peersB.error.c_str());
|
|
return 1;
|
|
}
|
|
printf("Node B's outgoing connected peers:\n");
|
|
for (const auto& p : peersB.value) {
|
|
printf(" %s\n", p.get<std::string>().c_str());
|
|
}
|
|
|
|
/// ## Step 5: Dial the ping protocol and exchange data
|
|
///
|
|
/// Now let's use the Ping protocol — a simple built-in protocol where
|
|
/// the client sends a payload and the server echoes it back.
|
|
///
|
|
/// `dial()` opens a stream on the remote peer for a specific protocol.
|
|
/// It returns the `streamId` we use for subsequent operations.
|
|
printf("\nDialing /ipfs/ping/1.0.0 on Node A...\n");
|
|
StdLogosResult dialRes = nodeB.dial(peerIdA, "/ipfs/ping/1.0.0");
|
|
if (!dialRes.success) {
|
|
fprintf(stderr, "Dial failed: %s\n", dialRes.error.c_str());
|
|
return 1;
|
|
}
|
|
|
|
uint64_t streamId = dialRes.value.get<uint64_t>();
|
|
printf("Stream opened, id: %llu\n", (unsigned long long)streamId);
|
|
|
|
/// Write a 32-byte ping payload:
|
|
std::string payload(32, '\0');
|
|
for (int i = 0; i < 32; ++i) {
|
|
payload[i] = static_cast<char>(i);
|
|
}
|
|
|
|
printf("Sending %zu bytes...\n", payload.size());
|
|
StdLogosResult writeRes = nodeB.streamWrite(streamId, payload);
|
|
if (!writeRes.success) {
|
|
fprintf(stderr, "Write failed: %s\n", writeRes.error.c_str());
|
|
return 1;
|
|
}
|
|
|
|
/// Read the echo (32 bytes back):
|
|
StdLogosResult readRes = nodeB.streamReadExactly(streamId, 32);
|
|
if (!readRes.success) {
|
|
fprintf(stderr, "Read failed: %s\n", readRes.error.c_str());
|
|
return 1;
|
|
}
|
|
|
|
std::string reply = base64Decode(readRes.value.get<std::string>());
|
|
|
|
/// Verify the echo matches:
|
|
if (reply == payload) {
|
|
printf("Ping successful — received matching echo back!\n");
|
|
} else {
|
|
fprintf(stderr, "Ping payload mismatch\n");
|
|
return 1;
|
|
}
|
|
|
|
/// ## Step 6: Clean up the stream and nodes
|
|
///
|
|
/// Always close and release streams when done:
|
|
nodeB.streamClose(streamId);
|
|
nodeB.streamRelease(streamId);
|
|
|
|
nodeA.stop();
|
|
nodeB.stop();
|
|
|
|
printf("\n=== Tutorial 3 Complete ===\n");
|
|
|
|
return 0;
|
|
}
|
|
|
|
/// ## Run tutorial
|
|
///
|
|
/// ```bash
|
|
/// ./build/tutorial/tutorial_3_connecting_peers
|
|
/// ```
|