Table of Contents

Laboratorul 07. Arhitecturi pe bază de mesaje. Chat interactiv

Obiective:

În cadrul acestui laborator, veți parcurge mai întâi un breviar teoretic pentru a înțelege cum funcționează RabbitMQ, care sunt principalele concepte ale acestei tehnologii, cum să rulați o instanță de RabbitMQ local și cum să o utilizați cu Python. În această primă parte nu este necesar să rulați nimic; materialul oferit are rol de suport pentru partea a doua.

Partea a doua constă într-o serie de exerciții care vor aplica tot ceea ce s-a explicat în breviarul teoretic și vă vor ajuta să aprofundați cunoștințele prin exemple practice.

Partea 1: Breviar teoretic:

Introducere în RabbitMQ

RabbitMQ este un broker de mesaje open-source bazat pe protocolul AMQP (Advanced Message Queuing Protocol) utilizat pentru a permite aplicațiilor să comunice eficient și asincron prin trimiterea de mesaje între ele. Acesta este folosit pentru a decupla componentele aplicațiilor, facilitând astfel comunicarea, scalabilitatea și reziliența în arhitecturi distribuite și microservicii.

Conținut

1. Caracteristici principale RabbitMQ
2. Componentele RabbitMQ
  1. Producer (Producător): Trimite mesaje către RabbitMQ.
  2. Exchange: Primește mesajele de la producători și decide cum să le ruteze către queue-uri pe baza regulilor de rutare.
  3. Queue (Coadă): Stochează mesajele până când acestea sunt preluate de consumatori.
  4. Consumer (Consumator): Preia mesajele din queue și le procesează.

3. Tipuri de Exchange
4. Cum funcționează RabbitMQ
  1. Producerea mesajelor: Producătorii creează mesaje și le trimit către un exchange.
  2. Rutarea mesajelor: Exchange-ul distribuie mesajele către queue-uri în funcție de regulile de rutare configurate.
  3. Stocarea mesajelor: Queue-urile stochează mesajele temporar până când sunt preluate de consumatori.
  4. Consumarea mesajelor: Consumatorii se conectează la queue-uri și preiau mesajele pentru a le procesa.

Pentru a conecta o aplicație Python la RabbitMQ, vei folosi biblioteca pika, care este un client AMQP popular pentru Python. Aceasta permite crearea conexiunilor, configurarea exchange-urilor și queue-urilor, și trimiterea sau recepționarea mesajelor prin RabbitMQ.

Instalare RabbitMQ

Pentru a putea folosi o instanță de RabbitMQ, aveți două posibilități: să o instalați pe calculatorul personal (Installing RabbitMQ | RabbitMQ) sau să rulați următoarea comandă Docker Compose: docker compose up folosind următorul fișier docker-compose.yamml:

version: '3.8'

services:
  rabbitmq:
    image: rabbitmq:3-management
    container_name: rabbitmq
    environment:
      RABBITMQ_DEFAULT_USER: guest
      RABBITMQ_DEFAULT_PASS: guest
    ports:
      - "5672:5672"  # Port for AMQP protocol
      - "15672:15672"  # Port for RabbitMQ management UI
    volumes:
      - rabbitmq_data:/var/lib/rabbitmq
    networks:
      - rabbitmq_network

volumes:
  rabbitmq_data:

networks:
  rabbitmq_network:
    driver: bridge

Comandă rulare pentru docker-compose.yaml prezentat anterior:

docker-compose up -d

Pentru o introducere în Docker, consultați: Quickstart | Docker Docs

Interfață grafică

