P2P – komunikacja serwerów nodejs z wykorzystaniem websockets

network - komunikacja

P2P i blockchain – komunikacja przyszłości?

Nadszedł czas na kolejną część. Tworzymy blockchain, a do tego trzeba stworzyć połączenia między wieloma serwerami. Będą kopać i przechowywać transakcje, więc muszą się łączyć i wymieniać dane. Na początek zrobimy coś prostszego. Nie ma sensu się rzucać na głęboką wodę.

Przygotujemy serwery, do których będziemy dodawać kolejne nody oraz przesyłać wiadomości w sieci. Będzie to najlepszy sposób na stworzenie prostej sieci p2p w ramach nauki. W kolejnym artykule zajmiemy się podłączeniem do blockchain, ale najpierw stwórzmy fundament.

Wykorzystam websockety do komunikacji pomiędzy nodami oraz http do zarządzania i pokazania efektów sieci.

Warto też zapoznać się z poprzednimi częściami:

1. Historia jak poznałem blockchain i bitcoin

2. Własny blockchain – implementacja

3. Matematyka tworząca blockchain i bitcoin

4. Kryptografia Blockchain, czyli dlaczego nikt nie może ukraść moich pieniędzy

5. Experty – praktyczny przykład użycia blockchain jako smart contract

6. Implementacja proof of work

7. Implementacja portfela i transakcji

Oraz z innymi projektami opartymi o blockchain: Projekty krypto

HTTP – postawmy serwer

Wystawimy prosty serwer http. Będzie wyświetlał wiadomości i liczbę podłączonych nodów. Wykorzystam express, aby przyspieszyć pracę.

Zaciągamy express do kodu.

const server = require('http').createServer();
const express = require('express');
const app = express();

Następnie umieszczamy informację o liczbie podłączonych nodów oraz przesyłane wiadomości.

app.get('/', (req, res) => {
  const msg = `No. sockets ${getSockets().length}`;
  const msg1 = `${getMessages()}`;

  const whole = `${msg} ${msg1}`;
  res.send(whole);
});

Będziemy potrzebować dwóch funkcji. getSockets oraz getMessages. Będą zwracać ilość podłączonych socketów oraz wiadomości jakie są obecnie na serwerze.
Wysyłamy te informacje do ‚/’, dzięki temu podejrzymy co się dzieje.

Dalej zrobimy proste dodawanie nowych serwerów. Wystawimy końcówkę, która będzie brała z parametrów adres innego serwera i połączy się z nim.

app.get('/add', (req, res) => {
  const addr = req.query.address;
  addNewConnection(addr);
  res.redirect('/');
});

Przyda się nam funkcja addConnection. Stworzy połączenie na websocketach. Po dodaniu połączenia wracamy na root i wyświetlamy stan.

Na koniec przesyłanie wiadomości. Musimy je jakoś dodawać.

app.get('/commit', (req, res) => {
  const msg = req.query.msg;
  addNewMessage(msg);
  res.redirect('/');
});

Pobieramy z parametrów wiadomość jaką chcemy przesłać i wykonujemy funkcję addNewMessage. Wiadomość zostanie dodana i rozesłana do wszystkich podłączonych nodów.

Na koniec tworzymy serwer p2p i http. P2P za chwilę omówimy.

initServer(parseInt(process.argv[2]) + 1);
app.listen(process.argv[2], () => console.log('Listening on http://localhost:' + process.argv[2]));

Pobieramy z argumentów port jaki chcemy użyć do nasłuchiwania. Czyli wykonując polecenie:

node index.js 1000

Odpalimy serwer http na porcie 1000, czyli adres strony będzie miał: http://localhost:1000 a websockety będą działać na porcie o jeden większym, czyli 1001.

Czas na P2P

Podsumowanie:

Potrzebujemy jeszcze:

  • Otrzymania ilości podłączonych serwerów
  • Przechowywane wiadomości
  • Dodawanie nowego połączenia
  • Dodawanie nowej wiadomości
  • Tworzenie serwera

Zacznijmy od tego jak przechowywać takie dane. Najprościej w zmiennej globalnej. Każda funkcja będzie miała do nich dostęp.

// Keep all peers connected to our server
const sockets = [];
// Keep addresses of servers
const addresses = [];

// Keep record of all messages send over the network
const messages = [];

Sockets będą trzymały nam wszystkie aktywne połączenia. Adresses zawierają adresy innych nodów a messages to wiadomości przesyłane między serwerami.

Zróbmy funkcje które przekażą na zewnątrz dane czyli:

const getSockets = () => sockets;
const getMessages = () => messages;

Czas na serwer. Potrzebne importy:

const WebSocketServer = require('ws').Server;
const WebSocket = require('ws');

oraz samo tworzenie serwera:

const initServer = (port) => {
  console.log('P2P server: ', port);
  const server = new WebSocketServer({ port });
  server.on('connection', (ws) => {
    initConnection(ws);
  });
};

Na wybranym porcie tworzymy serwer z websocketem. Jeżeli przyjdzie jakiekolwiek połączenie do niego to wykonujemy funkcję initConnection.

const initConnection = (ws, url = null) => {
  sockets.push(ws);
  console.log('Initializing...');

  handleMessages(ws);
  handleErrors(ws);
  // send information about another servers
  send(ws, { type: 'SOCKETS', addresses });
  if (url) addresses.push(url);
};

