📜 ⬆️ ⬇️

Writing code for Node.js in the state machine style

Do you write a server to Node.js, which accepts incoming TCP connections and conducts non-trivial dialog with clients using non-standard protocol? You may be interested in the example that I develop in my projects. What do I mean by nontrivial dialogue?

Let's compare.

A remote temperature sensor that knocks on the server and, after establishing a connection, writes several bytes of its identifier to the socket, and then several bytes of the current temperature — this is primitive.
')
The same sensor that can receive commands from the server to change the measurement frequency or the averaging period of the measured value is already more complicated. Add to this, for example, the functionality of sending a firmware update to the sensor and the code can easily lose conciseness.

In such cases, I organize my code in the “state machine” style, without pretending, however, to conform to the canons of automata theory.

An example for this article is posted here: github.com/kityan/fsmConnection . Next, I will explain a few key points.

Main application code.

Consider only the server part. The code is very simple:

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);}); 

Each time after the connection is established, the server creates an instance of ClientConnection, passing it a socket and a configuration object.

Code snippets of the ClientConnection module.

Initialize the instance fields:

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

In the prototype, we define the ClientConnection.to method, which will switch the machine.

 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; } 

When switching, we check if the previous state had an onExitHandler method and, if it had, call it.
Then we assign a pointer to the new state's inputHandler to the machine's handleInput method. And finally, we check if the new state has an onEnterHandler method. If there is - call it.

What happens next after calling ClientConnection.to (newState) ? If the onExitHandler and onEnterHandler calls did not switch to another state, the machine remains in it. And then everything depends on the socket data. All arriving packets will be sent to handleInput . Why?

The fact is that when creating an instance, we immediately switch to the initialization state, where we hang handlers for socket events:

 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"); } }, ... } 


And then we switch to the next state. In our case, this is 'waitingForHelloFromClient' .

All states are described in the ClientConnection.prototype.states object. Valid states that do not have inputHandler . When switching to such states, we work out some kind of algorithm inside onEnterHandler and immediately switch to another state. We stop at the one that inputHandler has, so that the next iteration of the Event Loop can call up code to handle the socket data, if it appears. It is strongly recommended not to switch to onExitHandler .

Actually everything. If the code seems convenient - apply to health. Criticism is welcome.

I want to note that there are solutions (for example, Machina.JS ), which in the general case may be more convenient.

Source: https://habr.com/ru/post/272983/


All Articles