说明
上篇实现了ws模块的基本用法,为了方便使用,可以将其封装一下。
ws_server.js
// require variables to be declared
"use strict";
var WebSocket = require('ws');
var WebSocketServer = WebSocket.Server;
/**
* Client socket object
*
* @class WebsocketIO
* @constructor
* @param ws {Object} ULR of the server or actual websocket
* @param strictSSL {Bool} require or not SSL verification with a certiifcate
* @param openCallback {Function} callback when the socket opens
*/
function WebSocketIO(ws, strictSSL, openCallback, logLevel) {
if (typeof ws === "string")
this.ws = new WebSocket(ws, null, {rejectUnauthorized: strictSSL});
else
this.ws = ws;
this.id = "";
var _this = this;
this.messages = {};
this.outbound = {};
if (this.ws.readyState === 1) {
this.remoteAddress = {address: this.ws._socket.remoteAddress, port: this.ws._socket.remotePort};
this.id = this.remoteAddress.address + ":" + this.remoteAddress.port;
}
this.closeCallbacks = [];
this.aliasCount = 1;
this.logLevel = logLevel || "quiet"
this.remoteListeners = {"#WSIO#addListener": "0000"};
this.localListeners = {"0000": "#WSIO#addListener"};
this.ws.on('error', function(err) {
if (err.errno === "ECONNREFUSED") return; // do nothing
});
this.ws.on('open', function() {
_this.ws.binaryType = "arraybuffer";
_this.remoteAddress = {address: _this.ws._socket.remoteAddress, port: _this.ws._socket.remotePort};
_this.id = _this.remoteAddress.address + ":" + _this.remoteAddress.port;
if(openCallback !== null) openCallback();
});
this.ws.on('message', function(message) {
var fName;
if (typeof message === "string") {
var msg = JSON.parse(message);
fName = _this.localListeners[msg.f];
if(fName === undefined) {
if (_this.logLevel != "quiet")
console.log("WebsocketIO>\tno handler for message");
}
// add lister to client
else if(fName === "#WSIO#addListener") {
_this.remoteListeners[msg.d.listener] = msg.d.alias;
if (_this.outbound.hasOwnProperty(msg.d.listener)) {
var i;
for (i=0; i<_this.outbound[msg.d.listener].length; i++) {
if (typeof _this.outbound[msg.d.listener][i] === "string") {
_this.emitString(msg.d.listener, _this.outbound[msg.d.listener][i]);
}
else {
_this.emit(msg.d.listener, _this.outbound[msg.d.listener][i]);
}
}
delete _this.outbound[msg.d.listener];
}
}
// handle message
else {
_this.messages[fName](_this, msg.d);
}
}
else {
var func = String.fromCharCode(message[0]) +
String.fromCharCode(message[1]) +
String.fromCharCode(message[2]) +
String.fromCharCode(message[3]);
fName = _this.localListeners[func];
var buf = message.slice(4, message.length);
_this.messages[fName](_this, buf);
}
});
this.ws.on('close', function() {
for(var i=0; i<_this.closeCallbacks.length; i++) {
_this.closeCallbacks[i](_this);
}
});
}
/**
* Setting a callback when the socket closes
*
* @method onclose
* @param callback {Function} function to execute after closing
*/
WebSocketIO.prototype.onclose = function(callback) {
this.closeCallbacks.push(callback);
};
/**
* Set a message handler for a given name
*
* @method on
* @param name {String} name for the handler
* @param callback {Function} handler to be called for a given name
*/
WebSocketIO.prototype.on = function(name, callback) {
var alias = ("0000" + this.aliasCount.toString(16)).substr(-4);
this.localListeners[alias] = name;
this.messages[name] = callback;
this.aliasCount++;
this.emit('#WSIO#addListener', {listener: name, alias: alias});
};
/**
* Send a message with a given name and payload (format> f:name d:payload)
*
* @method emit
* @param name {String} name of the message (i.e. RPC)
* @param data {Object} data to be sent with the message
*/
WebSocketIO.prototype.emit = function(name, data, attempts) {
if (this.ws.readyState === 1) {
if (name === null || name === "") {
if (this.logLevel != "quiet")
console.log("WebsocketIO>\tError, no message name specified");
return;
}
var _this = this;
var message;
var alias;
if (this.remoteListeners.hasOwnProperty(name)) {
alias = this.remoteListeners[name];
if (Buffer.isBuffer(data)) {
var funcName = new Buffer(alias);
message = Buffer.concat([funcName, data]);
// double error handling
try {
this.ws.send(message, {binary: true, mask: false}, function(err){
if (_this.logLevel != "quiet")
if(err) console.log("WebsocketIO>\t---ERROR (ws.send)---", name);
// else success
});
}
catch(e) {
if (_this.logLevel != "quiet")
console.log("WebsocketIO>\t---ERROR (try-catch)---", name);
}
}
else {
message = {f: alias, d: data};
// double error handling
try {
var msgString = JSON.stringify(message);
this.ws.send(msgString, {binary: false, mask: false}, function(err){
if (_this.logLevel != "quiet")
if(err) console.log("WebsocketIO>\t---ERROR (ws.send)---", name);
// else success
});
}
catch(e) {
if (_this.logLevel != "quiet")
console.log("WebsocketIO>\t---ERROR (try-catch)---", name);
}
}
}
else {
if (!this.outbound.hasOwnProperty(name)) {
this.outbound[name] = [];
}
this.outbound[name].push(data);
setTimeout(function() {
_this.removeOutbound(name);
}, 1000);
}
}
};
/**
* Removes outbound message from queue: called if no listener is registered after 1 sec
*
* @method removeOutbound
* @param name {String} name of sending message
*/
WebSocketIO.prototype.removeOutbound = function(name) {
if (this.outbound.hasOwnProperty(name) && this.outbound[name].length > 0) {
if (this.logLevel != "quiet")
console.log("WebsocketIO>\tWarning: not sending message, recipient has no listener (" + name + ")");
this.outbound[name].splice(0, 1);
if (this.outbound[name].length == 0) {
delete this.outbound[name];
}
}
};
/**
* Faster version for emit: No JSON stringigy and no check version
*
* @method emitString
* @param data {String} data to be sent as the message
*/
WebSocketIO.prototype.emitString = function(name, dataString, attempts) {
if (this.ws.readyState === 1) {
var _this = this;
var message;
var alias;
if (this.remoteListeners.hasOwnProperty(name)) {
alias = this.remoteListeners[name];
message = "{\"f\":\"" + alias + "\",\"d\":" + dataString + "}";
this.ws.send(message, {binary: false, mask: false});
}
else {
if (!this.outbound.hasOwnProperty(name)) {
this.outbound[name] = [];
}
this.outbound[name].push(dataString);
setTimeout(function() {
_this.removeOutbound(name);
}, 1000);
}
}
};
/**
* Update the remote address of the client
*
* @method updateRemoteAddress
* @param host {String} hostname / ip address
* @param port {Integer} port number
*/
WebSocketIO.prototype.updateRemoteAddress = function(host, port) {
if(typeof host === "string") this.remoteAddress.address = host;
if(typeof port === "number") this.remoteAddress.port = port;
this.id = this.remoteAddress.address + ":" + this.remoteAddress.port;
};
/**
* Server socket object
*
* @class WebsocketIOServer
* @constructor
* @param data {Object} object containing .server or .port information
*/
function WebSocketIOServer(data) {
if (data.server !== undefined)
this.wss = new WebSocketServer({server: data.server, perMessageDeflate: false});
else if(data.port !== undefined)
this.wss = new WebSocketServer({port: data.port, perMessageDeflate: false});
this.clients = {};
this.logLevel = data.logLevel || "quiet";
}
/**
* Setting a callback when a connection happens
*
* @method onconnection
* @param callback {Function} function taking the new client (WebsocketIO) as parameter
*/
WebSocketIOServer.prototype.onconnection = function(callback) {
var _this = this;
this.wss.on('connection', function(ws) {
ws.binaryType = "arraybuffer";
var wsio = new WebSocketIO(ws, null, null, this.logLevel);
wsio.onclose(function(closed) {
delete _this.clients[closed.id];
});
_this.clients[wsio.id] = wsio;
callback(wsio);
});
};
WebSocketIOServer.prototype.broadcast = function(name, data) {
var key;
var alias;
// send as binary buffer
if (Buffer.isBuffer(data)) {
for(key in this.clients) {
this.clients[key].emit(name, data);
}
}
// send data as JSON string
else {
var dataString = JSON.stringify(data);
for(key in this.clients) {
this.clients[key].emitString(name, dataString);
}
}
};
module.exports = WebSocketIO;
module.exports.Server = WebSocketIOServer;
ws_client.js
"use strict";
/**
* @module client
* @submodule WebsocketIO
*/
/**
* Lightweight object around websocket, handles string and binary communication
*
* @class WebsocketIO
* @constructor
*/
function WebsocketIO(url) {
if (url !== undefined && url !== null) {
this.url = url;
} else {
this.url = (window.location.protocol === "https:" ? "wss" : "ws") + "://" + window.location.host +
"/" + window.location.pathname.split("/")[1];
}
/**
* websocket object handling the communication with the server
*
* @property ws
* @type WebSocket
*/
this.ws = null;
/**
* list of messages to be handled (name + callback)
*
* @property messages
* @type Object
*/
this.messages = {};
/**
* number of aliases created for listeners
*
* @property aliasCount
* @type Integer
*/
this.aliasCount = 1;
/**
* list of listeners on other side of connection
*
* @property remoteListeners
* @type Object
*/
this.remoteListeners = {"#WSIO#addListener": "0000"};
/**
* list of local listeners on this side of connection
*
* @property localListeners
* @type Object
*/
this.localListeners = {"0000": "#WSIO#addListener"};
/**
* Open a websocket
*
* @method open
* @param callback {Function} function to be called when the socket is ready
*/
this.open = function(callback) {
var _this = this;
console.log('WebsocketIO> open', this.url);
this.ws = new WebSocket(this.url);
this.ws.binaryType = "arraybuffer";
this.ws.onopen = callback;
// Handler when a message arrives
this.ws.onmessage = function(message) {
var fName;
// text message
if (typeof message.data === "string") {
var msg = JSON.parse(message.data);
fName = _this.localListeners[msg.f];
if (fName === undefined) {
console.log('WebsocketIO> No handler for message');
}
if (fName === "#WSIO#addListener") {
_this.remoteListeners[msg.d.listener] = msg.d.alias;
return;
}
_this.messages[fName](msg.d);
} else {
var uInt8 = new Uint8Array(message.data);
var func = String.fromCharCode(uInt8[0]) +
String.fromCharCode(uInt8[1]) +
String.fromCharCode(uInt8[2]) +
String.fromCharCode(uInt8[3]);
fName = _this.localListeners[func];
var buffer = uInt8.subarray(4, uInt8.length);
_this.messages[fName](buffer);
}
};
// triggered by unexpected close event
this.ws.onclose = function(evt) {
console.log("WebsocketIO> socket closed");
if ('close' in _this.messages) {
_this.messages.close(evt);
}
};
};
/**
* Set a message handler for a given name
*
* @method on
* @param name {String} name for the handler
* @param callback {Function} handler to be called for a given name
*/
this.on = function(name, callback) {
var alias = ("0000" + this.aliasCount.toString(16)).substr(-4);
this.localListeners[alias] = name;
this.messages[name] = callback;
this.aliasCount++;
if (name === "close") {
return;
}
this.emit('#WSIO#addListener', {listener: name, alias: alias});
};
/**
* Send a message with a given name and payload (format> f:name d:payload)
*
* @method emit
* @param name {String} name of the message (i.e. RPC)
* @param data {Object} data to be sent with the message
*/
this.emit = function(name, data, attempts) {
if (name === null || name === "") {
console.log("Error: no message name specified");
return;
}
var _this = this;
var message;
var alias = this.remoteListeners[name];
if (alias === undefined) {
if (attempts === undefined) {
attempts = 16;
}
if (attempts >= 0) {
setTimeout(function() {
_this.emit(name, data, attempts - 1);
}, 4);
} else {
console.log("Warning: not sending message, recipient has no listener (" + name + ")");
}
return;
}
// send binary data as array buffer
if (data instanceof Uint8Array) {
// build an array with the name of the function
var funcName = new Uint8Array(4);
funcName[0] = alias.charCodeAt(0);
funcName[1] = alias.charCodeAt(1);
funcName[2] = alias.charCodeAt(2);
funcName[3] = alias.charCodeAt(3);
message = new Uint8Array(4 + data.length);
// copy the name of the function first
message.set(funcName, 0);
// then copy the payload
message.set(data, 4);
// send the message using websocket
this.ws.send(message.buffer);
} else {
// send data as JSON string
message = {f: alias, d: data};
this.ws.send(JSON.stringify(message));
}
};
/**
* Deliberate close function
*
* @method emit
*/
this.close = function() {
// disable onclose handler first
this.ws.onclose = function() {};
// then close
this.ws.close();
};
}
服务端
var WebSocketIOServer = require('./ws_server').Server;
var wss = new WebSocketIOServer({ port: 9000 });
wss.onconnection(function(ws) {
wss.broadcast('hi', 'I am server');
ws.emit('hi', { msg: 'test' });
ws.on('hello', function(ctx, data) {
console.log(data);
});
});
客户端
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WS</title>
</head>
<body>
<script src="ws_client.js"></script>
<script>
var ws = new WebsocketIO('ws://localhost:9000/');
ws.open(function() {
ws.on('hi', function(data) {
console.log(data)
ws.emit('hello', Date.now());
});
});
</script>
</body>
</html>