WEB3DEV

Cover image for Começando com a libp2p em Go
Dimitris Carvalho Calixto
Dimitris Carvalho Calixto

Posted on

Começando com a libp2p em Go

Esse artigo foi escrito por: Felipe Rosa e traduzido por Dimitris Calixto, artigo original disponível aqui

Este post dá uma breve introdução ao desenvolvimento de aplicações ponto a ponto utilizando a libp2p e usando a linguagem de programação Go.

Introdução

Essa seção explica os conceitos que vamos ver neste post.

O que é libp2p?

A partir dos documentos da libp2p:

a libp2p é um sistema modular de protocolos, especificações e bibliotecas que permitem o desenvolvimento de aplicações de rede peer-to-peer.

O que são aplicações de rede ponto a ponto?

Uma aplicação pura de rede ponto a ponto é aquela em que

As máquinas ligadas a e atuam como clientes e servidores, compartilhando assim os seus próprios recursos de hardware para fazer funcionar a rede.

Em vez de clientes e servidores, as máquinas ligadas às redes ponto a ponto são normalmente chamadas de "nós".

Codificando o nó

Criando hosts libp2p

O código abaixo facilmente cria um novo host libp2p com opções padrão.

package main

import (
    "context"
    "fmt"
    "os"
    "os/signal"
    "syscall"

    "github.com/libp2p/go-libp2p"
)

func main() {
    ctx := context.Background()

    host, err := libp2p.New(ctx)
    if err != nil {
        panic(err)
    }
    defer host.Close()

    fmt.Println(host.Addrs())

    sigCh := make(chan os.Signal)
    signal.Notify(sigCh, syscall.SIGKILL, syscall.SIGINT)
    <-sigCh
}
Enter fullscreen mode Exit fullscreen mode

Executando o código, obtive a seguinte saída:

[/ip6/2804:d45:3613:5400:4b34:ed8f:df00:5055/tcp/43937 /ip6/::1/tcp/43937 /ip4/192.168.1.68/tcp/45559 /ip4/127.0.0.1/tcp/45559]
Enter fullscreen mode Exit fullscreen mode

Podemos ver que a libp2p escolheu automaticamente os endereços IPv4 e IPv6 em todas as interfaces para que o host acessasse as ligações. Ao fazer isso, o nosso nó pode agora atuar como um servidor ao qual outros podem se ligar.

Conectando ao nó (a partir de outro nó)

Antes de conectar ao nó da seção anterior, vamos ver como funciona o endereçamento do nó na libp2p. Vamos explorar 2 conceitos necessários para nos conectarmos a um nó da libp2p: multiaddr e IDs de nó.


Multi Endereço

A libp2p faz muito para trabalhar em cima de diferentes transportes de rede (ou seja, a tecnologia utilizada para enviar e receber bits no fio). Isto requer um esquema de endereçamento flexível.

O endereço que vimos na saída da execução do nó são codificados usando multiaddr (ver a especificação). multiaddr permite a codificação de muitos protocolos em cima uns dos outros, juntamente com a sua informação de endereçamento.

Vamos dissecar a saída da execução do nó da seção anterior:

/ip4/127.0.0.1/tcp/45559
Enter fullscreen mode Exit fullscreen mode

Há dois protocolos codificados nesta cadeia multiaddr string: /ip4/127.0.0.1 que nos diz para usarmos o endereço 127.0.0.1 do protocolo IPv4 e /tcp/45559 que nos diz para colocarmos (em cima do IP) o protocolo TCP na porta 45559.

Id do nó

A libp2p define o protocolo /p2p e a parte de endereçamento da sua cadeia multiaddr é a identificação do nó ao qual queremos nos conectar. Isso significa que o endereço de um nó seria algo parecido:

Onde NODE_ID é o ID do nó.

Os nós precisam gerar um par de chaves criptográficas para assegurar as ligações com outros nós (ou pares).

O ID do nó é simplesmente um multihash da sua chave pública.

Desta forma (além de identificar diferentes nós) as identificações são únicas, podem ser tornadas permanentes e fornecer uma forma de outros nós verificarem a chave pública enviada por outro nó.


Conectando os nós

Com tudo isso dito, podemos voltar a escrever o código para ligar dois nós.

Primeiro, imprimiremos os endereços e a identificação do host:

fmt.Println("Addresses:", host.Addrs())
fmt.Println("ID:", host.ID())
Enter fullscreen mode Exit fullscreen mode

Iniciando o nó de novo, obtemos:

Addresses: [/ip4/192.168.1.68/tcp/44511 /ip4/127.0.0.1/tcp/44511 /ip6/2804:d45:3613:5400:4b34:ed8f:df00:5055/tcp/46471 /ip6/::1/tcp/46471]
ID: Qmdfuscj69bwzza5nyC1RCMRkV1aoYjQq2nvDYqUYG8Zoq
Enter fullscreen mode Exit fullscreen mode

Assim, a cadeia de endereços p2p para este nó seria (vou usar o endereço IPv4):