RabbitMQ oferă și o interfață grafică de management (Management UI) pentru o monitorizare și administrare mai ușoară a mesajelor, queue-urilor și exchange-urilor, fiind accesibilă în mod implicit pe portul 15672 (http://localhost:15672). User-ul și parola implicite sunt guest/guest, însă aceste credențiale funcționează doar pentru accesul de pe localhost; pentru accesul extern este necesară configurarea unor utilizatori noi.

RabbitMQ Tutorial: Connecting with Python

Pentru a conecta o aplicație Python la RabbitMQ, vei folosi biblioteca pika, un client AMQP popular pentru Python. Aceasta permite crearea conexiunilor, configurarea exchange-urilor și queue-urilor, și trimiterea sau recepționarea mesajelor prin RabbitMQ.

Pași pentru conectarea la RabbitMQ folosind Python
1. Instalarea Bibliotecii pika

Asigură-te că ai instalat biblioteca pika. Poți face acest lucru rulând următoarea comandă:

pip install pika
2. Configurarea unei Conexiuni la RabbitMQ

Pentru a te conecta la RabbitMQ, va trebui să definești parametrii de conectare. În mod implicit, RabbitMQ rulează local pe portul 5672, iar contul implicit este guest cu parola guest.

import pika
 
# Configurarea conexiunii la RabbitMQ
credentials = pika.PlainCredentials("guest", "guest")
connection_params = pika.ConnectionParameters(host='localhost', port=5672, credentials=credentials)
connection = pika.BlockingConnection(connection_params)  
channel = connection.channel()
 
print("Conexiunea la RabbitMQ a fost stabilită.")

Acest cod:

Notă: Dacă RabbitMQ rulează pe un alt server sau port, schimbă localhost cu adresa IP și/sau specifică portul prin ConnectionParameters.
3. Crearea unui Queue

Pentru a trimite și primi mesaje, ai nevoie de un queue (coadă) în care să fie stocate mesajele. Poți crea un queue folosind queue_declare:

queue_name = 'my_queue'
channel.queue_declare(queue=queue_name)
 
print(f"Queue-ul '{queue_name}' a fost creat.")

Acest cod va crea un queue numit my_queue (sau se va conecta la el dacă deja există). Queue-ul este o structură FIFO (First In, First Out) care stochează mesajele până când sunt preluate de consumatori.

4. Trimiterea unui Mesaj în Queue

Pentru a trimite un mesaj, vei folosi metoda basic_publish:

message = "Salut din RabbitMQ!"
channel.basic_publish(exchange="", routing_key=queue_name, body=message)
 
print(f"Mesaj trimis: {message}")
5. Primirea unui Mesaj din Queue

Pentru a primi mesaje, definești un consumator. Acesta va utiliza un callback pentru a procesa fiecare mesaj primit:

def callback(ch, method, properties, body):
    print(f"Mesaj primit: {body.decode()}")
 
channel.basic_consume(queue=queue_name, on_message_callback=callback, auto_ack=True)
 
print("Aștept mesaje...")
channel.start_consuming()
6. Închiderea Conexiunii

După ce ai terminat de trimis și primit mesaje, închide conexiunea la RabbitMQ pentru a elibera resursele:

connection.close()
print("Conexiunea la RabbitMQ a fost închisă.")
Extra

Pentru a vedea un exemplu complet despre cum două aplicații, un sender și un receiver, comunică folosind RabbitMQ, precum și pentru a citi explicații mai detaliate, vizitați acest tutorial: Hello World.

În cadrul acestuia veți găsi exemplificat întregul proces de conectare la RabbitMQ: crearea unei cozi (queue), trimiterea și recepția mesajelor, precum și închiderea conexiunii.

Exemplu de Utilizare a unui Exchange ''fanout'' în RabbitMQ

Până acum am văzut cum putem crea o coadă (queue), să trimitem un mesaj pe aceasta și să consumăm acel mesaj. În cadrul exemplului anterior, am presupus că un mesaj este trimis către o singură coadă. În ceea ce urmează, vom face ceva complet diferit – vom livra un mesaj către mai mulți consumatori (îl vom publica pe mai multe queue-uri). Acest pattern este cunoscut sub denumirea de „publish/subscribe”.

În acest exemplu, vom:

  1. Crea un exchange de tip fanout numit broadcast_exchange.
  2. Crea două queue-uri (queue1 și queue2).
  3. Conecta ambele queue-uri la exchange fără a folosi routing key (nefiind necesară pentru exchange-ul fanout).
  4. Trimite un mesaj către exchange, care va fi livrat tuturor queue-urilor conectate.
import pika
 
# Conectarea la RabbitMQ
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
 
# 1. Crearea unui exchange de tip 'fanout'
exchange_name = 'broadcast_exchange'
channel.exchange_declare(exchange=exchange_name, exchange_type='fanout')
print(f"Exchange-ul '{exchange_name}' de tip 'fanout' a fost creat.")
 
# 2. Crearea queue-urilor
queue1 = 'queue1'
queue2 = 'queue2'
channel.queue_declare(queue=queue1)
channel.queue_declare(queue=queue2)
 
# 3. Legarea queue-urilor la exchange fără routing key (nu este necesară pentru fanout)
channel.queue_bind(exchange=exchange_name, queue=queue1)
channel.queue_bind(exchange=exchange_name, queue=queue2)
print(f"Queue-urile '{queue1}' și '{queue2}' au fost conectate la exchange-ul '{exchange_name}'.")
 
# 4. Trimiterea unui mesaj către exchange-ul 'fanout'
message = "Salut tuturor consumatorilor!"
channel.basic_publish(exchange=exchange_name, routing_key='', body=message)
print(f"Mesaj trimis către exchange-ul '{exchange_name}': {message}")
 
# Închiderea conexiunii
connection.close()
Rezultatul Așteptat

- Mesajul „Salut tuturor consumatorilor!” va ajunge atât în queue1, cât și în queue2.

Utilizare

Exchange-urile fanout sunt utile pentru aplicații de tip difuzare (broadcast), unde același mesaj trebuie să fie recepționat de toate queue-urile conectate. Acest tip de exchange este ideal pentru scenarii precum notificările de sistem, unde toate instanțele trebuie să fie informate simultan.

Partea 2 - Exerciții:

Descărcați proiectul de pe GitHub: Messenger

Pentru a instala toate dependențele necesare pentru laboratorul de astăzi, rulați: pip install -r requirements.txt. Fișierul requirements.txt poate fi descărcat de aici: requirements.txt.

0. Rulați local o instanță de RabbitMQ, fie folosind docker-compose-ul pus la dispoziție mai sus, fie instalând RabbitMQ direct pe mașina voastră. Atenție, dacă optați pentru instalarea directă pe mașina voastră, va fi necesar să instalați separat plugin-ul care oferă acces la interfața web - Plugin.

1. Creează un script Python care să permită citirea textelor de la tastatură și publicarea acestora într-un exchange nou. (care este creat programatic după realizarea cu succes a conexiunii la RabbitMQ). Scriptul va citi textul de la utilizator și va publica fiecare mesaj în exchange-ul RabbitMQ (in mod fanout).

try:
   while True:
      message = input("Message: ")
      channel.basic_publish(exchange=exchange_name, routing_key='', body=message)
finally:
    connection.close()

2. Creează un script Python care să creeze un queue, să-l lege la exchange-ul din exercițiul 1 și să afișeze fiecare mesaj primit. Scriptul va crea un queue nou și îl va asocia (binding) cu exchange-ul creat în exercițiul anterior. Pentru fiecare mesaj primit în queue, scriptul va afișa conținutul acestuia în consolă.

channel.queue_bind(exchange=exchange_name, queue=queue_name)

3. Chat interactiv

Această aplicație presupune un server Flask care interacționează cu RabbitMQ și expune o interfață web pentru utilizator.

Implementare: Pornind de la codul pentru această aplicație de mesagerie, implementează funcționalitățile necesare, marcate cu TODO-uri in fisierul message_service.py, pentru ca aceasta să funcționeze complet:

Folosește instanța locală și rulează scriptul initialize.py din laborator, care configurează 3 utilizatori: stefan, alex și maria.

Testare: Pentru a testa aplicația, puteți rula două instanțe ale aplicației pe porturi diferite, pentru a simula doi utilizatori care folosesc aplicația pe dispozitive diferite. În cadrul testării, punctul a) poate fi verificat individual, iar punctele b) și c) vor fi testate împreună.

