Obsah
1. ØMQ: knihovna pro asynchronní předávání zpráv
2. Základní vlastnosti knihovny ØMQ; rozdíly oproti message brokerům popsaným minule
3. Instalace knihovny ØMQ a PyZMQ
4. Základní testy, zda instalace proběhla korektně
5. Nejjednodušší strategie komunikace: propojení dvou uzlů s jednosměrným posíláním zpráv
6. Implementace klienta i serveru využívajících strategii PAIR s jednosměrným posíláním zpráv
7. Obousměrná komunikace mezi klientem a serverem
8. Implementace klienta i serveru s oboustrannou komunikací (potvrzování zpráv
9. Komunikační strategie publish-subscribe
10. Implementace nástroje pro publikaci zpráv i pro jejich odebírání
11. Role filtru při výběru zpráv jejich odebíratelem
12. Komunikace typu požadavek-odpověď
13. Uvolnění prostředků (context, socket)
14. Využití knihovny ØMQ v programovacím jazyku C
15. Implementace klienta a serveru se strategií PAIR
16. Oboustranné posílání zpráv mezi klientem a serverem
17. Přidání kontrolních podmínek do příkladů naprogramovaných v jazyku C
18. Repositář s demonstračními příklady
19. Odkazy na předchozí části seriálu
1. ØMQ: knihovna pro asynchronní předávání zpráv
V dnešním článku se seznámíme se základními vlastnostmi knihovny ØMQ, která je též známá pod jmény 0MQ, ZMQ či ZeroMQ. Jedná se o relativně nízkoúrovňovou knihovnu vyvinutou v programovacím jazyce C++, která vývojářům nabízí implementaci různých tzv. komunikačních strategií. Tyto strategie je možné využít a popř. i vhodně zkombinovat při implementaci aplikací se složitější architekturou, v níž mezi sebou jednotlivé části komunikují s využitím synchronních či asynchronních zpráv, popř. přes takzvané brokery, s nimiž jsme se již seznámili v souvislosti s projekty Redis Queue [1], Celery [2][3] a RabbitMQ [4][5].
Důležité je, že ØMQ je sice naprogramována v jazyce C++, ovšem rozhraní pro ni existují i pro velké množství dalších programovacích jazyků. Ze zhruba čtyřiceti existujících rozhraní se musíme zmínit především o jazyku C (ten dnes bude použit ve čtyřech jednoduchých demonstračních příkladech), dále pak o jazycích Go, Haskellu, Javě (tím pádem jsou nepřímo podporovány i další jazyky pro JVM), jazycích Julia, Lua, samozřejmě je podporován i Python (i tento jazyk dnes použijeme) či R. Zajímavá je zejména podpora jazyků Julia a R, která znamená, že lze tyto jazyky orientované spíše pro vývojáře, kteří jsou současně profesionály v jiném oboru, relativně snadno propojit s dalšími aplikacemi či službami. Ostatně ØMQ je interně použit v populárním nástroji Jupyter, kde plní přesně tuto roli [6].
2. Základní vlastnosti knihovny ØMQ; rozdíly oproti message brokerům popsaným minule
V předchozích pěti článcích seriálu o systémech pro posílání zpráv (viz též devatenáctou kapitolu) jsme se seznámili s nástroji Redis Queue, Celery a RabbitMQ. Všechny tři zmíněné nástroje můžeme považovat za plnohodnotnou implementací brokera resp. přesněji řečeno message brokera. V praxi to znamená, že tyto systémy postačí nainstalovat a provést jen minimální konfiguraci k tomu, aby bylo možné začít používat plnohodnotné fronty zpráv (message queue) podporující v případě potřeby persistenci zpráv, clusterování, rozesílání zpráv typu fanout, tvorbu pipeline s definovaným tokem zpráv atd. Knihovna ØMQ je v tomto kontextu dosti odlišná, protože se nejedná o přímočarou implementaci brokera, ale o sadu funkcí tvořících základní stavební kameny, s jejichž použitím je možné implementovat různé komunikační strategie – vše v závislosti na tom, jakou architekturu je nutné v daném konkrétním případě navrhnout a použít.
Na knihovnu ØMQ se ovšem můžeme dívat i z jiného pohledu, protože se jedná o abstrakci nad klasickými Berkeley sockety, ovšem s mnoha vylepšeními. V ØMQ je totiž možné zprávy odesílat asynchronně; samotné zpracování zpráv je provedeno na pozadí (ve vlastním vláknu), nemusíme se starat o délku zpráv a o jejich případnou fragmentaci a do určité míry jsme odstíněni od toho, jaký konkrétní protokol bude pro komunikaci použit (IPC, TCP, atd.). Toto zjednodušení se ještě více projeví v těch programovacích jazycích, které se mohou postarat o automatické uvolňování prostředků (což je mj. i případ Pythonu, v němž je vytvořeno prvních pět demonstračních příkladů, s nimiž se seznámíme v navazujících kapitolách).
Knihovna ØMQ podporuje čtyři základní komunikační strategie:
- PAIR – jednosměrné či obousměrné propojení dvou procesů, z nichž každý může běžet na odlišném počítači. Tato strategie se nejvíce přibližuje běžnému použití klasických Berkeley socketů.
- REQ-REP – jedná se o komunikaci typu požadavek-odpověď. Požadavky posílají klienti, odpovědi generuje server, který dokáže obsloužit prakticky libovolné množství klientů.
- PUB-SUB – server zde publikuje zprávy, k jejichž odběru se mohou přihlásit různí klienti. Zprávy je možné filtrovat na straně klientů (tato vlastnost se ovšem ve starších verzích ØMQ odlišuje).
- PUSH-PULL – rozšíření předchozí strategie PUB-SUB: server či servery vytváří zprávy zpracovávané buď přímo připojenými workery nebo celou kolonou (pipeline) workerů.
Ve skutečnosti je ovšem možné knihovnu ØMQ využít i pro implementaci složitějších strategií, například REQ-ROUTER/DEALER-REP apod. Těmito strategiemi se budeme zabývat v navazujícím článku.
3. Instalace knihovny ØMQ a PyZMQ
Demonstrační příklady, které si dnes ukážeme, jsou naprogramovány v Pythonu a taktéž v programovacím jazyku C. Z tohoto důvodu bude instalace nepatrně složitější, protože budeme muset nainstalovat jak nativní část knihovny ØMQ (libzmq), tak i rozhraní pro Python. Nejprve je nutné nainstalovat samotnou ØMQ, pro níž samozřejmě existují balíčky pro většinu majoritních distribucí. Pokud nenaleznete balíček zeromq a zeromq-devel, můžete si ØMQ přeložit sami, viz též popis dostupný na stránce http://zeromq.org/intro:get-the-software.
Ukažme si instalaci nativní části ØMQ na Fedoře. Nainstalujeme balíček libzmq:
$ sudo dnf install zeromq
Knihovna se nainstaluje do adresáře /usr/lib nebo /usr/lib64.
Následuje instalace balíčku zeromq-devel, který bude nutné použít pro překlad a slinkování demonstračních příkladů naprogramovaných v céčku:
$ sudo dnf install zeromq-devel
Nainstalovat by se měl mj. i hlavičkový soubor zmq.h:
$ whereis zmq.h zmq: /usr/include/zmq.h
Po instalaci nativní části knihovny ØMQ ještě budeme potřebovat nainstalovat rozhraní pro programovací jazyk Python. Toto rozhraní je implementováno v knihovně PyZMQ a nainstalujeme ho standardním způsobem s využitím nástroje pip popř. pip3. V praxi bude pro odzkoušení demonstračních příkladů postačovat instalace pro aktivního uživatele, tj. použijeme přepínač –user:
$ pip3 install --user pyzmq Collecting pyzmq Downloading https://files.pythonhosted.org/packages/48/93/59592cb294761aaa40589b544eaa5175446d687ff95beeeb666de60f3274/pyzmq-17.1.2-cp36-cp36m-manylinux1_x86_64.whl (998kB) 100% |████████████████████████████████| 1.0MB 941kB/s Installing collected packages: pyzmq Successfully installed pyzmq-17.1.2
4. Základní testy, zda instalace proběhla korektně
Po (doufejme že úspěšné) instalaci balíčků s ØMQ i PyZMQ si otestujeme korektnost instalace. Nejprve spustíme interpret Pythonu. Jak pro test instalace, tak i pro demonstrační příklady budeme používat Python 3.x, nikoli Python 2.x:
$ python3
Následně provedeme import modulu zmq:
import zmq
V případě, že import proběhl bez chyby (měl by), můžeme se pokusit vypsat jak verzi samotného rozhraní PyZMQ, tak i verzi nainstalované nativní knihovny ØMQ:
print(zmq.pyzmq_version()) 17.1.2 print(zmq.__version__) 17.1.2 print(zmq.zmq_version()) 4.2.5
Druhým testem zjistíme, jestli jsou nainstalovány hlavičkové soubory ØMQ a sdílená knihovna, která se má slinkovat se zdrojovým kódem. Vytvoříme tento zdrojový kód a uložíme ho do souboru s názvem test_0mq.c:
#include <stdio.h> #include <zmq.h> int main() { int major, minor, patch; void *context; void *socket; zmq_version (&major, &minor, &patch); printf("ØMQ version %d.%d.%d\n", major, minor, patch); context = zmq_ctx_new(); socket = zmq_socket(context, ZMQ_PAIR); printf("%p\n", context); printf("%p\n", socket); zmq_close(socket); zmq_ctx_destroy(context); return 0; }
Posléze se pokusíme o překlad, slinkování a spuštění:
$ gcc -c -o test_0mq.o test_0mq.c $ gcc -o test_0mq test_0mq.o -lzmq $ ./test_0mq ØMQ version 4.1.6 0x1620750 0x1623e20
Při překladu by se neměla vypsat žádná chyba o neexistujícím hlavičkovém souboru či o neznámých funkcích. Ani ve fázi linkování by neměla nastat žádná chyba (neznámé symboly atd.).
5. Nejjednodušší strategie komunikace: propojení dvou uzlů s jednosměrným posíláním zpráv
Základní vlastnosti knihovny ØMQ si nejlépe vysvětlíme na několika konkrétních demonstračních příkladech. V prvním příkladu, který bude naprogramován v Pythonu a bude postaven na již výše zmíněném rozhraní PyZMQ, bude využita ta nejjednodušší možná komunikační strategie – bude se jednat o propojení dvou uzlů a přitom se bude využívat pouze jednosměrný přenos zpráv, zde konkrétně od serveru ke klientovi (ovšem stejně by bylo možné role klienta a serveru otočit a posílat zprávy klientem na server).
Jak klient, tak i server nejdříve musí inicializovat takzvaný kontext. Kontext si zjednodušeně řečeno můžeme představit jako kontejner (řekněme seznam – i když je to velmi nepřesné) s jednotlivými sockety, které klient/server otevřel a používá. Pro vytvoření kontextu se použije konstruktor Context z modulu zmq (ten samozřejmě nejdříve musíme naimportovat):
context = zmq.Context()
Jakmile je kontext vytvořen, můžeme se pokusit vytvořit takzvaný socket (nejedná se ovšem o Berkeley socket). Při vytváření popř. konstrukci socketů je nutné určit typ připojení resp. strategii komunikace. V prvním demonstračním příkladu budeme používat strategii nazvanou jednoduše PAIR, tj. propojení dvou uzlů (klienta a serveru). Jak klient, tak i server použijí strategii zmq.PAIR a socket tedy v obou případech zkonstruují následujícím způsobem:
socket = context.socket(zmq.PAIR)
V tomto bodě se ovšem cesty klienta a serveru rozdělí. Mezi oběma typy uzlů totiž existuje jeden podstatný rozdíl – server musí otevřít připojení na určeném portu, zatímco klient se k tomuto portu musí připojit (na jeho straně se v případě TCP/IP samozřejmě taktéž otevře port, ovšem ten nemusíme specifikovat, zvolí se automaticky).
Socket na straně serveru se otevře (spojí s portem) metodou bind, které se musí předat adresa. Na rozdíl od Berkeley socketů je ovšem adresa představována řetězcem s čitelným zápisem adresy, který se skládá ze specifikace protokolu (inproc, ipc, tcp, pgm, epgm) [takzvaný transport], oddělovače „:“ a vlastní adresy, jejíž formát závisí na vybraném protokolu. Pro TCP a při použití portu bude volání bind na straně serveru vypadat takto:
address = "tcp://*:5556" socket.bind(address)
Klient se bude připojovat k serveru na jím otevřený port. Pro připojení se použije metoda connect a adresa může vypadat (pokud server i klient běží na stejném počítači a komunikují lokálně) následovně:
address = "tcp://localhost:5556" socket.connect(address)
Na straně serveru můžeme posílat zprávy například metodou send_string. Tato metoda na svém vstupu akceptuje pythonní řetězce, které převede na sekvenci bajtů, přičemž se předpokládá, že kódování řetězce je UTF-8 (lze ho ovšem i explicitně specifikovat):
for i in range(10): message = "Message #{i}".format(i=i) socket.send_string(message) time.sleep(1)
Na straně klienta se řetězce mohou přijímat metodou recv_string():
while True: message = socket.recv_string() print("Received message '{m}'".format(m=message))
6. Implementace klienta i serveru využívajících strategii PAIR s jednosměrným posíláním zpráv
Úplná verze klienta komunikujícího se strategií PAIR, bude vypadat následovně:
import zmq def connect(port, connection_type): """Otevření socketu se specifikovaným typem spojení.""" context = zmq.Context() socket = context.socket(connection_type) address = "tcp://localhost:{port}".format(port=port) socket.connect(address) print("Connected to {a}".format(a=address)) return socket def start_client(): """Spuštění klienta.""" socket = connect(5556, zmq.PAIR) print("Waiting for messages...") while True: message = socket.recv_string() print("Received message '{m}'".format(m=message)) start_client()
Implementace serveru je nepatrně odlišná:
import zmq import time def bind(port, connection_type): """Otevření socketu se specifikovaným typem spojení.""" context = zmq.Context() socket = context.socket(connection_type) address = "tcp://*:{port}".format(port=port) socket.bind(address) print("Bound to address {a}".format(a=address)) return socket def send_message(socket, message): """Poslání zprávy.""" print("Sending message '{m}'".format(m=message)) socket.send_string(message) def start_server(): """Spuštění serveru.""" socket = bind(5556, zmq.PAIR) for i in range(10): send_message(socket, "Message #{i}".format(i=i)) time.sleep(1) start_server()
Pokud nyní server spustíme, otevře port 5556 a bude očekávat připojení klienta. Po připojení mu začne posílat zprávy (celkem deset zpráv):
$ python3 server.py Bound to address tcp://*:5556 Sending message 'Message #0' Sending message 'Message #1' Sending message 'Message #2' Sending message 'Message #3' Sending message 'Message #4' Sending message 'Message #5' Sending message 'Message #6' Sending message 'Message #7' Sending message 'Message #8' Sending message 'Message #9'
Klient se po svém spuštění připojí na server k jeho portu 5556, přijme prvních deset zpráv a bude čekat v nekonečné smyčce na další zprávy. Ukončit ho můžeme stiskem Ctrl+C:
$ python3 client.py Connected to tcp://localhost:5556 Waiting for messages... Received message 'Message #0' Received message 'Message #1' Received message 'Message #2' Received message 'Message #3' Received message 'Message #4' Received message 'Message #5' Received message 'Message #6' Received message 'Message #7' Received message 'Message #8' Received message 'Message #9'
7. Obousměrná komunikace mezi klientem a serverem
Jakmile je navázána komunikace mezi klientem a serverem s využitím strategie PAIR, je možné zprávy posílat oboustranně. Jinými slovy: všechny metody Socket.send(), Socket.send_string(), Socket.send_json(), Socket.send_pyobj(), Socket.recv(), Socket.recv_string(), Socket.recv_json() atd. můžeme volat jak na straně klienta, tak i na straně serveru. Jak se tedy v tomto případě klient a server odlišují? Server je ten komunikující uzel, který otevírá port metodou Socket.bind(), zatímco klient se na tento port připojuje metodou Socket.connect(). Další rozdíly jsou již plně konfigurovatelné programátorem.
8. Implementace klienta i serveru s oboustrannou komunikací (potvrzování zpráv)
Vyzkoušejme si nyní, jak je možné naprogramovat nepatrně složitější protokol, v němž klient nejdříve přijme zprávu ze serveru a následně tuto zprávu potvrdí zasláním jiné zprávy začínající na „Ack“. Úplná verze klienta vypadá takto:
import zmq def connect(port, connection_type): """Otevření socketu se specifikovaným typem spojení.""" context = zmq.Context() socket = context.socket(connection_type) address = "tcp://localhost:{port}".format(port=port) socket.connect(address) print("Connected to {a}".format(a=address)) return socket def send_message(socket, message): """Poslání zprávy.""" print("Sending message '{m}'".format(m=message)) socket.send_string(message) def start_client(): """Spuštění klienta.""" socket = connect(5556, zmq.PAIR) print("Waiting for messages...") while True: message = socket.recv_string() print(message) send_message(socket, "Acknowledge... " + message) start_client()
Server pochopitelně musí provádět opačnou činnost – nejdříve pošle svoji zprávu a posléze čeká, zda a kdy ji klient potvrdí zasláním své zprávy:
import zmq import time def bind(port, connection_type): """Otevření socketu se specifikovaným typem spojení.""" context = zmq.Context() socket = context.socket(connection_type) address = "tcp://*:{port}".format(port=port) socket.bind(address) print("Bound to address {a}".format(a=address)) return socket def send_message(socket, message): """Poslání zprávy.""" print("Sending message '{m}'".format(m=message)) socket.send_string(message) def receive_response(socket): """Zpracování odpovědi klienta.""" response = socket.recv_string() print("Received response from client: '{r}'".format(r=response)) def start_server(): """Spuštění serveru.""" socket = bind(5556, zmq.PAIR) for i in range(10): send_message(socket, "Message #{i}".format(i=i)) print("Sent, waiting for response...") receive_response(socket) time.sleep(1) print() start_server()
Příklad komunikace z pohledu serveru:
$ python3 server.py Bound to address tcp://*:5556 Sending message 'Message #0' Sent, waiting for response... Received response from client: 'Acknowledge... Message #0' Sending message 'Message #1' Sent, waiting for response... Received response from client: 'Acknowledge... Message #1' Sending message 'Message #2' Sent, waiting for response... Received response from client: 'Acknowledge... Message #2' Sending message 'Message #3' Sent, waiting for response... Received response from client: 'Acknowledge... Message #3' Sending message 'Message #4' Sent, waiting for response... Received response from client: 'Acknowledge... Message #4' Sending message 'Message #5' Sent, waiting for response... Received response from client: 'Acknowledge... Message #5' Sending message 'Message #6' Sent, waiting for response... Received response from client: 'Acknowledge... Message #6' Sending message 'Message #7' Sent, waiting for response... Received response from client: 'Acknowledge... Message #7' Sending message 'Message #8' Sent, waiting for response... Received response from client: 'Acknowledge... Message #8' Sending message 'Message #9' Sent, waiting for response... Received response from client: 'Acknowledge... Message #9'
A naopak komunikace z pohledu klienta:
$ python3 client.py Connected to tcp://localhost:5556 Waiting for messages... Message #0 Message #1 Message #2 Message #3 Message #4 Message #5 Message #6 Message #7 Message #8 Message #9
9. Komunikační strategie publish-subscribe
S komunikační strategií nazvanou publish-subscribe popř. jen zkráceně PUB-SUB či PUB/SUB jsme se již v tomto seriálu několikrát setkali, například při popisu nástroje Redis Queue (RQ), popř. při popisu nástroje RabbitMQ. Připomeňme si, že tato strategie je založena na existenci uzlu (typicky služby), která publikuje nějaké zprávy. Další uzly, jichž může být obecně libovolný počet, se mohou přihlásit k odběru těchto zpráv. Při publikování zpráv se (ve výchozím nastavení) nemusí hlídat, kolik odběratelů zprávy přijalo. Navíc je možné u každého odběratele nastavit filtr (konkrétně prefix zprávy), díky němuž lze přijímat pouze ty zprávy, které odběratele zajímají.
Připojení a vytvoření socketu se nyní bude na serveru (publikujícího zprávy) a klientu (příjemci zpráv) odlišovat. U serveru se použije:
socket = context.socket(zmq.PUB)
Příjemce zpráv naopak použije:
socket = context.socket(zmq.SUB)
Samotné posílání resp. příjem zpráv se lišit nebude, ovšem sémantika bude odlišná.
10. Implementace nástroje pro publikaci zpráv i pro jejich odebírání
Server – publisher – zpráv může v nejjednodušším případě vypadat následovně:
import zmq import time def bind(port, connection_type): """Otevření socketu se specifikovaným typem spojení.""" context = zmq.Context() socket = context.socket(connection_type) address = "tcp://*:{port}".format(port=port) socket.bind(address) print("Bound to address {a}".format(a=address)) return socket def send_message(socket, message): """Poslání zprávy.""" print("Publishing message '{m}'".format(m=message)) socket.send_string(message) def start_publisher(): """Spuštění publisheru.""" socket = bind(5556, zmq.PUB) for i in range(10): send_message(socket, "Message #{i}".format(i=i)) time.sleep(1) start_publisher()
Následuje výpis úplného zdrojového kódu příjemce zpráv:
import zmq def connect(port, connection_type): """Otevření socketu se specifikovaným typem spojení.""" context = zmq.Context() socket = context.socket(connection_type) address = "tcp://localhost:{port}".format(port=port) socket.connect(address) print("Connected to {a}".format(a=address)) return socket def start_subscriber(): """Spuštění příjemce.""" socket = connect(5556, zmq.SUB) socket.setsockopt_string(zmq.SUBSCRIBE, "") print("Waiting for messages...") while True: message = socket.recv_string() print("Received message '{m}'".format(m=message)) start_subscriber()
Ve chvíli, kdy spustíme server (publishera), začne vytvářet zprávy, bez ohledu na případné příjemce:
$ python3 publisher.py Bound to address tcp://*:5556 Publishing message 'Message #0' Publishing message 'Message #1' Publishing message 'Message #2' Publishing message 'Message #3' Publishing message 'Message #4' Publishing message 'Message #5' Publishing message 'Message #6' Publishing message 'Message #7' Publishing message 'Message #8' Publishing message 'Message #9'
Příjemce po svém spuštění začne zprávy postupně odebírat:
$ python3 subscriber.py Connected to tcp://localhost:5556 Waiting for messages... Received message 'Message #1' Received message 'Message #2' Received message 'Message #3' Received message 'Message #4' Received message 'Message #5' Received message 'Message #6' Received message 'Message #7' Received message 'Message #8' Received message 'Message #9'
11. Role filtru při výběru zpráv jejich odebíratelem
V implementaci předchozího klienta jste si mohli povšimnout tohoto programového řádku:
socket.setsockopt_string(zmq.SUBSCRIBE, "")
Tímto řádkem je specifikován filtr, který umožňuje, aby klient přijímal pouze ty zprávy, které se ho týkají. Typicky se ve filtru zadává prefix zprávy, takže například v případě, kdy budeme mít tři příjemce zpráv z ČR, SR a Polska můžeme v každé zprávě použít prefix „cz.“, „sk.“ a „pl.“. Každý z příjemců následně použije nějaký filtr:
socket.setsockopt_string(zmq.SUBSCRIBE, "cz.")
Pokud ovšem žádný filtr nebudeme specifikovat, nebudou ani žádné zprávy přijímány! To znamená, že filtr, i když prázdný, je většinou nutné nastavit. Ostatně se můžete sami pokusit o spuštění následující dvojice server+klient:
import zmq import time def bind(port, connection_type): """Otevření socketu se specifikovaným typem spojení.""" context = zmq.Context() socket = context.socket(connection_type) address = "tcp://*:{port}".format(port=port) socket.bind(address) print("Bound to address {a}".format(a=address)) return socket def send_message(socket, message): """Poslání zprávy.""" print("Publishing message '{m}'".format(m=message)) socket.send_string(message) def start_server(): """Spuštění serveru.""" socket = bind(5556, zmq.PUB) for i in range(10): send_message(socket, "Message #{i}".format(i=i)) time.sleep(1) start_server()
import zmq def connect(port, connection_type): """Otevření socketu se specifikovaným typem spojení.""" context = zmq.Context() socket = context.socket(connection_type) address = "tcp://localhost:{port}".format(port=port) socket.connect(address) print("Connected to {a}".format(a=address)) return socket def start_client(): """Spuštění klienta.""" socket = connect(5556, zmq.SUB) print("Waiting for messages...") while True: message = socket.recv_string() print("Received message '{m}'".format(m=message)) start_client()
Pokud si spustíte serverovou část, bude sice zveřejňovat zprávy, ovšem příjemce je neuvidí a tudíž ani nepřečte!
12. Komunikace typu požadavek-odpověď
Nyní si ukažme další strategii komunikace. Tato strategie se někdy nazývá REQ-REP. Při použití této strategie server přijímá požadavky (request) a odpovídá na ně (response), přičemž je možné, aby požadavky posílalo několik klientů (a jeden klient naopak může posílat požadavky více serverům). Tato velmi asymetrická komunikace se strategií REQ-REP je velmi často používaná, ostatně je na ní založen i protokol HTTP a jeho pozdější varianty.
Nejprve si ukažme, jak vypadá implementace klienta. Ta je velmi jednoduchá, protože postačuje otevřít socket se specifikací strategie REQ, poslat požadavek s využitím Socket.send_string() a přijmout výsledek metodou Socket.recv_string(). Úplný zdrojový kód klienta vypadá takto:
import zmq def connect(port, connection_type): """Otevření socketu se specifikovaným typem spojení.""" context = zmq.Context() socket = context.socket(connection_type) address = "tcp://localhost:{port}".format(port=port) socket.connect(address) print("Connected to {a}".format(a=address)) return socket def send_request(socket, request): """Poslání požadavku.""" print("Sending request '{r}'".format(r=request)) socket.send_string(request) def start_client(): """Spuštění klienta.""" socket = connect(5556, zmq.REQ) send_request(socket, "1") print(socket.recv_string()) print() send_request(socket, "10") print(socket.recv_string()) print() send_request(socket, "xyzzy") print(socket.recv_string()) print() start_client()
Server nyní uděláme nepatrně složitější, protože bude mít za úkol poskytovat jednoduchou službu – vypočte faktoriál čísla, které mu pošleme formou řetězce:
import zmq from math import factorial def bind(port, connection_type): """Otevření socketu se specifikovaným typem spojení.""" context = zmq.Context() socket = context.socket(connection_type) address = "tcp://*:{port}".format(port=port) socket.bind(address) print("Bound to address {a}".format(a=address)) return socket def send_response(socket, response): """Odeslání odpovědi.""" print("Sending response '{r}'".format(r=response)) socket.send_string(response) def receive_request(socket): """Zpracování požadavku klienta.""" request = socket.recv_string() print("Received request from client: '{r}'".format(r=request)) return request def start_server(): """Spuštění serveru.""" socket = bind(5556, zmq.REP) while True: request = receive_request(socket) try: n = int(request) fact = factorial(n) send_response(socket, "{n}! = {f}".format(n=n, f=fact)) except Exception as e: send_response(socket, "Wrong input") start_server()
Příklad komunikace z pohledu serveru:
$ python3 server.py Bound to address tcp://*:5556 Received request from client: '1' Sending response '1! = 1' Received request from client: '1' Sending response '1! = 1' Received request from client: '10' Sending response '10! = 3628800' Received request from client: 'xyzzy' Sending response 'Wrong input' Received request from client: '1' Sending response '1! = 1' Received request from client: '10' Sending response '10! = 3628800' Received request from client: 'xyzzy' Sending response 'Wrong input' Received request from client: '1' Sending response '1! = 1' Received request from client: '10' Sending response '10! = 3628800' Received request from client: 'xyzzy' Sending response 'Wrong input'
Příklad komunikace z pohledu klienta:
$ python3 client.py Connected to tcp://localhost:5556 Sending request '1' 1! = 1 Sending request '10' 10! = 3628800 Sending request 'xyzzy' Wrong input
13. Uvolnění prostředků (context, socket)
Demonstrační příklady ukázané v předchozích kapitolách, byly ve všech případech velmi jednoduché, čitelné a snadno pochopitelné. Je tomu tak mj. i z toho důvodu, že jsme se v nich vůbec nezabývali uvolňováním prostředků (resources), zejména uzavíráním socketu a zrušením kontextu. Tyto operace je totiž možné provést (z pohledu vývojáře) na pozadí, protože objekty typu context a socket jsou správci kontextu (context manager) (zjednodušeně – můžeme je použít v konstrukci with) a současně uzavírají své prostředky ve svých destruktorech, což sice není příliš korektní, ovšem lepší je uzavřít příslušné prostředky pozdě než nikdy :-). Navíc je možné použít dekorátory @context a @socket pro zcela automatické vytvoření/znovupoužití kontextu a vytvoření socketu, což jsou techniky, které si ukážeme příště.
Nic nám ovšem nebrání přepsat si první demonstrační příklad (komunikace dvou uzlů se strategií PAIR) tak, že oba prostředky budeme uvolňovat explicitně.
Upravená implementace klienta, resp. přesněji řečeno jedna z možných a nutno říci, že nepříliš čitelných implementací:
import zmq def connect(port, connection_type): """Otevření socketu se specifikovaným typem spojení.""" context = zmq.Context() socket = context.socket(connection_type) address = "tcp://localhost:{port}".format(port=port) socket.connect(address) print("Connected to {a}".format(a=address)) return context, socket def start_client(): """Spuštění klienta.""" try: context, socket = connect(5556, zmq.PAIR) print("Waiting for messages...") while True: message = socket.recv_string() print("Received message '{m}'".format(m=message)) finally: print("Trying to close socket...") socket.close() print("Trying to destroy context...") context.destroy() start_client()
Upravená implementace serveru:
import zmq import time def bind(port, connection_type): """Otevření socketu se specifikovaným typem spojení.""" context = zmq.Context() socket = context.socket(connection_type) address = "tcp://*:{port}".format(port=port) socket.bind(address) print("Bound to address {a}".format(a=address)) return context, socket def send_message(socket, message): """Poslání zprávy.""" print("Sending message '{m}'".format(m=message)) socket.send_string(message) def start_server(): """Spuštění serveru.""" context, socket = bind(5556, zmq.PAIR) try: for i in range(10): send_message(socket, "Message #{i}".format(i=i)) time.sleep(1) finally: print("Trying to close socket...") socket.close() print("Trying to destroy context...") context.destroy() start_server()
Podobným způsobem je samozřejmě možné upravit i všechny další demonstrační příklady, ovšem tento postup je v praxi možné ještě více zjednodušit s využitím dekorátorů, což si ukážeme v navazujícím článku.
14. Využití knihovny ØMQ v programovacím jazyku C
Z závěrečné části dnešního článku si ukážeme základní způsoby použití knihovny ØMQ z programovacího jazyka C. Kombinace ØMQ+C je v mnoha ohledech vlastně pochopitelná, protože ØMQ je navržena takovým způsobem, aby byla efektivní, a to jak z hlediska využití systémových prostředků (paměť, čas procesoru), tak i z hlediska využití síťového rozhraní. Podobné vlastnosti (až na ono zmíněné síťové rozhraní), samozřejmě nalezneme i u programovacího jazyka C. Zkusme si tedy nejdříve přepsat první demonstrační příklad z Pythonu do jazyka C. Připomeňme si, že se jednalo o příklad, ve kterém se vytvořilo propojení typu PAIR umožňující jednosměrné nebo obousměrné posílání zpráv mezi dvojicí uzlů. Tyto uzly jsme pro jednoduchost nazvali klient a server.
15. Implementace klienta a serveru se strategií PAIR
Nejprve si ukažme céčkovou variantu klienta:
#include <unistd.h> #include <stdio.h> #include <string.h> #include <zmq.h> #define BUFFER_LENGTH 32 int main() { char buffer[BUFFER_LENGTH]; char *address = "tcp://localhost:5556"; void *context = zmq_ctx_new(); void *socket = zmq_socket(context, ZMQ_PAIR); zmq_connect(socket, address); printf("Connected to address %s\n", address); while (1) { int num = zmq_recv(socket, buffer, BUFFER_LENGTH-1, 0); buffer[num] = '\0'; printf("Received '%s'\n", buffer); } zmq_close(socket); zmq_ctx_destroy(context); return 0; }
Následuje céčková varianta serveru:
#include <unistd.h> #include <stdio.h> #include <string.h> #include <zmq.h> #define BUFFER_LENGTH 32 int main() { char buffer[BUFFER_LENGTH]; char *address = "tcp://*:5556"; void *context = zmq_ctx_new(); void *socket = zmq_socket(context, ZMQ_PAIR); zmq_bind(socket, address); printf("Bound to address %s\n", address); int i; for (i=0; i<10; i++) { snprintf(buffer, BUFFER_LENGTH, "Message #%d", i+1); printf("Sending message '%s'\n", buffer); zmq_send(socket, buffer, strlen(buffer), 0); sleep(1); } zmq_close(socket); zmq_ctx_destroy(context); return 0; }
Pro překlad a slinkování jak serveru, tak i klienta použijte soubor Makefile:
CC=gcc LINKER=gcc LIBS=zmq CFLAGS=-O0 -Wall -std=c99 -pedantic LFLAGS=-l$(LIBS) .PHONY: clean all: client server %.o: %.c $(CC) -c -o $@ $(CFLAGS) $< client: client.o $(CC) -o $@ $(LFLAGS) $< server: server.o $(CC) -o $@ $(LFLAGS) $< clean: rm -f client.o \ rm -f server.o \ rm -f client \ rm -f server
Povšimněte si největšího rozdílu mezi implementací v Pythonu a v C. Jedná se o příjem zprávy, kdy je nutné na straně klienta dopředu alokovat buffer s takovou kapacitou, aby do něho bylo možné uložit maximální povolenou a domluvenou délku zprávy. Dále se kontroluje počet přenesených bajtů (zde se délka zprávy skutečně počítá v bajtech) a před tiskem zprávy pro jistotu za její konec doplníme nulu (tím se nám otevírá možnost použít takového klienta, který nepoužívá ukončující nulu – je to náš klient?). Z důvodu co nejjednodušší implementace navíc nepočítáme s možností, že zpráva nebyla z nějakého důvodu přijata. V takovém případě totiž funkce zmq_recv() vrátí hodnotu –1. V reálných případech tedy budeme muset s touto eventualitou počítat:
int num = zmq_recv(socket, buffer, BUFFER_LENGTH-1, 0); if (num < 0) { perror("zmq_recv() failed"); } else { buffer[num] = '\0'; printf("Received '%s'\n", buffer); }
I u posílání zpráv může nastat chyba, na kterou by bylo vhodné reagovat:
int rc = zmq_send(socket, buffer, strlen(buffer), 0); if (rc < 0) { perror("zmq_send() failed"); }
16. Oboustranné posílání zpráv mezi klientem a serverem
Pro zajímavost si ještě ukažme přepis druhého příkladu, v němž byla opět použita strategie PAIR, ovšem komunikace mezi serverem a klientem probíhala oboustranně.
Klientská část s dvojicí bufferů:
#include <unistd.h> #include <stdio.h> #include <string.h> #include <zmq.h> #define BUFFER_LENGTH 32 #define BUFFER2_LENGTH (32 + sizeof("Acknowledge: ")) int main() { char buffer[BUFFER_LENGTH]; char buffer2[BUFFER_LENGTH]; char *address = "tcp://localhost:5556"; void *context = zmq_ctx_new(); void *socket = zmq_socket(context, ZMQ_PAIR); zmq_connect(socket, address); printf("Connected to address %s\n", address); while (1) { int num = zmq_recv(socket, buffer, BUFFER_LENGTH-1, 0); buffer[num] = '\0'; printf("Received '%s'\n", buffer); snprintf(buffer2, BUFFER2_LENGTH, "Acknowledge: %s", buffer); zmq_send(socket, buffer2, strlen(buffer2), 0); } zmq_close(socket); zmq_ctx_destroy(context); return 0; }
Část s implementací serveru, opět využívající dvojici bufferů:
#include <unistd.h> #include <stdio.h> #include <string.h> #include <zmq.h> #define BUFFER_LENGTH 32 #define BUFFER2_LENGTH (32 + sizeof("Acknowledge: ")) int main() { char buffer[BUFFER_LENGTH]; char buffer2[BUFFER2_LENGTH]; char *address = "tcp://*:5556"; void *context = zmq_ctx_new(); void *socket = zmq_socket(context, ZMQ_PAIR); zmq_bind(socket, address); printf("Bound to address %s\n", address); int i; for (i=0; i<10; i++) { snprintf(buffer, BUFFER_LENGTH, "Message #%d", i+1); printf("Sending message '%s'\n", buffer); zmq_send(socket, buffer, strlen(buffer), 0); printf("Sent, waiting for response...\n"); int num = zmq_recv(socket, buffer2, BUFFER2_LENGTH-1, 0); buffer2[num] = '\0'; printf("Received response '%s'\n", buffer2); sleep(1); } zmq_close(socket); zmq_ctx_destroy(context); return 0; }
Soubor Makefile nedoznal dalších změn, takže jen pro úplnost:
CC=gcc LINKER=gcc LIBS=zmq CFLAGS=-O0 -Wall -std=c99 -pedantic LFLAGS=-l$(LIBS) .PHONY: clean all: client server %.o: %.c $(CC) -c -o $@ $(CFLAGS) $< client: client.o $(CC) -o $@ $(LFLAGS) $< server: server.o $(CC) -o $@ $(LFLAGS) $< clean: rm -f client.o \ rm -f server.o \ rm -f client \ rm -f server
17. Přidání kontrolních podmínek do příkladů naprogramovaných v jazyku C
Všechny čtyři zdrojové kódy naprogramované v céčku neprováděly žádnou kontrolu, zda se podařilo získat kontext, otevřít připojení s využitím socketu atd. V praxi je samozřejmě nutné tyto kontroly provádět a současně i uvolňovat všechny prostředky, v našem případě socket(y) a context manager. Zdrojový kód se nám ovšem poněkud znepřehlední, což ostatně můžete posoudit sami při porovnání těchto dvou dvojic příkladů:
Podrobnosti o tom, jak zpracovávat návratové kódy všech funkcí, si opět uvedeme příště.
18. Repositář s demonstračními příklady
Zdrojové kódy všech dnes popsaných demonstračních příkladů naprogramovaných v Pythonu a taktéž v programovacím jazyku C byly uloženy do Git repositáře, který je dostupný na adrese https://github.com/tisnik/message-queues-examples (stále na GitHubu :-). V případě, že nebudete chtít klonovat celý repositář (ten je ovšem – alespoň prozatím – velmi malý, dnes má doslova několik kilobajtů), můžete namísto toho použít odkazy na jednotlivé příklady, které naleznete v následující tabulce.
19. Odkazy na předchozí části seriálu
Následují odkazy na všech pět předchozích částí seriálu o různých způsobem implementace front zpráv:
- Použití nástroje RQ (Redis Queue) pro správu úloh zpracovávaných na pozadí
https://www.root.cz/clanky/pouziti-nastroje-rq-redis-queue-pro-spravu-uloh-zpracovavanych-na-pozadi/ - Celery: systém implementující asynchronní fronty úloh pro Python
https://www.root.cz/clanky/celery-system-implementujici-asynchronni-fronty-uloh-pro-python/ - Celery: systém implementující asynchronní fronty úloh pro Python (dokončení)
https://www.root.cz/clanky/celery-system-implementujici-asynchronni-fronty-uloh-pro-python-dokonceni/ - RabbitMQ: jedna z nejúspěšnějších implementací brokera
https://www.root.cz/clanky/rabbitmq-jedna-z-nejuspesnejsich-implementaci-brokera/ - Pokročilejší operace nabízené systémem RabbitMQ
https://www.root.cz/clanky/pokrocilejsi-operace-nabizene-systemem-rabbitmq/
20. Odkazy na Internetu
- ØMQ – Distributed Messaging
http://zeromq.org/ - ØMQ Community
http://zeromq.org/community - Get The Software
http://zeromq.org/intro:get-the-software - PyZMQ Documentation
https://pyzmq.readthedocs.io/en/latest/ - ZeroMQ is the answer, by Ian Barber
https://vimeo.com/20605470 - ZeroMQ RFC
https://rfc.zeromq.org/ - ZeroMQ and Clojure, a brief introduction
https://antoniogarrote.wordpress.com/2010/09/08/zeromq-and-clojure-a-brief-introduction/ - zeromq/czmq
https://github.com/zeromq/czmq - golang wrapper for CZMQ
https://github.com/zeromq/goczmq - ZeroMQ version reporting in Python
http://zguide.zeromq.org/py:version - A Go interface to ZeroMQ version 4
https://github.com/pebbe/zmq4 - Learning ØMQ with pyzmq
https://learning-0mq-with-pyzmq.readthedocs.io/en/latest/ - Céčková funkce zmq_ctx_new
http://api.zeromq.org/4–2:zmq-ctx-new - Céčková funkce zmq_ctx_destroy
http://api.zeromq.org/4–2:zmq-ctx-destroy - Céčková funkce zmq_bind
http://api.zeromq.org/4–2:zmq-bind - Céčková funkce zmq_unbind
http://api.zeromq.org/4–2:zmq-unbind - Céčková C funkce zmq_connect
http://api.zeromq.org/4–2:zmq-connect - Céčková C funkce zmq_disconnect
http://api.zeromq.org/4–2:zmq-disconnect - Céčková C funkce zmq_send
http://api.zeromq.org/4–2:zmq-send - Céčková C funkce zmq_recv
http://api.zeromq.org/4–2:zmq-recv - Třída Context (Python)
https://pyzmq.readthedocs.io/en/latest/api/zmq.html#context - Třída Socket (Python)
https://pyzmq.readthedocs.io/en/latest/api/zmq.html#socket - Python binding
http://zeromq.org/bindings:python - Why should I have written ZeroMQ in C, not C++ (part I)
http://250bpm.com/blog:4 - Why should I have written ZeroMQ in C, not C++ (part II)
http://250bpm.com/blog:8 - About Nanomsg
https://nanomsg.org/ - Advanced Message Queuing Protocol
https://www.amqp.org/ - Advanced Message Queuing Protocol na Wikipedii
https://en.wikipedia.org/wiki/Advanced_Message_Queuing_Protocol - Dokumentace k příkazu rabbitmqctl
https://www.rabbitmq.com/rabbitmqctl.8.html - RabbitMQ
https://www.rabbitmq.com/ - RabbitMQ Tutorials
https://www.rabbitmq.com/getstarted.html - RabbitMQ: Clients and Developer Tools
https://www.rabbitmq.com/devtools.html - RabbitMQ na Wikipedii
https://en.wikipedia.org/wiki/RabbitMQ - Streaming Text Oriented Messaging Protocol
https://en.wikipedia.org/wiki/Streaming_Text_Oriented_Messaging_Protocol - Message Queuing Telemetry Transport
https://en.wikipedia.org/wiki/MQTT - Erlang
http://www.erlang.org/ - pika 0.12.0 na PyPi
https://pypi.org/project/pika/ - Introduction to Pika
https://pika.readthedocs.io/en/stable/ - Langohr: An idiomatic Clojure client for RabbitMQ that embraces the AMQP 0.9.1 model
http://clojurerabbitmq.info/ - AMQP 0–9–1 Model Explained
http://www.rabbitmq.com/tutorials/amqp-concepts.html - Part 1: RabbitMQ for beginners – What is RabbitMQ?
https://www.cloudamqp.com/blog/2015–05–18-part1-rabbitmq-for-beginners-what-is-rabbitmq.html - Downloading and Installing RabbitMQ
https://www.rabbitmq.com/download.html - celery na PyPi
https://pypi.org/project/celery/ - Databáze Redis (nejenom) pro vývojáře používající Python
https://www.root.cz/clanky/databaze-redis-nejenom-pro-vyvojare-pouzivajici-python/ - Databáze Redis (nejenom) pro vývojáře používající Python (dokončení)
https://www.root.cz/clanky/databaze-redis-nejenom-pro-vyvojare-pouzivajici-python-dokonceni/ - Redis Queue (RQ)
https://www.fullstackpython.com/redis-queue-rq.html - Python Celery & RabbitMQ Tutorial
https://tests4geeks.com/python-celery-rabbitmq-tutorial/ - Flower: Real-time Celery web-monitor
http://docs.celeryproject.org/en/latest/userguide/monitoring.html#flower-real-time-celery-web-monitor - Asynchronous Tasks With Django and Celery
https://realpython.com/asynchronous-tasks-with-django-and-celery/ - First Steps with Celery
http://docs.celeryproject.org/en/latest/getting-started/first-steps-with-celery.html - node-celery
https://github.com/mher/node-celery - Full Stack Python: web development
https://www.fullstackpython.com/web-development.html - Introducing RQ
https://nvie.com/posts/introducing-rq/ - Asynchronous Tasks with Flask and Redis Queue
https://testdriven.io/asynchronous-tasks-with-flask-and-redis-queue - rq-dashboard
https://github.com/eoranged/rq-dashboard - Stránky projektu Redis
https://redis.io/ - Introduction to Redis
https://redis.io/topics/introduction - Try Redis
http://try.redis.io/ - Redis tutorial, April 2010 (starší, ale pěkně udělaný)
https://static.simonwillison.net/static/2010/redis-tutorial/ - Python Redis
https://redislabs.com/lp/python-redis/ - Redis: key-value databáze v paměti i na disku
https://www.zdrojak.cz/clanky/redis-key-value-databaze-v-pameti-i-na-disku/ - Praktický úvod do Redis (1): vaše distribuovaná NoSQL cache
http://www.cloudsvet.cz/?p=253 - Praktický úvod do Redis (2): transakce
http://www.cloudsvet.cz/?p=256 - Praktický úvod do Redis (3): cluster
http://www.cloudsvet.cz/?p=258 - Connection pool
https://en.wikipedia.org/wiki/Connection_pool - Instant Redis Sentinel Setup
https://github.com/ServiceStack/redis-config - How to install REDIS in LInux
https://linuxtechlab.com/how-install-redis-server-linux/ - Redis RDB Dump File Format
https://github.com/sripathikrishnan/redis-rdb-tools/wiki/Redis-RDB-Dump-File-Format - Lempel–Ziv–Welch
https://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Welch - Redis Persistence
https://redis.io/topics/persistence - Redis persistence demystified
http://oldblog.antirez.com/post/redis-persistence-demystified.html - Redis reliable queues with Lua scripting
http://oldblog.antirez.com/post/250 - Ost (knihovna)
https://github.com/soveran/ost - NoSQL
https://en.wikipedia.org/wiki/NoSQL - Shard (database architecture)
https://en.wikipedia.org/wiki/Shard_%28database_architecture%29 - What is sharding and why is it important?
https://stackoverflow.com/questions/992988/what-is-sharding-and-why-is-it-important - What Is Sharding?
https://btcmanager.com/what-sharding/ - Redis clients
https://redis.io/clients - Category:Lua-scriptable software
https://en.wikipedia.org/wiki/Category:Lua-scriptable_software - Seriál Programovací jazyk Lua
https://www.root.cz/serialy/programovaci-jazyk-lua/ - Redis memory usage
http://nosql.mypopescu.com/post/1010844204/redis-memory-usage - Ukázka konfigurace Redisu pro lokální testování
https://github.com/tisnik/presentations/blob/master/redis/redis.conf - Resque
https://github.com/resque/resque - Nested transaction
https://en.wikipedia.org/wiki/Nested_transaction - Publish–subscribe pattern
https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern - Messaging pattern
https://en.wikipedia.org/wiki/Messaging_pattern - Using pipelining to speedup Redis queries
https://redis.io/topics/pipelining - Pub/Sub
https://redis.io/topics/pubsub - ZeroMQ distributed messaging
http://zeromq.org/ - ZeroMQ: Modern & Fast Networking Stack
https://www.igvita.com/2010/09/03/zeromq-modern-fast-networking-stack/ - Publish/Subscribe paradigm: Why must message classes not know about their subscribers?
https://stackoverflow.com/questions/2908872/publish-subscribe-paradigm-why-must-message-classes-not-know-about-their-subscr - Python & Redis PUB/SUB
https://medium.com/@johngrant/python-redis-pub-sub-6e26b483b3f7 - Message broker
https://en.wikipedia.org/wiki/Message_broker - RESP Arrays
https://redis.io/topics/protocol#array-reply - Redis Protocol specification
https://redis.io/topics/protocol - Redis Pub/Sub: Intro Guide
https://www.redisgreen.net/blog/pubsub-intro/ - Redis Pub/Sub: Howto Guide
https://www.redisgreen.net/blog/pubsub-howto/ - Comparing Publish-Subscribe Messaging and Message Queuing
https://dzone.com/articles/comparing-publish-subscribe-messaging-and-message - Apache Kafka
https://kafka.apache.org/ - Iron
http://www.iron.io/mq - kue (založeno na Redisu, určeno pro node.js)
https://github.com/Automattic/kue - Cloud Pub/Sub
https://cloud.google.com/pubsub/ - Introduction to Redis Streams
https://redis.io/topics/streams-intro - glob (programming)
https://en.wikipedia.org/wiki/Glob_(programming) - Why and how Pricing Assistant migrated from Celery to RQ – Paris.py
https://www.slideshare.net/sylvinus/why-and-how-pricing-assistant-migrated-from-celery-to-rq-parispy-2 - Enqueueing internals
http://python-rq.org/contrib/ - queue — A synchronized queue class
https://docs.python.org/3/library/queue.html - Queues
http://queues.io/ - Windows Subsystem for Linux Documentation
https://docs.microsoft.com/en-us/windows/wsl/about - RestMQ
http://restmq.com/ - ActiveMQ
http://activemq.apache.org/ - Amazon MQ
https://aws.amazon.com/amazon-mq/ - Amazon Simple Queue Service
https://aws.amazon.com/sqs/ - Celery: Distributed Task Queue
http://www.celeryproject.org/ - Disque, an in-memory, distributed job queue
https://github.com/antirez/disque - rq-dashboard
https://github.com/eoranged/rq-dashboard - Projekt RQ na PyPi
https://pypi.org/project/rq/ - rq-dashboard 0.3.12
https://pypi.org/project/rq-dashboard/ - Job queue
https://en.wikipedia.org/wiki/Job_queue - Why we moved from Celery to RQ
https://frappe.io/blog/technology/why-we-moved-from-celery-to-rq - Running multiple workers using Celery
https://serverfault.com/questions/655387/running-multiple-workers-using-celery - celery — Distributed processing
http://docs.celeryproject.org/en/latest/reference/celery.html - Chains
https://celery.readthedocs.io/en/latest/userguide/canvas.html#chains - Routing
http://docs.celeryproject.org/en/latest/userguide/routing.html#automatic-routing - Celery Distributed Task Queue in Go
https://github.com/gocelery/gocelery/ - Python Decorators
https://wiki.python.org/moin/PythonDecorators - Periodic Tasks
http://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html - celery.schedules
http://docs.celeryproject.org/en/latest/reference/celery.schedules.html#celery.schedules.crontab - Pros and cons to use Celery vs. RQ
https://stackoverflow.com/questions/13440875/pros-and-cons-to-use-celery-vs-rq - Priority queue
https://en.wikipedia.org/wiki/Priority_queue - Jupyter
https://jupyter.org/ - How IPython and Jupyter Notebook work
https://jupyter.readthedocs.io/en/latest/architecture/how_jupyter_ipython_work.html