Websocket之ws模組(二)

weixin_33866037發表於2017-02-12

說明

上篇實現了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>

相關文章