Pentru a rula aplicația, folosește comanda: python app.py sau flask run.

Aceasta va rula implicit pe portul 5000, iar pentru a vizualiza interfața pusă la dispoziție de aceasta, deschideți http://localhost:5000.

Pentru a rula aplicația pe un alt port decât cel implicit, rulați flask run --port 5001.

Testare:

Host: acc-atomicsoftware.swedencentral.cloudapp.azure.com
User: student
Parola: întreabă coordonatorul de laborator

În cadrul acestui exercițiu, nu vei declara queue-uri sau exchange-uri, deoarece acestea sunt deja create pe instanța din cloud.

Important: Când te conectezi la instanța din cloud, utilizatorii vor avea formatul nume_prenume_grupa. Dacă nu ești sigur care este utilizatorul pregătit pentru tine, conectează-te la:

http://acc-atomicsoftware.swedencentral.cloudapp.azure.com:15672

cu utilizatorul si parola de mai sus și caută în tab-ul Queues and Streams după numele tău. Queue-ul o să se numească chatUser.username, iar tu trebuie să folosești doar username pentru a te conecta. În situația în care totuși nu te regăsești, poți folosi unul dintre utilizatorii următori: bob, alice, stefan, alex.

În situația în care întâmpinați o eroare similară cu “connection lost” sau “unable to pop from an empty queue” atunci când începeți să consumați mesaje, este indicat să adăugați o metodă nouă pentru a crea un channel și să utilizați un channel nou creat atunci când începeți să ascultați.

  def getChannel(self):
    connection = pika.BlockingConnection(self.connection_params)
    return connection.channel()

Diagrame explicative ale exercițiului 3:

Click to display ⇲

Click to hide ⇱