Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to write a websocket client

I'm trying to unit test my websocket on node.js and want to mock out a websocket client. I could create a HTML file that just connects to my server but then I can't run a single test on the server.

How would I go about (using either http.Client or net.Stream) to create a websocket client and have it interact with my server.

I'm targetting the (soon to be dead) draft 76 of the websocket spec.

The server side implementation I'm using is this

like image 704
Raynos Avatar asked Dec 12 '10 13:12

Raynos


1 Answers

Since you already know that all current WebSocket version will be obsolete very soon and you're using a WebSocket server which supports the old 75 draft, it's fairly trivial to make one if you already have some server code lying around, so no need for the "security" header stuff in 76.

Disclaimer: This thing had only 5 minute of testing or so but it should work for the most part.

Epic wall of code follows

var net = require('net');

function WebSocket(host, port, encoder, decoder) {
    this.encoder = encoder || function(data){return data.toString()};
    this.decoder = decoder || function(data){return data};
    this.socket = net.createConnection(port, host);

    this.connected = false;
    this.header = 0;
    this.bytesSend = 0;
    this.dataFrames = [];
    this.dataState = 0;

    var that = this;
    process.nextTick(function() {
        that.init(host, port);
    });
}


// Prototype -------------------------------------------------------------------
WebSocket.prototype = {
    onConnect: function() {console.log('connect');},
    onClose: function() {console.log('close');},
    onData: function(data) {console.log(data)},

    init: function(host, port) {
        var that = this;
        this.socket.addListener('connect', function() {
            var data ='GET / HTTP/1.1\r\n'
                  + 'Host: ' + host + ':' + port + '\r\n'
                  + 'Origin: websocket.node.js\r\n'
                  + 'Connection: Upgrade\r\n'
                  + 'Upgrade: WebSocket\r\n\r\n';

            that.socket.write(data, 'ascii');
            that.socket.flush();
        });
        this.socket.addListener('data', function(data) {that.read(data);});
        this.socket.addListener('end', function() {that.onClose();});
        this.socket.addListener('error', function(e) {console.log(e.message);that.close();});
    },

    send: function(data, encoded) {
        if (this.connected) {
            return this.write(encoded ? data : this.encoder(data));

        } else {
            return 0;
        }
    },

    close: function() {
        if (this.connected) {
            this.connected = false;
            this.write(null);
            this.socket.end();
            this.socket.destroy();
        }
    },

    read: function read(data) {
        for(var i = 0, l = data.length; i < l; i++) {
            var b = data[i];
            if (this.header < 4) {
                if ((this.header === 0 || this.header === 2) && b === 0x0d) {
                    this.header++;

                } else if ((this.header === 1 || this.header === 3) && b === 0x0a) {
                    this.header++;

                } else {
                    this.header = 0;
                }

                if (this.header === 4) {
                    this.connected = true;
                    this.onConnect();
                    this.header = 5;
                }

            } else {
                if (this.dataState === 0) {
                    this.dataState = b & 0x80 === 0x80 ? 2 : 1;

                // Low bit frame
                } else if (this.dataState === 1) {
                    if (b === 0xff) {
                        var buffer = new Buffer(this.dataFrames);
                        this.dataFrames = [];
                        this.dataState = 0;

                        if (!this.message(buffer.toString('utf8', 0, buffer.length))) {
                            this.send({error: 'Invalid Message.'});
                            this.close();
                            return;
                        }

                    } else {
                        this.dataFrames.push(b);
                    }

                // Unused high bit frames
                } else if (this.dataState === 2) {
                    if (b === 0x00) {
                        this.close();
                    }
                }
            }
        }
    },

    write: function(data) {
        var bytes = 0;
        if (!this.socket.writable) {
            return bytes;
        }

        try {
            this.socket.write('\x00', 'binary');
            if (typeof data === 'string') {
                this.socket.write(data, 'utf8');
                bytes += Buffer.byteLength(data);
            }
            this.socket.write('\xff', 'binary'); 
            this.socket.flush();
            bytes += 2;

        } catch(e) {}

        this.bytesSend += bytes;
        return bytes;
    },

    message: function(msg) {
        if (this.decoder) {
            try {
                msg = this.decoder(msg);

            } catch(e) {
                this.close();
                return false;
            }
        }
        this.onData(msg);
        return true;
    }
};

And here we set it up:

var bison = require('bison');

// automatically do some encoding/decoding magic
var test = new WebSocket('localhost', 28785, bison.encode, bison.decode);
test.onConnect = function() {

};

test.onData = function(data) {

};

Feel free to ask questions in the comments.

PS: For info on how it works, read the spec :P

like image 85
Ivo Wetzel Avatar answered Oct 06 '22 01:10

Ivo Wetzel