Starting from this article, we will enter the practical project phase, a file-sharing project based on a P2P network. This project uses Kademlia and the request-response protocol to build a basic file-sharing application, which can locate and retrieve files by their names.
Basic Requirements
Assume there are three nodes, A, B, and C. A and B each provide a file FA and FB, while C retrieves a file. A and B declare themselves as the providers of their files on the DHT (a network composed of the Kademlia protocol) using the “libp2p-kad” protocol. Other nodes in the network connect to these two nodes through DHT.
Node C can find the providers of files FA or FB through DHT without needing to connect to a specific node that provides the file;
then node C connects to the node that provides the corresponding file and requests the file’s content using the “libp2p-request-response” protocol.
Creating the Project
cargo new file-sharing
Add the following dependencies in the Cargo.toml file:
[dependencies]
libp2p = { version = "0.46", features = ["tcp-tokio"] }
tokio = { version = "1.19", features = ["full"] }
futures = "0.3.1"
clap = {version = "3.1.6", features = ["derive"]}
async-trait = "0.1"
Parsing Command-Line Arguments
We use the clap library to parse command-line arguments, with the command-line format as follows:
/// Start the network node that provides files
file-sharing --listen-address <listen address> \
--secret-key-seed <seed for generating key pair> \
provide \
--path <full path to file> \
--name <file name>
/// Connect to the node providing the file and retrieve the file content
file-sharing --peer <node address/PeerId> \
get \
--name <file name>
Create the args.rs file in the src directory:
use std::path::PathBuf;
use clap::Parser;
use libp2p::Multiaddr;
#[derive(Debug, Parser)]
#[clap(name = "P2P File Sharing")]
pub struct Opt {
// Seed for generating key pair
#[clap(long)]
pub secret_key_seed: Option<u8>,
// Node address
#[clap(long)]
pub peer: Option<Multiaddr>,
// Listen address
#[clap(long)]
pub listen_address: Option<Multiaddr>,
// Subcommand
#[clap(subcommand)]
pub argument: CliArgument,
}
#[derive(Debug, Parser)]
pub enum CliArgument {
// Provide file subcommand
Provide {
#[clap(long)]
path: PathBuf, // Full path to file
#[clap(long)]
name: String, // File name
},
// Get file content subcommand
Get {
#[clap(long)]
name: String, // File name
},
}
Write the following code in the main.rs file:
use std::error::Error;
use args::{Opt, CliArgument};
use clap::Parser;
mod args;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let opt = Opt::parse();
process_args(opt).await?;
Ok(())
}
// Parse command-line arguments
async fn process_args(opt: Opt) -> Result<(), Box<dyn Error>> {
match opt.listen_address {
Some(addr) => println!("{addr}"),
None => println!("/ip4/0.0.0.0/tcp/0"),
}
if let Some(addr) = opt.peer {
println!("{addr}");
}
match opt.argument {
CliArgument::Provide{ path, name} => {
println!("{:?}, {name}", path);
},
CliArgument::Get { name } => {
println!("{name}");
}
}
Ok(())
}
Use the following command for testing:
cargo run -- \
--listen-address /ip4/127.0.0.1/tcp/40837 \
--secret-key-seed 1 \
provide \
--path /Users/Justin/sharing_file.txt \
--name sharing_file
Execution Result:
/ip4/127.0.0.1/tcp/40837"/Users/Justin/sharing_file.txt", sharing_filemaster:file_sharing_part_1 Justin$
Execute the command:
cargo run -- \
--peer /ip4/127.0.0.1/tcp/40837/p2p/12D3KooWPjceQrSwdWXPyLLeABRXmuqt69Rg3sBYbU1Nft9HyQ6X \
get \
--name sharing_file
Execution Result:
/ip4/0.0.0.0/tcp/0/ip4/127.0.0.1/tcp/40837/p2p/12D3KooWPjceQrSwdWXPyLLeABRXmuqt69Rg3sBYbU1Nft9HyQ6Xsharing_filemaster:file_sharing_part_1 Justin$
Command-line argument parsing was successful. In the next article, we will create a Client to send commands.
Full Code:
https://github.com/Justin02180218/p2p_learn