Cómo construir una aplicación simple de igual a igual usando Rust

En este tutorial, le mostraremos cómo construir una aplicación peer-to-peer muy simple usando Rust y la fantástica biblioteca libp2p.

En los últimos años, debido en gran parte a la exageración que rodea a la cadena de bloques y las criptomonedas, las aplicaciones descentralizadas han ganado bastante impulso. Otro factor detrás del creciente interés en la descentralización es una mayor conciencia sobre las desventajas de poner la mayor parte de la web en manos de un pequeño grupo de empresas en términos de privacidad de datos y monopolización.

En cualquier caso, recientemente ha habido algunos desarrollos muy interesantes en la escena del software descentralizado, incluso aparte de toda la tecnología de criptografía y blockchain.

Los ejemplos notables incluyen IPFS ; la nueva plataforma de codificación distribuida Radicle ; la red social descentralizada Scuttlebutt ; y muchas más aplicaciones dentro del Fediverse , como Mastodon .

En este tutorial, le mostraremos cómo construir una aplicación peer-to-peer muy simple usando Rust y la fantástica libp2pbiblioteca, que existe en diferentes etapas de madurez para una amplia gama de idiomas.

Vamos a crear una aplicación de recetas de cocina con una sencilla interfaz de línea de comandos que nos permite:

  • Crea recetas
  • Publica recetas
  • Lista de recetas locales
  • Enumere otros pares que descubrimos en la red
  • Enumere las recetas publicadas de un par determinado
  • Enumere todas las recetas de todos los compañeros que conocemos

Haremos todo esto en alrededor de 300 líneas de Rust. ¡Empecemos!

Instalación de Rust

Para seguir, todo lo que necesita es una instalación reciente de Rust (1.47+).

Primero, cree un nuevo proyecto de Rust:

cargo new rust-p2p-example
cd rust-p2p-example

A continuación, edite el Cargo.tomlarchivo y agregue las dependencias que necesitará:

 

[dependencies]
libp2p = { version = "0.31", features = ["tcp-tokio", "mdns-tokio"] }
tokio = { version = "0.3", features = ["io-util", "io-std", "stream", "macros", "rt", "rt-multi-thread", "fs", "time", "sync"] }
serde = {version = "=1.0", features = ["derive"] }
serde_json = "1.0"
once_cell = "1.5"
log = "0.4"
pretty_env_logger = "0.4"

Como se mencionó anteriormente, usaremos libp2ppara la parte de redes peer-to-peer. Más específicamente, lo usaremos en conjunto con el tiempo de ejecución asíncrono de Tokio. Usaremos Serde para la serialización y deserialización de JSON y un par de bibliotecas auxiliares para registrar e inicializar el estado.

¿Qué es libp2p?

libp2p es un conjunto de protocolos para crear aplicaciones peer-to-peer que se centra en la modularidad.

Hay implementaciones de bibliotecas para varios lenguajes, como JavaScript, Go y Rust. Todas estas bibliotecas implementan las mismas libp2pespecificaciones, por lo que un libp2pcliente creado con Go puede interactuar sin problemas con otro cliente escrito en JavaScript, siempre que sean compatibles en términos de la pila de protocolos elegida. Estos protocolos cubren una amplia gama, desde protocolos básicos de transporte de red hasta protocolos de capa de seguridad y multiplexación.

No profundizaremos demasiado en los detalles de libp2pen esta publicación, pero si está interesado en profundizar, los documentos oficialeslibp2p ofrecen una descripción general muy agradable de los diversos conceptos que encontraremos en el camino.

Como libp2pfunciona

Para ver libp2pen acción, comencemos nuestra aplicación de recetas. Comenzaremos definiendo algunas constantes y tipos que necesitaremos:

const STORAGE_FILE_PATH: &str = "./recipes.json";

type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync + 'static>>;

static KEYS: Lazy<identity::Keypair> = Lazy::new(|| identity::Keypair::generate_ed25519());
static PEER_ID: Lazy<PeerId> = Lazy::new(|| PeerId::from(KEYS.public()));
static TOPIC: Lazy<Topic> = Lazy::new(|| Topic::new("recipes"));

Almacenaremos nuestras recetas locales en un archivo JSON simple llamado recipes.json, que la aplicación esperará que esté en la misma carpeta que el ejecutable. También definimos un tipo de ayuda para Result, que nos permite propagar errores arbitrarios.

Luego, usamos once_cell::Lazy, para inicializar perezosamente algunas cosas. En primer lugar, lo usamos para generar un par de claves y un PeerIdderivado de la clave pública. También creamos un Topic, que es otro concepto clave de libp2p.

¿Qué significa todo esto? En resumen, a PeerIdes simplemente un identificador único para un par específico dentro de toda la red peer to peer. Lo derivamos de un par de claves para garantizar su singularidad. Además, el par de claves nos permite comunicarnos de forma segura con el resto de la red, asegurándonos de que nadie pueda hacerse pasar por nosotros.

A Topic, por otro lado, es un concepto de Floodsub, que es una implementación de libp2pla interfaz pub / sub . A Topices algo a lo que podemos subscribeenviar mensajes, por ejemplo, para escuchar solo un subconjunto del tráfico en una red de publicación / subred.

También necesitaremos algunos tipos para la receta:

type Recipes = Vec<Recipe>;

#[derive(Debug, Serialize, Deserialize)]
struct Recipe {
    id: usize,
    name: String,
    ingredients: String,
    instructions: String,
    public: bool,
}

Y algunos tipos de mensajes que planeamos enviar:

#[derive(Debug, Serialize, Deserialize)]
enum ListMode {
    ALL,
    One(String),
}

#[derive(Debug, Serialize, Deserialize)]
struct ListRequest {
    mode: ListMode,
}

#[derive(Debug, Serialize, Deserialize)]
struct ListResponse {
    mode: ListMode,
    data: Recipes,
    receiver: String,
}

enum EventType {
    Response(ListResponse),
    Input(String),
}

La receta es bastante sencilla. Tiene una identificación, un nombre, algunos ingredientes e instrucciones para ejecutarlo. Además, agregamos una publicbandera para que podamos distinguir qué recetas queremos compartir y cuáles queremos guardar para nosotros.

Como se mencionó al principio, hay dos formas de obtener listas de otros pares: de todos o de uno, que está representado por la ListModeenumeración.

Los tipos ListRequesty ListResponseson solo envoltorios para este tipo y la fecha de envío que los usa.

La EventTypeenumeración distingue entre una respuesta de otro par y una entrada de nosotros mismos. Veremos más adelante por qué esta diferencia es relevante.

Creando un libp2pcliente

Comencemos a escribir la mainfunción para configurar un par dentro de una red peer-to-peer.

#[tokio::main]
async fn main() {
    pretty_env_logger::init();

    info!("Peer Id: {}", PEER_ID.clone());
    let (response_sender, mut response_rcv) = mpsc::unbounded_channel();

    let auth_keys = Keypair::<X25519Spec>::new()
        .into_authentic(&KEYS)
        .expect("can create auth keys");

Inicializamos el registro y creamos un async channelpara comunicarnos entre diferentes partes de la aplicación. Usaremos este canal más adelante para enviar respuestas desde la libp2ppila de red a nuestra aplicación para manejarlas.

Además, creamos algunas claves de autenticación para el protocolo criptográfico Noise , que usaremos para asegurar el tráfico dentro de la red. Para ello, creamos un nuevo par de claves y lo firmamos con nuestras claves de identidad utilizando la into_authenticfunción.

El siguiente paso es importante e involucra algunos conceptos básicos de libp2p: crear un llamado Transport .authenticate (NoiseConfig :: xx (auth keys) .into authenticated ()) // XX Patrón de protocolo de enlace, IX también existe e IK - solo XX actualmente proporciona interoperabilidad con otras impls de libp2p .multiplex (mplex :: MplexConfig :: new ()) .boxed ();).

    let transp = TokioTcpConfig::new()
        .upgrade(upgrade::Version::V1)
        .authenticate(NoiseConfig::xx(auth_keys).into_authenticated())
        .multiplex(mplex::MplexConfig::new())
        .boxed();

Un transporte es un conjunto de protocolos de red que permite la comunicación orientada a la conexión entre pares. También es posible utilizar varios transportes dentro de una aplicación, por ejemplo, TCP / IP y Websockets o UDP al mismo tiempo para diferentes casos de uso.

En este ejemplo, usaremos TCP como base usando el TCP asíncrono de Tokio. Una vez que se haya establecido una conexión TCP, la upgradeusaremos Noisepara una comunicación segura. Un ejemplo basado en la web de esto sería usar TLS sobre HTTP para crear una conexión segura.

Usamos el NoiseConfig:xxpatrón de apretón de manos, que es una de las tres opciones, porque es el único que tiene la garantía de ser interoperable con otras libp2paplicaciones.

Lo bueno libp2pes que podríamos escribir un cliente Rust y otro podría escribir un cliente JavaScript, y aún podrían comunicarse fácilmente siempre que los protocolos estén implementados en ambas versiones de la biblioteca.

Al final, también multiplexamos el transporte, lo que nos permite multiplexar múltiples subflujos, o conexiones, en el mismo transporte.

¡Uf, eso es bastante teoría! Pero todo esto se puede encontrar en los libp2pdocumentos . Esta es solo una de las muchas formas de crear un transporte de igual a igual.

El siguiente concepto es a NetworkBehaviour. Esta es la parte interna libp2pque realmente define la lógica de la red y todos los pares, por ejemplo, qué hacer con los eventos entrantes y qué eventos enviar.

    let mut behaviour = RecipeBehaviour {
        floodsub: Floodsub::new(PEER_ID.clone()),
        mdns: TokioMdns::new().expect("can create mdns"),
        response_sender,
    };

    behaviour.floodsub.subscribe(TOPIC.clone());

En este caso, como se mencionó anteriormente, usaremos el FloodSubprotocolo para lidiar con eventos. También usaremos mDNS, que es un protocolo para descubrir otros pares en la red local. También colocaremos la senderparte de nuestro canal aquí para que podamos usarlo para propagar eventos a la parte principal de la aplicación.

El FloodSubtema que creamos anteriormente ahora se suscribe desde nuestro comportamiento, lo que significa que recibiremos eventos y podremos enviar eventos sobre ese tema.

Casi hemos terminado con la libp2pconfiguración. El último concepto que necesitamos es el Swarm.

    let mut swarm = SwarmBuilder::new(transp, behaviour, PEER_ID.clone())
        .executor(Box::new(|fut| {
            tokio::spawn(fut);
        }))
        .build();

A Swarmgestiona las conexiones creadas mediante el transporte y ejecuta el comportamiento de la red que creamos, activando y recibiendo eventos y dándonos una forma de llegar a ellos desde el exterior.

Creamos el Swarmcon nuestro transporte, comportamiento e identificación de pares. La executorparte simplemente le dice Swarmque use el Tokiotiempo de ejecución para ejecutarse internamente, pero también podríamos usar otros tiempos de ejecución asíncronos aquí.

Lo único que queda por hacer es iniciar nuestro Swarm:

    Swarm::listen_on(
        &mut swarm,
        "/ip4/0.0.0.0/tcp/0"
            .parse()
            .expect("can get a local socket"),
    )
    .expect("swarm can be started");

Similar a iniciar, por ejemplo, un servidor TCP, simplemente llamamos listen_oncon una IP local, dejando que el sistema operativo decida el puerto por nosotros. Esto comenzará Swarmcon toda nuestra configuración, pero aún no hemos definido ninguna lógica.

Comencemos con el manejo de la entrada del usuario.

Manejo de entrada en libp2p

Para la entrada del usuario, simplemente confiaremos en el viejo STDIN. Entonces, antes de la Swarm::listen_onllamada, agregaremos:

    let mut stdin = tokio::io::BufReader::new(tokio::io::stdin()).lines();

Esto definió un lector asíncrono en STDIN, que lee la secuencia línea por línea. Entonces, si presionamos enter, habrá un nuevo mensaje entrante.

La siguiente parte es crear nuestro bucle de eventos, que escuchará eventos de STDIN, de Swarmy de nuestro canal de respuesta definido anteriormente.

    loop {
        let evt = {
            tokio::select! {
                line = stdin.next_line() => Some(EventType::Input(line.expect("can get line").expect("can read line from stdin"))),
                event = swarm.next() => {
                    info!("Unhandled Swarm Event: {:?}", event);
                    None
                },
                response = response_rcv.recv() => Some(EventType::Response(response.expect("response exists"))),
            }
        };
        ...
    }
}

Usamos la selectmacro de Tokio para esperar varios procesos asíncronos, manejando el primero que finaliza. No hacemos nada con los Swarmeventos; estos se manejan dentro de nuestro RecipeBehaviour, que veremos más adelante, pero aún necesitamos llamar swarm.next()para impulsar el Swarmavance.

