Главная > AJAX, Open Source, Высокопроизводительная архитектура, ж. Хакер > Атака на другой мир или серверный JavaScript. Статья в журнал Хакер

Атака на другой мир или серверный JavaScript. Статья в журнал Хакер

24 октября 2010

Статья написана специально для журнала Хакер и опубликована на сайте журнала (в несколько сокращенной и отредактированной версии). Ниже я публикую свой оригинальный вариант,  без правок или ограничения на объем материала.

Не просто JavaScript

Привет! Если ты сегодня, от нечего делать и летнего зноя решил вдруг чего-либо понаделать для веба, то кроме холодного пива, понадобится еще и выбрать себе язык программирования. Ты, конечно, крут и знаешь все модные течения – Ruby на своих рельсах гордо едет миром,  поклонники Питонов шарахаются в сторону и превозносят Django. Кучка людей в строгих костюмах – это любители Java, к ним не ходи, замучаешься писать и отлаживать код, прежде чем твоя поделка выдаст хоть бы Hello world. ASP.NET вообще не котируется, но, говорят, кто-то и на нем пишет.

Конечно, есть всеми любимый РНР – что может быть легче для веб-сайта! Но скажи честно, как-то не тянет, правда? Ведь чтобы сделать что-то стоящее, простого РНР не хватит, надо и Ajax прикрутить, чтобы на сайте кнопочки и формы сами грузились и работали незаметно. А ведь он не очень хорош, когда вдруг к тебе ломанется много народу. А все потому, что РНР (как и подавляющее большинство других языков, на которых пишут сайты) даже в 21 веке работает по классической схеме. Запрос страницы заставляет веб-сервер поднять указанный скрипт, выполнить его (линейно, строка за строкой весь твой код), а результат возвращает браузеру. После этого скрипт «умирает», а следующий запрос снова запустит всю эту адскую машинку. Если ты не в курсе, это называется CGI (интерфейс взаимодействия веб-сервера и интерпретатора твоего языка, на котором написана страница). Штуки вроде FastCGI, расширяют протокол, позволяя избегать выгрузки скрипта после первого запроса. Таким образом, когда второй пользователь запросит ту же страницу, для него будет уже все готово, останется только выполнить скрипт с новыми параметрами. Но это все не то…

Что такое хорошо…

Многие разработчики всегда считали javascript просто «примочкой» к браузеру, эдаким недоязыком (а некоторые до сих пор думают, что слово java в названии что-то да значит!), годным лишь для управления формами да манипуляциями с DOM-деревом веб-страницы. Однако настоящие профи так не считали, а трезво посмотрев на сам язык и его возможности, особенно в части работы с событиями, решили – а что, если написать сервер на JavaScript? Ты получаешь возможность написать все части сайта на одном и том же языке, что серверную часть, что саму страничку. Кроме этого, JS отлично, просто идеально подходит для веб-штучек разных – он очень простой и одновременно гибкий, позволяет писать код в разных парадигмах (от обычного процедурного, до ООП в смеси с функциональным стилем).

А главное – тотальная асинхронность, что означает, твой код будет выполняться не последовательном виде, а тогда, когда для него будут готовые все данные. Ведь для веба не надо большой вычислительности, большую часть времени сервер ожидает событий, вроде получения всей формы, получения результата запроса к базе данных или, что еще хуже, запроса к другому серверу. Обычной РНР-скрипт в такое время простаивает, а значит, простаивает весь поток, не позволяя серверу задействовать его для других пользователей. Даже Nginx не спасает.

В JavaScript ты просто указываешь, какую функцию выполнить (это называется callback), когда произойдет определенное событие, и все, в это время другой код может спокойно выполняться. Хотя писать действительно сложный код в таком стиле немного неудобно, особенно, если твоя функция зависит от нескольких событий сразу, но и для этого придумали уже свои фреймворки, зачастую гораздо более мощные и элегантные, чем все эти РНР/Ruby/Python.

Для примера я покажу тебе два типовых примера кода, делающих одно и то же:

