Létající cirkus (15)

26. 7. 2002
Doba čtení: 8 minut

Sdílet

Dnešní díl seriálu o jazyce Python bude věnován komunikaci mezi procesy. Probereme si klasiku všech UNIXů - roury a sokety. Povíme si, jak vytvořit rouru a k ní příslušející souborový objekt, jak vytvořit soket a kterak ho připojit ke svému protějšku.
ROURY

Jak již řekl náš nadpis, roury jsou klasikou mezi UNIXy. Ještě než si řekneme, co to roury jsou, musíme si vysvětlit, jakými způsoby lze přistupovat k libovolnému souboru. Pro programátory v jazyce C jsou samozřejmostí dva různé přístupy k souborům. Pomocí přímých volání operačního systému a pomocí standardní C knihovny.

Přístup pomocí volání operačního systému je charakteristický používáním deskriptorů souborů. Deskriptor souboru je malé celé číslo, kterým operační systém reprezentuje otevřený soubor. Standardní C knihovna je vrstva vybudovaná nad operačním systémem a programátorovi nabízí luxusnější přístup k souborům. Automaticky se stará například o bufferování dat. Z hlediska programu se již nejedná o číslo, ale o strukturu, obsahující veškeré důležité informace o souboru.

V Pythonu namísto souborů standardní C knihovny používáme souborové objekty, které získáme voláním interní funkce file(), jak jsme si ukázali v XI. dílu. U těchto souborých objektů můžeme přímo ovlivnit i velikost bufferu, který si C knihovna vytvoří pro přístup k souboru, mód souboru apod. Dále souborové objekty nabízejí i funkci fileno(), která vrátí číslo deskriptoru souboru.

Přímý přístup pomocí deskriptorů souborů je realizován funkcemi modulu os: open(), close(), read(), write(), dup() a dalšími. Jednoduchá ukázka použití:

>>> import os
>>> fd = os.open('/bin/bash', os.O_RDONLY)
>>> fd
3
>>> os.read(fd, 10)   # přečteme deset bytů
'\x7fELF\x01\x01\x01\x00\x00\x00'
>>> os.close(fd)

Protože Python je jazyk objektový, je zvykem na vše používat objektová rozhraní, a proto, pokud to vyloženě nevyžadujete, se nedoporučuje tyto nízkoúrovňové funkce používat. Přesto se jim nejspíše nevyhnete. Existuje však funkce, která dokáže z deskriptoru souboru vytvořit souborový objekt (obdobná funkce existuje i ve standardní C knihovně). Touto funkcí je os.fdopen(), které jako první argument předáme číslo deskriptoru souboru a další dva volitelné parametry mají stejný význam jako mód a velikost bufferu u interní funkce file().

Nyní již k rourám. Rouru získáme voláním funkce os.pipe(), která vrátí tuple dvou hodnot. Tyto hodnoty jsou deskriptory souborů, které jsou zvláštním způsobem propojené – cokoli zapíšete do druhého deskriptoru, přečtete zpět z deskriptoru prvního. Toto zdánlivě obyčejné chování pochopíte až ve spojení s funkcí os.fork(), která „zdvojí“ aktuální proces. Od zavolání této funkce se jedná o dva samostatné procesy, přičemž potomek zdědí od svého rodiče všechny otevřené deskriptory souborů. Oba procesy se navzájem „poznají“ podle návratové hodnoty os.fork(), v potomkovi volání této funkce vrátí 0, v rodičovském procesu vrátí PID (číslo procesu) potomka. Pomocí rour se tedy dá realizovat komunikace mezi rodičem a potomkem:

#!/usr/bin/env python
import os
rfd, wfd = os.pipe()   # vytvoříme rouru

pid = os.fork()
if pid == 0:
   # jsme potomek
   data = os.read(rfd, 100)
   print 'Potomek: přečetl jsem %d bytů: "%s"' % (len(data), data)
   os.close(rfd)
