Pracovní stanice v laboratoři fyziky

Na škole se mě a Tomáši Břinčilovi dostalo možnosti zařídit počítače v jedné z učeben, konkrétně v laboratoři fyziky – jak po hardwarové stránce, tak po softwarové. V tomto článku bych chtěl popsat, jak se dají softwarově zařídit pracovní stanice, ke kterým mají přístup studenti, aby byly jednoduše udržovatelné.

Jako operační systém bylo vybráno Ubuntu. Jednak vzhledem k preferenci na Linuxu založených systémech, druhak vzhledem k dobrým zkušenostem konkrétně s Ubuntu.

V laboratoři fyziky je osm pracovních stanic a server. Stanice jsou za NATem, server funguje jako router. Nicméně toto není podmínkou fungování dále popisovaného řešení.

Studenti na počítačích mají přístup k internetu, mohou k nim připojovat přenosná paměťová zařízení. Ve školní síti sice studenti mají své účty, kam si mohou ukládat data, avšak z počítačů v laboratoři se na ně nedostanou. Místo toho jsme pro to, aby studenti skladovali svá data pomocí internetových služeb k tomu určených. Veškeré změny, které studenti na počítačích učiní, jsou při dalším startu (resp. naběhnutí grafického prostředí) navráceny do původního stavu.

Po startu počítačů se nespouští GDM jako v základní instalaci Ubuntu. Místo toho je v /etc/init následující konfigurační soubor Upstartu:

start on (runlevel [2345] and net-device-up IFACE=eth[0123456789])
stop on runlevel [!2345]

script
    exec su - $(hostname) -c startx
end script

Grafické prostředí je spouštěno ve víceuživatelském runlevelu, ovšem až tehdy, když bylo zprovozněno připojení k internetu. Na každé stanci je vytvořen uživatel shodný s hostname počítače. V jeho home adresáři jsou tyto skripty. .xinitrc spustí Erlang node, kterýžto vyhledá ostatní počítače v síti, na kterých běží Erlang node s nbh serverem. Podle názvu identifikuje master node (který v tomto případě běží na serveru v laboratoři), čím zjistí jeho IP adresu. Na master node běží webserver na portu 80 a v rootu má home.tar.gz. Stanice zabalený home stáhne, vymaže všechno z dosavadního home a stažený home rozbalí. Poté spustí gnome-session a uživatel může normálně pracovat.

Erlang node na stanicích zde není pouze za účelem najít master node. Průběžně, při zapínání a vypínání stanic, si udržuje seznam sousedů. A umožňuje na nich spouštět sdílení plochy. Každá stanice může ostaní vyzvat, aby sdílely její plochu přes VNC.

Lpění na termínech

V komentářích na Zdrojáku ke článku o Comet si jeden čtenář posteskl, k čemu tak dlouhý článek, když princip je tak jednoduchý.

Představme si, že by neexistoval termín Comet. Vznikl by pak článek v takové podobě, v jaké vyšel? Absence termínu by podle mě vedla k tomu, že by se článek mnohem méně zabýval teorií, a naopak sváděla k tomu, že by vyšel prakticky zaměřený text popisující konkrétní implementaci (princip je tak jednoduchý, že ho není potřeba příliš rozvádět). Termíny mají jakousi magickou moc na sebe navázat veškerou pozornost.

Nedivím se, že si všichni stěžují na to, že pořád slyší o NoSQL. NoSQL, NoSQL, NoSQL! Pořád dokola jenom tahle zkratka. Je omýlána tak, až představa, co pod sebou vlastně skrývá, je mlhavá, až se úplně vytratí.

Dříve bych řekl, dejte blbci problém k řešení a on vám vyjmenuje 10 návrhových vzorů. Vyloženě špatné mi totiž přijde, že když si někdo nastuduje návrhové vzory, pak redukuje řešení všech problémů na jejich napasování do některého z vzorů. Vzory se staly mnohem důležitější než principy objektové kompozice a dekompozice – a vůbec takové věci jako přemýšlení.

Bez termínů a nazývání věcí by to nešlo. Ale nesmíme se nechat strhnout lpěním jen na nich, když důležité je to, co se pod nimi skrývá. Comet není důležitý, hlavní je princip dlouhotrvajících spojení. Na NoSQL se vyprdněte, pochopte jen, že neexistují jen relační databáze, že je tu na výběr – a samosebou, snažte se vyberte si tu správnou. A návrhové vzory nevyřeší všechno, nač si vzpomenete, používejte je jako prostředek ke zrychlení komunikace, ale nenechte se jimi ovládat – inspirujte se existujícími řešeními (třebas i vzory), implementujte to svoje a názvy návrhových vzorů používejte pro zrychlení komunikace (i když si myslím, že třídní a control flow diagramy poslouží účelu předání mentálního modelu mnohem lépe).

Nepřekombinovávejte control flow

Objektové programování, jů, jé, supér… Ale nenechávejte se tím tak unést. Teď jsem se hrabal ve zdrojácích PHPUnitu, protože vykazoval chování, které jsem považoval (a stále považují) za chybné. A čert aby se v tom kódu vyznal!

Začalo detektivní pátrání po tom, co vlastně spouští test. Jako první sahám po třídách, které mají v názvu Runner. Po prozkoumání zjišťují, že ty se nestarají o spouštění, jsou mezivrstvou mezi konfiguračními volbami a TestSuite. TestSuite::run() spouští metodu run() na jednotlivých testech, potomcích TestCase.

Jenže TestCase::run() udělá inicializací a zavolá run() na instanci TestRestult, kteroužto dostala jako argument, a předá ji $this. TestRestult::run() pozmění něco ze svých vnitřností a spustí TestCase::runBare(). Když jsem si tímhle honem za vlastním ocasem prošel poprvé, úplně se mi zamotala šiška.

Vrstvěte správně kód. Když každá vrstva bude volat pouze vrstvy nižší, je sledování a pochopení control flow mnohem jednodušší. Jestli kód pochopíte vy, budete drsní. Až ho jednoduše pochopí ostatní, budete hrdinové.


Ještě předtím, než se testy Nette začaly přepisovat do PHPUnitu, jsem se hrabal v NetteTest, protože a) mi to vůbec nefungovalo, b) a když jsem to rozchodil, neprošlo to vlastními testy. Udělal jsem nikdy neuveřejněnou proof-of-concept úpravu TestRunneru, která spustila několik worker instancí PHP, poslala jim testy, které mají spustit, a sesbírala výsledky. Mám dvoujádro, takže očekávatelný výsledek – testy proběhly přibližně za polovinu času. (Navíc testy cache v Nette, které jsem hlavně spouštěl, hojně používají sleep(). Ještě lepšího času by se podle mě dalo docílit, kdyby se o jejich spouštění staral scheduler, který by si při každém běhu testů sbíral statistická data. A předpokládáme-li, že testy poběží pokaždé přibliženě stejnou dobu, scheduler by je mezi workery mohl rozvrhnout tak, aby se nestalo, že by se nečekalo na jednoho workera, který permanentné chrní, nebo na jednoho, jenž má naopak práce až nad hlavu.)