РНР (линейный и синхронный код):
$result = $db->fetchOne(‘SELECT user_name FROM user_accounts WHERE id = 1’);
echo ‘Мое имя: ‘ . $result . ‘;’;

В первой строке мы посылаем простой SQL запрос к БД на выборку имени пользователя, у которого id = 1. Обрати внимание, в этом месте скрипт останавливается, и следующая строка не будет выполнена до того момента, пока запрос не будет обработан базой, а результат не возвратится в переменную $result. И хоть то тысячные доли секунды, но в реальности и запросы гораздо сложнее, и базы по размеру уже гигабайты, да и одновременно таких запросов будет пару тысяч.

В JS этот же код, записанный в асинхронном стиле, будет следующим:

db.query(‘SELECT user_name FROM user_accounts WHERE id = 1’, function(err, res){
if (!err)  sys.log(‘Мое имя: ‘ + res);
});
sys.log(‘Продолжаем выполнение’);

Посмотри внимательно – создается запрос к базе данных, однако кроме самого SQL передается еще и функция (callback), которую приложенные выполнит, но не прямо сейчас, а только тогда, когда придет ответ от базы. А вот следующая строка, где мы просто пишем в консоль, будет выполнена сразу после создания запроса, не ожидая его завершения.

В основе любого варианта серверного JavaScript заложена концепция событий и callback-ков, то есть обработчиков событий. Ты можешь и сам описать собственные события, и тогда твое приложение стает уже не простой страницей, а реагирует на те, или иные события, которые создаются или пользователем на странице, например, события «форма заполнена» или «новое сообщение», или генерируются внутри самого сервера, как в варианте с доступом к базе данных. На этом же принципе построено и взаимодействие с внешним миром – асинхронный ввод/вывод (Async I/O), ведь сама природа сетевого взаимодействия также асинхронная, что позволяет одному серверу (одному процессу ОС) обслуживать одновременно тысячи подключений.

Ну, в общем, ты понял, что на стороне сервера использовать JavaScript это хорошо, и даже приятно. Особенно для всяких сетевых штук, вроде сокет-серверов, Comet-а (для этого просто родная техника!), HTTP и прочего. Но вот что же выбрать?

Движок, вот в чем вопрос

Сегодня существует четыре основных движка, которые используются на серверах:

Rhino – от Mozilla, написан на Java и поддерживает последнюю, 1.7 версию стандарта JS, дополняя язык собственными расширениями и объектами. Основным преимуществом движка является работа поверх стандартной JVM, а значит, его можно использовать в любой среде, где работает Java. Можно применять современные сервера типа Jetty, но писать на любимом JS. Кстати, Rhino применяют на облачном хостинге от Google! А вот с производительностью сложнее. Она зависит, с одной стороны, от самого движка и применяемых там технологий вроде JIT-компиляции, и от работы самой Java-машины. Кстати, многие тестеры, которые говорят, что Rhino очень медленный, забывают, что движок имеет два режима работы – интерпретации, когда скрипт каждый раз преобразуется в Java байт-код (аналогично PHP), и компиляции, когда такое преобразование происходит только раз, а потом многократно исполняется. Первый режим выгоден, когда ты отлаживает код, который меняется каждую минуту, второй – уже рабочая версия, работающая под нагрузкой.

SpiderMonkey – ещё один движок от Mozilla, на этот раз на С. Кстати, это вообще первый в мире движок JS, написанный еще в Netscape, а сегодня открытый и используется в таких популярных продуктах, как Firefox, Adobe Acrobat и даже в одном из эмуляторов серверов онлайн игры Ultima Online. Для Firefox был модифицирован движок, добавлена компиляция JS напрямую в ассемблерный код, а движок переименован в TraceMonkey и используется в ветке 3.6 браузера. В основном, SpiderMonkey используют в ПО, которое написано на С/С++ и нуждается в скриптовом языке. Из известных продуктов – Comet-сервер APE, noSQL БД CouchDB, серверная платформа Jaxer и модуль к Apache mod_js.