else:
    # jsme rodič
      data = 'Ahoj, jak se máte?'
      bytu = os.write(wfd, data)
      print 'Rodič:   zapsal jsem %d bytů' % bytu
      os.close(wfd)

Tento nástin mechanismu rour berte pouze jako informativní, pro detaily k této problematice doporučuji libovolnou knihu o programování pod POSIXem (třeba kniha nakladatelství Computer Press „Linux: Začínáme programovat“, kde se dočtete o rourách a další dnešní látce – soketech, a nejen o nich).

SOKETY

Jak jste si jistě všimli, roury jsou prostředkem pro komunikaci mezi procesy na tomtéž počítači. Logickým rozšířením rour vznikly sokety, umožňující komunikaci mezi procesy na různých počítačích v síti. Podobně jako u rour, kde je přesně rozlišeno, který konec roury je pro čtení a který pro zápis, sokety rozlišují mezi klientem a serverem (i když samotné spojení je obousměrné). Typicky je server program, obsluhující klienty a poskytující jim nějaké služby. Většinou jeden server obsluhuje více klientů.

Server nejprve vytvoří soket, poté tento soket pojmenuje (čímž se stane přístupným pro ostatní procesy), následně vytvoří frontu pro příchozí spojení a začne čekat na klienty. Připojí-li se k serveru klient, server si jeho příchozí připojení převezme a pouze pro něj vytvoří nový soket, zatímco původní může nadále vyčkávat na další klienty. Nebudeme zabíhat příliš do detailů, a proto si ihned ukážeme, kterak vytvořit jednoduchý server:

#!/usr/bin/env python
import socket
HOST = ''
PORT = 50007
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))
s.listen(5)
print 'server ceka na pripojeni'
while 1:
    conn, addr = s.accept()
    print 'pripojen klient:', addr
    while 1:
        data = conn.recv(1024)
        if not data: break
        conn.send(' '.join(data))
    conn.close()

Jak vidíme, soketové služby poskytuje modul socket. Ten obsahuje několik funkcí, které odpovídají jejich protějškům nabízeným standardní C knihovnou. Jak již řekl minulý odstavec, nejprve soket vytvoříme – to za nás udělá funkce socket() modulu socket, které předáme dva argumenty – rodinu protokolu a jeho typ. Dostupné rodiny protokolů jsou AF_INET (sítě IPv4) a AF_INET6 (sítě IPv6). Na Unixech můžeme ještě použít AF_UNIX, což je lokální soket reprezentovaný souborem v souborovém systému. Typem soketu zase určíme, máme-li zájem o spojitou, nebo nespojitou cestu (v případě rodiny AF_INET je spojitá cesta implementována protokolem TCP, který zajišťuje spojitý „bezchybný“ kanál, zatímco nespojitá cesta využívá protokolu UDP). Spojitý typ soketu určíme konstantou SOCK_STREAM, nespojitý SOCK_DGRAM.

Proběhla-li funkce socket() v pořádku, vrátí soketový objekt, který se používá pro další komunikaci se soketem. V případě serveru tedy nejprve soket musíme pojmenovat. S tím nám pomůže metoda bind() soketového objektu. Metoda bind přejímá jako jeden argument adresu, jejíž datový typ se liší podle zvolené rodiny protokolu. Pro rodinu AF_INET jde o tuple o dvou prvcích, první je síťové jméno počítače (případně IP adresa) a druhý pak číslo portu, oba prvky určuje jméno, pod nímž bude náš server naslouchat.

Poněvadž vytváříme server, musíme vytvořit frontu, do níž se budou postupně řadit neobsloužená spojení. Proto zavoláme metodu listen() soketu, jíž předáme celé číslo reprezentující délku fronty.

