I made a package for Node.js that allowed the querying of game servers (or any UDP server(untested)) using UDP.
What it does is sends the query to the server and adds the query into a queue. When the message is received it retrieves the response from the server, parses it and removes the query from the queue.
Am I using the promises correctly and am I going about it the correct way?
Structure
- lib
- core.js
- index.js
- protocols
- iw4
- master.js
- iw4
core.js
var Q = require('q'),
Class = require('extend.class'), //https://www.npmjs.org/package/extend.class
lookup = Q.denodeify(require('dns').lookup);
module.exports = Class.extend({
init: function() {
this._super();
this.options = {};
this.maxAttempts = 1;
this.attempt = 1;
this.finished = false;
this.udpTimeoutTimer = false;
},
start: function() {
var self = this;
return this.lookup()
.then(function() {
return self.run();
});
},
lookup: function() {
var deferred = Q.defer();
if (!('host' in this.options))
deferred.reject(new Error('No host specified'));
if (this.options.host.match(/^\d+\.\d+\.\d+\.\d+$/)) { //matched IP only
if (this.options.debug) {
console.log('IP matched: ' + this.options.host);
}
this.options.address = this.options.host;
deferred.resolve();
} else if (this.options.host.match(/^\d+\.\d+\.\d+\.\d+\:\d+$/)) { //matched IP:PORT, parse accordingly
var temp = this.options.host.split(':');
this.options.address = temp[0];
this.options.port = temp[1];
deferred.resolve();
} else { //host is a domain, look it up and retrieve the IP
var self = this;
return lookup(this.options.host).then(function(address) {
if (self.options.debug) {
console.log('Looked up: ' + self.options.host + ' resolved to: ' + address);
}
self.options.address = address;
deferred.resolve();
});
}
return deferred.promise;
},
udpSend: function(buffer) {
var deferred = Q.defer(),
self = this;
if (!('port' in this.options)) {
return deferred.reject(new Error('Attempted to send without setting a port'));
}
if (!('address' in this.options)) {
return deferred.reject(new Error('Attempted to send without setting an address'));
}
if (typeof buffer === 'string') {
buffer = new Buffer(buffer, 'binary');
}
process.nextTick(function() {
self.udpSocket.send(buffer, 0, buffer.length, self.options.port, self.options.address, function() {
if (self.options.debug) {
console.log('Sent buffer: ' + buffer);
}
});
self.udpTimeoutTimer = setTimeout(function() {
//if query isn't finished and hasn't received a response yet, time it out.
if (!self.finished) {
return self.deferred.reject('Timeout');
}
}, self.options.timeout || 3000);
});
return deferred.promise;
},
udpResponse: function(buffer) {
this.finished = true;
var self = this;
clearTimeout(this.udpTimeoutTimer);
if (this.options.parse) {
this.deferred.resolve(this.parse(buffer));
} else {
this.deferred.resolve(buffer);
}
if (this.options.debug) {
console.log('Received response: ' + buffer);
}
return Q.fcall(function() {
return self;
});
},
});
index.js
var dgram = require('dgram'),
udpSocket = dgram.createSocket('udp4'),
activeQueries = [],
Q = require('q');
udpSocket.unref();
udpSocket.bind(1337);
udpSocket.on('message', function(buffer, rinfo) {
for (var i = 0; i < activeQueries.length; i++) {
var query = activeQueries[i];
if (query.options.address != rinfo.address) {
continue;
}
if (query.options.port != rinfo.port) {
continue;
}
return query.udpResponse(buffer)
.then(function(thisQuery) {
var i = activeQueries.indexOf(thisQuery);
if(i >= 0) {
activeQueries.splice(i, 1); //query is finished, remove it.
}
});
}
});
module.exports = {
query: function(options) {
var type = (options.type || '').replace(/[^0-9a-zA-Z\\\/\-]/g, '');
var protocol = require('./protocols/' + type);
var query = new protocol();
query.udpSocket = udpSocket;
query.type = type;
query.deferred = Q.defer();
for (var i in options) {
query.options[i] = options[i];
}
activeQueries.push(query);
process.nextTick(function() {
query.start();
});
return query.deferred.promise;
}
};
protocols/iw4/master.js
module.exports = require('../../core').extend({
run: function() {
return this.udpSend(this.options.buffer || '\xff\xff\xff\xffgetservers IW4 61586 full empty\x00');
},
parse: function(buffer) {
var servers = [];
var bufferString = buffer.toString('binary');
bufferString = bufferString.slice(0, -3);
var tempSplit = bufferString.split(/\\/);
tempSplit.forEach(function(server) {
var len = server.length;
if (len == 6) {
var ip = '';
for (var i = 0; i < len - 2; i++) {
ip += server[i].charCodeAt(0) + '.';
}
ip = ip.slice(0, -1);
port = (server[len - 2].charCodeAt(0) << 8) + server[len - 1].charCodeAt(0);
servers.push({
ip: ip,
port: port
});
}
});
return servers;
}
});
A generic request would look like so:
var request = require('./lib');
request.query({
type: 'iw4/master',
host: '127.0.0.1',
port: 20810,
timeout: 3000,
parse: true
}).then(function(servers) {
console.log(servers); //array of servers in {ip:ip, port:port} format
}, function(error) {
console.log(error);
});
this.options.address = address; deferred.resolve();
. Instead you could havedeferred.resolve(address);
. This would make the code easier to test, for one! Also, if you callreturn
from within athen
, thethen
call will return that value when it resolves. \$\endgroup\$