Blog

Websockety vo Flasku

Miroslav Beka - 15.08.2018 - Tipy a triky

Websockety vo Flasku

Ak si sa niekedy stretol s výrazom websocket a chcel by si sa dozvedieť, čo to vlastne je a ako sa to používa v Python aplikácii, tak tento článok je práve pre teba.

Štandardne tvoj prehliadač komunikuje na webe pomocou http protokolu. Klasický http protokol ponúka jednoduchú komunikáciu. Pošle sa request a ako odpoveď dostanem response. Tento klasický komunikačný spôsob nebol dostatočný pre dnešné moderné aplikácie. Bola potreba pre komunikačný kanál, ktorý bude slúžiť na obojsmernú komunikáciu.

HTTP by mal byť viac-menej bezstavový a klient a server medzi sebou komunikujú iba keď treba, inak je spojenie medzi nimi uzavreté. Navyše, prehliadač (klient) musí požiadať server o komunikáciu a server môže na túto žiadosť odpovedať. Tá žiadosť, to je ten http request. Inak server nevie kontaktovať klienta len tak sám od seba.

Pri websocketoch je to inak. Ide o komunikačný kanál, ktorý sa otvorí raz, na začiatku a potom sa používa na komunikáciu klienta a servera v oboch stranách. To znamená, že server môže posielať dáta zároveň čo klient posiela dáta na server. Toto sa odborne volá full-duplex.

Web socket má menší overheat prenosu dát, vie byť real-time a hlavne, server môže posielať dáta na klienta bez toho, aby si ich klient musel explicitne vyžiadať requestom. Toto je užitočné napríklad pri aplikáciách, ktoré zobrazujú real time dáta a server posiela tieto dáta klientovi. Takže ak nastane nejaká zmena dát, server ich proste pošle na klienta.

Toto predtým nebolo možné spraviť iba pomocou http protokolu.

Minimálny príklad

Najlepšie je vyskúšať si tieto koncepty v praxi. Dnes budeme pracovať s Flaskom, knižnicou SocketIO a javascript knižnicami socket.io a jQuery. Budem predpokladať, že Flask aplikácie aspoň trochu poznáš.

Začneme tým, že si vytvoríme nové virtuálne prostredie:

$ mkdir websockets_primer
$ cd websockets_primer
$ virtualenv venv
$ . venv/bin/activate
(venv) $

Nainštalujeme závislosti, ktoré budeme potrebovať:

(venv)$ pip install flask, flask-socketio

V čase písania tohto článku som používal verzie Flask==1.0.2 a Flask-SocketIO=3.0.1.

Keď už máme pripravené prostredie a nainštalované závislosti, spravíme nový súbor server.py

from flask import Flask
from flask import render_template
from flask_socketio import SocketIO
app = Flask(__name__)
app.config["SECRET_KEY"] = "secret"
socketio = SocketIO(app)
@app.route("/")
def index():
    return render_template("index.jinja")
@socketio.on("event")
def handle_event(data):
    print(data)
if __name__ == '__main__':
    socketio.run(app, debug=True)

Na začiatku máme importy ako pre každú inú Flask aplikáciu, avšak pribudlo nám tam from flask_socketio import SocketIO. Tento naimportovaný modul je v podstate to isté ako iné Flask rozšírenia.

Inicializáciu websocketov vo Flask aplikácií spravíme pomocou riadku socketio = SocketIO(app). Pomocou tohto objektu socketio budeme príjmať a odosielať správy.

Minimálna aplikácia by mala mať aspoň jednu stránku. V našom prípade to bude index.jinja. Toto je treba pretože musíme poskytnúť aj klientskú časť našej aplikácie. Tam bude javascript knižnica socketio a nejaké ďalšie funkcie.

Websockety vedia prijímať a posielať správy. Spravíme zatiaľ len prijímanie správ. Pomocou riadku socketio.on("event")definujem handler pre udalosť event. V tomto prípade jednoducho vypíšem dáta na konzolu.

@socketio.on("event")
def handle_event(data):
    print(data)

Posielanie a prijímanie dát na oboch stranách (klient a server) prebieha ako event. Toto je dôležitý fakt, pretože architektúra aplikácie založenej na eventoch (event driven architecture) funguje trošku inak ako klasické volanie funkcie. Nehovorím, aby si mal z toho paniku teraz, ale maj to na pamäti.

Ak poznáš Flask aplikácie, tak spustenie appky vyzerá zväčša takto

if __name__ == "__main__":
    app.run("0.0.0.0", debug=True)

My ale musíme appku spustiť inak, pretože používame websockety. Spustíme ju pomocou objektu socketio, ktorý sme si vytvorili na začiatku.

if __name__ == '__main__':
    socketio.run(app, debug=True)