Aby PHPUnit mohl testy paralelizovat, musel by doznat zásadních změn. Vypadá to však, že je to na dobré cestě.

Několikajádrové procesory jsou dnes standardem. Žel testovací frameworky toho nevyužívají. Přitom testy, vždyť to je téměř učebnicový příklad úlohy, jež může být paralelizována!

Pořádný balíčkovací systém pro PHP!

Ovšemže, je tu PEAR. Ale kdo používá PEAR? Skoro nikdo. PEAR jsem použil jedině tehdy, když jsem potřeboval nainstalovat PHPUnit (protože je to, krom DIY, jediný podporovaný způsob instalace).

Proč se PEAR nepoužívá? A proč bych měl používat balíčky ze systému, který sám nevyužívám na zveřejňování mého kódu? PEAR se snaží o to, aby v hlavním repozitáři na pear.php.net byl pouze dobrý prověřený kód. Myšlenka je to skvělá. Avšak znamená to, že protlačit tam svou knihovnu je běh na dlouhou trať. Nedostanu-li se do oficiálního PEAR repozitáře, můžu si samozřejmě zřídit svůj. Ale já chci jenom udělat jeden balíček a ten vyslat do světa… Pokud chci zveřejnit svůj kód, PEAR není nástroj, který mi to umožní, nýbrž překážka, jež mi to ztíží. Vystavit někde Git/Mercurial/SVN repozitář, někam nahrát archiv se zdrojáky – to je mnohem jednodušší.

Dobrý balíčkovač je pacman, balíčkovací systém Arch Linuxu. Existuje několik oficiálních repozitářů (core /základ systému/, extra /další důkladně ověřené balíčky/, community /balíčky, po kterých uživatelé touží/, testing /nejčertvější verze balíčků/) a dále je tu AUR. A AUR je opravdu príma věc! Arch Linux používá velice jednoduchý sestavovací systém s tzv. PKGBUILDy. PKGBUILD obsahuje název balíčku, popis, odkud ho získat (adresa archivu se zdrojovými kódy, Git/Mercurial/SVN repozitář) atp. a funkci, která balíček sestaví. Neuvěřitelně jednoduché na napsání (viz wiki), používání (příkazy makepkg a pacman -U) a sdílení (viz AUR). O tom, jestli je balíček dobrý a měl by se dostat do oficiálních repozitářů, se nerozhoduje v nějakém návrhovém řízení. Prostě, pokud se balíček uživateli líbí, hlasuje, aby se do ofiko repozitáře dostal. Až obdrží balíček dostatečný počet hlasů, popř. chce-li se o něj nějaký TU (trusted user) starat, dostane se do community. Ovšem, jestli balíček je, nebo není v oficiálních repozitářích je docela jedno, protože yaourt je jednoduchou nadstavbou nad makepkg a pacmanem, která automatizuje celý proces od stažení PKGBUILDu z AURu, přes nainstalování sestaveného balíčku až po aktualizace již nainstalovaných.

yaourt (resp. AUR + makepkg + pacman) je nástroj, a dle Unixové filozifie, se zaměřuje na jednu věc a tu dělá pořádně – správu balíčků – řešení distribuce nechává na autorech programů. Na stránkách PEARu se píše: „PEAR is a framework and distribution system for reusable PHP components.“ PEAR se snaží o moc věcí najednou. A jelikož minimálně jednu dělá vážně špatně (nutnost se přizpůsobit jeho způsobu distribuce)…

Balíčkovací systém pro PHP musí být v rukou programátora nástrojem, dobře použitelným nástrojem. Musí tedy akceptovat to, že vystavit někde archiv se zdrojovými kódy, nebo repozitář verzovacího systému je nejjednodušší způsob, jakým zveřejňovat kódy, musí se všemi těmito cestami pracovat, ne si prosazovat svou vlastní. Jelikož v PHP není běžné, aby programy/knihov­ny/frameworky závisely na vlastních C extenzích, není potřeba nic pro sestavování balíčků. Spíše než balíčkovací systém by se tedy mělo jednat o nástroj, kterým dokážu jednoduše získat PHP kód z různých zdrojů. Řekněme, že z Nette chci použít cache. Pak by mělo být pro balíčkovací systém jednoduché získat zdrojáky z Git repozitáře, prohledat je, vyjmout z nich pouze věci z jmenného prostoru Nette\Caching (+ všechny závislosti) a nainstalovat.

Každý framework a knihovna momentálně musí nějak řešit autoloading. A každý ho samozřejmě řeší jinak. PEAR na to šel specifikací coding style. Velcí hráči na to teď jdou podobně. Ale jak se ukázalo i v případě PEARu, není to dlouhodobé řešení. Místo toho by autoloading měl řešit balíčkovací systém a měl by využívat něco na způsob RobotLoaderu z Nette. Balíčkovací systém ví o tom, co je v jakém balíčku, co v PHP a může tedy řešit kolize mezi jmény, může si udržovat statistické informace o načítaných třídách a jejich načítání tak optimalizovat, může dokonce optimalizovat načítaný kód, může just-in-time zkompilovat třebas parser v phpegu a načíst ho atd. atp. Hlavní však zůstává myšlenka, že každá komponenta aplikace by se měla starat a o jednu věc a tu dělat pořádně, dobře – framework, ať si frameworčí (a nesere se do načítání), knihovna knihovničí (a nestará se o načítání) a balíčkovací systém se stará o načítání a správu ostatních komponent.

Konkurenčnost, paralelismus a PHP

Konkurenční programování, konkurenčnost, paralelismus. Pokud se člověk zajímá o programování, tyhle a další podobné výrazy v dnešní době slyší pořád. A co PHP, jak se s nimi kamarádí?

Nejdříve menší upřesnění pojmosloví (protože většinou bývají výše uvedené termíny volně zaměňovány). Konkurenční programování, jedním slovem konkurenčnost, nechť obé znamená, že se program skládá z více navzájem interagujících částí, které mohou (což znamená, že ale nemusí) běžet souběžně, tedy paralelně. Paralelismus nechť označuje tu skutečnost, že více procesů běží ve stejný čas.

Bude to znít zvlášťně, ale já bych o PHP řekl, že je kokurenční i paralelní by default. Když běží jako modul webového serveru, popř. CGI, poběží většinou několik instancí PHP interepreteru současně. Stejně tak v případě FastCGI. Servíruje-li tedy webové stránky pomocí PHP, každý požadavek je vyřizován konkurenčně a o paralelismus je postaráno implicitně sereverem / FastCGI process managerem.

Jediný rozdíl, než kdybyste vytvářeli nové procesy voláním API operačního systému, je v tom, že pokud chete vytvořit nový paralelní proces, musíte o to požádat server HTTP požadavkem. (Vyměnili jste správce procesů /server místo operačního systému/, musíte tedy vyměnit rozhraní, jakým se o procesy staráte /HTTP místo OS API/.) Pro komunikaci s dalšími procesy můžete využít roury, nebo sockety (pravděpodobně Unix domain sockets) anebo prosté soubory.

