Czy korciło was kiedyś, by do jednego projektu zastosować różne technologie? Każdy język programowania ma swoje plusy i minusy. Nie wszystko i nie wszędzie pisze się tak samo przyjemnie. W standardowych projektach było to niemożliwe. Czasy na szczęście się zmieniają, wszystko idzie do przodu, a my możemy tworzyć "aplikacje rozproszone". Pozwala to pisać różne części backend-u w oddzielnych językach programowania (oczywiście to nie jedyna zaleta, ale skupmy się na razie na tym ;). Osobiście jestem wielkim fanem programowania w .NET Core i Node.js, acz przede mną ocean możliwości. Planuje w niedalekiej przyszłości poznać takie języki jak Rust czy Haskell. Mając aplikację opartą na mikroserwisach mogę wprowadzać elementy z tych technologii bez większego uszczerbku na jakości finalnego produktu.
Zanim jednak rozpoczniemy proces tworzenia aplikacji, musimy podjąć kilka dość ważnych decyzji. Jedną z nich jest komunikacja pomiędzy poszczególnymi częściami aplikacji. Mamy kilka możliwości:
- Biblioteki oparte o protokół "amqp" takie jak np. "RabbitMQ, ZeroMQ"
- Frameworki RPC(Remote Procedure Call) takie jak np. "GRPC" od Google
- Komunikacja za pomocą RESTfull API
Każda z nich ma swoje wady i zalety/. Chwilowo nie zagłębiając się chciałbym przedstawić sposób komunikacji za pomocą biblioteki RabbitMQ i dwóch klientów, jednego napisanego w Node.js i drugiego w .NET Core.
Uruchomienie obrazu RabbitMQ
Rozpoczynając pracę nad komunikacją dwóch serwisów musimy wpierw stworzyć kolejkę RabbitMQ, która będzie służyła nam jako szyna komunikacyjna.
Najprostszym sposobem jest uruchomienie obrazu docker-a, który możecie znaleźć w docker hub-ie https://hub.docker.com/_/rabbitmq/
Pobieramy go za pomocą komendy
$ docker pull rabbitmq
Mając oficjalny obraz rabbitmq na naszej lokalnej maszynie, uruchamiamy go za pomocą komendy
$ docker run -d -p 5672:5672 -p 15672:15672 --name local-rabbit rabbitmq
UWAGA: jeżeli na środowisku lokalnym (do developmentu) nie potrzebujemy ustawiać zmiennej środowiskowej RABBITMQ_ERLANG_COOKIE nie uruchamiajcie w ten sposób obrazu rabbitmq na produkcji! Wyjątek stanowi moment, w którym nie musimy się przejmować takimi rzeczami jak logi, backup kolejki etc.
Jeżeli w tym momencie odpalimy komendę :
$ docker ps
powinno nam wyświetlić listę aktywnych obrazów docker-a. Powinien się tam znajdować wpis dotyczący RabbitMQ.
Czas przejść do implementacji serwisów!
Opis aplikacji
Zanim przejdziemy do klepania kodu, czas na krótki opis aplikacji, którą stworzymy. Będzie składać się z 3 części:
- Node.js -> Serwis, który wyśle wiadomość na szynę komunikacyjną
- .NET Core -> Serwis, który będzie nasłuchiwać na wiadomość na szynie komunikacyjnej
- RabbitMQ -> Szyna komunikacyjna, która zapewni nam możliwość komunikacji pomiędzy serwisami w Node.js i RabbitMQ
Wysyłanie wiadomości w Node.js
Zacznijmy od stworzenie serwisu w Node.js. Użyjemy biblioteki http://www.squaremobius.net/amqp.node/ ,która umożliwi nam komunikację z szyną RabbitMQ po protokole AMQP 0-9-1. Dodajemy ją do projektu za pomocą npm-a
$ npm install amqplib
Po zainstalowaniu paczki zaciągamy ją do naszego skryptu za pomocą funkcji require
const amqp = require('amqplib/callback_api');
Następnie, za pomocą metody "connect", możemy się połączyć z szyną komunikacyjną rabbita po protokole "amqp". Metoda ta w swoim pierwszym argumencie przyjmuje adres szyny komunikacyjnej, natomiast w drugim callback-a, który zostanie wywołany po połączeniu się z szyną rabbita.
amqp.connect('amqp://localhost', (err, conn) => {
// write some magic code to send message
});
W callback-u możemy utworzyć kanał komunikacyjny za pomocą metody "createChannel", która przyjmuje callback, wewnątrz, za pomocą metod:
- assertQueue -> Tworzymy kolejkę wiadomości
- sendToQueue -> Dodajemy wiadomość do kolejki wiadomości
conn.createChannel(function(err, ch) { var q = 'hello'; var msg = 'Hello World!';
ch.assertQueue(q, {durable: false}); setInterval(() => { ch.sendToQueue(q, new Buffer(msg)); console.log(" [x] Sent %s", msg); }); });
Dodatkowo dodałem tutaj wykorzystanie funkcji setInterval, aby proces Node-a wysyłał wiadomości co sekundę.
Dobra, ta część już za nim, mamy gotowy fragment aplikacji napisany w Node.js
Odebranie wiadomości w .NET Core
Zacznijmy od wygenerowania nowej aplikacji konsolowej .NET Core. Robimy to z pomocą komendy
$ dotnet new console -n net-recive
Dodajemy bibliotekę RabbitMQ.Client, która posłuży do komunikacji z szyną wiadomości.
$ dotnet add package RabbitMQ.Client $ dotnet restore
Użycie jej wymaga dodania odpowiednich using-ów:
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System;
using System.Text;
Czas przejść do implementacji komunikatów. Tworzymy instancję połączenia za pomocą klasy ConnectionFactory, gdzie jako propertis podajemy adres naszej szyny wiadomości
var factory = new ConnectionFactory() { HostName = "localhost" };
Za pomocą składni "using" musimy stworzyć obiekt połączenia i kanału. Zamykamy je w składnie using, ponieważ oba elementy implementują interfejs IDisposable
using(var connection = factory.CreateConnection())
using(var channel = connection.CreateModel())
{
// more code in here
}
Jeśli dotarliście tu to dobra wiadomość - teraz już będzie z górki. Pozostało zadeklarować odpowiednią kolejkę:
channel.QueueDeclare(queue: "hello", durable: false, exclusive: false, autoDelete: false, arguments: null);
Stworzyć event handler na odebranie wiadomości z kolejki:
var consumer = new EventingBasicConsumer(channel); consumer.Received += (model, ea) => { var body = ea.Body; var message = Encoding.UTF8.GetString(body); Console.WriteLine(" \[x\] Received {0}", message); }; channel.BasicConsume(queue: "hello", autoAck: true, consumer: consumer);
Dodajmy jeszcze oczekiwanie na wciśnięcie jakiegoś przycisku aby aplikacja nie zakończyła swojego działania przed odebraniem wiadomości.
Console.WriteLine(" Press [enter] to exit.");
Console.ReadLine();
Tak otrzymaliśmy implementację odbierania wiadomości z RabbitMQ w .NET Core. Czas uruchomić aplikacje i sprawdzić, czy wszystko działa jak należy.
Uruchomienie aplikacji
Najpierw powinniśmy uruchamiać .NET Core. Z reguły, kiedy mamy do czynienia z wydarzeniami asynchronicznymi, lepiej wpierw uruchamiać proces odpowiadający za nasłuchiwanie i reakcje na zdarzenia, a dopiero potem odpowiedzialny za ich wysyłanie.
Odpalamy .NET Core. W katalogu z plikiem *.csproj wywołujemy komende:
$ dotnet run
Analogicznie, w nowym oknie terminala, włączamy aplikację Node.js poleceniem:
$ node send.js
Po krótkiej chwili powinniśmy na konsolach ujrzeć wiadomości o wysłaniu i odebraniu danych.
Co ciekawe, dzięki zastosowaniu RabbitMQ, możemy uruchomić procesy w odwrotnej kolejności. Wiadomość zostanie zapisana w kolejce, ale bez uruchomienia procesu .NET Core nigdy nie zostanie odebrana. Dopiero po uruchomieniu aplikacji, klient Rabbita zapyta się kolejki czy nie ma już jakiś wiadomości do niego. Odnajdując taką, wrzuci ją jako zdarzenia do naszej aplikacji.
Podsumowanie
Jak widzicie podstawowa komunikacja pomiędzy serwisami napisanymi w całkowicie różnych technologiach i językach nie jest taka straszna. Dzięki zastosowaniu szyny komunikacyjnej w postaci RabbitMQ mamy możliwość wysyłanie i odbierania wiadomości.
Aplikacja opisana w tym poście jest dostępna na moim GitHub-ie pod linkiem: https://github.com/BlackDante/net-node-rabbitmq
Życzę wam miłego dnia i do następnego posta :)