Teraz musíme ešte vytvoriť 2 súbory. Snažíme sa renderovať index.jinja a taktiež musíme vytvoriť hlavný javascript súbor, do ktorého budeme písať klientskú časť našej websocketovej ukážky.

Vytvorím priečinok templates a do neho súbor index.jinja

<!DOCTYPE HTML>
<html>
<head>
    <title>Websockets test</title>
    <script type="text/javascript" src="//code.jquery.com/jquery-1.4.2.min.js"></script>
    <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/socket.io/1.3.5/socket.io.min.js"></script>
    <script type="text/javascript" src="{{ url_for("static", filename="js/main.js")}}"></script>
</head>
<body>
    <form id="emit_event" method="post">
        <input type="submit" value="emit">
    </form>
</body>
</html>

Dôležité sú 3 importy v hlavičke html dokumentu. Prvý importuje jQuery, druhý importuje knižnicu na prácu so socketmi socketio a posledný import je pre náš main.js súbor, ktorý musíme ešte vytvoriť.

Inak, tento html dokument obsahuje len jeden formulár s jedným tlačítkom. To budeme používať na posielanie správy cez websocket.

Vytvoríme priečinok static v ňom js a v ňom už konečne súbor main.js

Obsah bude vyzerať asi takto:

$(document).ready(function() {
    var url = location.protocol + "//" + document.domain + ":" + location.port
    var socket = io.connect(url);
    $("form#emit_event").submit(function(event) {
        socket.emit("event", "test message");
        return false;
    });
});

Toto je hlavná logika klientskej časti. Z tadeto budeme prijímať a posielať správy cez websockety tak isto ako na serverovej časti.

Pomocou riadku var socket = io.connect(url); sa pripojím na môj server. Následne pomocou jQuery upravím správanie buttonu, aby pri stlačení poslal správu. Na to slúži funkcia socket.emit()

Okej, základ máme hotový a môžeme teraz skúšať posielať správy. Aplikáciu spustím pomocou príkazu:

(venv)$ python server.py
WebSocket transport not available. Install eventlet or gevent and gevent-websocket for improved performance.
 * Serving Flask app "server" (lazy loading)
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
WebSocket transport not available. Install eventlet or gevent and gevent-websocket for improved performance.
 * Debugger is active!
 * Debugger PIN: 478-618-530

Otvorím prehliadač na http://localhost:5000 a zobrazí sa mi jeden button. Keď ho stlačím na konzole mi vyskočí:

test message

Poďme teda preskúmať, aké možnosti nám poskytuje táto knižnica socketio.

Príjmanie správ

Ako som už spomínal, prijímanie správ na oboch stranách prebieha ako event. V Pythone musíme pre takýto event definovať handler. V javascripte používame tzv. callbacky. V princípe ide o to isté, ale každý jazyk má svoje vlastné technické riešenie a my si toho musíme byť vedomí.

Každý event, ktorý chcem prijať musím mať nejaké meno. V príklade sme mali názov event. Môžem ale použiť čokoľvek

@socketio.on("foobar")
def handle_data(data):
    print(type(data))
    print(data)

Taktiež dáta sa automaticky menia na príslušný typ. Ak v javascripte pošlem string, tak string dostanem aj na serveri. Tak isto to platí pre iné dátové typy

...
    $("form#emit_event").submit(function(event) {
        socket.emit("foobar", "message");
        socket.emit("foobar", [1,2,3,]);
        socket.emit("foobar", {data : "message"});
        return false;
    });
...

Po odoslaní udalostí dostanem výpis na serveri

<class 'str'>
message
<class 'list'>
[1, 2, 3]
<class 'dict'>
{'data': 'message'}

Handler taktiež môže mať viacero argumentov

@socketio.on("sum")
def handle_sum(arg1, arg2):
    print(arg1 + arg2)

Upravíme javascriptovú časť a zavoléme event s viacerými argumentami

...
    $("form#emit_event").submit(function(event) {
        socket.emit("sum", 23, 47);
        return false;
    });
...

Namespace patrí medzi ďalšie funkcie, ktoré mám knižnica SocketIO ponúka. Každý event si môžeme rozdeliť podľa namespaceov. To nám dáva ďalšie možnosti organizácie eventov.

@socketio.on("sum", namespace="/math")
def handle_sum(arg1, arg2):
    print(arg1 + arg2)

Avšak pozor! Na strane klienta sa musíme teraz pripojiť na inú url

$(document).ready(function() {
    var namespace = "/math";
    var url = location.protocol + "//" + document.domain + ":" + location.port;
    var socket = io.connect(url + namespace);
    $("form#emit_event").submit(function(event) {
        socket.emit("sum", 23, 47);
        return false;
    });
});

Ďalšia vychytávka je to, že každý event, ktorý pošleme, vie zavolať callback potom, čo sa vykonal. Napríklad z javascriptu pošlem nejaké dáta na server a server mi ešte dodatočne potvrdí, že dáta boli spracované. Aha takto