Pak již můžeme čekat na příchozí připojení od klientů. Toto čekání zajistí metoda accept(), která převezme prvního klienta ve frontě a vrátí tuple o dvou prvcích – první prvek je soket určený pro komunikaci POUZE s tímto klientem, druhý pak informace o připojeném klientovi (ve stejném tvaru, jako jsme ji předali metodě bind()). Jestliže se žádný klient ještě nepřipojil, metoda accept() se zablokuje až do příchodu první žádosti o připojení. Je třeba upozornit, že serverový soket vzniklý funkcí socket() reprezentuje pouze jakousi ústřednu, ze které jsou potom získány sokety pro jednotlivé klienty, do serverového soketu tudíž nemůžeme zapisovat a rovněž z něj nelze číst.

Jestliže jsme již získali soket pro komunikaci s klientem, můžeme od něj číst data pomocí metody recv(), které předáme počet bytů, jež se má přečíst. Jestliže tato data ještě nedorazila, volání se zablokuje do doby, dokud data nedorazí nebo dokud nebude soket uzavřen, načež jsou data vrácena jako řetězec. Stejně tak můžeme data odesílat pomocí metody send(), které naopak předáme řetězec reprezentující data, která se mají do soketu zapsat a která tudíž bude moci přečíst proces na druhém konci spojení. Uložme tedy kód serveru do souboru dejme tomu ‚server.py‘ a spusťme ho pomocí příkazu python server.py.

Tento server čeká na klienta, když se k němu připojí, převezme od něj data, prostrká je mezerami a nakonec je vrátí zpět klientovi. Nyní pro tento server napíšeme klienta:

#!/usr/bin/env python
import socket

HOST = ''
PORT = 50007
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
print 'posilam data serveru'
s.send('Python umi vsechno')
data = s.recv(1024)
s.close()
print 'prijal jsem data od serveru:', `data`

Na straně klienta je práce viditelně jednodušší. Klient opět vytvoří soket a následně ho připojí k serveru (resp. k jeho pojmenovanému soketu). Toto připojení se uloží do fronty, odkud si ho server může vyzvednout a obsloužit. Připojení vykoná metoda connect(), která přejímá stejné parametry jako metoda bind(). Poté, co je soket připojen ke svému protějšku, můžeme posílat a přijímat data standardními metodami recv() a send() tak, jak jsme si je popsali dříve.

Náš klient se připojí k serveru, přičemž použije adresu a číslo portu stejné jako server (prázdný řetězec znamená totéž co ‚localhost‘, tedy rozhraní loopback lokálního počítače). Po připojení mu pošle nějaká data (v našem případě řetězec ‚Python umi vsechno‘) a přečte si zpět odpověď. Nakonec se od serveru odpojí a vytiskne odpověď serveru.

Jak jste si jistě všimli, rozhraní soketového objektu je jiné než rozhraní standardního souborového objektu. Proto každý soketový objekt nabízí metodu makefile(), která přebírá dva argumenty – mód a velikost bufferu – a vytvoří z nich souborový objekt asociovaný se soketem.

V případě, že se rozhodnete používat Unixové souborové sokety, použijte rodinu AF_UNIX a metodám bind() a connect() předejte ne internetovou adresu, ale cestu v souborovém systému k souboru, který bude reprezentovat tento soket.

VLASTNOSTI DESKRIPTORŮ

V dnešním povídání o rourách a soketech několikrát padlo slovo blokování. Blokování můžeme vypnout u soketových objektů pomocí metody setblocking(), která přejímá jeden argument, a to příznak, zda při volání metod typu recv(), případně accept(), a i dalších, má dojít k blokování, nebo ne. U rour si musíme vypomoci modulem fnctl, který obsahuje funkce pro nastavování vlastností deskriptorů. Pro bližší seznámení s tímto modulem nahlédněte do dokumentace jazyka Python.

ict ve školství 24

PŘÍŠTĚ

Jelikož jsme si dnes povídali o síťových službách, v příštím dílu si více řekneme o modulech, reprezentujících aplikační vrstvu. Povíme si o způsobech práce s jednotlivými iternetovými protokoly z jazyka Python. Jmenovitě se dostane na protokoly FTP, HTTP, SMTP a hlavně na knihovnu urllib.