V případě, že PHP běží v CLI, o procesy se stará operační systém, nebudete tedy volat webserver, ale přímo funkce OS. Můžete využít pcntl extenzi. Ta poskytuje práci s procesy v Unixovém stylu. Ke komunikaci mezi procesy můžete využít všechny z výše uvedených způsobů, nebo stream_socket_pair(). Jestliže nemáte k dispozici pcntl, můžete použít proc_open() (určite se bude hodit proměnná $_SERVER['_'], ve které se nachází cesta k aktuálně spustěné binárce PHP) a s vytvořeným procesem komunikovat některou z metod uvedených v případě PHP na serveru, nebo standardním vstupem a výstupem (ještě by se dal vytvořit socket pomocí stream_socket_pair() a předat ho jako deskriptor s vyšším číslem /např. 3/, ovšem problém asi bude s Windows kompatibilitou /viz poznámka v dokumentaci proc_open()/ a nenašel jsem způsob, jak v PHP otevřít socket podle čísla deskriptoru).

V PHP sice nenajdete žádnou knihovnu pro práci s vlákny, nicméně to vám nebrání, abyste mohli zpracovávat věci paralelně. Nebudete sice disponovat sdílenou pamětí jako v případě vláken, avšak v poslední době je patrný trend odklonu od sdílení paměti a příklonu k odděleným procesům, které komunikují pomocí zpráv. Tak i zní jedno z mott Go – „share memory by communicating“ (sdílejte pamět komunikací /posíláním zpráv/).

Pokud znáte (programujete / programovali jste v) Erlangu (doporučuji k přečtení Learn You Some Erlang for Great Good), nebo Go (menší úvod do Go), určitě víte, že tyto jazyky mají zabudované konstrukty pro práci s procesy (ne procesy ve smyslu procesů operačního systému, nýbrž konceptu konkurenčnosti). V Erlangu pošlete procesu zprávu pomocí Pid ! "hello, world!" a přijmete konstrukcí receive. Go má zase kanály, kdy zprávu pošlete do kanálu ch <- "hello, world!" a na druhém konci přijmete napsáním <-ch.

PHP nic takového nemá. Ale nemusí to být zas takový problém, jak by se mohlo na první pohled zdát. Při programování parsovacího stroje phpegu mě napadl poněkud šílený nápad – zkompilovat PHP do instrukcí podobného stroje a ty následně zkompilovat do PHP. PHP je totiž ke kódu kompilovanému do bytekódu pro Zend engine dost hr a nezdržuje se nějakými optimalizacemi – v době, kdy to byl ostrovní jazyk topící se v moři HTML, byla tato strategie asi nejlepší; avšak doba se změnila a jsou tací blázni, kdo v tom ošklivém lepicím jazyce píší dokonce aplikace, které by měly i chvíli běžet! Napadlo mě tedy, jestli bych dokázal skript zrychlit zkompilováním do instrukcí virtuálního stroje, ty bych zoptimalizoval a vygeneroval z nich PHP kód a ten by se až zpracovával Zend enginem.

Když ale stejně potřebuji naparsovat PHP, proč do něj ještě něco nepřidat? (Možná taky ubrat…?) A jednou z věcí by byla podpora konkurenčních jazykových procesů v jednom OS procesu. Jak jsem psal v článku o parsovacím stroji, uložit momentální stav výpočtu není problém. Navrátit se k němu jakbysmet.

Řekněme, že přidáme do jazyku konstrukty podobné těm v Erlangu. Pak by klasický ping-pong mezi procesy mohl vypadat následovně:

class Ping
{
    var $pid;
    function __construct($pid) { $this->pid = $pid; }
}

class Pong {}

class Finished {}

function pong()
{
    for (;;) {
        receive {
            Ping $p {
                echo "pong received ping\n";
                $p->pid ! new Pong;
            }

            Finished $f {
                echo "pong finished\n";
                return;
            }
        }
    }
}

function ping($n, $pong_pid)
{
    while ($n--) {
        $pong_pid ! new Ping(self());

        receive {
            Pong $p {
                echo "ping received pong\n";
            }
        }
    }

    $pong_pid ! new Finished;

    echo "ping finished\n";
}

$pond_pid = spawn('pong');
spawn('ping', array(3, $pong_pid));

Procesy si mezi sebou posílají objekty. Za klíčovým slovem receive následuje blok s podbloky. Podblok bude spuštěň, dostane-li proces zprávu dané třídy (podobně jako se catch bloky spouští podle toho, byla-li vyhozena výjimka dané třídy).

Rozchodit to není (principielně) těžké. Nad zkompilovaným kódem by ještě běžel scheduler. Ten by spustil jeden proces. Až by se proces dostal k receive konstrukci, uložil by svůj stav a nahlásil scheduleru, že čeká na zprávu těch a těch tříd (či na jakoukoli zprávu, byl-li by v receive bloku catch-all podblok) a předal běh programu scheduleru. Ten by se podíval, jestil v mailboxu daného procesu je zpráva, kterou je ochoten přijmout. Kdyby byla, náhlásil by procesu, že zpráva byla přijata a znovu by ho spustil se stavem, s jakým proces přerušil svůj běh, a na místě, kde ho přerušil. Jinak by spustil jiný proces, který by byl schopen běhu. (Tohle je samozřejmě jen jedna z možných strategií plánování procesů.)

Krom odděleného běhu více jazykových procesů v jenom OS procesu by bylo možné i procesy přesouvat mezi OS procesy (běžících klidně na více strojích, na různých webserverech). Tím by se dalo docílit i toho, aby PHP skripty mohly běžet, jak dlouho by potřebovaly, bez ohledu na max_exection_time – scheduler by, až by vycítil, že se blíží konec limitu, spustil by na nový OS proces scheduleru, poslal mu všechny běžící procesy a sám skončil běh svůj.

Parsovací stroj phpegu

V několika posledních dnech jsem se zaměřil na optimalizace phpegu. phpeg poskytuje dva typy výstupu – parser využívající rekurzivního sestupu, nebo „parsovací stroj“. Jelikož parsovací stroj byl (ano, byl, už není) pomalejší než rekurzivní sestup, zaměřil jsem se v optimalizacích hlavně na něj.

Parsovací stroj

Jak vůbec ten parsovací stroj funguje? Jedná se o registrový stroj s jednoduchou instrukční sadou (momentálně 16 instrukcí). Základem je 5 registrů – %stack, %value, %fail, %p a %s. %stack slouží k odkládání dat a návratových adres. %value obsahuje sémantickou hodnotu posledního výrazu. %fail je příznak značíčí úspěch (hodnota FALSE), nebo naopak selhání (TRUE) posledního výrazu. %s je vstupní řetězec a %p pozice v něm.

PEGy mají tři základní výrazy – nějaký znak (.), daný řetězec (např. "peg") a znak z určitého rozsahu (např. [a-zA-Z0-9_]). Každému ze základních těchto výrazů odpovídá jedna instrukce – any, literal a range.

