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
}
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]
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
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())
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
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
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
}
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)
}
}
}
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())
}
}
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)
})
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)
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())
}
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{})
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())
}
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.
Oldest comments (1)
Boa leitura!