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

от автора

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

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

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

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

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

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

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

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

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

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

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> 

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

Жду ваших комментариев, вопросов и пожеланий.

ссылка на оригинал статьи http://habrahabr.ru/post/186352/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *