===== Studium podyplomowe Sieci komputerowe =====
==== Konfiguracja PVM ====
1. Dodanie w pliku ~/.rhosts nazw komputerów w celu umożliwienia zdalnego logowania bez podawania hasła. Postać pliku:
nazwa_hosta nazwa_użytkownika
inna_nazwa_hosta nazwa_użytkownika
...
Nazwa użytkownika może być pominięta, jeżeli na komputerze zdalnym i lokalnym ta nazwa jest taka sama
2. Przetestowanie działania rsh:
rsh nazwa_zdalnego_hosta
W rezultacie wykonania polecenia powinniśmy zalogować się na wskazany komputer bez podawania hasła
3. Ustawienie zmiennych środowiskowych:
W pliku ~/.bashrc dopisujemy:
export PVM_ROOT=/usr/lib/pvm3
export PVM_ARCH=`$PVM_ROOT/lib/pvmgetarch`
export PVM_HOME=$HOME/pvm3/bin/$PVM_ARCH/
4. Stworzenie katalogu na pliki wykonywalne:
mkdir -p $PVM_HOME
5. Przygotowanie pliku konfiguracyjnego dla PVM. Przykładowa postać pliku:
lab-sec-1
lab-sec-2
lab-sec-3
6. Uruchomienie pvm:
pvm nazwa_pliku_konfiguracyjnego
7. Odczytanie informacji o dostępnych hostach poleceniem conf
8. Dodanie nowego hosta poleceniem:
add nazwa_hosta
9. Usunięcie jednego z hostów poleceniem:
delete nazwa_hosta
==== Zadanie ====
Napisać aplikację równlogłą do wyznaczania przybliżonej wartości liczby π metodą Monte Carlo. Realizację można oprzeć na poniżej wersji sekewncyjnej:
#include
#include
main(){
unsigned long n = 0, N = 0, i;
double x, y;
for( i = 0; i < 100000; i++ ){
x = drand48();
y = drand48();
N++;
if ( x*x + y*y <= 1 )
n++;
} // for
printf( "%lf\n", 4.0*n/N );
}
1. Zrealizować aplikację zgodnie ze schematem master-slave: master uruchamia procesy slave, które odsyłają do niego wyniki obliczeń.
Program mastera
#include
#include
#include
#define MAX_SLAVE 5
main() {
int tids[MAX_SLAVE];
int num_tids;
unsigned long n, N, suma_n = 0, suma_N = 0;
num_tids = pvm_spawn("pi_slave", NULL, PvmTaskDefault, "", MAX_SLAVE, tids);
if (num_tids <= 0){
fprintf(stderr, "Blad uruchomienia slave'ow\n");
pvm_exit();
exit(1);
}
for(;;){
pvm_recv(-1, -1);
pvm_upkulong(&n, 1, 1);
suma_n += n;
pvm_upkulong(&N, 1, 1);
suma_N += N;
printf( "%lf\n", 4.0*n/N );
}
}
Program slave'a
#include
#include
#include
main(){
unsigned long n = 0, N = 0, i;
double x, y;
int master_tid, my_tid;
my_tid = pvm_mytid();
srand48(my_tid);
master_tid = pvm_parent();
for(;;){
for( i = 0; i < 100000; i++ ){
x = drand48();
y = drand48();
N++;
if ( x*x + y*y <= 1 )
n++;
} // for
//printf( "%lf\n", 4.0*n/N );
pvm_initsend(PvmDataRaw);
pvm_pkulong(&n, 1, 1);
pvm_pkulong(&N, 1, 1);
pvm_send(master_tid, 1);
}
}
2. Zastosować mechanizm dynamicznych grup procesów w celu udostępnienia możliwości dodawania nowych zadań w trakcie obliczeń.
Program mastera
#include
#include
#include
#define MAX_SLAVE 5
main() {
int tids[MAX_SLAVE];
int num_tids;
unsigned long n, N, suma_n = 0, suma_N = 0;
pvm_mytid();
pvm_joingroup("pi");
/*
num_tids = pvm_spawn("pi_slave", NULL, PvmTaskDefault, "", MAX_SLAVE, tids);
if (num_tids <= 0){
fprintf(stderr, "Blad uruchomienia slave'ow\n");
pvm_exit();
exit(1);
}
*/
for(;;){
pvm_recv(-1, -1);
pvm_upkulong(&n, 1, 1);
suma_n += n;
pvm_upkulong(&N, 1, 1);
suma_N += N;
printf( "%lf\n", 4.0*n/N );
}
}
Program slave'a
#include
#include
#include
main(){
unsigned long n = 0, N = 0, i;
double x, y;
int master_tid, my_tid;
my_tid = pvm_mytid();
srand48(my_tid);
//master_tid = pvm_parent();
master_tid = pvm_gettid("pi", 0);
for(;;){
for( i = 0; i < 100000; i++ ){
x = drand48();
y = drand48();
N++;
if ( x*x + y*y <= 1 )
n++;
} // for
//printf( "%lf\n", 4.0*n/N );
pvm_initsend(PvmDataRaw);
pvm_pkulong(&n, 1, 1);
pvm_pkulong(&N, 1, 1);
pvm_send(master_tid, 1);
}
}
Sposób kompilacji
cc pi_master.c -o pi_master -L$PVM_ROOT/lib/LINUX64 -lpvm3 -lgpvm3
cc pi_slave.c -o pi_slave -L$PVM_ROOT/lib/LINUX64 -lpvm3 -lgpvm3
3. Zmodyfikować aplikację w taki sposób, żeby obliczenia rozpoczynały się dopiero po uzyskaniu gotowości do działania przez co najmniej 4 procesy typu slave.
===== Oprogramowanie middleware =====
==== Slajdy do wykładów ====
- Wprowadzenie {{slides:wstep_2s.pdf|2 slajdy/stronę}}, {{slides:wstep_3s.pdf|3 slajdy/stronę}}, {{slides:wstep_4s.pdf|4 slajdy/stronę}}, {{slides:wstep_6s.pdf|6 slajdów/stronę}}
- Wywoływanie zdalnych procedur (RPC) {{slides:rpc_2s.pdf|2 slajdy/stronę}}, {{slides:rpc_3s.pdf|3 slajdy/stronę}}, {{slides:rpc_4s.pdf|4 slajdy/stronę}}, {{slides:rpc_6s.pdf|6 slajdów/stronę}}
- Dostęp do zdalnych obiektów w środowisku Java (Java RMI) {{slides:rmi_2s.pdf|2 slajdy/stronę}}, {{slides:rmi_3s.pdf|3 slajdy/stronę}}, {{slides:rmi_4s.pdf|4 slajdy/stronę}}, {{slides:rmi_6s.pdf|6 slajdów/stronę}}
- Współbieżność w środowisku Java {{slides:java-threads_2s.pdf|2 slajdy/stronę}}, {{slides:java-threads_3s.pdf|3 slajdy/stronę}}, {{slides:java-threads_4s.pdf|4 slajdy/stronę}}, {{slides:java-threads_6s.pdf|6 slajdów/stronę}}
- Idempotentność i bezstanowość (NFS) {{slides:idemp-stateless_2s.pdf|2 slajdy/stronę}}, {{slides:idemp-stateless_3s.pdf|3 slajdy/stronę}}, {{slides:idemp-stateless_4s.pdf|4 slajdy/stronę}}, {{slides:idemp-stateless_6s.pdf|6 slajdów/stronę}}
- Message Oriented Middleware (JMS) {{slides:mom_jms_2s.pdf|2 slajdy/stronę}}, {{slides:mom_jms_3s.pdf|3 slajdy/stronę}}, {{slides:mom_jms_4s.pdf|4 slajdy/stronę}}, {{slides:mom_jms_6s.pdf|6 slajdów/stronę}}
- Przestrzeń krotek (JavaSpaces) {{slides:ts_2s.pdf|2 slajdy/stronę}}, {{slides:ts_3s.pdf|3 slajdy/stronę}}, {{slides:ts_4s.pdf|4 slajdy/stronę}}, {{slides:ts_6s.pdf|6 slajdów/stronę}}
==== Zagadnienia laboratoryjne ====
- Sun RPC/XDR {{slides:sun-rpc-xdr_2s.pdf|2 slajdy/stronę}}, {{slides:sun-rpc-xdr_3s.pdf|3 slajdy/stronę}}, {{slides:sun-rpc-xdr_4s.pdf|4 slajdy/stronę}}, {{slides:sun-rpc-xdr_6s.pdf|6 slajdów/stronę}}\\ [[RPC:licznik|Zdalny licznik w Sun RPC]]
- Wątki w Javie\\ [[Java:prod-kons|Synchronizacja producenta i konsumenta w Javie]]
- Java RMI\\ [[RMI:licznik|Zdalny licznik w Java RMI]]\\ [[RMI:bufor|Bufor 1-elementowy w Java RMI]]\\ [[RMI:bufor_juc|Bufor 1-elementowy w Java RMI z użyciem java.util.concurrent do synchronizacji]]
- JMS
===== Narzędzia przetwarzania rozproszonego =====
==== Slajdy do wykładów ====
- Wprowadzenie {{slides:wstep_2s.pdf|2 slajdy/stronę}}, {{slides:wstep_3s.pdf|3 slajdy/stronę}}, {{slides:wstep_4s.pdf|4 slajdy/stronę}}, {{slides:wstep_6s.pdf|6 slajdów/stronę}}
- Message Passing Interface (MPI) {{slides:mpi-tutorial.pdf|Tutorial Williama Groppa (1 slajd/stronę)}}
- Wywoływanie zdalnych procedur (RPC) {{slides:rpc_2s.pdf|2 slajdy/stronę}}, {{slides:rpc_3s.pdf|3 slajdy/stronę}}, {{slides:rpc_4s.pdf|4 slajdy/stronę}}, {{slides:rpc_6s.pdf|6 slajdów/stronę}}
- Współbieżność w środowisku Java {{slides:java-threads_2s.pdf|2 slajdy/stronę}}, {{slides:java-threads_3s.pdf|3 slajdy/stronę}}, {{slides:java-threads_4s.pdf|4 slajdy/stronę}}, {{slides:java-threads_6s.pdf|6 slajdów/stronę}}
- Wywoływanie zdalnych metod
* dostęp do zdalnych obiektów w środowisku Java (Java RMI) {{slides:rmi_2s.pdf|2 slajdy/stronę}}, {{slides:rmi_3s.pdf|3 slajdy/stronę}}, {{slides:rmi_4s.pdf|4 slajdy/stronę}}, {{slides:rmi_6s.pdf|6 slajdów/stronę}}
* integracja oparta na zdalnych obiektach (CORBA)
- Message Oriented Middleware (JMS) {{slides:mom_jms_2s.pdf|2 slajdy/stronę}}, {{slides:mom_jms_3s.pdf|3 slajdy/stronę}}, {{slides:mom_jms_4s.pdf|4 slajdy/stronę}}, {{slides:mom_jms_6s.pdf|6 slajdów/stronę}}
- Przestrzeń krotek (JavaSpaces) {{slides:ts_2s.pdf|2 slajdy/stronę}}, {{slides:ts_3s.pdf|3 slajdy/stronę}}, {{slides:ts_4s.pdf|4 slajdy/stronę}}, {{slides:ts_6s.pdf|6 slajdów/stronę}}
- Ada
* Wprowadzenie do języka Ada na podstawie opracowania Simona Johnstona "Ada dla programistów C/C++" {{slides:ada95-4-cpp.pdf|Podstawy}}, {{slides:ada95-4-cpp-packages.pdf|Pakiety}}
* {{slides:ada95-pointers.pdf|Wskaźniki}}
* {{slides:ada95-wsplobieznosc.pdf|Współbieżność}}
* {{slides:ada95-distcomp.pdf|Przetwarzanie rozproszone (Aneks E)}}
[[npr:synchronizacja#zadania|Zadania z synchronizacji]]
===== Środowiska przetwarzania rozproszonego (SUM SK) =====
==== Slajdy do wykładów ====
do uzupłenienia
==== Zagadnienia laboratoryjne ====
=== PVM (05.11.2016) ===
Przykładowy program korzystający z PVM: {{:example.tar.gz|}}
== Konfiguracja PVM ==
1. Dodanie w pliku ~/.rhosts nazw komputerów w celu umożliwienia zdalnego logowania bez podawania hasła. Postać pliku:
nazwa_hosta nazwa_użytkownika
inna_nazwa_hosta nazwa_użytkownika
...
Nazwa użytkownika może być pominięta, jeżeli na komputerze zdalnym i lokalnym ta nazwa jest taka sama
2. Przetestowanie działania rsh:
rsh nazwa_zdalnego_hosta
W rezultacie wykonania polecenia powinniśmy zalogować się na wskazany komputer bez podawania hasła
3. Ustawienie zmiennych środowiskowych:
W pliku ~/.bashrc dopisujemy:
export PVM_ROOT=/usr/lib/pvm3
export PVM_ARCH=`$PVM_ROOT/lib/pvmgetarch`
export PVM_HOME=$HOME/pvm3/bin/$PVM_ARCH/
4. Stworzenie katalogu na pliki wykonywalne:
mkdir -p $PVM_HOME
5. Przygotowanie pliku konfiguracyjnego dla PVM. Przykładowa postać pliku:
lab-sec-1
lab-sec-2
lab-sec-3
6. Pobranie i rozpakowanie pliku {{:example.tar.gz|}}
7. Zbudowanie programu master i salve poleceniem make
8. Uruchomienie pvm:
pvm nazwa_pliku_konfiguracyjnego
9. Odczytanie informacji o dostępnych hostach poleceniem conf
10. Dodanie nowego hosta poleceniem:
add nazwa_hosta
11. Usunięcie jednego z hostów poleceniem:
delete nazwa_hosta
12. Uruchomienie przykładowego programu:
spawn -> master
== Zadanie ==
Napisać aplikację równlogłą do wyznaczania przybliżonej wartości liczby π metodą Monte Carlo. Realizację można oprzeć na poniżej wersji sekewncyjnej:
#include
#include
main(){
unsigned long n = 0, N = 0, i;
double x, y;
for( i = 0; i < 100000; i++ ){
x = drand48();
y = drand48();
N++;
if ( x*x + y*y <= 1 )
n++;
} // for
printf( "%lf\n", 4.0*n/N );
}
- Zrealizować aplikację zgodnie ze schematem master-slave: master uruchamia procesy slave, które odsyłają do niego wyniki obliczeń. {{:pi.tar.gz|}}
- Zastosować mechanizm dynamicznych grup procesów w celu udostępnienia możliwości dodawania nowych zadań w trakcie obliczeń. {{:reduce.tar.gz|}}
- Zmodyfikować aplikację w taki sposób, żeby obliczenia rozpoczynały się dopiero po uzyskaniu gotowości do działania przez co najmniej 4 procesy typu slave.
=== RPC ===
== Prosta aplikacja klienta dla rusers ==
1. Pobranie implementacji serwera:
{{:rtime.tar|}}
2. Skompilowanie i uruchomienie serwera.
3. Sprawdzenie listy zarejestrowanych programów RPC:
/usr/sbin/rpcinfo -p
4. Implementacja klienta pobierającego aktualny czas (po stronie serwera). Wykorzystanie funkcji:
int callrpc(char *host, unsigned long prognum,
unsigned long versnum, unsigned long procnum,
xdrproc_t inproc, char *in,
xdrproc_t outproc, char *out);
zadeklarowanej w pliku nagłówkowym:
rpc/rpc.h
Numery programu i procedury pobrać można z pliku nagłówkowego:
rtime.h
gdzie zdefiniowane są stałe:
TIMEPROG
TIMEVER_1
get_time
Procedura get_time nie wymaga parametrów wejściowych (xdr_void), a jako wynik (parametr out) zwraca zmienną int (xdr_int) oznaczającą liczbę zalogowanych użytkowników.
5. Kompilacja programu klienta:
gcc -o rtime_client rtime_client.c
== Usługa zdalnego zabijania procesów ==
Zadanie: napisać aplikację (serwer+klient) umożliwiającą zdalne zabijanie procesów. Klient ma uruchamiać zdalną procedurę, która jako parametr przyjmuje PID procesu do zabicia. Procedura zdalna jako wynik zwraca int określający status zakończenia operacji (0 - proces został unicestwiony, -1 - wystąpił błąd)
1. Utworzenie pliku rkill.x z opisem usługi:
program RKILL_PROG {
version RKILL_VERS_1 {
int rkill(int pid) = 1;
} = 1;
} = 0x22000000;
2. Wygenerowanie części kodu programem:
rpcgen -a -N rkill.x
Program wygeneruje następujące pliki:
rkill.h - plik nagłówkowy z definicjami stałych
rkill_client.c - szkielet aplikacji klienta
rkill_server.c - szkielet aplikacji serwera
rkill_clnt.c - stub klienta
rkill_svc.c - stub serwera
Makefile.rkill
3. Kompilacja z wykorzystaniem Makefile:
make -f Makefile.rkill
4. Implementajca zabijania procesu w pliku rkill_server.c z wykorzystaniem funkcji:
int kill(pid_t pid, int sig) (man 2 kill)
5. Dodanie wyświetlania statusu wykonania po stronie klienta (plik rkill_clietn.c)
6. Dodanie drugiego argumentu dla zdalnej procedury: numer wysyłanego sygnału
a) zmiana definicji w pliku rkill.x:
int rkill(int pid, int signum) = 1;
b) ponowne wygenerowanie kodu programem rpcgen
c) implementajca funkcjonalności serwera i klienta
7. Dodanie procedury echo, która pozwala na wypisanie po stronie serwera tekstu przesłanego przez klienta
8. Przetestowanie działania mechanizmu timeout poprzez:
a) zabicie serwera
b) opóźnienie wysłania odpowiedzi przez serwer przy użyciu funkcji:
unsigned int sleep(unsigned int seconds);
test dla komunikacji po udp oraz po tcp
9. Ustawianie timeout dla klienta:
a) modyfikacja stałej:
TIMEOUT
zdefiniowanej w pliku:
rkill_clnt.c
b) wykorzystanie funkcji:
bool_t clnt_control(CLIENT *cl, int req, char *info);
req = CLSET_TIMEOUT - ustawianie wartości timeout
req = CLSET_RETRY_TIMEOUT - ustawianie czasu dla ponawiania żądania (tylko UDP)
Przykład:
struct timeval tm = { 60, 0 };
clnt = clnt_create (host, RKILL_PROG, RKILL_VERS_1, "udp");
clnt_control(clnt, CLSET_TIMEOUT, &tm);
== Wywołanie asynchroniczne ==
Zadanie: Zmodyfikować rkill tak, aby klient mógł kontynuować przetwarzanie bez oczekiwania na zakończenie obliczeń przez serwer (zakładamy, że klienta nie interesuje odpowiedź od serwera)
=== RMI ===
== HelloWorld ==
1. Utworzyć plik z interfejsem programu HelloWorld
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface HelloWorld extends Remote {
String getString(String name) throws RemoteException;
}
2. Napisać aplikację klienta odwołującego się do zdalnego obiektu implementującego interfejs HelloWorld:
a) rejestr, w którym zarejestrowany został zdalny obiekt znajduje się pod adresem wskazanym przez prowadzącego
b) program klienta pobiera z rejestru referencję na zdalny obiekt korzystając z metody Naming.lookup(String name);
c) program klienta wywołuje zdalną metodę //getString// jako argument podając tekst pobrany z linii komend przy uruchamianiu programu
d) napis zwrócony przez metodę //getString// należy wypisać na standardowe wyjście
e) skompilować program klienta
javac *.java
f) utworzyć plik client.policy z definicją polityk bezpieczeństwa:
grant {
permission java.security.AllPermission;
};
g) uruchomić program klienta
java -Djava.security.policy=client.policy HelloWorldClient
3. Napisać własną implementację serwera HelloWorld:
a) przygotować klasę HelloWorldObj rozszerzający klasę UnicastRemoteObject oraz implementujący interfejs HelloWorld
b) przygotować klasę HelloWorldServer, która w metodzie main utworzy instancję klasy HelloWorldObj, a następnie zarejestruje ten obiekt w rejestrze przy użyciu metody:
Naming.rebind("//127.0.0.1:2100/HelloWorld")
c) skompilować program
d) uruchomić rejestr RMI
rmiregistry nr_portu
e) uruchomić i przetestować serwer
java -Djava.rmi.server.hostname=127.0.0.1 -Djava.security.policy=server.policy HelloWorldServer
4. Przerobić program serwera tak, aby uruchamiał swój własny rejestr
Registry registry = LocateRegistry.createRegistry(2100);
registry.rebind("HelloWorld", obj);
== Przekazywanie obiektów przez wartość i przez referencję ==
1. Zaimplementować klasę ArgumentObject zawierającą prywatne pole //value// typu int oraz metody umożliwiające odczyt i nadpisanie wartości tego pola. Klasa powinna implementować interfejs java.io.Serializable
2. Umieścić implementację ArgumentObject w classpath klienta i serwera
3. Przerobić implementację //HelloWorldObj//, tak aby metoda getString przyjmowała jako argument obiekt klasy //ArgumentObject//, odczytywała wartość zdefiniowanego w tej klasie pola, nadpisywała wartość tego pola i zwracała odczytaną wcześniej wartość w postaci obiektu typu String.
4. Przerobić program klienta, tak aby tworzył obiekt //ArgumentObject// i podawał go jako argument wywołania metody //getString// ze zdalnego obiektu //HelloWorldObj//; następnie klient powinien wypisać odczytany String oraz aktualną wartość pola //value// z klasy //ArgumentObject//
5. Sprawdzić rezultat działania programu
6. Przerobić program, tak aby //ArgumentObject// przekazywany był przez referencję; w tym celu należy:
a) zdefiniować interfejs //Argument// rozszerzający interfejs //Remote// i umieścić go w classpath serwera i klienta,
b) przerobić implementację //ArguentObject//, tak aby rozszerzał //UnicastRemoteObjet// oraz implementował interfejs //Argument//,
c) usunąć klasę //ArgumentObject// z classpath serwera
7. Uruchomić program i zaobserwować zmiany w działaniu
== RemoteComputation ==
Zadanie: przygotować program umożliwiający wykonywanie po stronie serwera dowolnych obliczeń zleconych przez klienta:
1. Serwera udostępnia zdalny obiekt implementujący interfejs Compute
public interface Compute {
Object runTask(Task task, Args args);
}
2. Task jest interfejsem reprezentującym zadanie obliczeniowe, posiada metodę //run(Args args)//, która jest wywoływana wewnątrz metody runTask
3. Klient dostarcza co najmniej dwie implementacje interfejsu Task, np. wykonujące dodawanie i mnożenie; obie klasy muszą implementować interfejs Serializable
4. Klient korzystając z rejestru pobiera referencję na zdalny obiekt Compute i wywołuje metodę //runTask// podając jako argument jedną z implementacji interfejsu Task; wynik zwrócony przez //runTask// jest wypisywany na standardowe wyjście
5. Klient dostarcza serwerowi klasy z implementacją Task poprzez http lub system plików, przykładowe wywołanie programu klienta wygląda wówczas następująco:
java -Djava.rmi.server.codebase=file:///path/to/Task/implementation/ -Djava.security.policy=client.policy ComputeClient
== Accounts ==
Zadanie: zaimplementować program do zdalnego wykonywania operacji na kontach bankowych
1. Serwer udostępnia zdalny obiekt umożliwiający pobranie listy kont bankowych
public interface AccountList extends Remote {
List getList() throws RemoteException;
}
2. Interfejs //Account// rozszerza interfejs //Remote// i umożliwia przeprowadzenie podstawowych operacji na koncie
public interface Account extends Remote {
public Integer getBalance() throws RemoteException; //pobranie stanu konta
public Integer add(Integer amount) throws RemoteException; //dodanie określonej kwoty do stanu konta
public Integer withdraw(Integer amount) throws RemoteException; //pobranie określonej kwoty z konta
}
3. Klient korzystając ze zdalnego obiektu implementującego //AccountList// pobiera listę zdalnych referencji na obiekty typu //Account//
4. Do wybranego konta klient dopisuje określoną kwotę i sprawdza aktualny stan tego konta
5. Sprawdzenie, czy modyfikacja stanu konta zapamiętywana jest między kolejnymi uruchomieniami programu klienta
== Rozwiązania ==
=== JMS ===
1. Pobierz ze strony mq.java.net
Open MQ 4.5 (zip archive, no installer)
2. Uruchom program brokera:
bin/imqbrokerd
3. Zaimplementuj:
a) program HelloProvider wysyłający wiadomość tekstową do kolejki (Queue)
b) program HelloConsumer pobierający synchronicznie wiadomość z kolejki i wypisujący jej treść na ekranie
4. W aplikacji z punktu 3. zmień Queue na Topic i porównaj sposób działania.
5. Zaimplementuj program konsumenta, który odbiera wiadomości z kolejki asynchronicznie.
6. Zaimplementuj konsumenta korzystającego z mechanizmu trwałej subskrypcji.
7. Przetestuj mechanizm filtrowania wiadomości.