Agreguemos algo de lógica de manejo de eventos en lugar de :

        if let Some(event) = evt {
            match event {
                EventType::Response(resp) => {
                   ...
                }
                EventType::Input(line) => match line.as_str() {
                    "ls p" => handle_list_peers(&mut swarm).await,
                    cmd if cmd.starts_with("ls r") => handle_list_recipes(cmd, &mut swarm).await,
                    cmd if cmd.starts_with("create r") => handle_create_recipe(cmd).await,
                    cmd if cmd.starts_with("publish r") => handle_publish_recipe(cmd).await,
                    _ => error!("unknown command"),
                },
            }
        }

Si hay un evento, lo emparejamos y vemos si es Responseun Inputevento o un evento. Veamos los Inputeventos solo por ahora.

Hay un par de opciones. Apoyamos los siguientes comandos:

  • ls p enumera todos los compañeros conocidos
  • ls r enumera recetas locales
  • ls r {peerId} enumera las recetas publicadas de un determinado compañero
  • ls r all enumera recetas publicadas de todos los compañeros conocidos
  • publish r {recipeId} publica una receta determinada
  • create r {recipeName}|{recipeIngredients}|{recipeInstructions crea una nueva receta con los datos dados y un ID creciente

Enumerar todas las recetas de los compañeros, en este caso, significa enviar una solicitud de recetas a nuestros compañeros, esperar a que respondan y mostrar los resultados. En una red peer-to-peer, esto puede llevar un tiempo, ya que algunos pares pueden estar en el otro lado del planeta y no sabemos si todos nos responderán. Esto es bastante diferente a enviar una solicitud a un servidor HTTP, por ejemplo.

Veamos la lógica para enumerar pares primero:

async fn handle_list_peers(swarm: &mut Swarm<RecipeBehaviour>) {
    info!("Discovered Peers:");
    let nodes = swarm.mdns.discovered_nodes();
    let mut unique_peers = HashSet::new();
    for peer in nodes {
        unique_peers.insert(peer);
    }
    unique_peers.iter().for_each(|p| info!("{}", p));
}

En este caso, podemos usar mDNSpara darnos todos los nodos descubiertos, iterarlos y mostrarlos. Fácil.

A continuación, veamos cómo crear y publicar recetas, antes de abordar los comandos de lista:

async fn handle_create_recipe(cmd: &str) {
    if let Some(rest) = cmd.strip_prefix("create r") {
        let elements: Vec<&str> = rest.split("|").collect();
        if elements.len() < 3 {
            info!("too few arguments - Format: name|ingredients|instructions");
        } else {
            let name = elements.get(0).expect("name is there");
            let ingredients = elements.get(1).expect("ingredients is there");
            let instructions = elements.get(2).expect("instructions is there");
            if let Err(e) = create_new_recipe(name, ingredients, instructions).await {
                error!("error creating recipe: {}", e);
            };
        }
    }
}

async fn handle_publish_recipe(cmd: &str) {
    if let Some(rest) = cmd.strip_prefix("publish r") {
        match rest.trim().parse::<usize>() {
            Ok(id) => {
                if let Err(e) = publish_recipe(id).await {
                    info!("error publishing recipe with id {}, {}", id, e)
                } else {
                    info!("Published Recipe with id: {}", id);
                }
            }
            Err(e) => error!("invalid id: {}, {}", rest.trim(), e),
        };
    }
}

En ambos casos, necesitamos analizar la cadena para obtener los |datos separados, o el ID de receta dado en el caso de publishregistrar un error si la entrada dada no es válida.

En el createcaso, llamamos a la create_new_recipefunción auxiliar con los datos dados. Veamos todas las funciones auxiliares que necesitaremos para interactuar con nuestro almacenamiento JSON local simple para recetas:

async fn create_new_recipe(name: &str, ingredients: &str, instructions: &str) -> Result<()> {
    let mut local_recipes = read_local_recipes().await?;
    let new_id = match local_recipes.iter().max_by_key(|r| r.id) {
        Some(v) => v.id + 1,
        None => 0,
    };
    local_recipes.push(Recipe {
        id: new_id,
        name: name.to_owned(),
        ingredients: ingredients.to_owned(),
        instructions: instructions.to_owned(),
        public: false,
    });
    write_local_recipes(&local_recipes).await?;

    info!("Created recipe:");
    info!("Name: {}", name);
    info!("Ingredients: {}", ingredients);
    info!("Instructions:: {}", instructions);

    Ok(())
}

async fn publish_recipe(id: usize) -> Result<()> {
    let mut local_recipes = read_local_recipes().await?;
    local_recipes
        .iter_mut()
        .filter(|r| r.id == id)
        .for_each(|r| r.public = true);
    write_local_recipes(&local_recipes).await?;
    Ok(())
}

async fn read_local_recipes() -> Result<Recipes> {
    let content = fs::read(STORAGE_FILE_PATH).await?;
    let result = serde_json::from_slice(&content)?;
    Ok(result)
}

async fn write_local_recipes(recipes: &Recipes) -> Result<()> {
    let json = serde_json::to_string(&recipes)?;
    fs::write(STORAGE_FILE_PATH, &json).await?;
    Ok(())
}

Los bloques de construcción más básicos son read_local_recipesy write_local_recipes, que simplemente leen y deserializan o serializan y escriben recetas desde o hacia el archivo de almacenamiento.

El publish_recipeayudante obtiene todas las recetas del archivo, busca la receta con el ID proporcionado y establece su publicindicador en verdadero.

Al crear una receta, también obtenemos todas las recetas del archivo, agregamos una nueva receta al final y escribimos todos los datos, anulando el archivo. Esto no es súper eficiente, pero es simple y funciona.

Enviar mensajes con libp2p

Veamos los listcomandos a continuación y exploremos cómo podemos enviar mensajes a otros pares.

En el listcomando, hay tres casos posibles:

async fn handle_list_recipes(cmd: &str, swarm: &mut Swarm<RecipeBehaviour>) {
    let rest = cmd.strip_prefix("ls r ");
    match rest {
        Some("all") => {
            let req = ListRequest {
                mode: ListMode::ALL,
            };
            let json = serde_json::to_string(&req).expect("can jsonify request");
            swarm.floodsub.publish(TOPIC.clone(), json.as_bytes());
        }
        Some(recipes_peer_id) => {
            let req = ListRequest {
                mode: ListMode::One(recipes_peer_id.to_owned()),
            };
            let json = serde_json::to_string(&req).expect("can jsonify request");
            swarm.floodsub.publish(TOPIC.clone(), json.as_bytes());
        }
        None => {
            match read_local_recipes().await {
                Ok(v) => {
                    info!("Local Recipes ({})", v.len());
                    v.iter().for_each(|r| info!("{:?}", r));
                }
                Err(e) => error!("error fetching local recipes: {}", e),
            };
        }
    };
}

Analizamos el comando entrante, quitamos la ls rpieza y comprobamos lo que queda. Si no hay nada más en el comando, simplemente podemos buscar nuestras recetas locales e imprimirlas usando los ayudantes definidos en la sección anterior.

Si encontramos la allpalabra clave, creamos un ListRequestcon el ListMode::ALLconjunto, lo serializamos a JSON y, usando la FloodSubinstancia dentro de nuestro Swarm, lo publicamos en el mencionado anteriormente Topic.

Lo mismo sucede si encontramos una ID de peer en el comando, en cuyo caso simplemente enviaremos el ListMode::Onemodo con esa ID de peer. Podríamos comprobar si es una identificación de par válida, o incluso si es una identificación de par que hemos descubierto, pero hagámoslo simple: si no hay nadie que lo escuche, no pasa nada.

Eso es todo lo que necesitamos hacer para enviar mensajes a la red. Ahora la pregunta es, ¿qué pasa con esos mensajes? ¿Dónde se manejan?

En el caso de una aplicación peer-to-peer, recuerde que somos el Sendery Receiverde los eventos, por lo que debemos ocuparnos de los eventos entrantes y salientes en nuestra implementación.

Responder a mensajes con libp2p

Esta es finalmente la parte en la que RecipeBehaviourentra nuestro . Vamos a definirlo:

#[derive(NetworkBehaviour)]
struct RecipeBehaviour {
    floodsub: Floodsub,
    mdns: TokioMdns,
    #[behaviour(ignore)]
    response_sender: mpsc::UnboundedSender<ListResponse>,
}

El comportamiento en sí es simplemente una estructura, pero usamos libp2pla NetworkBehaviourmacro de derivación, por lo que no tenemos que implementar manualmente todas las funciones de rasgo nosotros mismos.

Esta macro de derivación implementa las NetworkBehaviour funciones del rasgo para todos los miembros de la estructura, que no están anotados behaviour(ignore). Nuestro canal se ignora aquí porque no tiene nada que ver directamente con nuestro comportamiento.

Lo que queda es implementar la inject_eventfunción para ambos FloodsubEventy MdnsEvent.

Empecemos por mDNS:

impl NetworkBehaviourEventProcess<MdnsEvent> for RecipeBehaviour {
    fn inject_event(&mut self, event: MdnsEvent) {
        match event {
            MdnsEvent::Discovered(discovered_list) => {
                for (peer, _addr) in discovered_list {
                    self.floodsub.add_node_to_partial_view(peer);
                }
            }
            MdnsEvent::Expired(expired_list) => {
                for (peer, _addr) in expired_list {
                    if !self.mdns.has_node(&peer) {
                        self.floodsub.remove_node_from_partial_view(&peer);
                    }
                }
            }
        }
    }
}

La inject_eventfunción se llama cuando entra un evento para este controlador. Por mDNSotro lado, solo hay dos eventos, Discoveredy Expired, que se activan cuando vemos un nuevo par en la red o cuando un par existente desaparece. En ambos casos, lo agregamos o lo eliminamos de nuestra FloodSub"vista parcial", que es una lista de nodos para propagar nuestros mensajes.

El inject_eventpara eventos pub / sub es un poco más complejo. Necesitamos reaccionar tanto en las cargas entrantes ListRequestcomo en las ListResponsecargas útiles. Si enviamos un ListRequest, el par que recibe la solicitud buscará sus recetas locales publicadas y luego necesitará una forma de devolverlas.

La única forma de devolverlos al par solicitante es publicarlos en la red. Dado que pub / sub es de hecho el único mecanismo que tenemos, debemos reaccionar tanto a las solicitudes entrantes como a las respuestas entrantes.

Veamos cómo funciona esto:

impl NetworkBehaviourEventProcess<FloodsubEvent> for RecipeBehaviour {
    fn inject_event(&mut self, event: FloodsubEvent) {
        match event {
            FloodsubEvent::Message(msg) => {
                if let Ok(resp) = serde_json::from_slice::<ListResponse>(&msg.data) {
                    if resp.receiver == PEER_ID.to_string() {
                        info!("Response from {}:", msg.source);
                        resp.data.iter().for_each(|r| info!("{:?}", r));
                    }
                } else if let Ok(req) = serde_json::from_slice::<ListRequest>(&msg.data) {
                    match req.mode {
                        ListMode::ALL => {
                            info!("Received ALL req: {:?} from {:?}", req, msg.source);
                            respond_with_public_recipes(
                                self.response_sender.clone(),
                                msg.source.to_string(),
                            );
                        }
                        ListMode::One(ref peer_id) => {
                            if peer_id == &PEER_ID.to_string() {
                                info!("Received req: {:?} from {:?}", req, msg.source);
                                respond_with_public_recipes(
                                    self.response_sender.clone(),
                                    msg.source.to_string(),
                                );
                            }
                        }
                    }
                }
            }
            _ => (),
        }
    }
}

Coincidimos con el mensaje entrante, tratando de deserializarlo con una solicitud o respuesta. En el caso de una respuesta, simplemente imprimimos la respuesta con la identificación del par de la persona que llama, que usamos msg.source. Cuando recibimos una solicitud entrante, debemos diferenciar entre los casos ALLy One.

En el Onecaso, verificamos si el ID de par proporcionado es el mismo que el nuestro, que la solicitud realmente está destinada a nosotros. Si es así, devolvemos nuestras recetas publicadas, que también es nuestra respuesta en el caso de ALL.

En ambos casos, llamamos al respond_with_public_recipesayudante:

fn respond_with_public_recipes(sender: mpsc::UnboundedSender<ListResponse>, receiver: String) {
    tokio::spawn(async move {
        match read_local_recipes().await {
            Ok(recipes) => {
                let resp = ListResponse {
                    mode: ListMode::ALL,
                    receiver,
                    data: recipes.into_iter().filter(|r| r.public).collect(),
                };
                if let Err(e) = sender.send(resp) {
                    error!("error sending response via channel, {}", e);
                }
            }
            Err(e) => error!("error fetching local recipes to answer ALL request, {}", e),
        }
    });
}

En este método de ayuda, usamos el spawn de Tokio para ejecutar de forma asincrónica un futuro, que lee todas las recetas locales, crea un archivo a ListResponsepartir de los datos y envía estos datos channel_sendera nuestro bucle de eventos, donde lo manejamos así:

                EventType::Response(resp) => {
                    let json = serde_json::to_string(&resp).expect("can jsonify response");
                    swarm.floodsub.publish(TOPIC.clone(), json.as_bytes());
                }

Si notamos un Responseevento enviado "internamente" , lo serializamos en JSON y lo enviamos a la red.

Probando con libp2p

Eso es todo por la implementación. Ahora probémoslo.

Para comprobar que nuestra implementación funciona, iniciemos la aplicación en varios terminales usando este comando:

RUST_LOG=info cargo run

Tenga en cuenta que la aplicación espera un archivo llamado recipes.jsonen el directorio desde el que lo está iniciando.

Cuando se inició la aplicación, obtenemos el siguiente registro, imprimiendo nuestro ID de par:

INFO  rust_peer_to_peer_example > Peer Id: 12D3KooWDc1FDabQzpntvZRWeDZUL351gJRy3F4E8VN5Gx2pBCU2

Ahora debemos presionar enter para iniciar el ciclo de eventos.

Al ingresar ls p, obtenemos una lista de nuestros compañeros descubiertos:

ls p
 INFO  rust_peer_to_peer_example > Discovered Peers:
 INFO  rust_peer_to_peer_example > 12D3KooWCK6X7mFk9HeWw69WF1ueWa3XmphZ2Mu7ZHvEECj5rrhG
 INFO  rust_peer_to_peer_example > 12D3KooWLGN85pv5XTDALGX5M6tRgQtUGMWXWasWQD6oJjMcEENA

Con ls r, obtenemos las recetas locales:

ls r
 INFO  rust_peer_to_peer_example > Local Recipes (3)
 INFO  rust_peer_to_peer_example > Recipe { id: 0, name: " Coffee", ingredients: "Coffee", instructions: "Make Coffee", public: true }
 INFO  rust_peer_to_peer_example > Recipe { id: 1, name: " Tea", ingredients: "Tea, Water", instructions: "Boil Water, add tea", public: false }
 INFO  rust_peer_to_peer_example > Recipe { id: 2, name: " Carrot Cake", ingredients: "Carrots, Cake", instructions: "Make Carrot Cake", public: true }

Llamar ls r alldesencadena el envío de una solicitud a los otros pares y devuelve sus recetas:

ls r all
 INFO  rust_peer_to_peer_example > Response from 12D3KooWCK6X7mFk9HeWw69WF1ueWa3XmphZ2Mu7ZHvEECj5rrhG:
 INFO  rust_peer_to_peer_example > Recipe { id: 0, name: " Coffee", ingredients: "Coffee", instructions: "Make Coffee", public: true }
 INFO  rust_peer_to_peer_example > Recipe { id: 2, name: " Carrot Cake", ingredients: "Carrots, Cake", instructions: "Make Carrot Cake", public: true }

Lo mismo ocurre si usamos ls rcon un peer ID:

ls r 12D3KooWCK6X7mFk9HeWw69WF1ueWa3XmphZ2Mu7ZHvEECj5rrhG
 INFO  rust_peer_to_peer_example > Response from 12D3KooWCK6X7mFk9HeWw69WF1ueWa3XmphZ2Mu7ZHvEECj5rrhG:
 INFO  rust_peer_to_peer_example > Recipe { id: 0, name: " Coffee", ingredients: "Coffee", instructions: "Make Coffee", public: true }
 INFO  rust_peer_to_peer_example > Recipe { id: 2, name: " Carrot Cake", ingredients: "Carrots, Cake", instructions: "Make Carrot Cake", public: true }

¡Funciona! También puede probar esto con una gran cantidad de clientes en la misma red.

Puede encontrar el código de ejemplo completo en GitHub .

Conclusión

En esta publicación, cubrimos cómo construir una pequeña aplicación de red descentralizada usando Rust y libp2p.

Si viene de un entorno web, muchos de los conceptos de redes le resultarán algo familiares, pero la creación de una aplicación de igual a igual exige un enfoque fundamentalmente diferente para el diseño y la construcción.

La libp2pbiblioteca es bastante madura y, debido a la popularidad de Rust dentro de la escena criptográfica, existe un ecosistema rico y emergente de bibliotecas para construir aplicaciones descentralizadas poderosas.

Fuente del artículo original en https://blog.logrocket.com

#rust

What is GEEK

Buddha Community

Cómo construir una aplicación simple de igual a igual usando Rust

Cómo construir una aplicación simple de igual a igual usando Rust

En este tutorial, le mostraremos cómo construir una aplicación peer-to-peer muy simple usando Rust y la fantástica biblioteca libp2p.

En los últimos años, debido en gran parte a la exageración que rodea a la cadena de bloques y las criptomonedas, las aplicaciones descentralizadas han ganado bastante impulso. Otro factor detrás del creciente interés en la descentralización es una mayor conciencia sobre las desventajas de poner la mayor parte de la web en manos de un pequeño grupo de empresas en términos de privacidad de datos y monopolización.

En cualquier caso, recientemente ha habido algunos desarrollos muy interesantes en la escena del software descentralizado, incluso aparte de toda la tecnología de criptografía y blockchain.

Los ejemplos notables incluyen IPFS ; la nueva plataforma de codificación distribuida Radicle ; la red social descentralizada Scuttlebutt ; y muchas más aplicaciones dentro del Fediverse , como Mastodon .

En este tutorial, le mostraremos cómo construir una aplicación peer-to-peer muy simple usando Rust y la fantástica libp2pbiblioteca, que existe en diferentes etapas de madurez para una amplia gama de idiomas.

Vamos a crear una aplicación de recetas de cocina con una sencilla interfaz de línea de comandos que nos permite:

  • Crea recetas
  • Publica recetas
  • Lista de recetas locales
  • Enumere otros pares que descubrimos en la red
  • Enumere las recetas publicadas de un par determinado
  • Enumere todas las recetas de todos los compañeros que conocemos

Haremos todo esto en alrededor de 300 líneas de Rust. ¡Empecemos!

Instalación de Rust

Para seguir, todo lo que necesita es una instalación reciente de Rust (1.47+).

Primero, cree un nuevo proyecto de Rust:

cargo new rust-p2p-example
cd rust-p2p-example

A continuación, edite el Cargo.tomlarchivo y agregue las dependencias que necesitará:

 

[dependencies]
libp2p = { version = "0.31", features = ["tcp-tokio", "mdns-tokio"] }
tokio = { version = "0.3", features = ["io-util", "io-std", "stream", "macros", "rt", "rt-multi-thread", "fs", "time", "sync"] }
serde = {version = "=1.0", features = ["derive"] }
serde_json = "1.0"
once_cell = "1.5"
log = "0.4"
pretty_env_logger = "0.4"

Como se mencionó anteriormente, usaremos libp2ppara la parte de redes peer-to-peer. Más específicamente, lo usaremos en conjunto con el tiempo de ejecución asíncrono de Tokio. Usaremos Serde para la serialización y deserialización de JSON y un par de bibliotecas auxiliares para registrar e inicializar el estado.

¿Qué es libp2p?

libp2p es un conjunto de protocolos para crear aplicaciones peer-to-peer que se centra en la modularidad.

Hay implementaciones de bibliotecas para varios lenguajes, como JavaScript, Go y Rust. Todas estas bibliotecas implementan las mismas libp2pespecificaciones, por lo que un libp2pcliente creado con Go puede interactuar sin problemas con otro cliente escrito en JavaScript, siempre que sean compatibles en términos de la pila de protocolos elegida. Estos protocolos cubren una amplia gama, desde protocolos básicos de transporte de red hasta protocolos de capa de seguridad y multiplexación.

No profundizaremos demasiado en los detalles de libp2pen esta publicación, pero si está interesado en profundizar, los documentos oficialeslibp2p ofrecen una descripción general muy agradable de los diversos conceptos que encontraremos en el camino.

Como libp2pfunciona

Para ver libp2pen acción, comencemos nuestra aplicación de recetas. Comenzaremos definiendo algunas constantes y tipos que necesitaremos:

const STORAGE_FILE_PATH: &str = "./recipes.json";

type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync + 'static>>;

static KEYS: Lazy<identity::Keypair> = Lazy::new(|| identity::Keypair::generate_ed25519());
static PEER_ID: Lazy<PeerId> = Lazy::new(|| PeerId::from(KEYS.public()));
static TOPIC: Lazy<Topic> = Lazy::new(|| Topic::new("recipes"));

Almacenaremos nuestras recetas locales en un archivo JSON simple llamado recipes.json, que la aplicación esperará que esté en la misma carpeta que el ejecutable. También definimos un tipo de ayuda para Result, que nos permite propagar errores arbitrarios.

Luego, usamos once_cell::Lazy, para inicializar perezosamente algunas cosas. En primer lugar, lo usamos para generar un par de claves y un PeerIdderivado de la clave pública. También creamos un Topic, que es otro concepto clave de libp2p.

¿Qué significa todo esto? En resumen, a PeerIdes simplemente un identificador único para un par específico dentro de toda la red peer to peer. Lo derivamos de un par de claves para garantizar su singularidad. Además, el par de claves nos permite comunicarnos de forma segura con el resto de la red, asegurándonos de que nadie pueda hacerse pasar por nosotros.

A Topic, por otro lado, es un concepto de Floodsub, que es una implementación de libp2pla interfaz pub / sub . A Topices algo a lo que podemos subscribeenviar mensajes, por ejemplo, para escuchar solo un subconjunto del tráfico en una red de publicación / subred.

También necesitaremos algunos tipos para la receta:

type Recipes = Vec<Recipe>;

#[derive(Debug, Serialize, Deserialize)]
struct Recipe {
    id: usize,
    name: String,
    ingredients: String,
    instructions: String,
    public: bool,
}

Y algunos tipos de mensajes que planeamos enviar:

#[derive(Debug, Serialize, Deserialize)]
enum ListMode {
    ALL,
    One(String),
}

#[derive(Debug, Serialize, Deserialize)]
struct ListRequest {
    mode: ListMode,
}

#[derive(Debug, Serialize, Deserialize)]
struct ListResponse {
    mode: ListMode,
    data: Recipes,
    receiver: String,
}

enum EventType {
    Response(ListResponse),
    Input(String),
}

La receta es bastante sencilla. Tiene una identificación, un nombre, algunos ingredientes e instrucciones para ejecutarlo. Además, agregamos una publicbandera para que podamos distinguir qué recetas queremos compartir y cuáles queremos guardar para nosotros.

Como se mencionó al principio, hay dos formas de obtener listas de otros pares: de todos o de uno, que está representado por la ListModeenumeración.

Los tipos ListRequesty ListResponseson solo envoltorios para este tipo y la fecha de envío que los usa.

La EventTypeenumeración distingue entre una respuesta de otro par y una entrada de nosotros mismos. Veremos más adelante por qué esta diferencia es relevante.

Creando un libp2pcliente

Comencemos a escribir la mainfunción para configurar un par dentro de una red peer-to-peer.

#[tokio::main]
async fn main() {
    pretty_env_logger::init();

    info!("Peer Id: {}", PEER_ID.clone());
    let (response_sender, mut response_rcv) = mpsc::unbounded_channel();

    let auth_keys = Keypair::<X25519Spec>::new()
        .into_authentic(&KEYS)
        .expect("can create auth keys");

Inicializamos el registro y creamos un async channelpara comunicarnos entre diferentes partes de la aplicación. Usaremos este canal más adelante para enviar respuestas desde la libp2ppila de red a nuestra aplicación para manejarlas.

Además, creamos algunas claves de autenticación para el protocolo criptográfico Noise , que usaremos para asegurar el tráfico dentro de la red. Para ello, creamos un nuevo par de claves y lo firmamos con nuestras claves de identidad utilizando la into_authenticfunción.

El siguiente paso es importante e involucra algunos conceptos básicos de libp2p: crear un llamado Transport .authenticate (NoiseConfig :: xx (auth keys) .into authenticated ()) // XX Patrón de protocolo de enlace, IX también existe e IK - solo XX actualmente proporciona interoperabilidad con otras impls de libp2p .multiplex (mplex :: MplexConfig :: new ()) .boxed ();).

    let transp = TokioTcpConfig::new()
        .upgrade(upgrade::Version::V1)
        .authenticate(NoiseConfig::xx(auth_keys).into_authenticated())
        .multiplex(mplex::MplexConfig::new())
        .boxed();

Un transporte es un conjunto de protocolos de red que permite la comunicación orientada a la conexión entre pares. También es posible utilizar varios transportes dentro de una aplicación, por ejemplo, TCP / IP y Websockets o UDP al mismo tiempo para diferentes casos de uso.

En este ejemplo, usaremos TCP como base usando el TCP asíncrono de Tokio. Una vez que se haya establecido una conexión TCP, la upgradeusaremos Noisepara una comunicación segura. Un ejemplo basado en la web de esto sería usar TLS sobre HTTP para crear una conexión segura.

Usamos el NoiseConfig:xxpatrón de apretón de manos, que es una de las tres opciones, porque es el único que tiene la garantía de ser interoperable con otras libp2paplicaciones.

Lo bueno libp2pes que podríamos escribir un cliente Rust y otro podría escribir un cliente JavaScript, y aún podrían comunicarse fácilmente siempre que los protocolos estén implementados en ambas versiones de la biblioteca.

Al final, también multiplexamos el transporte, lo que nos permite multiplexar múltiples subflujos, o conexiones, en el mismo transporte.

¡Uf, eso es bastante teoría! Pero todo esto se puede encontrar en los libp2pdocumentos . Esta es solo una de las muchas formas de crear un transporte de igual a igual.

El siguiente concepto es a NetworkBehaviour. Esta es la parte interna libp2pque realmente define la lógica de la red y todos los pares, por ejemplo, qué hacer con los eventos entrantes y qué eventos enviar.

    let mut behaviour = RecipeBehaviour {
        floodsub: Floodsub::new(PEER_ID.clone()),
        mdns: TokioMdns::new().expect("can create mdns"),
        response_sender,
    };

    behaviour.floodsub.subscribe(TOPIC.clone());

En este caso, como se mencionó anteriormente, usaremos el FloodSubprotocolo para lidiar con eventos. También usaremos mDNS, que es un protocolo para descubrir otros pares en la red local. También colocaremos la senderparte de nuestro canal aquí para que podamos usarlo para propagar eventos a la parte principal de la aplicación.

El FloodSubtema que creamos anteriormente ahora se suscribe desde nuestro comportamiento, lo que significa que recibiremos eventos y podremos enviar eventos sobre ese tema.

Casi hemos terminado con la libp2pconfiguración. El último concepto que necesitamos es el Swarm.

    let mut swarm = SwarmBuilder::new(transp, behaviour, PEER_ID.clone())
        .executor(Box::new(|fut| {
            tokio::spawn(fut);
        }))
        .build();

A Swarmgestiona las conexiones creadas mediante el transporte y ejecuta el comportamiento de la red que creamos, activando y recibiendo eventos y dándonos una forma de llegar a ellos desde el exterior.

Creamos el Swarmcon nuestro transporte, comportamiento e identificación de pares. La executorparte simplemente le dice Swarmque use el Tokiotiempo de ejecución para ejecutarse internamente, pero también podríamos usar otros tiempos de ejecución asíncronos aquí.

Lo único que queda por hacer es iniciar nuestro Swarm:

    Swarm::listen_on(
        &mut swarm,
        "/ip4/0.0.0.0/tcp/0"
            .parse()
            .expect("can get a local socket"),
    )
    .expect("swarm can be started");

Similar a iniciar, por ejemplo, un servidor TCP, simplemente llamamos listen_oncon una IP local, dejando que el sistema operativo decida el puerto por nosotros. Esto comenzará Swarmcon toda nuestra configuración, pero aún no hemos definido ninguna lógica.

Comencemos con el manejo de la entrada del usuario.

Manejo de entrada en libp2p

Para la entrada del usuario, simplemente confiaremos en el viejo STDIN. Entonces, antes de la Swarm::listen_onllamada, agregaremos:

    let mut stdin = tokio::io::BufReader::new(tokio::io::stdin()).lines();

Esto definió un lector asíncrono en STDIN, que lee la secuencia línea por línea. Entonces, si presionamos enter, habrá un nuevo mensaje entrante.

La siguiente parte es crear nuestro bucle de eventos, que escuchará eventos de STDIN, de Swarmy de nuestro canal de respuesta definido anteriormente.

    loop {
        let evt = {
            tokio::select! {
                line = stdin.next_line() => Some(EventType::Input(line.expect("can get line").expect("can read line from stdin"))),
                event = swarm.next() => {
                    info!("Unhandled Swarm Event: {:?}", event);
                    None
                },
                response = response_rcv.recv() => Some(EventType::Response(response.expect("response exists"))),
            }
        };
        ...
    }
}

Usamos la selectmacro de Tokio para esperar varios procesos asíncronos, manejando el primero que finaliza. No hacemos nada con los Swarmeventos; estos se manejan dentro de nuestro RecipeBehaviour, que veremos más adelante, pero aún necesitamos llamar swarm.next()para impulsar el Swarmavance.

Agreguemos algo de lógica de manejo de eventos en lugar de :

        if let Some(event) = evt {
            match event {
                EventType::Response(resp) => {
                   ...
                }
                EventType::Input(line) => match line.as_str() {
                    "ls p" => handle_list_peers(&mut swarm).await,
                    cmd if cmd.starts_with("ls r") => handle_list_recipes(cmd, &mut swarm).await,
                    cmd if cmd.starts_with("create r") => handle_create_recipe(cmd).await,
                    cmd if cmd.starts_with("publish r") => handle_publish_recipe(cmd).await,
                    _ => error!("unknown command"),
                },
            }
        }

Si hay un evento, lo emparejamos y vemos si es Responseun Inputevento o un evento. Veamos los Inputeventos solo por ahora.

Hay un par de opciones. Apoyamos los siguientes comandos:

  • ls p enumera todos los compañeros conocidos
  • ls r enumera recetas locales
  • ls r {peerId} enumera las recetas publicadas de un determinado compañero
  • ls r all enumera recetas publicadas de todos los compañeros conocidos
  • publish r {recipeId} publica una receta determinada
  • create r {recipeName}|{recipeIngredients}|{recipeInstructions crea una nueva receta con los datos dados y un ID creciente

Enumerar todas las recetas de los compañeros, en este caso, significa enviar una solicitud de recetas a nuestros compañeros, esperar a que respondan y mostrar los resultados. En una red peer-to-peer, esto puede llevar un tiempo, ya que algunos pares pueden estar en el otro lado del planeta y no sabemos si todos nos responderán. Esto es bastante diferente a enviar una solicitud a un servidor HTTP, por ejemplo.

Veamos la lógica para enumerar pares primero:

async fn handle_list_peers(swarm: &mut Swarm<RecipeBehaviour>) {
    info!("Discovered Peers:");
    let nodes = swarm.mdns.discovered_nodes();
    let mut unique_peers = HashSet::new();
    for peer in nodes {
        unique_peers.insert(peer);
    }
    unique_peers.iter().for_each(|p| info!("{}", p));
}

En este caso, podemos usar mDNSpara darnos todos los nodos descubiertos, iterarlos y mostrarlos. Fácil.

A continuación, veamos cómo crear y publicar recetas, antes de abordar los comandos de lista:

async fn handle_create_recipe(cmd: &str) {
    if let Some(rest) = cmd.strip_prefix("create r") {
        let elements: Vec<&str> = rest.split("|").collect();
        if elements.len() < 3 {
            info!("too few arguments - Format: name|ingredients|instructions");
        } else {
            let name = elements.get(0).expect("name is there");
            let ingredients = elements.get(1).expect("ingredients is there");
            let instructions = elements.get(2).expect("instructions is there");
            if let Err(e) = create_new_recipe(name, ingredients, instructions).await {
                error!("error creating recipe: {}", e);
            };
        }
    }
}

async fn handle_publish_recipe(cmd: &str) {
    if let Some(rest) = cmd.strip_prefix("publish r") {
        match rest.trim().parse::<usize>() {
            Ok(id) => {
                if let Err(e) = publish_recipe(id).await {
                    info!("error publishing recipe with id {}, {}", id, e)
                } else {
                    info!("Published Recipe with id: {}", id);
                }
            }
            Err(e) => error!("invalid id: {}, {}", rest.trim(), e),
        };
    }
}

En ambos casos, necesitamos analizar la cadena para obtener los |datos separados, o el ID de receta dado en el caso de publishregistrar un error si la entrada dada no es válida.

En el createcaso, llamamos a la create_new_recipefunción auxiliar con los datos dados. Veamos todas las funciones auxiliares que necesitaremos para interactuar con nuestro almacenamiento JSON local simple para recetas:

async fn create_new_recipe(name: &str, ingredients: &str, instructions: &str) -> Result<()> {
    let mut local_recipes = read_local_recipes().await?;
    let new_id = match local_recipes.iter().max_by_key(|r| r.id) {
        Some(v) => v.id + 1,
        None => 0,
    };
    local_recipes.push(Recipe {
        id: new_id,
        name: name.to_owned(),
        ingredients: ingredients.to_owned(),
        instructions: instructions.to_owned(),
        public: false,
    });
    write_local_recipes(&local_recipes).await?;

    info!("Created recipe:");
    info!("Name: {}", name);
    info!("Ingredients: {}", ingredients);
    info!("Instructions:: {}", instructions);

    Ok(())
}

async fn publish_recipe(id: usize) -> Result<()> {
    let mut local_recipes = read_local_recipes().await?;
    local_recipes
        .iter_mut()
        .filter(|r| r.id == id)
        .for_each(|r| r.public = true);
    write_local_recipes(&local_recipes).await?;
    Ok(())
}

async fn read_local_recipes() -> Result<Recipes> {
    let content = fs::read(STORAGE_FILE_PATH).await?;
    let result = serde_json::from_slice(&content)?;
    Ok(result)
}

async fn write_local_recipes(recipes: &Recipes) -> Result<()> {
    let json = serde_json::to_string(&recipes)?;
    fs::write(STORAGE_FILE_PATH, &json).await?;
    Ok(())
}

Los bloques de construcción más básicos son read_local_recipesy write_local_recipes, que simplemente leen y deserializan o serializan y escriben recetas desde o hacia el archivo de almacenamiento.

El publish_recipeayudante obtiene todas las recetas del archivo, busca la receta con el ID proporcionado y establece su publicindicador en verdadero.

Al crear una receta, también obtenemos todas las recetas del archivo, agregamos una nueva receta al final y escribimos todos los datos, anulando el archivo. Esto no es súper eficiente, pero es simple y funciona.

Enviar mensajes con libp2p

Veamos los listcomandos a continuación y exploremos cómo podemos enviar mensajes a otros pares.

En el listcomando, hay tres casos posibles:

async fn handle_list_recipes(cmd: &str, swarm: &mut Swarm<RecipeBehaviour>) {
    let rest = cmd.strip_prefix("ls r ");
    match rest {
        Some("all") => {
            let req = ListRequest {
                mode: ListMode::ALL,
            };
            let json = serde_json::to_string(&req).expect("can jsonify request");
            swarm.floodsub.publish(TOPIC.clone(), json.as_bytes());
        }
        Some(recipes_peer_id) => {
            let req = ListRequest {
                mode: ListMode::One(recipes_peer_id.to_owned()),
            };
            let json = serde_json::to_string(&req).expect("can jsonify request");
            swarm.floodsub.publish(TOPIC.clone(), json.as_bytes());
        }
        None => {
            match read_local_recipes().await {
                Ok(v) => {
                    info!("Local Recipes ({})", v.len());
                    v.iter().for_each(|r| info!("{:?}", r));
                }
                Err(e) => error!("error fetching local recipes: {}", e),
            };
        }
    };
}

Analizamos el comando entrante, quitamos la ls rpieza y comprobamos lo que queda. Si no hay nada más en el comando, simplemente podemos buscar nuestras recetas locales e imprimirlas usando los ayudantes definidos en la sección anterior.

Si encontramos la allpalabra clave, creamos un ListRequestcon el ListMode::ALLconjunto, lo serializamos a JSON y, usando la FloodSubinstancia dentro de nuestro Swarm, lo publicamos en el mencionado anteriormente Topic.

Lo mismo sucede si encontramos una ID de peer en el comando, en cuyo caso simplemente enviaremos el ListMode::Onemodo con esa ID de peer. Podríamos comprobar si es una identificación de par válida, o incluso si es una identificación de par que hemos descubierto, pero hagámoslo simple: si no hay nadie que lo escuche, no pasa nada.

Eso es todo lo que necesitamos hacer para enviar mensajes a la red. Ahora la pregunta es, ¿qué pasa con esos mensajes? ¿Dónde se manejan?

En el caso de una aplicación peer-to-peer, recuerde que somos el Sendery Receiverde los eventos, por lo que debemos ocuparnos de los eventos entrantes y salientes en nuestra implementación.

Responder a mensajes con libp2p

Esta es finalmente la parte en la que RecipeBehaviourentra nuestro . Vamos a definirlo:

#[derive(NetworkBehaviour)]
struct RecipeBehaviour {
    floodsub: Floodsub,
    mdns: TokioMdns,
    #[behaviour(ignore)]
    response_sender: mpsc::UnboundedSender<ListResponse>,
}

El comportamiento en sí es simplemente una estructura, pero usamos libp2pla NetworkBehaviourmacro de derivación, por lo que no tenemos que implementar manualmente todas las funciones de rasgo nosotros mismos.

Esta macro de derivación implementa las NetworkBehaviour funciones del rasgo para todos los miembros de la estructura, que no están anotados behaviour(ignore). Nuestro canal se ignora aquí porque no tiene nada que ver directamente con nuestro comportamiento.

Lo que queda es implementar la inject_eventfunción para ambos FloodsubEventy MdnsEvent.

Empecemos por mDNS:

impl NetworkBehaviourEventProcess<MdnsEvent> for RecipeBehaviour {
    fn inject_event(&mut self, event: MdnsEvent) {
        match event {
            MdnsEvent::Discovered(discovered_list) => {
                for (peer, _addr) in discovered_list {
                    self.floodsub.add_node_to_partial_view(peer);
                }
            }
            MdnsEvent::Expired(expired_list) => {
                for (peer, _addr) in expired_list {
                    if !self.mdns.has_node(&peer) {
                        self.floodsub.remove_node_from_partial_view(&peer);
                    }
                }
            }
        }
    }
}

La inject_eventfunción se llama cuando entra un evento para este controlador. Por mDNSotro lado, solo hay dos eventos, Discoveredy Expired, que se activan cuando vemos un nuevo par en la red o cuando un par existente desaparece. En ambos casos, lo agregamos o lo eliminamos de nuestra FloodSub"vista parcial", que es una lista de nodos para propagar nuestros mensajes.

El inject_eventpara eventos pub / sub es un poco más complejo. Necesitamos reaccionar tanto en las cargas entrantes ListRequestcomo en las ListResponsecargas útiles. Si enviamos un ListRequest, el par que recibe la solicitud buscará sus recetas locales publicadas y luego necesitará una forma de devolverlas.

La única forma de devolverlos al par solicitante es publicarlos en la red. Dado que pub / sub es de hecho el único mecanismo que tenemos, debemos reaccionar tanto a las solicitudes entrantes como a las respuestas entrantes.

Veamos cómo funciona esto:

impl NetworkBehaviourEventProcess<FloodsubEvent> for RecipeBehaviour {
    fn inject_event(&mut self, event: FloodsubEvent) {
        match event {
            FloodsubEvent::Message(msg) => {
                if let Ok(resp) = serde_json::from_slice::<ListResponse>(&msg.data) {
                    if resp.receiver == PEER_ID.to_string() {
                        info!("Response from {}:", msg.source);
                        resp.data.iter().for_each(|r| info!("{:?}", r));
                    }
                } else if let Ok(req) = serde_json::from_slice::<ListRequest>(&msg.data) {
                    match req.mode {
                        ListMode::ALL => {
                            info!("Received ALL req: {:?} from {:?}", req, msg.source);
                            respond_with_public_recipes(
                                self.response_sender.clone(),
                                msg.source.to_string(),
                            );
                        }
                        ListMode::One(ref peer_id) => {
                            if peer_id == &PEER_ID.to_string() {
                                info!("Received req: {:?} from {:?}", req, msg.source);
                                respond_with_public_recipes(
                                    self.response_sender.clone(),
                                    msg.source.to_string(),
                                );
                            }
                        }
                    }
                }
            }
            _ => (),
        }
    }
}

Coincidimos con el mensaje entrante, tratando de deserializarlo con una solicitud o respuesta. En el caso de una respuesta, simplemente imprimimos la respuesta con la identificación del par de la persona que llama, que usamos msg.source. Cuando recibimos una solicitud entrante, debemos diferenciar entre los casos ALLy One.

En el Onecaso, verificamos si el ID de par proporcionado es el mismo que el nuestro, que la solicitud realmente está destinada a nosotros. Si es así, devolvemos nuestras recetas publicadas, que también es nuestra respuesta en el caso de ALL.

En ambos casos, llamamos al respond_with_public_recipesayudante:

fn respond_with_public_recipes(sender: mpsc::UnboundedSender<ListResponse>, receiver: String) {
    tokio::spawn(async move {
        match read_local_recipes().await {
            Ok(recipes) => {
                let resp = ListResponse {
                    mode: ListMode::ALL,
                    receiver,
                    data: recipes.into_iter().filter(|r| r.public).collect(),
                };
                if let Err(e) = sender.send(resp) {
                    error!("error sending response via channel, {}", e);
                }
            }
            Err(e) => error!("error fetching local recipes to answer ALL request, {}", e),
        }
    });
}

En este método de ayuda, usamos el spawn de Tokio para ejecutar de forma asincrónica un futuro, que lee todas las recetas locales, crea un archivo a ListResponsepartir de los datos y envía estos datos channel_sendera nuestro bucle de eventos, donde lo manejamos así:

                EventType::Response(resp) => {
                    let json = serde_json::to_string(&resp).expect("can jsonify response");
                    swarm.floodsub.publish(TOPIC.clone(), json.as_bytes());
                }

Si notamos un Responseevento enviado "internamente" , lo serializamos en JSON y lo enviamos a la red.

Probando con libp2p

Eso es todo por la implementación. Ahora probémoslo.

Para comprobar que nuestra implementación funciona, iniciemos la aplicación en varios terminales usando este comando:

RUST_LOG=info cargo run

Tenga en cuenta que la aplicación espera un archivo llamado recipes.jsonen el directorio desde el que lo está iniciando.

Cuando se inició la aplicación, obtenemos el siguiente registro, imprimiendo nuestro ID de par:

INFO  rust_peer_to_peer_example > Peer Id: 12D3KooWDc1FDabQzpntvZRWeDZUL351gJRy3F4E8VN5Gx2pBCU2

Ahora debemos presionar enter para iniciar el ciclo de eventos.

Al ingresar ls p, obtenemos una lista de nuestros compañeros descubiertos:

ls p
 INFO  rust_peer_to_peer_example > Discovered Peers:
 INFO  rust_peer_to_peer_example > 12D3KooWCK6X7mFk9HeWw69WF1ueWa3XmphZ2Mu7ZHvEECj5rrhG
 INFO  rust_peer_to_peer_example > 12D3KooWLGN85pv5XTDALGX5M6tRgQtUGMWXWasWQD6oJjMcEENA

Con ls r, obtenemos las recetas locales:

ls r
 INFO  rust_peer_to_peer_example > Local Recipes (3)
 INFO  rust_peer_to_peer_example > Recipe { id: 0, name: " Coffee", ingredients: "Coffee", instructions: "Make Coffee", public: true }
 INFO  rust_peer_to_peer_example > Recipe { id: 1, name: " Tea", ingredients: "Tea, Water", instructions: "Boil Water, add tea", public: false }
 INFO  rust_peer_to_peer_example > Recipe { id: 2, name: " Carrot Cake", ingredients: "Carrots, Cake", instructions: "Make Carrot Cake", public: true }

Llamar ls r alldesencadena el envío de una solicitud a los otros pares y devuelve sus recetas:

ls r all
 INFO  rust_peer_to_peer_example > Response from 12D3KooWCK6X7mFk9HeWw69WF1ueWa3XmphZ2Mu7ZHvEECj5rrhG:
 INFO  rust_peer_to_peer_example > Recipe { id: 0, name: " Coffee", ingredients: "Coffee", instructions: "Make Coffee", public: true }
 INFO  rust_peer_to_peer_example > Recipe { id: 2, name: " Carrot Cake", ingredients: "Carrots, Cake", instructions: "Make Carrot Cake", public: true }

Lo mismo ocurre si usamos ls rcon un peer ID:

ls r 12D3KooWCK6X7mFk9HeWw69WF1ueWa3XmphZ2Mu7ZHvEECj5rrhG
 INFO  rust_peer_to_peer_example > Response from 12D3KooWCK6X7mFk9HeWw69WF1ueWa3XmphZ2Mu7ZHvEECj5rrhG:
 INFO  rust_peer_to_peer_example > Recipe { id: 0, name: " Coffee", ingredients: "Coffee", instructions: "Make Coffee", public: true }
 INFO  rust_peer_to_peer_example > Recipe { id: 2, name: " Carrot Cake", ingredients: "Carrots, Cake", instructions: "Make Carrot Cake", public: true }

¡Funciona! También puede probar esto con una gran cantidad de clientes en la misma red.

Puede encontrar el código de ejemplo completo en GitHub .

Conclusión

En esta publicación, cubrimos cómo construir una pequeña aplicación de red descentralizada usando Rust y libp2p.

Si viene de un entorno web, muchos de los conceptos de redes le resultarán algo familiares, pero la creación de una aplicación de igual a igual exige un enfoque fundamentalmente diferente para el diseño y la construcción.

La libp2pbiblioteca es bastante madura y, debido a la popularidad de Rust dentro de la escena criptográfica, existe un ecosistema rico y emergente de bibliotecas para construir aplicaciones descentralizadas poderosas.

Fuente del artículo original en https://blog.logrocket.com

#rust

Serde Rust: Serialization Framework for Rust

Serde

*Serde is a framework for serializing and deserializing Rust data structures efficiently and generically.*

You may be looking for:

Serde in action

Click to show Cargo.toml. Run this code in the playground.

[dependencies]

# The core APIs, including the Serialize and Deserialize traits. Always
# required when using Serde. The "derive" feature is only required when
# using #[derive(Serialize, Deserialize)] to make Serde work with structs
# and enums defined in your crate.
serde = { version = "1.0", features = ["derive"] }

# Each data format lives in its own crate; the sample code below uses JSON
# but you may be using a different one.
serde_json = "1.0"

 

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let point = Point { x: 1, y: 2 };

    // Convert the Point to a JSON string.
    let serialized = serde_json::to_string(&point).unwrap();

    // Prints serialized = {"x":1,"y":2}
    println!("serialized = {}", serialized);

    // Convert the JSON string back to a Point.
    let deserialized: Point = serde_json::from_str(&serialized).unwrap();

    // Prints deserialized = Point { x: 1, y: 2 }
    println!("deserialized = {:?}", deserialized);
}

Getting help

Serde is one of the most widely used Rust libraries so any place that Rustaceans congregate will be able to help you out. For chat, consider trying the #rust-questions or #rust-beginners channels of the unofficial community Discord (invite: https://discord.gg/rust-lang-community), the #rust-usage or #beginners channels of the official Rust Project Discord (invite: https://discord.gg/rust-lang), or the #general stream in Zulip. For asynchronous, consider the [rust] tag on StackOverflow, the /r/rust subreddit which has a pinned weekly easy questions post, or the Rust Discourse forum. It's acceptable to file a support issue in this repo but they tend not to get as many eyes as any of the above and may get closed without a response after some time.

Download Details:
Author: serde-rs
Source Code: https://github.com/serde-rs/serde
License: View license

#rust  #rustlang 

Awesome  Rust

Awesome Rust

1654894080

Serde JSON: JSON Support for Serde Framework

Serde JSON

Serde is a framework for serializing and deserializing Rust data structures efficiently and generically.

[dependencies]
serde_json = "1.0"

You may be looking for:

JSON is a ubiquitous open-standard format that uses human-readable text to transmit data objects consisting of key-value pairs.

{
    "name": "John Doe",
    "age": 43,
    "address": {
        "street": "10 Downing Street",
        "city": "London"
    },
    "phones": [
        "+44 1234567",
        "+44 2345678"
    ]
}

There are three common ways that you might find yourself needing to work with JSON data in Rust.

  • As text data. An unprocessed string of JSON data that you receive on an HTTP endpoint, read from a file, or prepare to send to a remote server.
  • As an untyped or loosely typed representation. Maybe you want to check that some JSON data is valid before passing it on, but without knowing the structure of what it contains. Or you want to do very basic manipulations like insert a key in a particular spot.
  • As a strongly typed Rust data structure. When you expect all or most of your data to conform to a particular structure and want to get real work done without JSON's loosey-goosey nature tripping you up.

Serde JSON provides efficient, flexible, safe ways of converting data between each of these representations.

Operating on untyped JSON values

Any valid JSON data can be manipulated in the following recursive enum representation. This data structure is serde_json::Value.

enum Value {
    Null,
    Bool(bool),
    Number(Number),
    String(String),
    Array(Vec<Value>),
    Object(Map<String, Value>),
}

A string of JSON data can be parsed into a serde_json::Value by the serde_json::from_str function. There is also from_slice for parsing from a byte slice &[u8] and from_reader for parsing from any io::Read like a File or a TCP stream.

use serde_json::{Result, Value};

fn untyped_example() -> Result<()> {
    // Some JSON input data as a &str. Maybe this comes from the user.
    let data = r#"
        {
            "name": "John Doe",
            "age": 43,
            "phones": [
                "+44 1234567",
                "+44 2345678"
            ]
        }"#;

    // Parse the string of data into serde_json::Value.
    let v: Value = serde_json::from_str(data)?;

    // Access parts of the data by indexing with square brackets.
    println!("Please call {} at the number {}", v["name"], v["phones"][0]);

    Ok(())
}

The result of square bracket indexing like v["name"] is a borrow of the data at that index, so the type is &Value. A JSON map can be indexed with string keys, while a JSON array can be indexed with integer keys. If the type of the data is not right for the type with which it is being indexed, or if a map does not contain the key being indexed, or if the index into a vector is out of bounds, the returned element is Value::Null.

When a Value is printed, it is printed as a JSON string. So in the code above, the output looks like Please call "John Doe" at the number "+44 1234567". The quotation marks appear because v["name"] is a &Value containing a JSON string and its JSON representation is "John Doe". Printing as a plain string without quotation marks involves converting from a JSON string to a Rust string with as_str() or avoiding the use of Value as described in the following section.

The Value representation is sufficient for very basic tasks but can be tedious to work with for anything more significant. Error handling is verbose to implement correctly, for example imagine trying to detect the presence of unrecognized fields in the input data. The compiler is powerless to help you when you make a mistake, for example imagine typoing v["name"] as v["nmae"] in one of the dozens of places it is used in your code.

Parsing JSON as strongly typed data structures

Serde provides a powerful way of mapping JSON data into Rust data structures largely automatically.

use serde::{Deserialize, Serialize};
use serde_json::Result;

#[derive(Serialize, Deserialize)]
struct Person {
    name: String,
    age: u8,
    phones: Vec<String>,
}

fn typed_example() -> Result<()> {
    // Some JSON input data as a &str. Maybe this comes from the user.
    let data = r#"
        {
            "name": "John Doe",
            "age": 43,
            "phones": [
                "+44 1234567",
                "+44 2345678"
            ]
        }"#;

    // Parse the string of data into a Person object. This is exactly the
    // same function as the one that produced serde_json::Value above, but
    // now we are asking it for a Person as output.
    let p: Person = serde_json::from_str(data)?;

    // Do things just like with any other Rust data structure.
    println!("Please call {} at the number {}", p.name, p.phones[0]);

    Ok(())
}

This is the same serde_json::from_str function as before, but this time we assign the return value to a variable of type Person so Serde will automatically interpret the input data as a Person and produce informative error messages if the layout does not conform to what a Person is expected to look like.

Any type that implements Serde's Deserialize trait can be deserialized this way. This includes built-in Rust standard library types like Vec<T> and HashMap<K, V>, as well as any structs or enums annotated with #[derive(Deserialize)].

Once we have p of type Person, our IDE and the Rust compiler can help us use it correctly like they do for any other Rust code. The IDE can autocomplete field names to prevent typos, which was impossible in the serde_json::Value representation. And the Rust compiler can check that when we write p.phones[0], then p.phones is guaranteed to be a Vec<String> so indexing into it makes sense and produces a String.

The necessary setup for using Serde's derive macros is explained on the Using derive page of the Serde site.

Constructing JSON values

Serde JSON provides a json! macro to build serde_json::Value objects with very natural JSON syntax.

use serde_json::json;

fn main() {
    // The type of `john` is `serde_json::Value`
    let john = json!({
        "name": "John Doe",
        "age": 43,
        "phones": [
            "+44 1234567",
            "+44 2345678"
        ]
    });

    println!("first phone number: {}", john["phones"][0]);

    // Convert to a string of JSON and print it out
    println!("{}", john.to_string());
}

The Value::to_string() function converts a serde_json::Value into a String of JSON text.

One neat thing about the json! macro is that variables and expressions can be interpolated directly into the JSON value as you are building it. Serde will check at compile time that the value you are interpolating is able to be represented as JSON.

let full_name = "John Doe";
let age_last_year = 42;

// The type of `john` is `serde_json::Value`
let john = json!({
    "name": full_name,
    "age": age_last_year + 1,
    "phones": [
        format!("+44 {}", random_phone())
    ]
});

This is amazingly convenient, but we have the problem we had before with Value: the IDE and Rust compiler cannot help us if we get it wrong. Serde JSON provides a better way of serializing strongly-typed data structures into JSON text.

Creating JSON by serializing data structures

A data structure can be converted to a JSON string by serde_json::to_string. There is also serde_json::to_vec which serializes to a Vec<u8> and serde_json::to_writer which serializes to any io::Write such as a File or a TCP stream.

use serde::{Deserialize, Serialize};
use serde_json::Result;

#[derive(Serialize, Deserialize)]
struct Address {
    street: String,
    city: String,
}

fn print_an_address() -> Result<()> {
    // Some data structure.
    let address = Address {
        street: "10 Downing Street".to_owned(),
        city: "London".to_owned(),
    };

    // Serialize it to a JSON string.
    let j = serde_json::to_string(&address)?;

    // Print, write to a file, or send to an HTTP server.
    println!("{}", j);

    Ok(())
}

Any type that implements Serde's Serialize trait can be serialized this way. This includes built-in Rust standard library types like Vec<T> and HashMap<K, V>, as well as any structs or enums annotated with #[derive(Serialize)].

Performance

It is fast. You should expect in the ballpark of 500 to 1000 megabytes per second deserialization and 600 to 900 megabytes per second serialization, depending on the characteristics of your data. This is competitive with the fastest C and C++ JSON libraries or even 30% faster for many use cases. Benchmarks live in the serde-rs/json-benchmark repo.

Getting help

Serde is one of the most widely used Rust libraries, so any place that Rustaceans congregate will be able to help you out. For chat, consider trying the #rust-questions or #rust-beginners channels of the unofficial community Discord (invite: https://discord.gg/rust-lang-community), the #rust-usage or #beginners channels of the official Rust Project Discord (invite: https://discord.gg/rust-lang), or the #general stream in Zulip. For asynchronous, consider the [rust] tag on StackOverflow, the /r/rust subreddit which has a pinned weekly easy questions post, or the Rust Discourse forum. It's acceptable to file a support issue in this repo, but they tend not to get as many eyes as any of the above and may get closed without a response after some time.

No-std support

As long as there is a memory allocator, it is possible to use serde_json without the rest of the Rust standard library. This is supported on Rust 1.36+. Disable the default "std" feature and enable the "alloc" feature:

[dependencies]
serde_json = { version = "1.0", default-features = false, features = ["alloc"] }

For JSON support in Serde without a memory allocator, please see the serde-json-core crate.

Link: https://crates.io/crates/serde_json

#rust  #rustlang  #encode   #json 

joe biden

1617255938

¿Cómo migrar los buzones de correo de Exchange a la nube de Office 365?

Si tiene problemas para migrar los buzones de correo de Exchange a Office 365, debe leer este artículo para saber cómo migrar los buzones de correo de Exchange EDB a Office 365. Al migrar a Office 365, los usuarios pueden acceder a sus buzones de correo desde cualquier lugar y desde cualquier dispositivo.

En esta publicación, explicaremos las razones detrás de esta migración y una solución profesional para migrar de Exchange a Office 365.

Razones para migrar Exchange Server a la nube de Office 365

Office 365 apareció por primera vez en 2011 y, dado que se considera la mejor plataforma para aquellas organizaciones que desean administrar todo su sistema de correo electrónico en la nube. Estas son las características clave de Office 365:

  1. Permite trabajar desde cualquier lugar y desde cualquier lugar.
  2. No se preocupe por el spam y el malware.
  3. La seguridad proporcionada por Office 365 es altamente confiable.
  4. Controla el costo total y brinda flexibilidad financiera.
  5. Todas las actualizaciones y mejoras son administradas por Microsoft.

¿Cómo migrar los buzones de correo de Exchange a Office 365?

Hay varias formas manuales de migrar los buzones de correo de Exchange EDB a Office 365, pero para evitar estos complicados y prolongados procedimientos, presentamos una solución de terceros, es decir, la herramienta de migración de Exchange, que es automatizada y directa para la migración de Exchange a Office 365. La herramienta funciona rápidamente y migra todos los elementos del buzón de Exchange Server a Office 365.

La herramienta de migración de Datavare Exchange es demasiado fácil de usar y ofrece pasos sencillos para migrar EDB a Office 365:

  1. Descargue e instale el software en su sistema.
  2. Agregue el archivo EDB de Exchange con el botón Examinar.
  3. Seleccione exportar a buzones de correo de Office 365.
  4. Proporcione los detalles de inicio de sesión de la cuenta de Office 365.
  5. Seleccione la carpeta y presione el botón Finalizar.

Por lo tanto, todos sus buzones de correo de Exchange EDB ahora se migran a Office 365.
Nota: puede usar filtros para migrar los elementos de datos deseados de la cuenta de Exchange a la de Office 365

Líneas finales

Este blog le indica una solución profesional para la migración de buzones de correo de Exchange a la cuenta de Office 365. Dado que las soluciones manuales son complicadas, sugerimos la herramienta de migración de Exchange, que es demasiado simple de usar. Los usuarios no se enfrentan a problemas al operar el programa. La mejor parte de este software es que no necesita habilidades técnicas para realizar la migración. Se puede comprender el funcionamiento del software descargando la versión de demostración que permite la migración de los primeros 50 elementos por carpeta.

Más información:- https://www.datavare.com/software/edb-migration.html

#herramienta de migración de intercambio #migración de intercambio #migrar buzones de correo de exchange

Cree Una Aplicación De Igual A Igual En Rust

En los últimos años, debido en gran parte a la exageración que rodea a la cadena de bloques y las criptomonedas, las aplicaciones descentralizadas han ganado bastante impulso. Otro factor detrás del creciente interés en la descentralización es una mayor conciencia sobre las desventajas de poner la mayor parte de la web en manos de un pequeño grupo de empresas en términos de privacidad de datos y monopolización.

En cualquier caso, recientemente ha habido algunos desarrollos muy interesantes en la escena del software descentralizado, incluso aparte de toda la tecnología de criptografía y blockchain.

Los ejemplos notables incluyen IPFS ; la nueva plataforma de codificación distribuida Radicle ; la red social descentralizada Scuttlebutt ; y muchas más aplicaciones dentro del Fediverse , como Mastodon .

En este tutorial, le mostraremos cómo construir una aplicación peer-to-peer muy simple usando Rust y la fantástica libp2pbiblioteca, que existe en diferentes etapas de madurez para una amplia gama de idiomas.

Vamos a crear una aplicación de recetas de cocina con una sencilla interfaz de línea de comandos que nos permite:

  • Crea recetas
  • Publica recetas
  • Lista de recetas locales
  • Enumere otros pares que descubrimos en la red
  • Enumere las recetas publicadas de un par determinado
  • Enumere todas las recetas de todos los compañeros que conocemos

Haremos todo esto en alrededor de 300 líneas de Rust. ¡Empecemos!

Instalación de Rust

Para seguir, todo lo que necesita es una instalación reciente de Rust (1.47+).

Primero, cree un nuevo proyecto de Rust:

cargo new rust-p2p-example
cd rust-p2p-example

A continuación, edite el Cargo.tomlarchivo y agregue las dependencias que necesitará:

[dependencies]
libp2p = { version = "0.31", features = ["tcp-tokio", "mdns-tokio"] }
tokio = { version = "0.3", features = ["io-util", "io-std", "stream", "macros", "rt", "rt-multi-thread", "fs", "time", "sync"] }
serde = {version = "=1.0", features = ["derive"] }
serde_json = "1.0"
once_cell = "1.5"
log = "0.4"
pretty_env_logger = "0.4"

Como se mencionó anteriormente, lo usaremos libp2ppara la parte de redes peer-to-peer. Más específicamente, lo usaremos en concierto con el tiempo de ejecución asíncrono de Tokio. Usaremos Serde para la serialización y deserialización de JSON y un par de bibliotecas auxiliares para registrar e inicializar el estado.

¿Qué es libp2p?

libp2p es un conjunto de protocolos para crear aplicaciones peer-to-peer que se centra en la modularidad.

Hay implementaciones de bibliotecas para varios lenguajes, como JavaScript, Go y Rust. Todas estas bibliotecas implementan las mismas libp2pespecificaciones, por lo que un libp2pcliente creado con Go puede interactuar sin problemas con otro cliente escrito en JavaScript, siempre que sean compatibles en términos de la pila de protocolos elegida. Estos protocolos cubren una amplia gama, desde protocolos básicos de transporte de red hasta protocolos de capa de seguridad y multiplexación.

No profundizaremos demasiado en los detalles de libp2pen esta publicación, pero si está interesado en profundizar, los documentos oficialeslibp2p ofrecen una descripción general muy agradable de los diversos conceptos que encontraremos en el camino.

Como libp2pfunciona

Para ver libp2pen acción, comencemos nuestra aplicación de recetas. Comenzaremos definiendo algunas constantes y tipos que necesitaremos:

const STORAGE_FILE_PATH: &str = "./recipes.json";

type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync + 'static>>;

static KEYS: Lazy<identity::Keypair> = Lazy::new(|| identity::Keypair::generate_ed25519());
static PEER_ID: Lazy<PeerId> = Lazy::new(|| PeerId::from(KEYS.public()));
static TOPIC: Lazy<Topic> = Lazy::new(|| Topic::new("recipes"));

Almacenaremos nuestras recetas locales en un archivo JSON simple llamado recipes.json, que la aplicación esperará que esté en la misma carpeta que el ejecutable. También definimos un tipo de ayuda para Result, que nos permite propagar errores arbitrarios.

Luego, usamos once_cell::Lazy, para inicializar perezosamente algunas cosas. En primer lugar, lo usamos para generar un par de claves y un PeerIdderivado de la clave pública. También creamos a Topic, que es otro concepto clave de libp2p.

¿Qué significa todo esto? En resumen, a PeerIdes simplemente un identificador único para un par específico dentro de toda la red peer to peer. Lo derivamos de un par de claves para garantizar su singularidad. Además, el par de claves nos permite comunicarnos de forma segura con el resto de la red, asegurándonos de que nadie pueda hacerse pasar por nosotros.

A Topic, por otro lado, es un concepto de Floodsub, que es una implementación de libp2pla interfaz pub / sub . A Topices algo a lo que podemos subscribeenviar mensajes, por ejemplo, para escuchar solo un subconjunto del tráfico en una red de publicación / subred.

También necesitaremos algunos tipos para la receta:

type Recipes = Vec<Recipe>;

#[derive(Debug, Serialize, Deserialize)]
struct Recipe {
    id: usize,
    name: String,
    ingredients: String,
    instructions: String,
    public: bool,
}

Y algunos tipos de mensajes que planeamos enviar:

#[derive(Debug, Serialize, Deserialize)]
enum ListMode {
    ALL,
    One(String),
}

#[derive(Debug, Serialize, Deserialize)]
struct ListRequest {
    mode: ListMode,
}

#[derive(Debug, Serialize, Deserialize)]
struct ListResponse {
    mode: ListMode,
    data: Recipes,
    receiver: String,
}

enum EventType {
    Response(ListResponse),
    Input(String),
}

La receta es bastante sencilla. Tiene una identificación, un nombre, algunos ingredientes e instrucciones para ejecutarlo. Además, agregamos una publicbandera para que podamos distinguir qué recetas queremos compartir y cuáles queremos guardar para nosotros.

Como se mencionó al principio, hay dos formas de obtener listas de otros pares: de todos o de uno, que está representado por la ListModeenumeración.

Los tipos ListRequesty ListResponseson solo envoltorios para este tipo y la fecha de envío que los usa.

La EventTypeenumeración distingue entre una respuesta de otro par y una entrada de nosotros mismos. Veremos más adelante por qué esta diferencia es relevante.

Creando un libp2pcliente

Comencemos a escribir la mainfunción para configurar un par dentro de una red peer-to-peer.

#[tokio::main]
async fn main() {
    pretty_env_logger::init();

    info!("Peer Id: {}", PEER_ID.clone());
    let (response_sender, mut response_rcv) = mpsc::unbounded_channel();

    let auth_keys = Keypair::<X25519Spec>::new()
        .into_authentic(&KEYS)
        .expect("can create auth keys");

Inicializamos el registro y creamos un async channelpara comunicarnos entre diferentes partes de la aplicación. Usaremos este canal más adelante para enviar respuestas desde la libp2ppila de red a nuestra aplicación para manejarlas.

Además, creamos algunas claves de autenticación para el protocolo criptográfico Noise , que usaremos para asegurar el tráfico dentro de la red. Para ello, creamos un nuevo par de claves y lo firmamos con nuestras claves de identidad utilizando la into_authenticfunción.

El siguiente paso es importante e involucra algunos conceptos básicos de libp2p: crear un llamado Transport .authenticate (NoiseConfig :: xx (auth keys) .into authenticated ()) // XX Patrón de protocolo de enlace, IX también existe e IK - solo XX actualmente proporciona interoperabilidad con otras impls de libp2p .multiplex (mplex :: MplexConfig :: new ()) .boxed ();).

    let transp = TokioTcpConfig::new()
        .upgrade(upgrade::Version::V1)
        .authenticate(NoiseConfig::xx(auth_keys).into_authenticated())
        .multiplex(mplex::MplexConfig::new())
        .boxed();

Un transporte es un conjunto de protocolos de red que permite la comunicación orientada a la conexión entre pares. También es posible utilizar varios transportes dentro de una aplicación, por ejemplo, TCP / IP y Websockets o UDP al mismo tiempo para diferentes casos de uso.

En este ejemplo, usaremos TCP como base usando el TCP asíncrono de Tokio. Una vez que se haya establecido una conexión TCP, la upgradeusaremos Noisepara una comunicación segura. Un ejemplo basado en la web de esto sería usar TLS sobre HTTP para crear una conexión segura.

Usamos el NoiseConfig:xxpatrón de apretón de manos, que es una de las tres opciones, porque es la única que se garantiza que es interoperable con otras libp2paplicaciones.

Lo bueno de esto libp2pes que podríamos escribir un cliente Rust y otro podría escribir un cliente JavaScript, y aún podrían comunicarse fácilmente siempre que los protocolos estén implementados en ambas versiones de la biblioteca.

Al final, también multiplexamos el transporte, lo que nos permite multiplexar múltiples subflujos o conexiones en el mismo transporte.

¡Uf, eso es bastante teoría! Pero todo esto se puede encontrar en los libp2pdocumentos . Esta es solo una de las muchas formas de crear un transporte de igual a igual.

El siguiente concepto es a NetworkBehaviour. Esta es la parte interna libp2pque realmente define la lógica de la red y todos los pares, por ejemplo, qué hacer con los eventos entrantes y qué eventos enviar.

    let mut behaviour = RecipeBehaviour {
        floodsub: Floodsub::new(PEER_ID.clone()),
        mdns: TokioMdns::new().expect("can create mdns"),
        response_sender,
    };

    behaviour.floodsub.subscribe(TOPIC.clone());

En este caso, como se mencionó anteriormente, usaremos el FloodSubprotocolo para lidiar con eventos. También usaremos mDNS, que es un protocolo para descubrir otros pares en la red local. También colocaremos la senderparte de nuestro canal aquí para que podamos usarlo para propagar eventos a la parte principal de la aplicación.

El FloodSubtema que creamos anteriormente ahora se suscribe desde nuestro comportamiento, lo que significa que recibiremos eventos y podremos enviar eventos sobre ese tema.

Casi hemos terminado con la libp2pconfiguración. El último concepto que necesitamos es el Swarm.

    let mut swarm = SwarmBuilder::new(transp, behaviour, PEER_ID.clone())
        .executor(Box::new(|fut| {
            tokio::spawn(fut);
        }))
        .build();

A Swarmgestiona las conexiones creadas mediante el transporte y ejecuta el comportamiento de la red que creamos, activando y recibiendo eventos y dándonos una forma de llegar a ellos desde el exterior.

Creamos el Swarmcon nuestro transporte, comportamiento e identificación de pares. La executorparte simplemente le dice Swarmque use el Tokiotiempo de ejecución para ejecutarse internamente, pero también podríamos usar otros tiempos de ejecución asíncronos aquí.

Lo único que queda por hacer es iniciar nuestro Swarm:

    Swarm::listen_on(
        &mut swarm,
        "/ip4/0.0.0.0/tcp/0"
            .parse()
            .expect("can get a local socket"),
    )
    .expect("swarm can be started");

Similar a iniciar, por ejemplo, un servidor TCP, simplemente llamamos listen_oncon una IP local, dejando que el sistema operativo decida el puerto por nosotros. Esto comenzará Swarmcon toda nuestra configuración, pero aún no hemos definido ninguna lógica.

Comencemos con el manejo de la entrada del usuario.

Manejo de entrada en libp2p

Para la entrada del usuario, simplemente confiaremos en el viejo STDIN. Entonces, antes de la Swarm::listen_onllamada, agregaremos:

    let mut stdin = tokio::io::BufReader::new(tokio::io::stdin()).lines();

Esto definió un lector asíncrono en STDIN, que lee la secuencia línea por línea. Entonces, si presionamos enter, habrá un nuevo mensaje entrante.

La siguiente parte es crear nuestro bucle de eventos, que escuchará eventos de STDIN, de Swarmy de nuestro canal de respuesta definido anteriormente.

    loop {
        let evt = {
            tokio::select! {
                line = stdin.next_line() => Some(EventType::Input(line.expect("can get line").expect("can read line from stdin"))),
                event = swarm.next() => {
                    info!("Unhandled Swarm Event: {:?}", event);
                    None
                },
                response = response_rcv.recv() => Some(EventType::Response(response.expect("response exists"))),
            }
        };
        ...
    }
}

Usamos la selectmacro de Tokio para esperar varios procesos asíncronos, manejando el primero que finaliza. No hacemos nada con los Swarmeventos; estos se manejan dentro de nuestro RecipeBehaviour, que veremos más adelante, pero aún necesitamos llamar swarm.next()para impulsar el Swarmavance.

Agreguemos algo de lógica de manejo de eventos en lugar de :

        if let Some(event) = evt {
            match event {
                EventType::Response(resp) => {
                   ...
                }
                EventType::Input(line) => match line.as_str() {
                    "ls p" => handle_list_peers(&mut swarm).await,
                    cmd if cmd.starts_with("ls r") => handle_list_recipes(cmd, &mut swarm).await,
                    cmd if cmd.starts_with("create r") => handle_create_recipe(cmd).await,
                    cmd if cmd.starts_with("publish r") => handle_publish_recipe(cmd).await,
                    _ => error!("unknown command"),
                },
            }
        }

Si hay un evento, lo emparejamos y vemos si es Responseun Inputevento o un evento. Veamos los Inputeventos solo por ahora.

Hay un par de opciones. Admitimos los siguientes comandos:

  • ls p enumera todos los compañeros conocidos
  • ls r enumera recetas locales
  • ls r {peerId} enumera las recetas publicadas de un determinado compañero
  • ls r all enumera recetas publicadas de todos los compañeros conocidos
  • publish r {recipeId} publica una receta determinada
  • create r {recipeName}|{recipeIngredients}|{recipeInstructions crea una nueva receta con los datos dados y un ID creciente

Enumerar todas las recetas de los compañeros, en este caso, significa enviar una solicitud de recetas a nuestros compañeros, esperar a que respondan y mostrar los resultados. En una red peer-to-peer, esto puede llevar un tiempo, ya que algunos pares pueden estar en el otro lado del planeta y no sabemos si todos nos responderán. Esto es bastante diferente a enviar una solicitud a un servidor HTTP, por ejemplo.

Veamos la lógica para enumerar pares primero:

async fn handle_list_peers(swarm: &mut Swarm<RecipeBehaviour>) {
    info!("Discovered Peers:");
    let nodes = swarm.mdns.discovered_nodes();
    let mut unique_peers = HashSet::new();
    for peer in nodes {
        unique_peers.insert(peer);
    }
    unique_peers.iter().for_each(|p| info!("{}", p));
}

En este caso, podemos usar mDNSpara darnos todos los nodos descubiertos, iterarlos y mostrarlos. Fácil.

A continuación, veamos cómo crear y publicar recetas, antes de abordar los comandos de lista:

async fn handle_create_recipe(cmd: &str) {
    if let Some(rest) = cmd.strip_prefix("create r") {
        let elements: Vec<&str> = rest.split("|").collect();
        if elements.len() < 3 {
            info!("too few arguments - Format: name|ingredients|instructions");
        } else {
            let name = elements.get(0).expect("name is there");
            let ingredients = elements.get(1).expect("ingredients is there");
            let instructions = elements.get(2).expect("instructions is there");
            if let Err(e) = create_new_recipe(name, ingredients, instructions).await {
                error!("error creating recipe: {}", e);
            };
        }
    }
}

async fn handle_publish_recipe(cmd: &str) {
    if let Some(rest) = cmd.strip_prefix("publish r") {
        match rest.trim().parse::<usize>() {
            Ok(id) => {
                if let Err(e) = publish_recipe(id).await {
                    info!("error publishing recipe with id {}, {}", id, e)
                } else {
                    info!("Published Recipe with id: {}", id);
                }
            }
            Err(e) => error!("invalid id: {}, {}", rest.trim(), e),
        };
    }
}

En ambos casos, necesitamos analizar la cadena para obtener los |datos separados, o el ID de receta dado en el caso de publishregistrar un error si la entrada dada no es válida.

En el createcaso, llamamos a la create_new_recipefunción auxiliar con los datos dados. Veamos todas las funciones auxiliares que necesitaremos para interactuar con nuestro almacenamiento JSON local simple para recetas:

async fn create_new_recipe(name: &str, ingredients: &str, instructions: &str) -> Result<()> {
    let mut local_recipes = read_local_recipes().await?;
    let new_id = match local_recipes.iter().max_by_key(|r| r.id) {
        Some(v) => v.id + 1,
        None => 0,
    };
    local_recipes.push(Recipe {
        id: new_id,
        name: name.to_owned(),
        ingredients: ingredients.to_owned(),
        instructions: instructions.to_owned(),
        public: false,
    });
    write_local_recipes(&local_recipes).await?;

    info!("Created recipe:");
    info!("Name: {}", name);
    info!("Ingredients: {}", ingredients);
    info!("Instructions:: {}", instructions);

    Ok(())
}

async fn publish_recipe(id: usize) -> Result<()> {
    let mut local_recipes = read_local_recipes().await?;
    local_recipes
        .iter_mut()
        .filter(|r| r.id == id)
        .for_each(|r| r.public = true);
    write_local_recipes(&local_recipes).await?;
    Ok(())
}

async fn read_local_recipes() -> Result<Recipes> {
    let content = fs::read(STORAGE_FILE_PATH).await?;
    let result = serde_json::from_slice(&content)?;
    Ok(result)
}

async fn write_local_recipes(recipes: &Recipes) -> Result<()> {
    let json = serde_json::to_string(&recipes)?;
    fs::write(STORAGE_FILE_PATH, &json).await?;
    Ok(())
}

Los bloques de construcción más básicos son read_local_recipesy write_local_recipes, que simplemente leen y deserializan o serializan y escriben recetas desde o hacia el archivo de almacenamiento.

El publish_recipeayudante obtiene todas las recetas del archivo, busca la receta con el ID proporcionado y establece su publicindicador en verdadero.

Al crear una receta, también obtenemos todas las recetas del archivo, agregamos una nueva receta al final y escribimos todos los datos, anulando el archivo. Esto no es muy eficiente, pero es simple y funciona.

Enviar mensajes con libp2p

Veamos los listcomandos a continuación y exploremos cómo podemos enviar mensajes a otros compañeros.

En el listcomando, hay tres casos posibles:

async fn handle_list_recipes(cmd: &str, swarm: &mut Swarm<RecipeBehaviour>) {
    let rest = cmd.strip_prefix("ls r ");
    match rest {
        Some("all") => {
            let req = ListRequest {
                mode: ListMode::ALL,
            };
            let json = serde_json::to_string(&req).expect("can jsonify request");
            swarm.floodsub.publish(TOPIC.clone(), json.as_bytes());
        }
        Some(recipes_peer_id) => {
            let req = ListRequest {
                mode: ListMode::One(recipes_peer_id.to_owned()),
            };
            let json = serde_json::to_string(&req).expect("can jsonify request");
            swarm.floodsub.publish(TOPIC.clone(), json.as_bytes());
        }
        None => {
            match read_local_recipes().await {
                Ok(v) => {
                    info!("Local Recipes ({})", v.len());
                    v.iter().for_each(|r| info!("{:?}", r));
                }
                Err(e) => error!("error fetching local recipes: {}", e),
            };
        }
    };
}

Analizamos el comando entrante, quitamos la ls rpieza y comprobamos lo que queda. Si no hay nada más en el comando, simplemente podemos buscar nuestras recetas locales e imprimirlas usando los ayudantes definidos en la sección anterior.

Si encontramos la allpalabra clave, creamos un ListRequestcon el ListMode::ALLconjunto, lo serializamos a JSON y, usando la FloodSubinstancia dentro de nuestro Swarm, lo publicamos en el mencionado anteriormente Topic.

Lo mismo sucede si encontramos una ID de peer en el comando, en cuyo caso simplemente enviaremos el ListMode::Onemodo con esa ID de peer. Podríamos comprobar si es una identificación de par válida, o incluso si es una identificación de par que hemos descubierto, pero hagámoslo simple: si no hay nadie que lo escuche, no pasa nada.

Eso es todo lo que necesitamos hacer para enviar mensajes a la red. Ahora la pregunta es, ¿qué pasa con esos mensajes? ¿Dónde se manejan?

En el caso de una aplicación peer-to-peer, recuerde que somos el Sendery Receiverde los eventos, por lo que debemos ocuparnos de los eventos entrantes y salientes en nuestra implementación.

Responder a mensajes con libp2p

Esta es finalmente la parte en la que RecipeBehaviourentra nuestro . Vamos a definirlo:

#[derive(NetworkBehaviour)]
struct RecipeBehaviour {
    floodsub: Floodsub,
    mdns: TokioMdns,
    #[behaviour(ignore)]
    response_sender: mpsc::UnboundedSender<ListResponse>,
}

El comportamiento en sí es simplemente una estructura, pero usamos libp2pla NetworkBehaviourmacro de derivación, por lo que no tenemos que implementar manualmente todas las funciones de rasgo nosotros mismos.

Esta macro de derivación implementa las NetworkBehaviour funciones del rasgo para todos los miembros de la estructura, que no están anotados behaviour(ignore). Nuestro canal se ignora aquí porque no tiene nada que ver directamente con nuestro comportamiento.

Lo que queda es implementar la inject_eventfunción para ambos FloodsubEventy MdnsEvent.

Empecemos por mDNS:

impl NetworkBehaviourEventProcess<MdnsEvent> for RecipeBehaviour {
    fn inject_event(&mut self, event: MdnsEvent) {
        match event {
            MdnsEvent::Discovered(discovered_list) => {
                for (peer, _addr) in discovered_list {
                    self.floodsub.add_node_to_partial_view(peer);
                }
            }
            MdnsEvent::Expired(expired_list) => {
                for (peer, _addr) in expired_list {
                    if !self.mdns.has_node(&peer) {
                        self.floodsub.remove_node_from_partial_view(&peer);
                    }
                }
            }
        }
    }
}

La inject_eventfunción se llama cuando entra un evento para este controlador. Por mDNSotro lado, solo hay dos eventos, Discoveredy Expired, que se activan cuando vemos un nuevo par en la red o cuando un par existente desaparece. En ambos casos, lo agregamos o lo eliminamos de nuestra FloodSub"vista parcial", que es una lista de nodos para propagar nuestros mensajes.

El inject_eventpara eventos pub / sub es un poco más complejo. Necesitamos reaccionar tanto en las cargas entrantes ListRequestcomo en las ListResponsecargas útiles. Si enviamos un ListRequest, el par que recibe la solicitud buscará sus recetas locales publicadas y luego necesitará una forma de devolverlas.

La única forma de devolverlos al par solicitante es publicarlos en la red. Dado que pub / sub es de hecho el único mecanismo que tenemos, necesitamos reaccionar tanto a las solicitudes entrantes como a las respuestas entrantes.

Veamos como funciona esto:

impl NetworkBehaviourEventProcess<FloodsubEvent> for RecipeBehaviour {
    fn inject_event(&mut self, event: FloodsubEvent) {
        match event {
            FloodsubEvent::Message(msg) => {
                if let Ok(resp) = serde_json::from_slice::<ListResponse>(&msg.data) {
                    if resp.receiver == PEER_ID.to_string() {
                        info!("Response from {}:", msg.source);
                        resp.data.iter().for_each(|r| info!("{:?}", r));
                    }
                } else if let Ok(req) = serde_json::from_slice::<ListRequest>(&msg.data) {
                    match req.mode {
                        ListMode::ALL => {
                            info!("Received ALL req: {:?} from {:?}", req, msg.source);
                            respond_with_public_recipes(
                                self.response_sender.clone(),
                                msg.source.to_string(),
                            );
                        }
                        ListMode::One(ref peer_id) => {
                            if peer_id == &PEER_ID.to_string() {
                                info!("Received req: {:?} from {:?}", req, msg.source);
                                respond_with_public_recipes(
                                    self.response_sender.clone(),
                                    msg.source.to_string(),
                                );
                            }
                        }
                    }
                }
            }
            _ => (),
        }
    }
}

Coincidimos con el mensaje entrante, tratando de deserializarlo con una solicitud o respuesta. En el caso de una respuesta, simplemente imprimimos la respuesta con la identificación del par de la persona que llama, que usamos msg.source. Cuando recibimos una solicitud entrante, debemos diferenciar entre los casos ALLy One.

En el Onecaso, verificamos si el ID de par proporcionado es el mismo que el nuestro, que la solicitud realmente está destinada a nosotros. Si es así, devolvemos nuestras recetas publicadas, que también es nuestra respuesta en el caso de ALL.

En ambos casos, llamamos al respond_with_public_recipesayudante:

fn respond_with_public_recipes(sender: mpsc::UnboundedSender<ListResponse>, receiver: String) {
    tokio::spawn(async move {
        match read_local_recipes().await {
            Ok(recipes) => {
                let resp = ListResponse {
                    mode: ListMode::ALL,
                    receiver,
                    data: recipes.into_iter().filter(|r| r.public).collect(),
                };
                if let Err(e) = sender.send(resp) {
                    error!("error sending response via channel, {}", e);
                }
            }
            Err(e) => error!("error fetching local recipes to answer ALL request, {}", e),
        }
    });
}

En este método de ayuda, usamos el spawn de Tokio para ejecutar de forma asincrónica un futuro, que lee todas las recetas locales, crea un archivo a ListResponsepartir de los datos y envía estos datos channel_sendera nuestro bucle de eventos, donde lo manejamos así:

                EventType::Response(resp) => {
                    let json = serde_json::to_string(&resp).expect("can jsonify response");
                    swarm.floodsub.publish(TOPIC.clone(), json.as_bytes());
                }

Si notamos un Responseevento enviado "internamente" , lo serializamos en JSON y lo enviamos a la red.

Probando con libp2p

Eso es todo por la implementación. Ahora probémoslo.

Para comprobar que nuestra implementación funciona, iniciemos la aplicación en varios terminales usando este comando:

RUST_LOG=info cargo run

Tenga en cuenta que la aplicación espera un archivo llamado recipes.jsonen el directorio desde el que lo está iniciando.

Cuando se inició la aplicación, obtenemos el siguiente registro, imprimiendo nuestro ID de par:

INFO  rust_peer_to_peer_example > Peer Id: 12D3KooWDc1FDabQzpntvZRWeDZUL351gJRy3F4E8VN5Gx2pBCU2

Ahora debemos presionar enter para iniciar el ciclo de eventos.

Al ingresar ls p, obtenemos una lista de nuestros compañeros descubiertos:

ls p
 INFO  rust_peer_to_peer_example > Discovered Peers:
 INFO  rust_peer_to_peer_example > 12D3KooWCK6X7mFk9HeWw69WF1ueWa3XmphZ2Mu7ZHvEECj5rrhG
 INFO  rust_peer_to_peer_example > 12D3KooWLGN85pv5XTDALGX5M6tRgQtUGMWXWasWQD6oJjMcEENA

Con ls r, obtenemos las recetas locales:

ls r
 INFO  rust_peer_to_peer_example > Local Recipes (3)
 INFO  rust_peer_to_peer_example > Recipe { id: 0, name: " Coffee", ingredients: "Coffee", instructions: "Make Coffee", public: true }
 INFO  rust_peer_to_peer_example > Recipe { id: 1, name: " Tea", ingredients: "Tea, Water", instructions: "Boil Water, add tea", public: false }
 INFO  rust_peer_to_peer_example > Recipe { id: 2, name: " Carrot Cake", ingredients: "Carrots, Cake", instructions: "Make Carrot Cake", public: true }

Llamar ls r alldesencadena el envío de una solicitud a los otros pares y devuelve sus recetas:

ls r all
 INFO  rust_peer_to_peer_example > Response from 12D3KooWCK6X7mFk9HeWw69WF1ueWa3XmphZ2Mu7ZHvEECj5rrhG:
 INFO  rust_peer_to_peer_example > Recipe { id: 0, name: " Coffee", ingredients: "Coffee", instructions: "Make Coffee", public: true }
 INFO  rust_peer_to_peer_example > Recipe { id: 2, name: " Carrot Cake", ingredients: "Carrots, Cake", instructions: "Make Carrot Cake", public: true }

Lo mismo ocurre si usamos ls rcon un peer ID:

ls r 12D3KooWCK6X7mFk9HeWw69WF1ueWa3XmphZ2Mu7ZHvEECj5rrhG
 INFO  rust_peer_to_peer_example > Response from 12D3KooWCK6X7mFk9HeWw69WF1ueWa3XmphZ2Mu7ZHvEECj5rrhG:
 INFO  rust_peer_to_peer_example > Recipe { id: 0, name: " Coffee", ingredients: "Coffee", instructions: "Make Coffee", public: true }
 INFO  rust_peer_to_peer_example > Recipe { id: 2, name: " Carrot Cake", ingredients: "Carrots, Cake", instructions: "Make Carrot Cake", public: true }

¡Funciona! También puede probar esto con una gran cantidad de clientes en la misma red.

Conclusión

En esta publicación, cubrimos cómo construir una pequeña aplicación de red descentralizada usando Rust y libp2p.

Si viene de un entorno web, muchos de los conceptos de redes le resultarán algo familiares, pero la creación de una aplicación de igual a igual exige un enfoque fundamentalmente diferente para el diseño y la construcción.

La libp2pbiblioteca es bastante madura y, debido a la popularidad de Rust dentro de la escena de la criptografía, existe un ecosistema rico y emergente de bibliotecas para construir poderosas aplicaciones descentralizadas.

Enlace: https://blog.logrocket.com/libp2p-tutorial-build-a-peer-to-peer-app-in-rust/

#rust