/ip4/127.0.0.1/tcp/44511/p2p/Qmdfuscj69bwzza5nyC1RCMRkV1aoYjQq2nvDYqUYG8Zoq
Enter fullscreen mode Exit fullscreen mode

A fim de conectarmos outros nós, podemos estender o nosso código para aceitar um endereço de par como argumento e alguma lógica de ligação:

package main

import (
    "context"
    "flag"
    "fmt"
    "os"
    "os/signal"
    "syscall"

    "github.com/libp2p/go-libp2p"
    "github.com/libp2p/go-libp2p-core/peer"
    "github.com/multiformats/go-multiaddr"
)

func main() {
    // Add -peer-address flag
    peerAddr := flag.String("peer-address", "", "peer address")
    flag.Parse()

    // Create the libp2p host.
    //
    // Note that we are explicitly passing the listen address and restricting it to IPv4 over the
    // loopback interface (127.0.0.1).
    //
    // Setting the TCP port as 0 makes libp2p choose an available port for us.
    // You could, of course, specify one if you like.
    host, err := libp2p.New(context.Background(), libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0"))
    if err != nil {
        panic(err)
    }
    defer host.Close()

    // Print this node's addresses and ID
    fmt.Println("Addresses:", host.Addrs())
    fmt.Println("ID:", host.ID())

    // If we received a peer address, we should connect to it.
    if *peerAddr != "" {
        // Parse the multiaddr string.
        peerMA, err := multiaddr.NewMultiaddr(*peerAddr)
        if err != nil {
            panic(err)
        }
        peerAddrInfo, err := peer.AddrInfoFromP2pAddr(peerMA)
        if err != nil {
            panic(err)
        }

        // Connect to the node at the given address.
        if err := host.Connect(context.Background(), *peerAddrInfo); err != nil {
            panic(err)
        }
        fmt.Println("Connected to", peerAddrInfo.String())
    }

    sigCh := make(chan os.Signal)
    signal.Notify(sigCh, syscall.SIGKILL, syscall.SIGINT)
    <-sigCh
}
Enter fullscreen mode Exit fullscreen mode

Enviando e recebendo dados

Quando queremos enviar e receber dados diretamente de outros pares, podemos utilizar uma stream libp2p.

Vamos fazer com que os nós comecem um contador para cada nova ligação (entrada e saída) e enviar através de um fluxo a cada segundo. Ao mesmo tempo, os nós vão continuar lendo os contadores enviados nessa mesma stream.

Primeiro, criamos uma função para escrever dados para a stream:

func writeCounter(s network.Stream) {
    var counter uint64

    for {
        <-time.After(time.Second)
        counter++

        err := binary.Write(s, binary.BigEndian, counter)
        if err != nil {
            panic(err)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Depois disso, criamos uma função para ler os dados da stream:

func readCounter(s network.Stream) {
    for {
        var counter uint64

        err := binary.Read(s, binary.BigEndian, &counter)
        if err != nil {
            panic(err)
        }

        fmt.Printf("Received %d from %s\n", counter, s.ID())
    }
}
Enter fullscreen mode Exit fullscreen mode

Depois modificamos o código para fazer 2 coisas adicionais:

  • Configurar um manipulador de stream usando a função SetStreamHandler (a função do manipulador é chamada cada vez que um par abre uma stream)
  • Criar uma nova stream usando a função NewStream após a ligação a um par

Depois de criarmos a instância anfitriã, podemos configurar a função de manipulador da stream com o seguinte código:

// This gets called every time a peer connects 
// and opens a stream to this node.
host.SetStreamHandler(protocolID, func(s network.Stream) {
    go writeCounter(s)
    go readCounter(s)
})
Enter fullscreen mode Exit fullscreen mode

Depois de nos ligarmos a um ponto, podemos abrir uma nova_ stream_ fazendo:

s, err := host.NewStream(
    context.Background(), 
    peerAddrInfo.ID, 
    protocolID,
)
if err != nil {
    panic(err)
}

go writeCounter(s)
go readCounter(s)
Enter fullscreen mode Exit fullscreen mode

Encontrando pontos adicionais

As redes ponto a ponto não requerem um servidor central para que as máquinas façam uma ligação. Tudo o que é necessário é o endereço de um dos nós da rede.

Mas o que acontece se esse nó ficar offline? Perdemos a nossa ligação.

Para evitar que isso aconteça, queremos encontrar e recordar o endereço dos pares adicionais na rede.

Cada nó da rede manterá uma lista de pares que conhece. Cada nó anunciará também aos pares que conhecem os seus próprios endereços para que possam ser encontrados por outros.

Como último passo neste post, vamos implementar a descoberta dos pares.

Em primeiro lugar, precisamos de um novo tipo para definir um método a ser chamado quando o serviço de descoberta encontrar pares.

type discoveryNotifee struct{}

func (n *discoveryNotifee) HandlePeerFound(peerInfo peer.AddrInfo) {
    fmt.Println("found peer", peerInfo.String())
}

Enter fullscreen mode Exit fullscreen mode

HandlePeerFound será chamada pelo serviço de descoberta sempre que for encontrado um par (mesmo que já o soubéssemos).

A seguir, criamos uma instância do serviço de descoberta. Neste exemplo, estamos utilizando o protocolo mDNS que tenta encontrar pares na rede local.

discoveryService, err := discovery.NewMdnsService(
    context.Background(),
    host,
    time.Second,
    discoveryNamespace,
    )
if err != nil {
    panic(err)
}
defer discoveryService.Close()

discoveryService.RegisterNotifee(&discoveryNotifee{})
Enter fullscreen mode Exit fullscreen mode

Depois de adicionarmos este pedaço de código, devemos ser capazes de iniciar os nós que se podem ligar diretamente a outros nós e começar a enviar à eles os valores contrários. O nó também procurará periodicamente os seus pares na rede local e imprimirá as suas identificações e endereço.

Código completo

Este é o código final completo que desenvolvemos neste post:

package main

import (
    "context"
    "encoding/binary"
    "flag"
    "fmt"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/libp2p/go-libp2p"
    "github.com/libp2p/go-libp2p-core/host"
    "github.com/libp2p/go-libp2p-core/network"
    "github.com/libp2p/go-libp2p-core/peer"
    "github.com/libp2p/go-libp2p/p2p/discovery"
    "github.com/multiformats/go-multiaddr"
)

const protocolID = "/example/1.0.0"
const discoveryNamespace = "example"

func main() {
    // Add -peer-address flag
    peerAddr := flag.String("peer-address", "", "peer address")
    flag.Parse()

    // Create the libp2p host.
    //
    // Note that we are explicitly passing the listen address and restricting it to IPv4 over the
    // loopback interface (127.0.0.1).
    //
    // Setting the TCP port as 0 makes libp2p choose an available port for us.
    // You could, of course, specify one if you like.
    host, err := libp2p.New(context.Background(), libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0"))
    if err != nil {
        panic(err)
    }
    defer host.Close()

    // Print this node's addresses and ID
    fmt.Println("Addresses:", host.Addrs())
    fmt.Println("ID:", host.ID())

    // Setup a stream handler.
    //
    // This gets called every time a peer connects and opens a stream to this node.
    host.SetStreamHandler(protocolID, func(s network.Stream) {
        go writeCounter(s)
        go readCounter(s)
    })

    // Setup peer discovery.
    discoveryService, err := discovery.NewMdnsService(
        context.Background(),
        host,
        time.Second,
        discoveryNamespace,
    )
    if err != nil {
        panic(err)
    }
    defer discoveryService.Close()

    discoveryService.RegisterNotifee(&discoveryNotifee{h: host})

    // If we received a peer address, we should connect to it.
    if *peerAddr != "" {
        // Parse the multiaddr string.
        peerMA, err := multiaddr.NewMultiaddr(*peerAddr)
        if err != nil {
            panic(err)
        }
        peerAddrInfo, err := peer.AddrInfoFromP2pAddr(peerMA)
        if err != nil {
            panic(err)
        }

        // Connect to the node at the given address.
        if err := host.Connect(context.Background(), *peerAddrInfo); err != nil {
            panic(err)
        }
        fmt.Println("Connected to", peerAddrInfo.String())

        // Open a stream with the given peer.
        s, err := host.NewStream(context.Background(), peerAddrInfo.ID, protocolID)
        if err != nil {
            panic(err)
        }

        // Start the write and read threads.
        go writeCounter(s)
        go readCounter(s)
    }

    sigCh := make(chan os.Signal)
    signal.Notify(sigCh, syscall.SIGKILL, syscall.SIGINT)
    <-sigCh
}

func writeCounter(s network.Stream) {
    var counter uint64

    for {
        <-time.After(time.Second)
        counter++

        err := binary.Write(s, binary.BigEndian, counter)
        if err != nil {
            panic(err)
        }
    }
}

func readCounter(s network.Stream) {
    for {
        var counter uint64

        err := binary.Read(s, binary.BigEndian, &counter)
        if err != nil {
            panic(err)
        }

        fmt.Printf("Received %d from %s\n", counter, s.ID())
    }
}

type discoveryNotifee struct {
    h host.Host
}

func (n *discoveryNotifee) HandlePeerFound(peerInfo peer.AddrInfo) {
    fmt.Println("found peer", peerInfo.String())
}
Enter fullscreen mode Exit fullscreen mode

Conclusão

Desenvolvemos um exemplo básico para mostrar várias características de redes ponto a ponto (ligações diretas, stream de dados e descoberta de pares). Creio que isso pode dar à você uma ideia dos tipos de aplicações que podem ser construídas utilizando este tipo de arquitetura (exemplos bem conhecidos incluem o BitTorrent e o Sistema de Ficheiros InterPlanetary).

E finalmente, espero que tenha gostado do conteúdo e que isto o deixe um pouco mais interessado em começar a trabalhar no mundo das redes ponto a ponto.

Top comments (1)

Collapse
 
beperello profile image
Bernardo Perelló

Boa leitura!