any zkontroluje, jestli existuje nějaký znak v řetezci %s na pozici %p a uspěje-li zapíše do %fail hodnotu FALSE, %p zvýší o jedničku a %value bude obsahovat daný znak. Jestliže znak na pozici %p neexistuje, registr %fail bude obsahovat TRUE.

Podobné je to s range, akorát že ta ještě otestuje, že přečtený znak je ze zadaného rozsahu. literal ze vstupního řetězce přečte N znaků a porovná je s řetězcem délky N – jestliže se řetězce shodují nastaví %fail na FALSE, zvýší %p o N a do %value uloží přečtený řetězec.

Sekvence

Prvním ze složených výrazů je sekvence výrazů. Ta uspěje tehdy, uspějí-li všechny podvýrazy. Řekněme, že máme výraz <e1> <e2> <e3>.

START: <e1>
       jumpif %fail, END
       <e2>
       jumpif %fail, END
       <e3>
END:   …

Pro ty, co o assembleru ani neslyšeli: Každý řádek je jedna instrukce. Na začátku řádku může být tzv. „label“ oddělený od instrukce dvojtečkou (např. END:). Pak následuje název instrukce a poté již seznam argumentů oddělených čárkami. Používám AT&T řazení argumentů, takže u instrukcí se zdrojovým registrem / hodnotou a cílovým registrem je vždy první zdroj, pak cíl.

(V příkladech s instrukcemi parsovacího stroje bude <e> či <eN> /kde N je nějaké číslo/ znamenat instrukce pro daný výraz z předchozího příkladu zapsaného v gramatice phpegu.)

jumpif skočí na label ve svém druhém argumentu, je-li hodnota prvního argumentu TRUE. Obecně neuspěje-li některý z podvýrazů sekvence, skočí se na první instrukci po instrukcích sekvence.

Aby byla výsledkem správná sémantická hodnota, kód by vypadal trochu složitěji. (Jsou-li všechny podvýrazy, v hantýrce phpegu, „jednoduché“, neboli je jisté, že vrátí řetězec, výsledná sémantická hodnota bude spojením řetězců vrácených z podvýrazů /to aby se pravidlo pro identifikátor dalo napsat jednoduše jako id = [a-zA-Z_] [a-zA-Z0-9_]*/).

Volba

Mějme volbu <e1> / <e2> / <e3>. Ta uspěje tehdy, uspěje-li první podvýraz, nebo druhý, nebo třetí.

START: push   %p, %stack
       <e1>
       jumpif !%fail, END
       set    %stack[0], %p
       <e2>
       jumpif !%fail, END
       set    %stack[0], %p
       <e3>
END:   pop    %stack

push uloží hodnotu svého prvního argumentu na vrchol zásobníku předaného v druhém argumentu. set uloží hodnotu prvního argumentu do registru v druhém argumentu. %stack[0] je hodnota vrcholu zásobníku (%stack[1] je první hodnota pod vrcholem, %stack[2] druhá atd.).

Nejdříve tedy na zásobník uložíme aktuální pozici v řetězci. Poté vyzkoušíme vyhodnotit první výraz. Uspěje-li (!%fail), vyhodíme uloženou pozici ze zásobníku a jde se na další výrazy. Jestliže ale podvýraz neuspěje, nastavíme pozici v textu na pozici uloženou na vrcholu zásobník (set %stack[0], %p) a vyzkoušíme další podvýraz.

Predikáty

PEGy obsahují dva predikáty – &<e> a !<e>. První, tzv. and predikát, uspěje tehdy, uspěje-li daný podvýraz. not uspěje tehdy, neuspěje-li daný podvýraz. Sémantická hodnota predikátů je NULL (alespoň v phpegu) a ať uspějí nebo ne, neposunou pozici v řetězci.

Instrukce pro and predikát:

START: push %p, %stack
       <e>
       pop  %stack, %p

not predikát:

START: push %p, %stack
       <e>
       pop  %stack, %p
       set  !%fail, %fail

pop %stack, %p uloží hodnotu sejmutou z vrcholu zásobníku %stack do registru %p.

Je vidět, že instrukce pro oba predikáty jsou prakticky stejné. Akorát not predikát na konci obrátí pravdivostní hodnotu %fail registru.

Nepovinný výraz

<e>? znamená, že na dané pozici v textu se může daný podvýraz vyskytnou, ovšem také nemusí. V obou případech je výsledkem celého výrazu úspěch. Neuspěje-li podvýraz, pozice v textu se vrátí na původní místo a v phpegu bude mít celý výraz sémantickou hodnotu NULL.

START: push   %p, %stack
       <e>
       pop    %stack, %a
       jumpif !%fail, END
       set    FALSE, %fail
       set    NULL, %value
       set    %a, %p
END:   …

Opakování

Jsou dva druhy opakování – <e>*, kdy se podvýraz může opakovat nula a vícekrát; nebo <e>+, kdy se podvýraz na dané pozici v textu musí vyskytnou minimálně jednou. (Mohlo by vás napadnout, že <e>* je to samé jako (<e>+)?. Ale není. V případě, že <e> na momentální pozici v textu neuspěje, bude sémantická hodnota prvního prázdné pole /array()/, zatímco u druhého výrazu to bude NULL.)

Instrukce pro <e>*:

START: push        %p, %stack
       push        [], %stack        ; [] je prázdné pole
LOOP:  <e>
       jumpif      %fail, END
       arrayappend %value, %stack[0]
       set         %p, %stack[1]
       jump        START
END:   pop         %stack, %value
       pop         %stack, %p
       set         FALSE, %fail

arrayappend přidá hodnotu prvního argumentu na konec pole, které je v registru specifikovaném druhým argumentem. jump je nepodmíněný skok (jump LABEL je ekvivalentní zápisu jumpif TRUE, LABEL).

Pokud by byl podvýraz <e> jednoduchý, místo prázdného pole by se na zásobník uložil prázdný řetězec a místo arrayappend by tam byla instrukce append (která připojí řetězec z prvního argumentu do registru udaného druhým argumentem).

Kód pro <e>+ je trochu složitější, protože musíme ještě zjišťovat, jestli se nejedná o první iteraci, a když ano, nastavíme %fail na TRUE.

Volání jiného pravidla

Chceme-li zavolat pravidlo <r> dané gramatiky, phpeg vygeneruje následující instrukce:

START: push RET, %stack
       jump r
RET:   …

Neboli uloží adresu první instrukce po vygenerovaném volání na zásobník a skočí na adresu volaného pravidla. Na konec každého pravidla je přidán kód podobný následujícímu:

pop  %stack, %a ; v %a je adresa RET
jump %a

Ze zásobníku je tedy sejmuta hodnota návratové adresy a na tu skočíme.

Optimalizace parsovacího stroje

phpeg zkompiluje každé pravidlo zvlášť, poté je slinkuje dohromady, takže vznikne jedna velká kupa instrukcí (kterážto představuje program parseru) a tu phpeg instrukci po instrukci převede na jejich PHP reprezentaci. Díky goto v PHP 5.3>= je to víceméně přímočará operace, až na pár drobností.