Futhark - это движок от Opera, который, кроме браузера, используется в их инновационном сервисе Unite (типа встроенный сервер в каждом браузере), а также на их серверах, обслуживающих мобильный браузер Opera Mini. Жаль, что движок закрыт и его пока нигде за пределами самой Opera не применяют.

V8 – движок от Google, используемый в Chrome, а также основной движок в будущей Chrome OS. Сегодня это самый крутой, быстрый и мощный движок, в котором JS-код напрямую преобразуется в ассемблер целевого процессора, что позволяет обойти по скорости все остальные движки. Кроме этого, гугловцы используют множество ухищрений для оптимизации, хранят в памяти скомпилированный код, оптимизируют его на лету (например, удаляют блоки кода, которые по решению компилятора вообще не могут быть задействованы и т.п.). На базе этого движка построена самая популярная и быстро развивающаяся серверная платформа – Node.JS.

Наверное именно после выхода Сhrome разработчики смекнули, что такой быстрый движок можно было бы использовать и на сервере – первым был проект V8cgi, который просто позволял писать страницы, работающие с любым веб-сервером по стандартному протоколу CGI. Дальнейшее развитие привело к проекту NodeJS – полностью самостоятельной платформе, включающей, кроме движка, встроенный сервер (HTTP и TCP/UDP/Unix-soket) и базовый набор библиотек, а также полностью асинхронная работа с файлами и сетевыми устройствами.

Конечно, по правде говоря, еще раньше был (и есть) Aptana Jaxer – поговаривают, что это реально первый Ajax сервер, который включает в себя все необходимые библиотеки (работа с базой данных, файлами и т.п.), а также позволяющий размечать прямо в HTML-странице блоки кода, которые будут выполняться на сервере или клиенте. Их облачный хостинг (Aptana Cloud) первым предложил поддержку JS на сервере, однако минусом jaxer-а есть тяжеловесность и все же медленность, а также отсутствие встроенного веб-сервера, поэтому он работает только в паре с Apache.

Node.JS развивается безумно быстро и активно, текущая версия 0.1.98 но он уже давно готов к реальному промышленному использованию, что и доказали парни из Plurk (азиатский аналог твиттера), переписав свой comet-сервер с Java (на базе солидного JBoss Netty) на Node и, по отзывам, сократили буквально на гигабайты потребление памяти. А масштабы у них еще те – более сотни тысяч одновременных соединений.

А ещё бы, если запустить простейший HTTP-сервер, способный обрабатывать асинхронно тысячи подключений, это несколько строк:

