Вы пишите сервер на Node.js, который принимает входящие TCP-соединения и ведёт с клиентами нетривиальный диалог по нестандартному протоколу? Возможно вам будет интересен пример, который я развиваю в своих проектах. Что я имею ввиду под нетривиальным диалогом?
Давайте сравним.
Удалённый датчик температуры, который стучится к серверу и после установления соединения пишет в сокет несколько байт своего идентификатора, а затем несколько байт текущей температуры — это примитивно.
Тот же датчик, который может получать от сервера команды на изменение частоты измерений или периода усреднения измеряемой величины — уже посложнее. Добавьте к этому, например, функционал отправки датчику обновлений прошивки и код запросто может потерять лаконичность.
В подобных случаях я организую свой код в стиле «машины состояний», не претендуя, впрочем, на соответствие канонам теории автоматов.
Пример для этой статьи выложен здесь: github.com/kityan/fsmConnection. Далее поясню несколько ключевых моментов.
Код основного приложения.
Рассмотрим только серверную часть. Код очень простой:
Каждый раз после установления соединения сервер создаёт экземпляр ClientConnection, передавая ему сокет и конфигурационный объект.
Фрагменты кода модуля ClientConnection.
Инициализируем поля экземпляра:
В прототипе определяем метод ClientConnection.to, который будет осуществлять переключение машины.
При переключении мы проверяем, имело ли предыдущее состояние метод onExitHandler и, если имело, вызываем его.
Затем назначаем методу handleInput машины указатель на inputHandler нового состояния. И, наконец, проверяем, есть ли у нового состояния метод onEnterHandler. Если есть — вызываем его.
Что же происходит дальше после вызова ClientConnection.to(newState)? Если в вызовах onExitHandler и onEnterHandler не произошло переключения в другое состояние, машина остаётся в этом. И далее уже всё зависит от данных сокета. Все прилетающие пакеты будут направляться в handleInput. Почему?
Дело в том, что при создании экземпляра мы сразу же переключаемся в состояние инициализации, где вешаем обработчики на события сокета:
И уже затем переключаемся в следующее состояние. В нашем случае это 'waitingForHelloFromClient'.
Все состояния описываются в объекте ClientConnection.prototype.states. Допустимы состояния, у которых нет inputHandler. При переключении в такие состояния, мы отрабатываем какой-то алгоритм внутри их onEnterHandler и сразу же переключаемся в другое состояние. Останавливаемся мы в том, которое имеет inputHandler, чтобы следующая итерация Event Loop могла вызывать код для обработки данных сокета, если они появятся. Категорически не рекомендуется делать переключения в onExitHandler.
Собственно всё. Если код покажется удобным — применяйте на здоровье. Критика приветствуется.
Хочу отметить, что есть решения (например, Machina.JS), которые в общем случае могут оказаться более удобными.
Давайте сравним.
Удалённый датчик температуры, который стучится к серверу и после установления соединения пишет в сокет несколько байт своего идентификатора, а затем несколько байт текущей температуры — это примитивно.
Тот же датчик, который может получать от сервера команды на изменение частоты измерений или периода усреднения измеряемой величины — уже посложнее. Добавьте к этому, например, функционал отправки датчику обновлений прошивки и код запросто может потерять лаконичность.
В подобных случаях я организую свой код в стиле «машины состояний», не претендуя, впрочем, на соответствие канонам теории автоматов.
Пример для этой статьи выложен здесь: 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), которые в общем случае могут оказаться более удобными.