6.0 KiB
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:
-
Connect — Establish a transport-level connection to a remote peer, identified by its Peer ID and a multiaddress.
-
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:
dial()— Open a stream (returns astreamId)streamWrite()/streamWriteLp()— Send datastreamReadLp()/streamReadExactly()— Receive datastreamClose()orstreamCloseWithEOF()— Close gracefullystreamRelease()— Free server-side resources
#include <cstdio>
#include <cstdint>
#include <string>
#include <vector>
#include "plugin.h"
int main()
{
printf("=== Tutorial 3: Connecting Peers ===\n\n");
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);
if (!nodeA.start().success) {
fprintf(stderr, "Node A failed to start\n");
return 1;
}
printf("Node A started\n");
if (!nodeB.start().success) {
fprintf(stderr, "Node B failed to start\n");
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().
auto infoA = nodeA.peerInfo();
if (!infoA.success) {
fprintf(stderr, "Failed to get node A info\n");
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");
if (!nodeB.connectPeer(peerIdA, addrsA, 5000).success) {
fprintf(stderr, "Failed to connect\n");
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:
0means "outgoing connections" (this node connected to other peers).1means "incoming connections" (other peers connected to this node).
auto peersA = nodeA.connectedPeers(0);
if (peersA.success) {
printf("\nNode A's connected peers:\n");
for (const auto& p : peersA.value) {
printf(" %s\n", p.get<std::string>().c_str());
}
}
auto peersB = nodeB.connectedPeers(0);
if (peersB.success) {
printf("Node B's 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");
auto 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());
if (!nodeB.streamWrite(streamId, payload).success) {
fprintf(stderr, "Write failed\n");
return 1;
}
Read the echo (32 bytes back):
auto 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
./build/tutorial/tutorial_3_connecting_peers
| ← Custom Node Configuration | Custom Protocol Handlers → |