push & pop

Jak je vidět z příkladů, se zásobníkem se pracuje dost. Nejdříve byl implementován velmi přímočaře. Na začátku byla inicializována proměnná $_stack = array();, push byl array_push($_stack, $value); a pop $value = array_pop($_stack);.

Problém je, že funkce array_push() je neuvěřitelně pomalá (což bych věděl i dřív, kdybych si jen přečetl komentáře v dokumentaci). Tak pryč s array_push() a $_stack[] = $value; místo toho!

Jenže array_pop() je taktéž pomalá.

Jako nejrychlejší se ukázalo udržovat si „stack pointer“ ($_stack_sp). Takže inicializace vypadá následovně:

$_stack = array();
$_stack_sp = -1;

$_stack_sp je inicializováno na -1, protože ukazuje na vrchol zásobníku (takže přístup k vrcholu znamená pouhé $_stack[$_stack_sp], což je taky rychlé) a když tam ještě žádný prvek není… A takhle vypadají push a pop operace:

// push
$_stack[++$_stack_sp] = $value;

// pop
$value = $_stack[$_stack_sp];
unset($_stack[$_stack_sp--]);

Návrat z pravidel

PHP bohužel nepodpoje (nebo o tom nevím) něco jako:

$_stack[++$_stack_sp] = &&LABEL; // ulož adresu labelu na stack

// …

$_addr = $_stack[$_stack_sp];
unset($_stack[$_stack_sp--]);
goto $_addr;

Proto člověk musí použít nějakou návratovou „tabulku“. Nejjednodušší je:

$_stack[++$_stack_sp] = "LABEL";

// …

$_addr = $_stack[$_stack_sp];
unset($_stack[$_stack_sp--]);
goto TABLE;

// …

TABLE:
switch ($_addr) {
    case "LABEL": goto LABEL;
    // …
}

Leč nejrychlejší způsob to zrovínka není. Proto phpeg předvypočítá všechny adresy, na které by se pravidlo mohlo vrátit a vygeneruje následující kód:

L123: $_stack[++$_stack_sp] = 125;
L124: goto L422;
L125: // …

// …

L422: // …
// … kód pravidla …
L489: $_addr = $_stack[$_stack_sp];
      unset($_stack[$_stack_sp--]);
L490: if ($_addr === 125) {
          goto L125;
      } else if (/* … */) {
          // …
      } // …

Více zábavy s parsovacím strojem

Věřím, že se ještě pár optimalizací generovaných instrukcí najde. Krom toho bych řekl, že by výkonu pomohlo kompilovat nejběžnější vzory v gramatikách do specializovaných instrukcí – např. ze sekvence [a-zA-Z] [a-zA-Z0-9_]* by se mohl vytvořit regulární výraz a ten předhodit preg_match() (což mě odrazuje, poněvadž by se phpeg stal závislý na pcre extenzi; ovšem kde byste sehnali PHP bez nainstalovaného pcre, že…). Anebo velice běžné (!<e> .)*, kde <e> může být kupř. "\n" / "\r" "\n"?, by se mohlo dost zrychlit, kdyby se s tím zacházelo specielně.

Když jsem psal shell pro pssh, naštvalo mě, že elegantní phpeg gramatiku pro shell prostě nenapíšu (tak jsem se nakonec uchýlil k ručnímu recursive descent parseru).

Napíšu-li totiž v shellu program $a, není jisté, kolik argumentů programu vlastně předávám – obsahuje-li totiž proměnná $a řetězec "foo bar", program nedostane v argv dvouprvkové pole ["program", "foo bar"], nýbrž tříprvkové ["program", "foo", "bar"]. Obnášelo by to na rozvíjení proměnných a příkazů pouštět parser několikrát. Lepší by bylo, kdybych přímo v parseru mohl obsah proměnné uložit do bufferu, který by se zpracoval dříve, než by se pokračovalo dál v původním vstupním textu.

A chci-li parsovat shell interaktivně, hodilo by se, aby parser nemusel znát celý vstupní řetězec předem. Takže v případě, že parser potřebuje další znaky (jinak by parsování skončilo chybou), parser si uloží svůj stav, vrátí běh program zpátky shellu, ten si po uživateli vyžádá další znaky, pošle je parseru a parsování může pokračovat. U rekurzivního sestupu asi nemožné implementovat (kdybychom se tedy nespokojili s tím, že parser nenavrátí běh programu o úroveň výš, ale bude volat nějaký kód, jenž vrátí další znaky), u parsovacího stroje hračka – stačí poupravit generovaný kód základních instrukcí a přidat ukládání stavu a jeho znovuobnovování.

Parsovací stroj tedy dospěl do stavu, kdy je stejně rychlý (né-li rychlejší) jako rekurzivní sestup, místo pro další optimalizace je myslím větší než u rekurzivního sestupu a poskytuje možnost dalšího rozšíření funkčnosti (ukládání a znovuobnovování stavu). Je ale jen PHP 5.3>=. Uvažuji, jestli se vyplatí generátor pro recursive descent parser stále udržovat, nebo ho odstranit ve prospěch parsovacího stroje.

Jsou transakce u webových aplikací potřeba?

Vždy, když někdo píše o NoSQL, a nejsou to články typu „hej, to je dobrý, to musím odteď používat všude a na všechno“, autor zmíní, že NoSQL databáze nemůžete použít tehdy, potřebujete-li transakce. Ovšem, omezím-li se na webové aplikace, jsou vůbec potřeba?

Závislá data

Prošel jsem naprosto nereprezentativní vzorek ošklivého kódu, totiž můj vlastnoručně vytvořený e-shop shopaholic, jenž používá pro ukládání dat relační databázi, a musím říct, že kdybych použil třebas CouchDB, leč teď v kódu pár transakcí je, nepotřeboval bych je. Proč? Protože všechny transakce v shopaholicu tam jsou pouze z důvodu ukládání souvisejících dat do více tabulek. Myslím, že podobně tomu je u většiny webových aplikací využívajících relačních databází. Jelikož se do CouchDB ukládají dokumenty, je nasnadě související data uložit do jednoho dokumentu. Jeho uložení již splňuje ACID kriteria.

Závislá data v oddělených dokumentech

„Ale co když prostě potřebuju, aby data, i když na sobě závislá, byla v oddělených dokumentech? Hm?“

Většinou se ukládá jeden „objekt“ (obecně, ne ve smyslu OOP – ač i ten může korespondovat s objektem obecným), např. produkt v e-shopu, článek v redakčním systému, informace o člověku aj. Ten může záviset na dalších objektech a bez toho hlavního nemají samy o sobě smysl, avšak zároveň ke své existenci ten hlavní nepotřebují.