Jak widzimy trochę się tutaj dzieje. Po pierwsze dodajemy socket do listy. Potem robimy obsługę wiadomości i oddzielnie błędów. Po wszystkim wysyłamy na serwer informację z adresami jakie są do nas połączone. Na koniec dodajemy url serwera jeśli istnieje.

const handleMessages = (ws) => {
  /*
  { type: 'MESSAGE', data: string }
  { type: 'SOCKETS', addresses: array }
  */
  ws.on('message', (data) => {
    const message = JSON.parse(data);

    switch(message.type) {
      case 'MESSAGE':
        messages.push(message.data);
        console.log('New message');
        break;
      case 'SOCKETS':
        const newConnections = message.addresses.filter(add => !addresses.includes(add));
        newConnections.forEach((con) => {
          addNewConnection(con);
        });
        break;
      default:
        break;
    }
  });
};

Mamy tylko dwa typy wiadomości: MESSAGE, SOCKETS.
Wiadomość SOCKETS przekazuje nam listę adresów do których mamy się połączyć. Dzieje się to na początku działania serwera. Wybieramy wszystkie połączenia których nie mamy na liście i wykonujemy dla każdego addNewConnection.

Co do typu MESSAGE to dodaje on po prostu do naszej listy wiadomości otrzymane dane. Dzięki temu mamy na każdym serwerze te same wiadomości. Nie zawsze są ułożone chronologicznie ale zależy nam tylko na przechowywaniu ich.

const handleErrors = (ws) => {
  ws.on('close', () => removeConnection(ws));
  ws.on('error', () => removeConnection(ws));
};

Obsługa błędów jest raczej prosta i jak coś się złego dzieje to po prostu usuwamy połączenie 🙂 Może nie jest to idealne rozwiązanie, ale w tym przypadku wystarczające.

const addNewConnection = (url) => {
  const ws = new WebSocket(url);
  console.log('Adding new connection with', url);
  ws.on('open', () => initConnection(ws, url));
  ws.on('error', () => console.log('Connection failed. Addr: ', url));
};

Dodawanie nowych połączeń opiera się na podłączeniu się do serwera przy użyciu websocketów. Używamy do tego adresu url. Po połączeniu wywołujemy znane nam już initConnection.

const removeConnection = (ws) => sockets.splice(sockets.indexOf(ws), 1);

Usuwanie połączeń opiera się na wyrzuceniu socketa z naszej listy.

const send = (ws, message) => ws.send(JSON.stringify(message));
const broadcast = (message) => sockets.forEach((socket) => send(socket, message));

Wysyłanie wiadomości to prosta funkcja. Bierzemy naszą wiadomość i zmieniamy ją na stringa. Gdy chcemy rozesłać coś do wszystkich to uruchamiamy broadcast, który iteruje po wszystkich socketach i uruchamia na nich funkcję send.

Pełen kod znajduje się na https://github.com/Patys/P2P_nodejs Zostaw gwiazdkę lub zrób forka i dodaj coś od siebie. Z chęcią omówię kod na github i potencjalne poprawki.

Podsumowanie

Mamy nasz serwer. Możemy przesyłać wiadomości. Wyświetlą się na wszystkich aktywnych nodach. To dobry start do implementacji tego w blockchain. W kolejnych postach postaramy się dodać łączność. Dalej serwer http, żeby wyświetlać stan noda.

Po tym wszystkim będziemy mieli działający blockchain oparty o dowód pracy. A to dopiero początek.

Trzymajcie się. Zamierzam podziałać mocniej w zdecentralizowanych tematach i z pewnością będziecie zadowoleni.


Bądź na bieżąco!

Wszystko wrzucam na facebook: https://www.facebook.com/patysblog/ więc zostaw like i dowiedz się pierwszy o nowym poście.

Lub wesprzyj mnie na steemit: https://steemit.com/@patys/

Programista? Tak. Specjalista od blockchain? Tak. Przynajmniej sam tak uważa. Programuje i tworzy własną kryptowalutę na blogu. Realizuje pomysły i projekty. Nie narzeka i walczy o swoje. Chce być najlepszym programistą. Tworzył aplikację do konsultacji wideo. Wszystko płatne w krypto: Experty Teraz jest programistą w Netguru. Najszybciej rozwijająca się firmą w Europie. Programuje z najlepszymi i stara im się dorównać. Podróżuje z marzeniami o byciu ekspertem i blogerem. Od czegoś jednak trzeba zacząć. Od zapału, zajawki, pomysłu. Od dzisiaj.

4
Dodaj komentarz

avatar
2 Comment threads
2 Thread replies
0 Followers
 
Most reacted comment
Hottest comment thread
3 Comment authors
Patryk Szczygło - PatysBlogMichałMaciej Loper Recent comment authors
  Subscribe  
najnowszy najstarszy oceniany
Powiadom o
Maciej Loper
Gość
Maciej Loper

Świetne 🙂 Jakbyś mógł jeszcze dodać komentarze do kodu, to byłoby super. Pozdrawiam i życzę dalszych sukcesów.

Michał
Gość
Michał

Chciałbym tylko przypomnieć, że używanie portów poniżej numeru 1024 w środowisku Unix i nie tylko, jest niewskazane i uznawane za błąd w sztuce. Porty z zakresu 1 -1024 są zarezerwowane (zgodnie z dyrektywą IANA), aka well known ports. Pozdrawiam.