Очередной велосипед про легкий способ бросить курить создать устойчивый асинхронный поток данных между практически любым сервером данных и браузером.
Преамбула: один из проектов, который я сопровождаю, — это комплексная система GPS-мониторинга автотранспорта. В ней присутствует сервер обработки и хранения данных от автомобильных трекеров и десктопный клиент, который рисует движение машинок в реальном времени на грубоватой растровой карте, которая побита на тайлы общим объемом порядка гигабайта. Руководство проекта поручило мне создать веб-клиент на базе гугл-яндекс и прочих мимимишечных векторных карт для быстрого доступа к визуальным данным из любого места и с любого устройства, а не только с десктопа.
Задача была выполнена максимально быстрым и минимально затратным путем: написаны простецкие скрипты на php, которые подключаются к серверу обработки GPS данных, дают запрос, дожидаются ответа, и возвращают ответ в веб-клиент. Соответственно, был сверстан простенький клиент, который систематично, по таймеру, посредством старого доброго $.ajax() давал POST-запросы php-скриптам и красиво рисовал ответ на вышеупомянутых замечательных векторных он-лайн картах.
При всех достоинствах, имелись очевидные недостатки такого пути — синхронные запросы иногда требовали немало времени на ожидание ответа, поскольку из сокета сервера данных лезли асинхронные ответы этого же сервера на изменение состояния подключенных трекеров, их необходимо было фильтровать и ждать ответ на исходный запрос. Ну и конечно же, браузер на стороне клиента иногда начинал преступно пользовать свой кэш и игнорировать свежие данные. Опять же, при увеличении количества подключенных клиентов апач начинал активно жрать память впс-ки, на которой это все крутилось. Опытным путем было вычислено, что интервал опроса сервера размером в 20 секунд не особо сильно напряжет сервер и в то же время сохранит интеракивность в веб-клиенте.
Такая схема вполне устроила руководство — продукт успешно стартанул, пользователи перешли на использование веб-клиента. Но внедренное решение не устроило меня, как любителя всего прекрасного и гармоничного.
Дальнейшие попытки написать прокси-сервер для передачи данных в веб-клиент на Java были в итоге похоронены из-за недостаточности познаний в оной и необходимости разворачивать на сервере Tomcat или что-то подобное, что в итоге существенного прироста производительности не дало бы.
И тут на помощь пришла Node.js и библиотечка SockJS , которая реализует удачную эмуляцию асинхронного вебсокетного соединения и делает это несколько лучше, чем socket.io, о чем уже здесь писали в свое время.
Забегая вперед, сразу скажу почему я пишу об этом — внедрение нижеописанного решения раз в тридцать сократило нагрузку на сервер, работает во всех современных браузерах (конечно же, я не имею ввиду IE9-) и обеспечивает весьма высокую скорость передачи данных. Решение предлагается достаточно универсальное, таким же методом можно организовать обработку практически любого потока асинхронных данных (парсинг сайтов, чат-сервер, он-лайн игрушка, система управления марсоходом… ) и не требует глубоких знаний программирования, поэтому может быть развернуто достаточно оперативно любым достаточно подготовленным кодером.
Итак, сервер, который будет обрабатывать информацию с сервера данных и асинхронно передавать ее посредством вебсокетного соединения в веб-клиент:
var http = require('http'),
net = require('net'),
sockjs = require('sockjs'),
ADDR_GPS = "127.0.0.1", // адрес сервера данных, желательно ай-пи
PORT_GPS = 3201, // порт сервера
server = sockjs.createServer();
server.on('connection', function(conn) {
// при подключении клиента создается экземпляр функции с аргументом - SockJS объект подключения
// создаем новый класс, который будет обрабатывать поток асинхронного трафика,
// при создании перехватываем возможную ошибку, если сервер данных недоступен
var com = new Commander(ADDR_GPS, PORT_GPS, conn, function(e){
console.log("! We had an Error in socket: ", e, "at ", new Date());
conn.close();
});
conn.on('data', function(data) {
// при получении команды от браузера клиента в формате JSON
// парсим ответ и выбираем действие согласно команды
var dat = JSON.parse(data);
if(dat.command == "@auth") {
// логинимся к серверу данных,
com.auth(dat.param.log, dat.param.pwd);
} else
if(dat.command == "@bye") {
// веб-клиент решил завершить рабту с потоком
com.bye();
conn.close();
}
});
conn.on('close', function() {
// веб-слиент отключился, явно удаляем объект-обработчик
delete com;
});
});
// создаем объект-HTTP-сервер и вешаем на него обработчик вебсокетных соединений
// SockJS, привязанный к адресу http://mydomen.com:8081/data
var srv = http.createServer();
server.installHandlers(srv, {prefix:'/data'});
srv.listen(8081, '0.0.0.0');
var Commander = function (adr, port, clientConn, onError) {
// создаем прямое сокетное подключение к серверу данных
var self = this;
this.status = 0;
this.chunk = ""; // цепочка символов текущего ответа
this.answers = []; // массив строк ответов сервера данных
this.connection = clientConn; // ссылка на вебсокетное подключение,
// туда мы будем проксировать ответы сервера данных
this.client = new net.Socket(); // клиент подключения к серверу данных
this.client.connect(port,adr,function(){
// подключаемся к серверу данных
console.log("New connect to created...");
});
this.client.on('data', function(data) {
// при поступлении асинхронных данных от сервера данных вызываем функцию-оработчик
self.onData(data);
});
this.client.on('error', function(e) {
// ловим ошибку и рвем связь
onError(e);
self.client.destroy();
});
};
Commander.prototype.auth = function(login, pass) {
// аутентифицируемся на сервере данных и запишем пользователя в консоль для контроля
console.log("written auth for "+ login);
this.client.write('(auth "'+login+'" "'+pass+'")n');
};
Commander.prototype.bye = function() {
// отключаемся от сервера данных
this.client.write('(exit)n');
};
Commander.prototype.onData = function(data) {
// данные приходят из сокета в виде цепочек, которые необходимо клеить до тех пор,
// пока не появится стоп-символ, обычно это код конца строки
// когда конец строки появился, передаем склеенные цепочки через вебсокетное соединение
// на наш веб-клиент и обнуляем цепочку для записи следующей строки
var pos;
this.chunk+=data.toString();
pos=this.chunk.indexOf('n');
if(pos > -1) {
this.connection.write(this.chunk.substring(0,pos));
this.chunk = "";
}
};
Клиент, требуется подключение модуля-обработчика SockJS. Извините за простыню, но разносить стили/скрипты по файлам для примера на понимание, имхо, не нужно
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Монитор асинхронного вебсокетного подключения</title>
<style>
body {
padding:0;
margin: 0;
font: 10pt sans-serif, Arial, Tahoma;
}
h1 {
font-size: 2em;
margin: 0.8em 0;
}
h3 {
font-size:1.5em;
margin: 0.1em;
}
#content {
position: relative;
margin: 0 auto;
width:960px;
min-width:800px;
}
#left {
position:absolute;
top:0px;
left:0px;
padding:2px;
width:220px;
height:560px;
}
#right {
position:absolute;
top:0px;
left:250px;
padding:2px;
width:710px;
height:560px;
}
#scroller {
position:relative;
width: 400px;
height:90%;
overflow-y:auto;
border:1px dotted black;
padding:5px;
margin-top:10px;
}
.off {
color:red;
}
.on {
color: green;
}
.inBottom {
position: absolute;
bottom: 20px;
}
</style>
<script src="sockjs-0.3.4.min.js" type="text/javascript"></script>
<script>
var sock;stat = document.getElementById("status");
function connect() {
// объект доступа к вебсокетному соединению
sock = new SockJS('http://mysite.com:8081/data');
var l = document.getElementById("login").value,
p = document.getElementById("passw").value
stat = document.getElementById("status");
setTimeout(function(){
// соединение устанавливается не мгновенно, перед авторизацией лучше обождать пару секунд
sock.send(toJSON("@auth", { log: l, pwd: p }));
},2000);
sock.onopen = function() {
// если соединение установлено, индикатор статуса радостно зазеленится
stat.innerHTML = "ON";
stat.className = "on";
};
sock.onmessage = function(e) {
// обработчик асинхронных данных, которые приходят в виде объекта, а непосредственные
// данные из сокета доступны в поле "data"
document.getElementById("scroller").innerHTML += "<p>"+e.data+"</p>";
};
sock.onclose = function() {
// если вебсокетное соединение потеряно, статус покраснеет от отчаяния
stat.innerHTML = "OFF";
stat.className = "off";
};
}
function disconnect() {
// рвем связь
if(sock !== undefined) {
sock.send(toJSON("@bye", {}));
}
}
function toJSON (com, param){
return JSON.stringify({ command: com, param: param });
}
</script>
</head>
<body>
<div id="content">
<div id="left">
<p style="width:100%;">Логин <input type="text" id="login" style="float:right;"></p>
<p style="width:100%;">Пароль <input type="password" id="passw" style="float:right;"></p>
<button onclick="connect();">Подключить</button><button onclick="disconnect();">Отключить</button><br>
<p class="inBottom">Подключение: <span id="status"></span></p>
</div>
<div id="right">
<div id="scroller"></div>
</div>
</div>
</body>
</html>
Я постарался снабдить код комментариями на всех ключевых моментах. Живой он-лайн демки дать не могу — на моем сервере крутятся данные реальных организаций, которые не будут рады столь пристальному вниманию к своему транспорту. Хотя, забавно наблюдать в онлайне как работает комбайн — за пол-дня на карте ровненько заштриховывается кусок поля, а также четко виден аппендикс в сторону ближайшего села, когда капитан комбайна возжелает отобедать.
Жду ваших комментариев, вопросов и пожеланий.
Автор: deivan