Как стать автором
Обновить

Пишем код для Node.js в стиле «машина состояний»

Время на прочтение 3 мин
Количество просмотров 13K
Вы пишите сервер на Node.js, который принимает входящие TCP-соединения и ведёт с клиентами нетривиальный диалог по нестандартному протоколу? Возможно вам будет интересен пример, который я развиваю в своих проектах. Что я имею ввиду под нетривиальным диалогом?

Давайте сравним.

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

Тот же датчик, который может получать от сервера команды на изменение частоты измерений или периода усреднения измеряемой величины — уже посложнее. Добавьте к этому, например, функционал отправки датчику обновлений прошивки и код запросто может потерять лаконичность.

В подобных случаях я организую свой код в стиле «машины состояний», не претендуя, впрочем, на соответствие канонам теории автоматов.

Пример для этой статьи выложен здесь: github.com/kityan/fsmConnection. Далее поясню несколько ключевых моментов.

Код основного приложения.

Рассмотрим только серверную часть. Код очень простой:

var net = require('net');
var ClientConnection = require('./ClientConnection.js');
var config = {"socketTimeout":3000, "port": 30000}
net.createServer(function(socket) {var clientConnection = new ClientConnection(socket, config);})
	.listen(config.port, function () {console.log('Listening on: ' + config.port);});

Каждый раз после установления соединения сервер создаёт экземпляр ClientConnection, передавая ему сокет и конфигурационный объект.

Фрагменты кода модуля ClientConnection.

Инициализируем поля экземпляра:

var ClientConnection = function (socket, config){...}

В прототипе определяем метод ClientConnection.to, который будет осуществлять переключение машины.

ClientConnection.prototype.to = function (newState) {
	// есть onExitHandler?
	if (this.currentState && this.states[this.currentState].onExitHandler && 
		typeof this.states[this.currentState].onExitHandler == 'function') { 
			this.states[this.currentState].onExitHandler.call(this); 
	}

	var prevState = this.currentState; 
	this.currentState = newState;

	// есть inputHandler?
	if (this.currentState && this.states[this.currentState].inputHandler && 
		typeof this.states[this.currentState].inputHandler == 'function') {
			this.handleInput = this.states[this.currentState].inputHandler.bind(this);
	} else { 
		this.handleInput = this.noInputHandler 
	}
	
	// есть onEnterHandler?
	if (this.states[this.currentState].onEnterHandler && 
		typeof this.states[this.currentState].onEnterHandler == 'function') { 
			this.states[this.currentState].onEnterHandler.call(this, prevState); 
	}

	return this;
}

При переключении мы проверяем, имело ли предыдущее состояние метод onExitHandler и, если имело, вызываем его.
Затем назначаем методу handleInput машины указатель на inputHandler нового состояния. И, наконец, проверяем, есть ли у нового состояния метод onEnterHandler. Если есть — вызываем его.

Что же происходит дальше после вызова ClientConnection.to(newState)? Если в вызовах onExitHandler и onEnterHandler не произошло переключения в другое состояние, машина остаётся в этом. И далее уже всё зависит от данных сокета. Все прилетающие пакеты будут направляться в handleInput. Почему?

Дело в том, что при создании экземпляра мы сразу же переключаемся в состояние инициализации, где вешаем обработчики на события сокета:

ClientConnection.prototype.states  = {
	'inital': {
		'onEnterHandler': function(){

			// socket events
			this.socket.on('timeout', function() {this.to('socket-timeout');}.bind(this));
			this.socket.on('end', function() {this.to("socket-end");}.bind(this));
			this.socket.on('error', function (exc) {this.to("socket-error").handleInput(exc);}.bind(this));
			this.socket.on('close', function () {this.to("socket-close");}.bind(this));
			this.socket.on('data', function (data) {this.handleInput(data);}.bind(this)); 
					
			this.to("waitingForHelloFromClient");
		}
	}, 
...
}


И уже затем переключаемся в следующее состояние. В нашем случае это 'waitingForHelloFromClient'.

Все состояния описываются в объекте ClientConnection.prototype.states. Допустимы состояния, у которых нет inputHandler. При переключении в такие состояния, мы отрабатываем какой-то алгоритм внутри их onEnterHandler и сразу же переключаемся в другое состояние. Останавливаемся мы в том, которое имеет inputHandler, чтобы следующая итерация Event Loop могла вызывать код для обработки данных сокета, если они появятся. Категорически не рекомендуется делать переключения в onExitHandler.

Собственно всё. Если код покажется удобным — применяйте на здоровье. Критика приветствуется.

Хочу отметить, что есть решения (например, Machina.JS), которые в общем случае могут оказаться более удобными.
Теги:
Хабы:
+4
Комментарии 29
Комментарии Комментарии 29

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн