Přejít k navigační liště

Zdroják » JavaScript » JavaScript na serveru: Architektura a první Hello World

JavaScript na serveru: Architektura a první Hello World

Články JavaScript, Různé

V dalším díle seriálu se dozvíte něco málo o architektuře Node.js, o asynchronním programováním a o významu pojmů, které se vážou speciálně k Node.js. V průběhu článku si navíc napíšeme první skript, tradiční Hello World.

Node.js je platforma pro vývoj webových aplikací. Pojem platforma zahrnuje i webový server, není tedy nezbytně nutné žádný zvláštní webový server (jako je třeba Apache pro PHP) k Node.js pro provoz instalovat, nicméně v produkčním nasazení je lepší Node.js používat společně s jiným webovým serverem, který zajistí minimálně vyřizování požadavků na statické soubory, které by neměly být zpracovány přes Node.js. Velmi často se pro tyto účely používá webový server nginx. Postup pro jeho instalaci a nastavení pod Windows si můžete přečíst na českém blogu o Node.js. V dalším průběhu seriálu budu používat pouze Node.js bez nginx.

Pouze jedno vlákno

Pokud se o Node.js trochu zajímáte, určitě jste narazili na pojem single-thread. To znamená, že Node.js pracuje pouze v jednom vlákně, což je rozdíl oproti častějším případům, kdy webový server odpověď na každý požadavek zpracovává v jiném vlákně. Známe to všichni například z kombinace PHP + Apache, kdy pro každý požadavek Apache alokuje určité množství paměti, ve které PHP zpracuje daný HTTP požadavek. I v případě, že je odpověď záležitostí jednoduchého skriptu, musí dojít např. k načtení konfigurace projektu, inicializace spojení s databází, načtení celého frameworku, routeru atd., což bývá často zbytečné a neefektivní.

Protože Node.js pracuje pouze v jednom vlákně, při spuštění aplikace (před zpracováním prvního HTTP požadavku) dojde k načtení a inicializaci všeho potřebného a jakmile je vše načteno, Node.js poslouchá příchozí požadavky a ty pak směřuje na konkrétní controllery a akce (v případě MVC architektury). Vše objasní následující ukázka:

var http = require('http');
console.log('Nacteni konfigurace, databaze, lokalizace ap.');
http.createServer(function (req, res) {
  console.log('Zpracovani pozadavku z URL: ' + req.url);
  res.end('Hello World');
}).listen(1337);

Nejprve přes funkci require() načteme modul http, který vytváří webový server a zpracovává HTTP požadavky. Metoda createServer() vytváří objekt reprezentující server. Přijímá jako jediný parametr anonymní funkci, která obsahuje dva parametry: parametr req reprezentuje požadavek, res odpověď. Metoda listen() pak říká, že má server poslouchat na portu 1337 na příchozí HTTP požadavky.

Tento soubor uložte třeba jako server.js, vložte ho třeba do složky C:zdrojak. Spusťte příkazový řádek (příkaz cmd) a přejděte do složky s tímto skriptem (příkaz cd C:zdrojak). Zde pak projekt spustíte přes příkaz node server.js.

Následně v prohlížeči přejděte na adresu http://localhost:1337. Měli byste vidět text “Hello World”, který byl předán metodě res.end(), která odešle odpověď serveru. V konzoli navíc uvidíte text “Zpracování z požadavku: /”. Jakýkoliv další požadavek bude zpracováván uvnitř anonymní funkce, jejíž volání je již unikátní pro každou URL a každého uživatele, jak je to obvyklé. Po spuštění se vám vypíše obsah vložený do metody console.log(). Cokoliv napíšete do skriptu server.js mimo obsahu anonymní funkce předané funkci createServer() bude zavoláno. Můžete zde připravit spojení s databází, načíst framework, načíst lokalizaci webu atd.

Požadavky jsou zpracovány v pořadí, v jakém přicházely na server, a že jsou opravdu zpracovány v jednom vlákně, se přesvědčíme jednoduchým pozměněním skriptu:

var http = require('http');
var pocet = 0;
http.createServer(function (req, res) {
  res.end(++pocet + '');
}).listen(1337);

Při každém načtení prohlížeče se vypíše vyšší hodnota (pravděpodobně se vám bude zobrazovat po každém načtení prohlížeče číslo o 2 vyšší než to předchozí, protože prohlížeč posílá navíc požadavek na favicon.ico, takže se zpracovávají dva požadavky).

