P2P – komunikacja serwerów nodejs z wykorzystaniem websockets

09/10/20183 min czytania — w Programowanie, Blockchain

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.

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.

Patryk Szczygło
Programista w Netguru. Bloger od 2017 roku. Miłośnik podróży, książek i elektroniki. Stworzył własny blockchain w JavaScript. Marzy o automatyzacji i robotyce w życiu.