// Copyright (c) 2015-2017 The Regents of the University of California.
// All rights reserved.
//
// Permission is hereby granted, without written agreement and without
// license or royalty fees, to use, copy, modify, and distribute this
// software and its documentation for any purpose, provided that the above
// copyright notice and the following two paragraphs appear in all copies
// of this software.
//
// IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
// ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
// THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.
//
// THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
// PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
// CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
// ENHANCEMENTS, OR MODIFICATIONS.
/**
* Module supporting UDP (datagram) sockets.
* @module @accessors-modules/udp-socket
* @author Hokeun Kim, Edward A. Lee and Elizabeth Osyk (node port)
* @version $$Id$$
*/
// Stop extra messages from jslint. Note that there should be no
// space between the / and the * and global.
/*globals Java, exports, require, util */
/*jshint globalstrict: true */
"use strict";
var dgram = require('dgram');
var util = require('util');
var EventEmitter = require('events').EventEmitter;
///////////////////////////////////////////////////////////////////////////////
//// createSocket
/** Create a socket of the specified type.
* This returns an instance of the Socket class.
* @param type One of "udp4" or "udp6". Defaults to "udp4" if not given.
* @param callback Optional function to bind to "message" events.
* @param enableBroadcast Boolean to enable broadcasting or not
*/
exports.createSocket = function (type, callback, enableBroadcast) {
if (!type) {
type = "udp4";
}
var socket;
if (enableBroadcast) {
socket = new exports.Socket(type, true);
} else {
socket = new exports.Socket(type, false);
}
if (callback) {
socket.on("message", callback);
}
return socket;
};
///////////////////////////////////////////////////////////////////////////////
//// supportedReceiveTypes
/** Return an array of the types supported by the current host for
* receiveType arguments.
*/
exports.supportedReceiveTypes = function () {
return this.RECEIVE_DATA_TYPES;
};
///////////////////////////////////////////////////////////////////////////////
//// supportedSendTypes
/** Return an array of the types supported by the current host for
* sendType arguments.
*/
exports.supportedSendTypes = function () {
return this.SEND_DATA_TYPES;
};
///////////////////////////////////////////////////////////////////////////////
//// Socket
/** Construct an instance of a UDP (datagram) socket that can send or receive messages.
* To receive messages, call bind() on the returned object.
* To send messages, call send().
* The returned object is an event emitter that emits
* 'listening', 'message', 'close', or 'error'.
* For example,
* <pre>
* var UDPSocket = require('udpSocket');
* var socket = UDPSocket.createSocket();
* socket.on('message', function(message) {
* print('Received from web socket: ' + message);
* });
* socket.bind(8084);
* </pre>
* This class is fashioned after the Socket class in Node's dgram module,
* with the only exception being that the messages it emits are not instances
* of Buffer, but rather appropriate data types as specified by the receiveType
* argument to setReceiveType(). Similarly, the data provided to send() will be
* converted to a Buffer according to the type set by setSendType().
*
* Node dgram module: https://nodejs.org/api/dgram.html
* @param type One of "udp4" or "udp6".
* @param enableBroadcast boolean, setting to true enables the socket broadcast
* messages.
*/
exports.Socket = function(type, enableBroadcast) {
EventEmitter.call(this);
var self = this;
// Allowed data types. From
// $PTII/ptolemy/actor/lib/jjs/VertxHelperBase in the Ptolemy II
// tree. Don't include long and unsignedint because they cannot
// be represented in JavaScript.
this.RECEIVE_DATA_TYPES = ['byte', 'double', 'float', 'image', 'int',
'number', 'short', 'string',
'unsignedbyte', 'unsignedshort'];
this.SEND_DATA_TYPES = ['number', 'string', 'unsignedbyte'];
this.sendType = "string"; // Default data type. Strings can hold most content.
this.receiveType = "string";
this.rawBytes = false;
this.socket = dgram.createSocket(type);
if (enableBroadcast) {
this.socket.setBroadcast(true);
}
this.type = type;
this.socket.on('close', function(){
self.emit('close');
});
this.socket.on('error', function(error){
self.emit('error', error);
});
this.socket.on('listening', function(){
self.emit('listening');
});
this.socket.on('message', function(message, rinfo){
// Convert buffer to desired data type.
// TODO: Other types.
if (self.receiveType === "string") {
if (!this.rawBytes) {
self.emit('message', message.toString(), JSON.stringify(rinfo));
} else {
self.emit('message', message, JSON.stringify(rinfo));
}
} else if (typeof message === 'object' && message.buffer !== 'undefined') {
// Buffer
var arr = new Uint8Array(message.buffer, message.byteOffset, message.byteLength / Uint8Array.BYTES_PER_ELEMENT);
var values = arr.values();
var arr2 = [];
for (let n of values) {
arr2.push(n);
}
self.emit('message', arr2, JSON.stringify(rinfo));
} else {
self.emit('message', message, JSON.stringify(rinfo));
}
});
};
util.inherits(exports.Socket, EventEmitter);
/** Listen for datagram messages on the specified port and optional address.
* If no port is specified, then attempt to bind to a random port.
* If no address is specified, attempt to listen on all addresses.
* Once binding is complete, a 'listening' event is emitted and the
* optional callback function is called. The value of 'this' in the
* callback invocation will be this Socket object.
* @param port The port to listen on.
* @param address The network interface on which to listen.
* @param callback A function to call when the binding is complete.
*/
exports.Socket.prototype.bind = function (port, address, callback) {
if (!address) {
if (this.type === "udp4") {
address = "0.0.0.0";
} else {
address = "::0";
}
}
if (!callback) {
callback = null;
}
var options = {};
options.port = port;
options.address = address;
this.socket.bind(options, callback);
};
/** Close the current connection with the server.
* If there is data that was passed to this.send() but has not yet
* been successfully sent (because the socket was not open),
* then throw an exception.
*/
exports.Socket.prototype.close = function () {
try {
this.socket.close();
} catch (error) {
console.log("Warning: udpSocket.js: close() failed? See https://github.com/nodejs/node/issues/7061. Ignored exception was " + error);
console.log(error.stack);
}
};
/** Send a datagram message.
* @param data The data to send.
* @param port The destination port.
* @param hostname The name of the destination host (a hostname or IP address).
* @param callback An optional callback function to invoke when the send is complete,
* or if an error occurs. In the latter case, the cause of the error will be passed
* as an argument to the callback.
*/
exports.Socket.prototype.send = function (data, port, hostname, callback) {
if (!callback) {
callback = null;
}
if (typeof data === 'buffer') {
this.socket.send(data, 0, data.length, port, hostname, callback);
} else if (typeof data === 'string') {
var buffer = data;
if (this.rawBytes) {
buffer = new Buffer(data, 'hex');
}
this.socket.send(buffer, 0, data.length, port, hostname, callback);
} else {
this.socket.send(Buffer.from(data), port, hostname, callback);
}
};
/** Set if the exchanged packets will be considered as raw bytes or not.
* @param value Boolean set or reset raw byte
*/
exports.Socket.prototype.setRawBytes = function (value) {
if (typeof value == 'boolean') {
this.rawBytes = value;
} else {
this.emit('error', 'setRawBytes parameter should be a boolean.');
}
};
/** Set the receive type. If this is not called, the type defaults to "string".
* @param type The name of the receive type.
*/
exports.Socket.prototype.setReceiveType = function(type) {
if (this.RECEIVE_DATA_TYPES.indexOf(type.toLowerCase()) > 0) {
this.receiveType = type.toLowerCase();
} else {
self.emit('error', 'Type ' + type + ' is not a supported receive type.');
}
};
/** Set the send type. If this is not called, the type defaults to "string".
* @param type The name of the send type.
*/
// FIXME: These are copied from Cape Code module. Node automatically converts
// all types to a buffer. What is appropriate here?
// See https://nodejs.org/api/dgram.html
exports.Socket.prototype.setSendType = function(type) {
if (this.SEND_DATA_TYPES.indexOf(type.toLowerCase()) > 0) {
this.sendType = type.toLowerCase();
} else {
this.emit('error', 'Type ' + type + ' is not a supported send type.');
}
};