Asynchronní zpracování

Dále se můžete o Node.js dočíst, že používá událostmi řízený (event-driven) neblokující I/O model. Co to přesně znamená, si opět vysvětlíme na příkladu s PHP.

V PHP píšete obvykle jeden příkaz na každý řádek. Jakmile skript zpracuje jeden příkaz, pokračuje dále. Podstatné je, že dokud není dokončen předchozí příkaz, není možné zpracovat ten další, protože jeden příkaz blokuje ostatní. Takže např. po dokončení objednávky v e-shopu nejprve vložíte objednávku do databáze, čekáte, dokud nepřijde odpověď od databáze, dále odešlete e-mail správci, počkáte, dokud neodejde, pak e-mail zákazníkovi atd.

V Node.js můžete psát také tímto způsobem, ale pouze tam, kde nedochází k zpracování požadavků uživatele (např. při zmíněném načítání konfigurace, lokalizaci, inicializaci frameworku ap.). V případě zpracování konkrétního HTTP požadavku už vždy musíte používat asynchronní metody a funkce, protože pracujete v jednom vlákně a synchronní metody zablokují celý server, dokud nejsou zpracovány.

Co si představit pod pojmem asynchronní zpracování? Všechny I/O operace v Node.js jsou napsány tak, aby server neblokovaly. Podívejte se na příklad čtení dat z disku:

var fs = require('fs');
fs.readFile('soubor.txt', function(err, data){
  console.log('A');
});
console.log('B');

Co myslíte, v jakém pořadí se vypíší písmena do konzole? Pokud hádáte, že bude pořadí „B, A“, pak máte pravdu. Ve chvíli, kdy skript narazí na metodu readFile(), spustí na pozadí čtení z disku a skript pokračuje ihned dále. Až se data z disku načtou, zavolá se anonymní funkce předaná jako druhý parametr a předá se jí výsledek zpracování souboru. Teprve tehdy se do konzole vypíše písmeno A.

V Node.js tedy existuje něco jako fronta událostí, které se volají podle toho, jak byly do fronty vkládány. Ve svých programech byste měli vždy psát neblokující kód a operace, které mohou zabrat více času, pak řešit jiným způsobem (nejčastěji spuštěním samostatného procesu přes balíček child_process, což je obdoba Web Workers z klientského JavaScriptu, o tom někdy v pozdějších dílech seriálu).

Poslední zmínka už jen k I/O operacím a jejich fungování v Node.js. Operace jako práce s databází, čtení a zápis se spouštějí zcela mimo frontu událostí. Spouštějí se na pozadí a ve vláknech tak, jak je to běžné i u ostatních jazyků. Pokud tedy zadáváte dotaz na databázi trvající třeba 2 vteřiny, neznamená to, že je server na 2 vteřiny zablokován. Vše pokračuje dále, pouze asynchronní funkce čeká na to, až databáze zpracuje a vrátí výsledky a pak bude pokračovat dále.

Co dále?

Dnešní díl byl hodně teoretický a je možné, že některé zmíněné části architektury Node.js vám nejsou zcela jasné. Netrapte se tím. V dalších dílech se k dnešním pojmům určitě ještě mnohokrát vrátím a rozšířím jejich výklad na dalších příkladech. V příštím díle se můžete těšit na seznámení s nástrojem npm, přes který můžete snadno instalovat tisíce různých modulů.

Komentáře

Subscribe
Upozornit na
guest
20 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
LiborS

děkuji za článek. Do teď jsem si myslel, že node.js má s
javascriptem společnou jen syntax, teď vidím, že i
způsob programování – javascript na klientské straně má
také jedno vlákno a všechny operace až na alert, confirm
jsou neblokující.

julochrobak

Tak ako to je? Citujem z clanku: „Operace jako práce s databází, čtení a zápis se spouštějí zcela mimo frontu událostí. Spouštějí se na pozadí a ve vláknech tak, jak je to běžné i u ostatních jazyků.“

Ondřej Štoček

