Node.js в роли проксирующего сервера данных через websockets

Очередной велосипед про легкий способ бросить курить создать устойчивый асинхронный поток данных между практически любым сервером данных и браузером.

Преамбула: один из проектов, который я сопровождаю, — это комплексная система GPS-мониторинга автотранспорта. В ней присутствует сервер обработки и хранения данных от автомобильных трекеров и десктопный клиент, который рисует движение машинок в реальном времени на грубоватой растровой карте, которая побита на тайлы общим объемом порядка гигабайта. Руководство проекта поручило мне создать веб-клиент на базе гугл-яндекс и прочих мимимишечных векторных карт для быстрого доступа к визуальным данным из любого места и с любого устройства, а не только с десктопа.

Задача была выполнена максимально быстрым и минимально затратным путем: написаны простецкие скрипты на php, которые подключаются к серверу обработки GPS данных, дают запрос, дожидаются ответа, и возвращают ответ в веб-клиент. Соответственно, был сверстан простенький клиент, который систематично, по таймеру, посредством старого доброго $.ajax() давал POST-запросы php-скриптам и красиво рисовал ответ на вышеупомянутых замечательных векторных он-лайн картах.

При всех достоинствах, имелись очевидные недостатки такого пути — синхронные запросы иногда требовали немало времени на ожидание ответа, поскольку из сокета сервера данных лезли асинхронные ответы этого же сервера на изменение состояния подключенных трекеров, их необходимо было фильтровать и ждать ответ на исходный запрос. Ну и конечно же, браузер на стороне клиента иногда начинал преступно пользовать свой кэш и игнорировать свежие данные. Опять же, при увеличении количества подключенных клиентов апач начинал активно жрать память впс-ки, на которой это все крутилось. Опытным путем было вычислено, что интервал опроса сервера размером в 20 секунд не особо сильно напряжет сервер и в то же время сохранит интеракивность в веб-клиенте.

Такая схема вполне устроила руководство — продукт успешно стартанул, пользователи перешли на использование веб-клиента. Но внедренное решение не устроило меня, как любителя всего прекрасного и гармоничного.

Дальнейшие попытки написать прокси-сервер для передачи данных в веб-клиент на Java были в итоге похоронены из-за недостаточности познаний в оной и необходимости разворачивать на сервере Tomcat или что-то подобное, что в итоге существенного прироста производительности не дало бы.

И тут на помощь пришла Node.js и библиотечка SockJS , которая реализует удачную эмуляцию асинхронного вебсокетного соединения и делает это несколько лучше, чем socket.io, о чем уже здесь писали в свое время.

Забегая вперед, сразу скажу почему я пишу об этом — внедрение нижеописанного решения раз в тридцать сократило нагрузку на сервер, работает во всех современных браузерах (конечно же, я не имею ввиду IE6-7, хотя в ИЕ8 уже работает) и обеспечивает весьма высокую скорость передачи данных. Решение предлагается достаточно универсальное, таким же методом можно организовать обработку практически любого потока асинхронных данных (парсинг сайтов, чат-сервер, он-лайн игрушка, система управления марсоходом… ) и не требует глубоких знаний программирования, поэтому может быть развернуто достаточно оперативно любым достаточно подготовленным кодером.

Итак, сервер, который будет обрабатывать информацию с сервера данных и асинхронно передавать ее посредством вебсокетного соединения в веб-клиент:

Node.js
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() {
		console.log("Websocket connection closed");
		com = null;
    });
});

// создаем объект-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 = this.chunk.substring(pos);
	}
};



Клиент, требуется подключение модуля-обработчика SockJS. Извините за простыню, но разносить стили/скрипты по файлам для примера на понимание, имхо, не нужно
index.html
<!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>



Я постарался снабдить код комментариями на всех ключевых моментах. Живой он-лайн демки дать не могу — на моем сервере крутятся данные реальных организаций, которые не будут рады столь пристальному вниманию к своему транспорту. Хотя, забавно наблюдать в онлайне как работает комбайн — за пол-дня на карте ровненько заштриховывается кусок поля, а также четко виден аппендикс в сторону ближайшего села, когда капитан комбайна возжелает отобедать.

