Продолжаем копать HTML5: Comet, WebSockets, EventSource, NodeJS
пост написан и отправлен в печать 2011-07-18 примерно в 08:01
Все помнят мой гневный (скорее, ироничный вентиляторно-лопаточный вброс, на который многие купились, смотри UPD внизу следующей ссылки) пост про HTML5 и про весь тот шум, который нагнетается вокруг сырой и неготовой к массовому использованию технологии. В какой-то мере, данный пост станет очередным тому доказательством, но, в то же время, если этот эфемерный набор стандартов будет когда-то окончен, то мы, вебразработчики, получим отличный инструментарий, который лишит необходимости собирать велосипеды с квадратными колесами для езды по стеклянным рельсам. Ну вы поняли.

Давайте по порядку. Сейчас в моде real-time коммуникации, в техническом смысле этого слова. Контакт, лицокнига, гуглоплюсы - все перечисленные социальные сети имею свой WebIM, позволяющий в реальном времени общаться со своими друзьями и подругами. Правда, концепция там немного другая, чаты этих социалок, как правило, представляют собой JavaScript обертку вокруг XMPP протокола, но смысл понятен. Еще, справедливости ради, стоит заметить, что WebSockets и Server-Sent Events в настоящее время выделены из спецификации HTML5 в отдельные доки, но, опять таки, я утверждаю и буду утверждать, что HTML5 - не технология, а маркетинговый бренд и писькомерялка современных браузеров, поэтому отождествление понятий тут допустимо. Передо мной была поставлена задача создать приложение, позволяющее в реальном времени обмениваться информацией между сервером и клиентом с минимальными задержками. Благо, приложение это исключительно интранетовское и платформа использование - Google Chrome, а значит технически я не был ограничен. Вариантов было несколько: long-polling HTTP запросы, HTTP Streaming или fast-polling, все это называется Comet моделью. И у каждой реализации есть свой недостаток. В спецификации HTTP/1.1 четко указано, что браузер не должен создавать одновременно более 2х параллельных соединений к серверу. Конечно, современные браузеры могут поддерживать десяток connections и подобное нарушение стандарта не является фатальным, но в каждом из вариантов решения есть свои недостатки. Fast-polling, который представляет собой многократное повторение запроса (с завершением сессии) создает приличную нагрузку на сервер, а "висящие" запросы - костыли, которые приводят либо к memory-leak (в последнем фаерфоксе сделать garbage collector для буфера запроса не удосужились, в хроме и сафари память чистится, но только для запросов, возвращающих "Transfer-Encoding: chunked"), к тому же, контролировать Comet сессию сложнее, она может отваливаться без какой-либо видимой причины, да и изначально протокол создавался не для этого. Есть еще BOSH, который используется в вышеупомянутом XMPP, но он заточен больше под создание чатов и готовых решений для этой технологии очень мало. В PaaS Google App Engine, на котором расположен этот блог, есть Channel API, что-то вроде Comet-а, я даже пытался попробовать, но возможности тоже сведены до минимума. Конечно можно еще использовать обертки вокруг flash socket или java-апплета, но не этому учила нас партия.

Вот именно таким путем, перепробовав все вышеназванные методы и оценив ресурсоемкость, я решил остановиться на server-sent events и web-sockets, которые вроде даже выведены из статуса эксперементальных в последних версиях Webkit-а. Начнем с SSE, спецификацию можно почитать тут. По сути, это тот же HTTP Streaming на стероидах. Его смысл заключается в том, что в JavaScript коде или DOM создается объект EventSource со скриптом в src атрибуте или методе-конструкторе. Этот скрипт, прежде всего, должен в контексте "висящего" соединения (хотя и не обязательно) возвращать корректный Content-Type: text/event-stream и в дальнейшем push-ить обновление клиенту в виде строки data: any_text_info \n\n. То бишь, протокол абсолютно односторонний, нас это вполне устраивает, общаться обратно мы можем и через милый сердцу XHR. Есть в нем и вкусности, к примеру, ответ event: test-remote-event \n data: success \n\n, если верить стандарту, можно обрабатывать в addEventListener('test-remote-event' ,function(data){}), правда, это пока только на бумаге. В Chrome Canary у меня работал только event onmessage. Со стороны все всегда кажется таким красивым, но тут начались проблемы, бывает, что поток просто висит. Никакой onerror или onclose не вызывался, сервер продолжает исправно посылать данные, что проверяется curl-ом в консоли, но браузеру пофиг, хотя рестарт страницы помогает. Пробовал в Chrome Stable - та же  заморочка. ЧЯДНТ? Вторая и более серьезная проблема - отсутствие поддержки CORS, Access-Control-Allow-Origin: * отсылается, но браузерам на него с большой колокольни, они возвращают DOM Exception 18. Очередная брешь в стандарте, как так - XHR работает, а EventSource нет. Конечно, это все придирки, можно было бы завернуть трафик через proxy_pass в nginx, но в любом случае, осадок остался. К тому же, нужно уже было думать о проверке наличия \n\n в контенте, иначе была бы вероятность получить побитый ответ. Таким вот путем я и пришел к WebSockets. Рассказывать что-либо про клиентскую часть не имеет смысла, есть спецификация, все это делается одной строкой кода, давайте лучше кратко поговорим, как реализовать подобное на сервере. В качестве server-side для подобных проектов я всегда рекомендую брать NodeJS. Сама парадигма event based неблокирующего асинхронного сервера идеально подходит для высокозагруженных API, а этот самый интранет, он очень большой и запросы ресурсоемкие. К тому же, имеющегося в NPM добра с головой хватит для решения любых задач.

Для демонстрации возможностей, я написал небольшое приложение. Прописано оно по адресу. Работать должно в Chrome, Safari (в том числе и на iPad, iPhone с 4.2) и FF. Но в последнем нужно включить поддержку сокетов. Вот вам мануал. Долго думал, откуда бы взять большой поток данных для наглядного примера, как вспомнил про Twitter Streaming API. Код готового приложения можно невозбранно скачать архивом. Если демонстрация перестанет работать - прошу меня простить, мой старенький сервер очень нежный и неторопливый, и больше времени заняла компиляция node, чем прочтение документации или написание самого кода. Разумеется, в server.js нужно вписать свои логин и пароль. Вообще, я использую sample поток, а вы, прочитав документацию по твиттер апи и twitter-node, можете создать какой-то полезный сервис мониторинга и аналитики хештегов, или выводить в форме границ Украины аватары пользователей, написавших что-то на мове. Фантазии ограничены исключительно возможностями API твиттера, а их более чем хватает.

В заключение, хочу сказать, что я постепенно пересматриваю формат блога, если вам понравилась подобная статья - не поленитесь перейти на веб версию, если в РСС читаете, и воспользоваться кнопкой Google +1. Если соберется немного плюсов, то в ближайшее время мы поговорим про WebWorkers, Forms Validation, нарисуем мужской половой хуй в Canvas, напишем вращающийся чайник на WebGL и попробуем накладывать фильтры и получать информацию из тега <video>.