Skript se skutečně interpretuje v jednom vlákně, což je požadavek přímo ze specifikace JavaScriptu. Systémová volání nad rámec interpretace jazyka (třeba čtení databáze, XMLHTTPRequest) se řeší v dalších vláknech, která pak vkládají události do fronty událostí vlákna interpreteru. Tím se události serializují. Interpret je pak postupně spustí opět v hlavním vlákně.

julochrobak

ok, chapem, dik. Napriklad v pripade volania do DB, novy thread nadviaze spojenie, posle data, caka na odozvu a v pripade odozvy, posle celu odozvu do vlakna interpreteru? Inymi slovami, thread zodpovedny za DB vlanie neinterpretuje vystupne data ale anonymna funkcia, ktora spracovava data sa spusti hlavnym vlaknom.

TMa

Je mozne pouzit node.js nejak v browseru? Narazil jsem na nej pri hledani moznosti jak otevrit v javascriptu listening TCP socket (chci z Google Earth publikovat online NMEA data ale GE ma API uz jen javascriptovy).

Pavel Lang

Node nelze použít v browseru, node je platforma. Kód napsaný pro node v určitých případech ano. Nelze však používat specifická API pro server v klientu a to je dobře.

Důvod, proč browser nemůže otevírat TCP a UDP spojení a místo toho se implementují WebSockets (založené na HTTP protokolu) je prostý – bezpečnost.

TMa

Jenže websockets pod sebou mají nějaký HTTP protokol a to potřebuje podporovat i klient. Nebo si udělat vlastní proxy, která bude na jedné straně mít websockets a na druhý TCP/UDP.
Jasně potenciální bezpečnostní díra proč to nejde jednoduše, pro použití pouze pro localhost to je ale jedno a vše se zasložiťuje a vnáší platformní komponenty aby se to obešlo.

Na druhou stranu ten websocket-TCP/UCP proxy je univerzální věc, víte někdo o nějakým už funkčním?

Razor

Node.js neznam prakticky, ale nemuzu si pomoct, ta jednovlaknovost se mi zda jako nevyhoda. Nevite v cem je to vyhodnejsi napr oproti vice vlaknovemu java servlet kontaineru? Proc to tak navrhli?

LiborS

Neznám java servlet kontainer, ale node js (jednovláknost) mi příjde výhodnější:

– pokud nemám v jednom vláknu blokující operace, tak je celý proces rychlý
– složitější výpočty by měli probíhat asynchronně
– stejně jako v klientském javascriptu neřeším synchronizační problémy u sdílených zdrojů, prostě vím, že kdykoli můžu třeba číst/zapisovat z jakékoli proměnné.

Pavel Lang

Single-thread je skutečně výhodnější. Viz C10k problem (Wikipedia, Search Google).
Kód v hlavním threadu musí být neblokující. Jediné místo, kde se „blokuje“ je v hlavní smičce (EventLoop), pokud je fronta prázdná.

„Single Thread“ je v tomto často chápán špatně. Není pravda, že Node.js beží v jednom vlákně. V jednom vlákně běží pouze výkonný kód programu. Takový kód se pak chová jako manažer, pouze rozhoduje o úkolech, které se mají vykonat a nechá se informovat o dokončení úkolu pomocí callbacku. Samotné blokující úkoly se vykonávají v ThreadPoolu mimo hlavní vlákno. Výhoda tohoto přístupu je menší režije spojená se samotnými vlákny — vytvoření threadu, Context Switch (cz, en), nepotřeba synchronizačních atomů

Také programy pro node.js lze spouštět ve více vláknech pomocí cluster modulu, který se stará např. o delegování HTTP požadavků mezi „worker instance“.

Stejná architektura (Event-driven) se používá právě v nginx, HAProxy a mnoha dalších projektech založených na libuv (knihovna postavená pro node), libev, libeio…

Rax

Je-li to uděláno tak, že řídící program je single thread a tento spouští další desítky vláken v ThreadPoolu, tak to nic neřeší, jenom to není tak na očích.

Principem ke zlikvidování C10k problému je že dodatečné vlákna vůbec neexistují, prostě se hrnou na OS požadavky a tento je zpracovává bez vláken, viz IOCP.

Dále je problém že jedno vlákno nedokáže plně využít osmijadernou mašinu, zde má node.js a javascript těžké nedostatky.

Pavel Lang

Asi jsem to nenapsal přesně.

Node má modul cluster a ten umí zařídit, že těch řídících threadů je kolik chcete, třeba 8.