Závislosti tedy tvoří strom, kdy kořenem je objekt, který chceme bezpečně uložit. Začneme tedy strom ukládat směrem od listů ke kořeni co objekt, to dokument (jestliže můžeme dokument eliminovat vložením objektu do jiného dokumentu, uděláme tak, viz předchozí kapitolka), a pokud se u uzlu podaří uložit všechny poduzly, uložíme daný uzel – takto postupujeme až ke kořeni. Jestliže se podaří uložit všechny poduzly kořene i kořen, objekt i všechny jeho závislosti jsou bezpečně uloženy.

Příkladem budiž Git.

Chceme-li uložit nový commit, začneme od toho nejspodnějšího – od blobů. Bloby jsou seskupeny do stromů, takže až jsou uloženy všechny bloby a stromy, na které strom odkazuje, uložíme daný strom. Atd.

A co když někde uprostřed ukládání selže? To už závisí na aplikaci. Ovšem aby se v databázi nekumulovala nevyužitá data, je nutné vytvořit garbage collector, který čas od času databázi projde a vymaže nepotřebné.

Typický příklad: převod peněz mezi účty

„Ale co když mám, řekněme, dva účty a potřebuju odečíst peníze z jednoho a přičíst je k druhému? To se už bez transakce neobejde!“

V relační databázi je řešení bankovních účtů jasné: budu mít tabulku účty, spustím transakci, odečtu od zůstatku jednoho účtu převáděnou částku, hned ji zase přičtu k zůstatku druhého a potvrdím transakci.

V CouchDB místo toho budu mít dokument pro jeden účet (třebas s ID 001) a druhý účet (s ID 002). Převod spočívá ve vložení dalšího dokumentu – dokumentu s transakcí:

{
    "typ":"transakce",
    "odesilatel":"001",
    "prijemce":"002",
    "castka":4200
}

Momentální zůstatek na účtu zjistím dotazem na následující view, kdy jako klíč použiji ID účtu, jehož zůstatek chci zjistit.

// map
function (doc) {
    if (doc.typ == "transakce") {
        emit(doc.odesilatel, -doc.castka);
        emit(doc.prijemce, doc.castka);
    }
}

// reduce
function (keys, values, rereduce) {
    return sum(values);
}

Problém změny dvou (ba i více) nezávislých dokumentech se dá převést na vložení nového dokumentu, který bude danou změnu zachycovat. V případě CouchDB se o získání výsledného stavu postará dotaz na view. Asi nejdůležitější je začít myslet o transakcích ne jako o sekvenci příkazů, které se musí vykonat všechny, nebo žádný, nýbrž jako o atomické změně stavu.

(Pozn. i do relační databáze byste museli ukládat informace o dané transakci kvůli různým reportům apod.)

Potíž nastává, je-li součástí transakce nejen změna stavu (kupř. chceme-li ověřit, že je na účtu vůbec dost peněz na převod).

Závěr

U webových aplikací je podle mě minimum případů, kdy se bez transakcí opravdu neobejdete – většinou se totiž ukládají nová na sobě závislá data, nebo se pouze nezávislé dokumenty. Mazání dokumentu je podobné jako vkládání – pokud vymažete uzel stromu závislostí, všechny poduzly a uzly níž budou vymazány při další iteraci garbage collectoru. S naduzly, které na uzel odkazují, je to těžší. Odkazy na vymazané poduzly se mohou odstranit při mazání uzlu (což se samozřejmě projeví na rychlosti takové operace), nebo se o to může garbage collector postarat později.

Git tip: ó né, commitnul jsem do špatné větve!

Nejdříve doporučení (hlavně sám k sobě, jelikož jsem to právě udělal několikrát za sebou): než začneš upravovat kód, zkontroluj si, jestli jsi ve větvi, do které ho chceš commitovat, sakra!

Řekněme, že pracuji na nějaké fičuře ve větvi super-feature. Commitnu, odběhnu, vrátím se ke kódu v domnění, že jsem na master, a začnu pracovat na opravě chyby. Chyba opravena, commit, nohy na stůl. A ejhle, oprava chyby je ve větvi super-feature! Tak ji přesuňme do master:

$ git checkout master
$ git cherry-pick super-feature
$ git checkout super-feature
$ git reset –hard HEAD^

A chceme-li, aby se oprava projevila i v super-feature:

$ git rebase master

SSH přístup na jakýkoli hosting

Minule jsem o tom, jak funguje SSH, jenom psal. Tentokrát praktická ukázka.

U sdílených PHP hostingů (alespoň v Česku) není obvyklé, aby poskytovaly SSH přístup. Někteří se vymlouvají na to, že je to nebezpečné (čti neumějí to nakonfigurovat), jiní rovnou přiznají, že to neumí nakonfigurovat a další jsou jen líní. Proto jsem napsal pssh.

pssh tuneluje SSH spojení skrz HTTP a na serveru implementuje POSIX-like shell a základní utility. pssh je napsané v PHP 5.3 a pro svůj běh potřebuje extenze gmp (nebo bcmath), mcrypt a openssl.

Používáte-li OpenSSH klienta, stáhněte si zdrojáky, nahrajte je na hosting, skript scripts/proxy uložte někam do $PATH (předpokládá PHP CLI binárku v /usr/bin/php) a do ~/.ssh/config přijdete (v případě, že hosting je na doméně domena.tld a zdrojáky jsou v adresáři /pssh):

Host pssh
    ProxyCommand proxy -h domena.tld -u /pssh/scripts/server.php

Poté přejděte do adresáře se zdrojovými soubory a spusťte: ssh -i users/nobody.priv nobody@pssh 'echo hello, world!' (volba -i nastavuje, z jakého souboru se bude číst soukromý klíč). Výstupem by mělo být hello, world!. Interaktivní shell spustíte přes ssh -i users/nobody.priv nobody@pssh.

pssh je ve velmi rané fázi vývoje. Bude třeba zapracovat na implementaci shellu, aby mohla lépe spolupracovat se „SSH serverem“ a vůbec na SSH serveru.

Plány s pssh jsou velké. První na řadě je interaktivní shell. Rozhodně by neuškodilo kopírovat soubory na web bezpečně, a tedy SFTP je další. A z toho hlavního ještě deployment pomocí Gitu (což obnáší napsat v knihovnu, která bude umět pracovat s Git repozitáři /číst je a zapisovat do nich/ a bude umět mluvit git-upload-pack protokolem).

Nemám k dispozici Windows a nevím, jaké klienty Windowsáři používají. (Jaké klienty používáte, Windowsáři?) Avšak podpora Windows (jak v případě serveru, tak klienta) by rozhodně nebyla k zahození.

Všechny návrhy, připomínky a patche jsou vítány – buď tady v komentářích, nebo na GitHubu, nebo na e-mail jakub.kulhan@gmail.com.

Jak funguje SSH

Jednou ze základní vlastností dobrých programátorů je dle mě, že se zajímají o to, jak fungují věcí, které „prostě fungují“ (ach ta samochvála!). Jedna z věcí, která prostě funguje a nad kterou se moc lidi nepozastavují, je SSH – protokol pro vzdálené spouštění příkazů, bezpečnou výměnu dat (např. SFTP, nebo Git /i když u toho vlastně výměna dat souvisí spíš s předchozím bodem/), tunelování TCP spojení ad.