Жду ваших комментариев, вопросов и пожеланий.
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 16

    +2
    Можно подробней, чем SockJS лучше socket.io и почему в IE9- не работает, ведь подобные библиотеки служат именно для решения данной проблемы.
      0
      положив лапу на сердце, вынужден признать: в ИЕ8 такая связка работает уверенно. Ну не люблю я старые ИЕ :)

      здесь же, на хабре была статья о носке-жысы, там автор цитировал создателя этой среды, который писал о том, что создатели сокет.ио пошли куда-то «не туда», и нужно было сделать нечто свое, подобное, но более устойчивое. что у него вполне успешно получилось, я считаю.
      ну и простота АПИ и использования.
        –1
        Кто ж старые IE любит :) IE8- — лютый ад для js разработчика, а вот IE9 вполне себе годный браузер, отсутствие поддержки WebSocket'ов в котором один из немногих недостатков, легко затыкаемый библиотеками в стиле этого же носка. Странно. Сколько с WebSocket'ами мучился — ни разу на SockJS не натыкался. Пойду, поиграюсь.
        0
        чем SockJS лучше socket.io

        Разница примерно такая:
        SockJS = websocket + эмуляция для старых браузеров (long polling и т.д.)
        Socket.io = websocket + эмуляция для старых браузеров (long polling и т.д.) + обертка (каналы, бродкастинг, встроеная сериализация и еще куча полезностей)

        Есть еще один момент socket.io содержит более 500 issues на гитхабе (неисправленных ошибок) и не обновлялся 7 месяцев… :(, у sock.js такого нет.
          0
          мне сокет.ио как раз не подошел из-за «кучи полезностей», которые применительно к моим задачам пользы не давали, а торможения — прибавляли.
          поэтому выбрал сокЖС, ибо мне нужен был механизм передачи только, остальное я доделал сам.
          0
          почему в IE9- не работает
          Работает со всеми браузерами. Автор ошибается.
          0
          Раз браузеры современные, то можно и на querySelectorAll переходить:

          window.$ = document.querySelectorAll.bind(document);
          $('#foo')[0].value;
          $('#bar')[0].value;
          // etc ...
          
            –1
            пример клиента я делал максимально упрощенным, «на понимание», ну а обработку очереди сообщений можно реализовать любым удобным кодеру образом, это да…
            0
            delete com;

            delete к переменным не применим, только к свойствам объекта.
              0
              да, спасибо. это по инерции от другого проекта…
              0
              Если не ошибаюсь это комбайны русского солода, авангард агро кажеться называется компания. Только вчера читал статью про него и как его команда контролирует трактористов. Трактористы на аутсорсе было интересно почитать :)
                0
                ошибаетесь :) организация, поля и машины — украинские.
                кстати, в начале года выполнили заказ — поставили трекер на баржу.
                Трудно представить, зачем он там надо, ведь баржа дальше фарватера не убежит, но тем не менее…
                  0
                  Судя по коду Commander.prototype.onData, если вам придет сразу две команды ("command1\ncommand2\n"), вторую вы просто пропустите, потому что у вас сбрасывается chunk после каждой команды.
                    –1
                    да, точно — будет теряться. спасибо, поправлю.
                    0
                    Еще мне кажется, при закрытии соединения со стороны брарузера без посылки "@bye" (просто вкладку закрыли) у вас останется висеть в памяти соединение, Commander и Commander.client. А возможно, они останутся висеть даже при посылке "@bye", потому что непонятно, что последует за Commander.client.write('(exit)\n');. Закроет ли сервер данных соединение сам?
                      –2
                      приведенный мной код — максимально упрощен для примера на понимание.
                      реальная структура, конечно же, обрабатывает отвалы соединений и не только.

                      Only users with full accounts can post comments. Log in, please.