...
    $("form#emit_event").submit(function(event) {
        var ack = function(arg){console.log(arg)};
        socket.emit("sum", 23, 47, ack);
        return false;
    });
...

Ak chcem, aby sa callback zavolal, musím v Pythone vrátiť nejakú hodnotu z vykonaného handlera => return True

@socketio.on("sum", namespace="/math")
def handle_sum(arg1, arg2):
    print(arg1 + arg2)
    return True

Musím si otvoriť v prehliadači konzolu (ja používam chrome) a keď stlačím tlačítko, dostanem výpis na konzolu


Posielanie správ

Posielať eventy sme už posielali, ale iba z javascriptu. V Pythone to vyzerá veľmi podobne. Používame 2 funkcie send a emit medzi ktorými je zásadný rozdiel.

Najprv musíme importovať z knižnice flask-socketio

from flask_socketio import send
from flask_socketio import emit

upravíme funkciu na sčítavanie

@socketio.on("sum", namespace="/math")
def handle_sum(arg1, arg2):
    value = arg1 + arg2
    print("{} + {} = {}".format(arg1, arg2, value))
    send(value)

a pridáme handler v javascripte aby sme mohli tento event zachytiť.

...
    $("form#emit_event").submit(function(event) {
        socket.emit("sum", 23, 47);
        return false;
    });
    socket.on("message", function(data){
        console.log("received message: " + data)
    });
...

Všimni si, že teraz som použil handler, ktorý spracováva event s názvom message. Nie je to náhoda. Ide totiž o to, že funkcia send posiela tzv. unnamed event. Tieto eventy sa vždy posielajú na handler, ktorý spracúva message.

Narozdiel od funkcie send, funkcia emit posiela už konkrétny event a musíš mu dať názov. Skúsme teda pozmeniť náš príklad

@socketio.on("sum", namespace="/math")
def handle_sum(arg1, arg2):
    value = arg1 + arg2
    print("{} + {} = {}".format(arg1, arg2, value))
    emit("result", value)
...
    socket.on("result", function(data){
        console.log("sum is: " + data)
    });
...

Broadcasting

Veľmi užitočná funkcia je broadcastovanie, čo už z názvu vyplýva, že eventy sa budú vysielať na všetkých pripojených klientov. Dajme tomu, že zmeníme funciu emit na broadcastovanie

@socketio.on("sum", namespace="/math")
def handle_sum(arg1, arg2):
    value = arg1 + arg2
    print("{} + {} = {}".format(arg1, arg2, value))
    emit("result", value, broadcast=False)

Teraz, keď si otvoríš 2 prehliadače a v jednom stlačíš button, výsledok súčtu sa ukáže vo všetkých prehliadačoch


note: callbacky sa pri broadcastovaní nebudú vykonávať

Záver

Websockety majú mnoho využití. Tento článok bol len úvod a prehľad niektorých základných funkcií. V budúcom blogu spravíme malú aplikáciu postavenú na websocketoch.

Máš nejaké otázky k článku? Napíš ju do komentára.


Pd s club 27 12 2013

Miroslav Beka

Ahoj, volám sa Miro a som Pythonista. Programovať som začal na strednej. Vtedy frčal ešte turbo pascal. Potom prišiel matfyz, kadejaké zveriny ako Haskell, no najviac sa mi zapáčil Python.

Od vtedy v Pythone robím všetko. Okrem vlastných vecí čo si programujem pre radosť, som pracoval v ESETe ako automatizér testovania. Samozrejme, všetko v Pythone. Potom som skočil do inej firmy, tam taktiež Python na automatické testovanie aj DevOps. Viacej krát som účinkoval ako speaker na PyCon.sk, kde som odovzdával svoje skúsenosti.

Medzi moje obľúbené oblasti teda parí DevOps, Automatizovanie testovania a web development (hlavne backend).

Okrem programovania sa venujem hlavne hudbe 🤘


7 dôvodov, prečo sa ľudia na vašom webe neregistrujú

Tipy a triky

Weby sú o biznise. A teraz nemám na mysli vývojárske firmy a mladé dynamické “desing studios”. Hovorím o biznise ako takom - o činnosti, pri ktorej sa...

Ako vie programátor samouk dobehnúť tých, čo študovali na univerzite

Tipy a triky

Čo sa učia na univerzitách Si programátor samouk a rozmýšľaš, ako to vyzerá na univerzite? Už máš pozreté všetky online kurzy, 10 vlastných projektov a...

Ako naprogramovať hru Čierny Peter v Jave

Tipy a triky

Programovanie hry Čierny Peter v Jave V tomto tutoriáli si spolu naprogramujeme kartovú hru Čierny Peter. Použijeme programovací jazyk Java a zameriame...