Chuť prozkoumat SSH důkladněji ve mně rozdmýchalo to, že někdo napsal sshd (SSH server, resp. démon) v PHP. Ihned jsem se tedy dal do zkoumání této implementace, ale hlavně mě to donutilo přečíst si RFC popisující SSH 2.0 (SSH existují dvě verze, verzí 1 jsem se vůbec nezabýval).

SSH 2.0 je popsáno v pěti RFC: 4250, 4251, 4252, 4253 a 4254. První se zabývá přiřazenými čísly, druhé základní architekturou protokolu. Protokol je členěn do tzv. „vrstev“ – zbývající RFC popisují tyto vrstvy, jedná se transportní vrstvu, autentizační vrstvu a vstvu spojení.

Mně, abych pochopil, jak protokol funguje, vždycky nejvíce pomáhá, když si projdu, jak postupně probíhá výměna dat mezi serverem a klientem. Řekněme tedy, že chceme na vzdáleném počítači spustit příkaz echo 'hello, world!'.

První věc, co obě strany udělají, je, že pošlou tzv. „identifikační řetězec“:

SSH-protoversion-softwareversion SP comment CR LF

Kde protoversion je 2.0 (SSH verze 1 samozřejmě využívá 1.x), softwareversion je název software, který na dané straně obsluhuje prokol, SP je mezera (ASCII 32), comment obsahuje doplňující informace o software, CR a LF je konec řádku (ASCII 13 a 10, řetězec "\r\n" v PHP).

Poté již následují pakety posílané binárním protkolem. Pakety vypadají následovně:

uint32 packet_length
byte padding_length
byte[n1] payload; n1 = packet_length - padding_length - 1
byte[n2] random padding; n2 = padding_length
byte[m] mac (Message Authentication Code - MAC); m = mac_length

uint32 je 32-bitové číslo v „network byte order“. (V PHP slouží k práci s binárními daty funkce pack() a unpack(). Číslo z PHP překonvertujete do uint32 pomocí $uint32 = pack('N', $int);.) bytem se rozumí oktet.

Jako první obě strany uskuteční tzv. „výměnu klíčů“ (anglicky key exchange, zkráceně kex). V ní se dojednají algoritmy pro kompresi dat (pole payload v paketu), šifrování (šifruje se všechno kromě mac) a mac (slouží k ověřování, že pakety dorazily v pořádku). Jako výchozí se bere komprese žádná, šifrování žádné a MAC také žádný (tudíž je mac_length nulová).

Server i klient odešlou SSH_MSG_KEXINIT pakety (S označuje server, C klienta, jsou uváděny pouze položky v payload výsledného paketu).

S: byte SSH_MSG_KEXINIT
byte[16] … ;cookie
name-list "diffie-hellman-group14-sha1,diffie-hellman-group14-sha1" ;kex_algorithms
name-list "ssh-rsa,ssh-dss" ;server_host_key_algorithms
name-list "3des-cbc" ;encryption_algorithms_client_to_server
name-list "3des-cbc" ;encryption_algorithms_server_to_client
name-list "hmac-sha1" ;mac_algorithms_client_to_server
name-list "hmac-sha1" ;mac_algorithms_server_to_client
name-list "none" ;compression_algorithms_client_to_server
name-list "none" ;compression_algorithms_server_to_client
name-list "" ;languages_client_to_server
name-list "" ;languages_server_to_client
boolean FALSE ;first_kex_packet_follows
uint32 0 ;reserved for future extension

C: byte SSH_MSG_KEXINIT
byte[16] … ;cookie
name-list "diffie-hellman-group14-sha1,diffie-hellman-group14-sha1" ;kex_algorithms
name-list "ssh-rsa,ssh-dss" ;server_host_key_algorithms
name-list "3des-cbc" ;encryption_algorithms_client_to_server
name-list "3des-cbc" ;encryption_algorithms_server_to_client
name-list "hmac-sha1" ;mac_algorithms_client_to_server
name-list "hmac-sha1" ;mac_algorithms_server_to_client
name-list "none" ;compression_algorithms_client_to_server
name-list "none" ;compression_algorithms_server_to_client
name-list "" ;languages_client_to_server
name-list "" ;languages_server_to_client
boolean FALSE ;first_kex_packet_follows
uint32 0 ;reserved for future extension

SSH protokol je maximálně variabilní, co se týče používaných algoritmů. Speficikace např. uvádí 16 protokolů pro šifrování (z nichž musí implementace nezbytně podporovat pouze jediný, právě ten 3des-cbc v ukázkových v paketech) a implementace mohou přidávat dle libosti vlastní, jen se musí řídit jistými pojmenovávacími zásadami.

Přibyl zde nový typ – name-list:

uint32 n
byte[n] comma_separated_list

Je uvozen číslem s délkou pole bytů, ve kterým je uložen seznam prvků oddělených čárkami (např. z PHP pole array("foo", "bar") bude pole bytů "foo,bar").

Poté každá ze stran odvodí, co se použije, podle zaslaných name-listů druhé ze stran. První algoritmus v name-listu je nejvíce preferovaný, druhý méně a postupně preference algoritmu klesá. Je vidět, že pro komunikaci klient->server a server->klient mohou být použity různé algoritmy, avšak je běžné, že oběma směry se používají stejné. V příkladu je to jednoduché, bude se používat 3des-cbc pro šifrování, hmac-sha1 pro MAC a payload nebude kompimovaný (none).

Jako metoda výměny klíčů byla vybrána diffie-hellman-group14-sha1, Diffie-Hellmanova výměna klíčů se SHA1 hashem.

První je na řadě klient. Vygeneruje náhodné číslo x, vypočítá e = g^x mod p (modulární umocňování; kde g a p jsou definována v RFC 3526) a e odešle serveru.

C: byte SSH_MSG_KEXDH_INIT
mpint e

Typ mpint je kódování pro velká čísla, konkrétně viz RFC 4251.

Server přijme paket od klienta, získá tak číslo e a vygeneruje svoje náhodné číslo y. Poté spočte f = g^y mod p, K = e^y mod p a H, což je SHA1 hash z čísel e, f, K a dalších věcí jako identifikačních řetězců a SSH_MSG_KEXINIT paketů obou stran (viz specifikace). Pomocí svého soukromého klíče podepíše H a odešle klientovi paket:

S: byte SSH_MSG_KEXDH_REPLY
string … ;server public host key and certificates
mpint f
string … ;signature of H

Typ string je sekvence znaků (bytů) určité délky:

uint32 n
byte[n] string

Klient ověří, je-li veřejný klíč poslaný serverem autentický (jestli opravdu náleží danému serveru – většinou zobrazí uživateli fingerprint daného klíče a nechá ho rozhodnout o autentičnosti, pro další sezení je již klíč uložen v databázi a klient ho kontroluje podle ní; když se klíč změní, uživatel je na to výrazně upozorněn a např. OpenSSH klient hned ukončí spojení), a vypočítá K = f^x mod p.