Na ThreadPoolu (pokud se použije) nevznikají stovky vláken, ale pracovní fronty a ty požadavky pěkně serializují. Klasický thread pool mohou využívat nativní addony.

Ve skutečnosti se využívají mechanismy epoll, kqueue, /dev/poll nebo select podle toho, který je dostupný. Pro přístup k souborům ale i k TCP, UDP se používá právě dostupný mechanismus jádra. No je to pěkně udělané, takové podrobnosti jsem nezkoumal, kdo má zájem, ať si laskavě prostuduje libuv (GitHub, HTML kniha)

Nedalo mi to a napsal jsem benchmark. Server běžel celkem v pěti thredech:

  • 1 hlavní (cluster master). slouží pro IPC a kontrolu workerů.
  • 2 workery. Ty se řídí z kódu clusteru JavaScriptu
  • Každý worker si vytvořil po jednom threadu (to už řídí libuv)

Spustil jsem 10 x 1000 paralelních požadavků přez ab.

Server držel každý request otevřený 1000 ms aby to bylo zajímavější a nedalo se namítat, že se požadavky nezpracovávají paralelně:

$ ab -n 100000 -c 1000 http://local.host:8000/
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
...
Document Path:          /
Document Length:        22 bytes

Concurrency Level:      1000
Time taken for tests:   103.548 seconds
Complete requests:      100000
Failed requests:        0
Write errors:           0
Total transferred:      9700000 bytes
HTML transferred:       2200000 bytes
Requests per second:    965.74 [#/sec] (mean)
Time per request:       1035.481 [ms] (mean)
Time per request:       1.035 [ms] (mean, across all concurrent requests)
Transfer rate:          91.48 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0   22 295.4      1    9001
Processing:   998 1007  10.7   1004    1232
Waiting:      997 1005   9.5   1003    1225
Total:        999 1030 299.0   1005   10023

Pro představu přidám jen relevantní úryvek z kódu testu:

http.createServer(function(req, res) {
  setTimeout(function() {
    res.writeHead(200);
    res.end('hello world from '+process.pid+'n');
  }, 1000);
}).listen(8000);

Komletní zdroják testu a výstupy jsou v gistu zde.

Z výstupu testu je patrné, že se střídaly i thready.
Z odečtu lze tedy vyčíst, že režije serveru na jeden požadavek byla cca 35 ms — 1000 aktivních spojení; 2,956 ms při concurrency 10)
Ale je podstatný fakt, že více než díky node serveru byl můj stroj vytížen benchmarkovacím nástrojem ab a také to, že server a ApacheBench sdíleli stejný hardware.
Nejdůležitější je, že node skutečně problém C10k řeší.

Doufám, alespoň o trochu více osvětlil podstatu problému, pokud ne, rád se pokusím zodpovědět další dotazy.

Pavel Lang

Ještě jsem zapomněl pár věcí:

  • Uvést verzi — node v0.8.11
  • Jediný stream, který je v node blokující, je zápis do stdout a stderr, pokud ukazují na konzoli.

    Proto jsem spustil test znovu s přesměrovaným stdout do souboru a režije na jeden požadavek při 1000 aktivních spojení se snížila na 28,596 ms. (Při 10 aktivních spojeních 2,835 ms)

Rax

Děkuji za měření, jasně prokázalo že se ThreadPool při této úloze nepoužívá :-) Pracovní fronta žádné vlákna nepotřebuje, kromě těch workerů.

Srigi

Myslim, ze autor sa mohol viac „odviazat“ a ukazat viac ako jednoduchy Hello World. Hned takto na zaciatku sa da pekne pohrat napr. so spracovanim POST rq. z formulara.

Uzitocne je aj pridat odkazy na API preberanej latky (http modul).

Martin Hassman

Však to postupně rozjedeme, malý moment 8-)

Jan Pobořil

Bylo by fajn napsat taky o obdobě pro PHP – React.PHP. Funguje v podstatě stejně, jen je nový a tedy zatím málo modulů.

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.

Pocta C64

Za prvopočátek své programátorské kariéry vděčím počítači Commodore 64. Tehdy jsem genialitu návrhu nemohl docenit. Dnes dokážu lehce nahlédnout pod pokličku. Chtěl bych se o to s vámi podělit a vzdát mu hold.