http = require('http');
http.createServer(function (request, response) {
response.writeHead(200, {'Content-Type': 'text/plain'});
response.end(‘Хакер рулез!\n');
}).listen(80);

Сам сервер написан на С++, однако большая часть библиотек из дистрибутива на JavaScript, и совсем немножко ассемблера. В состав базового набора сервера входят только основные функции, остальное – решают разработчики, написав уже сотни разных библиотек и фреймворков. Плохо, что пока проект молодой, очень многих вещей еще нет, а то, что есть, зачастую такое же молодое – код в версии 0.0.1 вполне привычное явление в мире Node. Многие задачи могут не иметь решения вообще, либо наоборот, количество решений, зачастую радикально разных по архитектуре, исчисляется десятками (доступ к базе MySQL или разные веб-фреймворки). Хотя большинство библиотек написано на чистом JavaScript, есть и такие, что требуют компиляции модуля к серверу, что обещает гораздо большую скорость – они просто расширяют стандартный API сервера.

Основной особенностью Node является, кроме полной асинхронности, является его однопоточная модель. Да, все операции выполняются в одном и том же потоке ОС, даже если у твоего сервера тысяча одновременных пользователей. Правда, доступно создание дочерних процессов и низкоуровневое управление исполнением скриптов (загрузка, компиляция, работа с ассемблерным кодом, исполнение).

Для реализации многопоточности и задействования всех ядер современных процессоров рекомендуют просто загружать несколько копий приложения, но можно взять на вооружение WebWorker из стандарта HTML5 и распределить работу приложения по нескольким дочерним процессам. Не думай, что раз нет многопоточности, это тормоз и отстой. Вспомни, что веб-приложение делает полезную работу очень быстро, а большую часть времени просто ожидает чего-то, данных от базы, от мемкеша или новомодной NoSQL базы, либо просто держит в памяти открытые соединения для Comet-а, поэтому в одном потоке можно обработать и десяток тысяч, не прибегая к кластеризации.

Второй особенностью архитектуры Node является событийность. Почти каждая операция имеет каллбеки, генерирует событие, а пользователю доступен объект EventEmiter, через который можно буквально одной строкой генерировать свои события (это не сложно, ведь событие это просто строка с названием, а также список параметров, которые передаются в обработчик).

Сам по себе Node построен вокруг EventLoop - глобального цикла обработки событий, который на каждом тике проверяет, готовы ли данные для какого-либо из определенных пользователем каллбеков. Если есть – начинается выполнение кода, если не осталось больше кода, ожидаем следующего вызова (этот цикл выполняется вне JS, в самом движке, на С, поэтому очень и очень быстро, порядка сотен тысяч раз в секунду). Ага, ты прав – это типа бесконечного цикла. В дополнение к этому в сервер встроен очень эффективный сборщик мусора (GC), поэтому даже тысячи подключений не вызывают переполнение памяти и падения сервера.

В том же РНР событий, к сожалению нет, но если очень хочется, то можно выкрутиться, как это сделали ребята из ezComponents. Для эмуляции используют возможность выполнять функцию, имея ее имя в виде строки (а можно и метод класса, передавай массив из название класса и метода, но сработает только для static public методов), за это расплачиваясь снижением быстродействия и сложностями самой программы. Например:

РНР:

$eventEmitter->emit(‘simple_event’, 1); //генерируем событие simple_event с параметром.
//внутри emit ищем соответствие методов указанному сигналу.
call_user_function(array(‘MyClass’,’simple_event_method’), 1);

Отдельно надо писать код, чтобы на событие реагировало несколько методов друг за другом и т.п., в общём, это десятки килобайт кода. NodeJS обладает встроенной родной системой работы с событиями. Просто напиши:

var emitter = new events.EventEmitter();
emitter. addListener(‘some_event’, function(x){sys.log(‘fn1’);});
//а вдруг надо ещё один обработчик? В РНР это сложно, в JS – фигня
emitter. addListener(‘some_event’, function(x){ sys.log(‘fn2’);});
//вызвать событие
emitter.emit(‘some_event’, 1);

А вокруг да около

Конечно, ты можешь писать прямо с чистого листа, что и делают многие правильные пацаны, но другие умные люди подумали за тебя и начали создавать различные фреймворки и спецификации. Если ты будешь писать что-то серьезное на серверном JS, то наверняка захочешь облегчить себе жизнь.

Одним из самых мощных фреймворков, превращающих просто движок в целую платформу, сейчас является Narwhal, который первоначально был придумал, чтобы расширить до нормального уровня Rhino, но с появлением других движков может работать и с ними. Такой набор библиотек нужен, чтобы программеры не парились по поводу различий каждого движка и его базовых возможностей. В таком виде Narwhal очень даже приятен – он дает единый API для всех базовых операций, что нужны разработчику. Здесь и загрузка модулей (асинхронная и в рантайме, с учетом зависимостей), работа с файловой системой, абстракция системы I/O, работа с бинарными данными (вот это для JS в новинку, поэтому приходится извращаться, например, представляя данные в виде массивов целых чисел), расширение стандартных примитивов языка, логгирование, шифрование и много другого функционала, обеспечивающего комфортную разработку более сложных программ.

А что, нельзя придумать один общий стандарт для такой платформы? Можно! Это CommonJS – попытка стандартизировать платформу и дать общий API для всех движков с одной стороны, и заложить основу для написания других фреймворков. Narwhall, как и другие фреймворки, пытаются реализовывать эту спецификацию, однако каждый расширяет ее по своему, пытаясь всё равно выйти за рамки.

CommonJS предоставляет два типа API – низкоуровневый, который включает в себя расширение базовых объектов языка, ведение логов, общий интерфейс доступа к базам данных (попытка сделать JDBC для javascript), многопоточность и работа с модулями.

Кстати, о модулях. Это самая основная часть вообще любого языка – так как позволяет использовать код других программистов и упрощать себе работу. В твоем любимом РНР для этого используют или autoload, функцию, если работа идёт с классами, или функции require/include, которые просто подключают любой РНР код. Хотя в JS также используется метод require, но он работает только с объектами, поэтому нельзя просто обычный js файл подключить и получить его функции. Это означает, что модулем не может быть произвольный код, а только специально подготовленный. Для этого используй специальный встроенный класс exports, а все определенные в нем методы и своства будут после подключения модуля доступны в программе.

Например:

//модуль Email (email.js)
exports.version = ‘1.0’;
Email = {host: ‘mail.xakep.ru’, port: 25, subject: ‘Mail from Node’};
//в таком варианте объект Email не будет доступен после подключения, хотя кажется, что он определен глобально. В РНР это работает. В NodeJS надо:
exports.email = Email;
//в основной программе пишешь:
var Email = require(‘./lib/email’).Email;

в РНР аналогом будет:

include_once(‘email.php’);
$email = new Email();

Высокоуровневая часть спецификации предлагает работать сразу с конечными сервисами, например, с помощью нескольких вызовов библиотечных функций можно сделать HTTP-сервер или клиент, работать с Jabber/XMPP протоколом и другие фишки.
Жаль, что пока все части стандарта достаточно молодые, поэтому для реальной работы применимы только соглашения и модульности и некоторые другие API из базового набора, остальное пока в активном проектировании.

Так как ограничения обычного протокола CGI уже всем давно известны, а приложения на javaScript призваны существенно изменить работу веба, решено было создать новую спецификацию, которая бы связывала сам движок и внешний веб-сервер (или любой другой сервер, который стоит перед платформой). Если для Python-а есть WSGI, то для JS логично предположить, есть JSGI – JavaScript gate interface. Это соглашение о том, каким образом HTTP-сервер будет взаимодействовать с движком приложения и наоборот. Для следования стандарту достаточно просто передавать специальный JSON-объект с параметрами и уметь понимать аналогичный ответ от приложения. Сам протокол очень простой и в случае, если все его станут придерживаться, он очень облегчит жизнь программисту. Пока только Rhino в окружении сервера jetty полностью поддерживает JSGI, для остальных движков есть адаптеры и библиотеки. Да и разработчики разных фреймворков, активно продвигают свои творения, заявляя полную поддержку стандарта, так что, видимо, писать на низком уровне обработку HTTP на JS уже не придется.

Кстати, обрати внимание на платформу Persevere 2.0/Pintura ( от создателей супермощного Dojo. Это еще одна попытка сделать серверную часть, которая бы оберегала программиста от движка, взамен предоставляя стандартные API и еще собственные «плюшки» там, где CommonJS/JSGI недостаточно. Данная библиотека очень удобна для создания разных веб-сервисов на базе REST, включает в себя работу с базами данных и кешами, а то, что создатели далеко не новички в мире JS/AJAX означает, что продукт реально хорош. Ложкой дегтя будет его сложность и громоздкость – если для того, чтобы написать простейшую страницу на Node.JS достаточно 10 строк, здесь придется еще разбираться в описаниях модулей и документации, хотя если раз поймешь, дальше все будет просто сказка.

Вот и сказочке конец.

Сейчас, выбирая на чем бы таком написать очередное вебдванольное приложение, где не только красивый клиентский код, но и что-то надо делать на сервере, тебя парит одна грустная мысль, что все уже изобрели и написали до тебя. Те же языки, что и десять лет назад, те же библиотеки, протоколы и сервера. РНР уже ого сколько лет, Perl так вообще седой, Python у всех на слуху, а Ruby успел надоесть. Писать для веба стало рутиной – посмотри, как твои друзья сидят думают, что же сделать с 25-мегабайтным монстром Zend-framework. А тебе хочется что-то нового, быть на острие прогресса, создавать то, на чем потом будут писать все, а сейчас знают только увлеченные хакеры и ищущие себя дзен-программеры? Реально, посмотри на JavaScript в сервере, он просто создан для этого, очень простой, мощный, еще не погрязший в тонне библиотек, а те что есть- пока еще небольшие и их можно понять буквально за вечер с пивом. Задачи, которые на РНР не решить вообще, на базе Node.JS решаются буквально десятком строк. И возможно именно такое программирование наконец принесет утраченное чувство наслаждения от кодирования!

Интересности:

  • Все основные JS движки: Rhino www.mozilla.org/rhino/, SpiderMonkey www.mozilla.org/js/spidermonkey
  • Материалы по NodeJS: Google Groups (en): groups.google.com/group/nodejs, русскоязычный сайт и форум: http://forum.nodejs.ru
  • Если твоему приложению нужна универсальная система обмена сообщениями между пользоватеми в реальном времени, попробуй супербиблиотеку Socket.IO – она позволяет организовать Comet-соединение (клиент и сервер), используя все, что есть под рукой, от новомодного websocket-а, до традиционного polling-а или бесконечного iFrame. Исходники здесь: http://github.com/LearnBoost/Socket.IO-node
  • Большинство, если не все, библиотеки и проекты на Node.JS сосредоточены на Github, поэтому если какого-то модуля, нужного тебе, нет, ищи его там.
  • Если хочешь узнать все о всех серверных вариантах JavaScript, посмотри здесь: http://en.wikipedia.org/wiki/Server-side_JavaScript
  • Хорошая презентация-введение в Node.JS - http://www.slideshare.net/the_undefined/nodejs-a-quick-tour

Иллюстрированное введение в NodeJS

  1. 24 октября 2010 в 15:33 | #1

    …а пользователю доступен объект EventEmiter, через который можно буквально одной строкой генерировать свои события

    Есть переведённая русская документация, можно ссылаться на неё.

  2. 24 октября 2010 в 20:31 | #2

    1. не все так печально с php — например тот же WP на котором крутиться этот сайт имеет хуки

    2. С тотальной асинхронностью JS код становиться очень быстро лапшеобразным. Что с этим делать мне лично пока не понятно, но делать надо. Я боюсь увидеть код на сервере, которого в разы больше чем на клиенте в том же стиле.

  3. 25 октября 2010 в 07:30 | #3

    Отличная статья, спасибо. Вот бы было хорошо, если блог почаще обновлялся.

  4. 25 октября 2010 в 14:39 | #4

    @Sultanov Eduard
    Да я не говорю, что так все плохо 🙂 для РНР есть несколько вариантов сигнальных (событийных) архитектур, сам под свои проекты сделал целый мини-фреймворк для роутинга URL в сигналы. Да и анонимные функции в последних версиях появились. Но это немного не то, вернее, недостаточно.

    2 — Если код правильно проектировать и, самое главное, использовать там, где это оправданно — тогда все ок. Следует понимать, что изменяеться весь подход к архитектуре приложения, а не просто переписать на другой язык.

  5. Eqe
    25 февраля 2012 в 14:32 | #5

    помоему лажа  это всё… Нигде  ничерта  ничего толком не известно про этот ваш мифический «серверный Java Script», хотя искал про него уже давненько, лет 5 или  более назад… Но так ничего и не нашёл.
    Всё как и в этой статейке, вокруг-да около… теории красивые, но ничего конкретного.
    Лучше бы написали, какой известный русский хостинг, вообще этот серверный скрипт поддерживает, ато я что-то ни у кого не видел до сих пор… Или чтобы пытаться писать на серверном скрипте сайты, надо свой собственный специальный сервер делать?! 
    А денвер? Денвер-то тоже наверняка не поддерживает этот ваш скрипт!
    А без денвера как-бы, фиг чего напишешь… эт оосновная тестовая среда для разработки проектов сайтов.

Комментирование отключено.
Developers.org.ua