Tím získali server i klient tzv. „sdílené tajemství“ (ang. shared secret) – server vypočítal K = e^y mod p = (g^x mod p)^y mod p = g^xy mod p a klient zase K = f^x mod p = (g^y mod p)^x mod p = g^yx mod p = g^xy mod p – a to bez možnosti, aby se o tomto tajemství dozvěděl útočník. Jelikož x i y jsou náhodně generovaná čísla, bezpečnost celé výměny se odvíjí od toho, kolik entropie mají server i klient k dispozici.

Nyní mají obě strany k dispozici vše pro to, aby mohly komunikovat šifrovaně podle dříve dohodnutých algoritmů. Vygenerují klíče pro kryptografické funkce a odešlou SSH_MSG_NEWKEYS (ještě nezašifrované):

C: byte SSH_MSG_NEWKEYS
S: byte SSH_MSG_NEWKEYS

Všechny další pakety odeslané jednou stranou po SSH_MSG_NEWKEYS jsou již šifrované podle dohodnutých algoritmů. Je to z důvodu toho, aby mohl např. klient poslat serveru odpojovací paket (SSH_MSG_DISCONNECT), pokud by neshledal poslaný veřejný klíč autentickým.

(Pozn. až doposud jsem si myslel, když se u SSH pořád mluví o těch veřejných a soukromých klíčích, že jsou všechny zprávy šifrovány asymetricky. Ovšem jak je vidět, klíče slouží pouze k tomu, aby se server autentizoval /a také klient, viz dále/ a komunikace je šifrována symetricky.)

Doposud probrané zprávy se týkaly transportní vrstvy. Ta vytvořila zabezpečený tunel, jímž mohou protékat pakety vrstev vyšších. Jako další se dostáváme k autentizační vrstvě, jejímž cílem je určit identitu klienta. Je více způsobů, jak klient může prokázat svou identitu. Předpokládejme, že využije veřejného klíče:

C: byte SSH_MSG_USERAUTH_REQUEST
string "jakub" ;user name
string "ssh-connection" ;service name
string "publickey" ;method name
boolean FALSE
string "ssh-rsa" ;public key algorithm name
string … ;public key blob

S: byte SSH_MSG_USERAUTH_PK_OK
string "ssh-rsa" ;public key algorithm name from the request
string … ;public key blob from the request

C: byte SSH_MSG_USERAUTH_REQUEST
string "jakub"
string "ssh-connection"
string "publickey"
boolean TRUE
string "ssh-rsa"
string …
string … ;signature

S: byte SSH_MSG_USERAUTH_SUCCESS

Nejdříve se klient serveru dotáže, může-li vůbec danou metodu autentizace použít (jestli ji server podporuje). Typ veřejného klíče, který může klient poslat, nezávisí na typu klíče dohodnutém při výměně klíčů – ten se týkal pouze serveru –, takže i když server se autentizoval pomocí ssh-rsa, klient může klidně použít ssh-dss.

Jestliže server může daného uživatele autentizovat pomocí poslaného klíče, odešle SSH_MSG_USERAUTH_PK_OK.

Klient pak provede opravdový pokus o autentizaci tím, že připojí k požadavku i signaturu z hashe H z výměny klíčů a části SSH_MSG_USERAUTH_REQUEST paketu (bez pole pro signaturu, viz specifikace).

Server ověří, jestli je signatura správná, a odešle zprávu o úspěšné autentizaci. Samozřejmě pokud vyhodnotí, že signatura nesouhlasí, oznámí klientovi chybu autentizace. Ovšem může ji poslat i tehdy, když signatura sedí – to z důvodu, že jeden způsob autentizace nemusí být dostatečný, klient se tedy musí autentizovat i dalšími metodami.

Poslední vrstvou je spojení – ssh-connection. Nás bude zajímat, jak spustit vzdálený příkaz:

C: byte SSH_MSG_CHANNEL_OPEN
string "session" ;channel type
uint32 0 ;sender channel
uint32 65536 ;initial window size
uint32 65536 ;maximum packet size

S: byte SSH_MSG_CHANNEL_OPEN_CONFIRMATION
uint32 0 ;recipient channel
uint32 1 ;sender channel
uint32 65536 ;initial window size
uint32 65536 ;maximum packet size

C: byte SSH_MSG_CHANNEL_REQUEST
uint32 1 ;recipient channel
string "exec" ;request type
boolean TRUE ;want reply
string "echo 'hello, world!'" ;command

S: byte SSH_MSG_CHANNEL_SUCCESS
uint32 0 ;recipient channel

S: byte SSH_MSG_CHANNEL_DATA
uint32 0 ;recipient channel
string "hello, world!\n" ;data

S: byte SSH_MSG_CHANNEL_EOF
uint32 0 ;recipient channel

S: byte SSH_MSG_CHANNEL_REQUEST
uint32 0
string "exit-status"
boolean FALSE
uint32 0

S: byte SSH_MSG_CHANNEL_CLOSE
uint32 0 ;recipient channel

C: byte SSH_MSG_CHANNEL_CLOSE
uint32 1

SSH pracuje s mechanismem tzv. „kanálů“, veškeré posílání dat, vzdálené spouštění příkazů atd. probíhá přes ně. Nejdříve tedy klient otevře kanál typu session (používá se pro vzdálený terminál a spouštění příkazů). window size je objem dat (velikost dat v paketech SSH_MSG_CHANNEL_DATA a SSH_MSG_CHANNEL_EXTENDED_DATA), kterou ať klient či server obslouží, než si druhá strana bude muset požádat o zvětšení okna. Na každé straně může být alokován kanál pod jiným číslem (v příkladě je u klienta jako kanál číslo 0 a na serveru jako 1).

Poté klient pošle na kanál požadavek, aby spustil příkaz echo 'hello, world!'. Jelikož chce odpověď, server zašle, proběhl-li požadavek v pořádku, SSH_MSG_CHANNEL_SUCCESS. Následně server pošle data ze standardního výstupu programu (výstup ze sterr se dá poslat také), oznámí konec výstupních dat, pošle stavový kód, s jakým program skončil, a uzavře kanál. Klient uzavžení kanálu potvrdí stejnou zprávou (akorát jako číslo kanálu příjemnce samozřejmě uvede číslo, pod jakým má kanál alokován server).

Je-li to všechno, co klient chtěl, uzavře soket, čímž se odpojí od serveru a ten může využít zdroje alokované pro tohoto klienta znovupoužít.

Protokol SSH 2.0 není těžké implementovat, specifikace mluví jasně a nejsou moc dlouhé (dohromady mají 123 stran /z nichž na 34 jsou obsahy, reference apod./; pro srovnání specifikace HTTP 1.1 má 176 stran). Nejtěžší na implementaci jsou funkce pro šifrovanou komunikaci, ovšem na ty již existují velmi dobré knihovny, které je možné využít (v PHP bindingy na Mcrypt a OpenSSL, ale i pure-PHP implementace jako phpseclib).