├── .gitignore ├── LICENSE ├── README.md ├── examples └── echo │ ├── README.md │ ├── server.js │ └── session.js ├── lib ├── optparse.js └── websocket.js └── wsbench /.gitignore: -------------------------------------------------------------------------------- 1 | README.html 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Peter Griess 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | * Neither the name wsbench nor the names of its contributors 13 | may be used to endorse or promote products derived from this software 14 | without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | `wsbench` is a scriptable benchmarking tool for Web Socket servers. 2 | 3 | An installation of [NodeJS](http://nodejs.org) is required, but there are no 4 | other external dependencies. See [Benchmarking Web Socket servers with 5 | wsbench](http://blog.std.in/2010/09/24/benchmarking-web-socket-servers/) for 6 | more background on the motivation and intended usage of this tool. 7 | 8 | ## Basic usage 9 | 10 | At its simplest, `wsbench` can be invoked with a Web Socket URL pointing to the 11 | server. For example, this opens and closes 100 connections in serial to the 12 | server running on localhost, port 8080. No messages are sent down the 13 | connections. 14 | 15 | % wsbench ws://localhost:8080 16 | 17 | A parallel, rate-driven model is available using the `-r` option. For example, 18 | the following opens and closes 10 connections per second and runs indefinitely. 19 | This uses `-c NNN` to specify termination after `NNN` connections, with 0 20 | indicating no limit. 21 | 22 | % wsbench -r 10 ws://localhost:8080 23 | 24 | We can also send messages over each connection using the `-m NNN` option to 25 | indicate how many messages to send. The `-s NNN` option can be used to set the 26 | size of each message in bytes. When operating in this mode, the websocket 27 | connection is guaranteed to remain open until all messages have been 28 | transmitted. 29 | 30 | ## Session scripting 31 | 32 | The `wsbench` tool supports execution of arbitrary JavaScript code to drive the 33 | interaction over open connections (e.g. to send and receive messages) using the 34 | `-S FILE` option. This allows testing of rich, application-specific behavior. 35 | As a trivial example, the following file will send a `Hello` message for the 36 | first 10 connections and `world!` for each connection after and then close the 37 | connection. Note that the session function is invoked once for each web socket 38 | opened, so we keep our counter in the module scope. 39 | 40 | var cnt = 0; 41 | 42 | module.exports = function(ws) { 43 | ws.onopen = function() { 44 | ws.send((++cnt <= 10) ? 'Hello' : 'world!'); 45 | ws.close(); 46 | }; 47 | }; 48 | 49 | A more involved example is available in the `examples/echo/` 50 | [directory](http://github.com/pgriess/wsbench/tree/master/examples/echo/). 51 | 52 | Finally, the session logic can use any NodeJS module installed in the 53 | system. Significantly, this provides access to the built-in HTTP stack; 54 | constructing heterogeneous workloads consisting of a mix of Web Socket and 55 | HTTP requests is trivial. 56 | 57 | ## Full usage 58 | 59 | The complete usage is 60 | 61 | usage: wsbench [options] 62 | 63 | Kick off a benchmarking run against the given ws:// URL. 64 | 65 | We can execute our workload in one of two ways: serially, wherein each 66 | connection is closed before the next is initiated; or in parallel, wherein 67 | a desired rate is specified and connections initiated to meet this rate, 68 | independent of the state of other connections. Serial execution is the 69 | default, and parallel execution can be specified using the -r 70 | option. Parallel execution is bounded by the total number of connections 71 | to be made, specified by the -c option. 72 | 73 | Available options: 74 | -c, --num-conns NUMBER number of connections to open (default: 100) 75 | -h, --help display this help 76 | -m, --num-msgs NUMBER number of messages per connection (dfeault: 0) 77 | -p, --protocol PROTO set the Web Socket protocol to use (default: empty) 78 | -r, --rate NUMBER number of connections per second (default: 0) 79 | -s, --msg-size NUMBER size of messages to send, in bytes (default: 32) 80 | -S, --session FILE file to use for session logic (default: None) 81 | -------------------------------------------------------------------------------- /examples/echo/README.md: -------------------------------------------------------------------------------- 1 | This example benchmarks an echo server: the client opens a connection, sends 2 | a message, waits for a message to be received, and then closes the 3 | connection. For this, we use the support for sessions via `wsbench -S`. 4 | 5 | The server requires a working installation of 6 | [node-websocket-server](http://github.com/miksago/node-websocket-server). 7 | -------------------------------------------------------------------------------- /examples/echo/server.js: -------------------------------------------------------------------------------- 1 | var ws = require('websocket-server'); 2 | 3 | var srv = ws.createServer(); 4 | srv.addListener('connection', function(c) { 5 | c.addListener('message', function(m) { 6 | console.log(m); 7 | c.write(m); 8 | }); 9 | }); 10 | srv.listen(8000); 11 | -------------------------------------------------------------------------------- /examples/echo/session.js: -------------------------------------------------------------------------------- 1 | module.exports = function(ws) { 2 | ws.onopen = function() { 3 | ws.send('biff'); 4 | }; 5 | 6 | ws.onmessage = function(m) { 7 | console.log(m); 8 | ws.close(); 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /lib/optparse.js: -------------------------------------------------------------------------------- 1 | // Optparse.js 1.0.2 - Option Parser for Javascript 2 | // 3 | // Copyright (c) 2009 Johan Dahlberg 4 | // 5 | // See README.md for license. 6 | // 7 | var optparse = {}; 8 | try{ optparse = exports } catch(e) {}; // Try to export the lib for node.js 9 | (function(self) { 10 | var VERSION = '1.0.2'; 11 | var LONG_SWITCH_RE = /^--\w/; 12 | var SHORT_SWITCH_RE = /^-\w/; 13 | var NUMBER_RE = /^(0x[A-Fa-f0-9]+)|([0-9]+\.[0-9]+)|(\d+)$/; 14 | var DATE_RE = /^\d{4}-(0[0-9]|1[0,1,2])-([0,1,2][0-9]|3[0,1])$/; 15 | var EMAIL_RE = /^([0-9a-zA-Z]+([_.-]?[0-9a-zA-Z]+)*@[0-9a-zA-Z]+[0-9,a-z,A-Z,.,-]*(.){1}[a-zA-Z]{2,4})+$/; 16 | var EXT_RULE_RE = /(\-\-[\w_-]+)\s+([\w\[\]_-]+)|(\-\-[\w_-]+)/; 17 | var ARG_OPTIONAL_RE = /\[(.+)\]/; 18 | 19 | // The default switch argument filter to use, when argument name doesnt match 20 | // any other names. 21 | var DEFAULT_FILTER = '_DEFAULT'; 22 | var PREDEFINED_FILTERS = {}; 23 | 24 | // The default switch argument filter. Parses the argument as text. 25 | function filter_text(value) { 26 | return value; 27 | } 28 | 29 | // Switch argument filter that expects an integer, HEX or a decimal value. An 30 | // exception is throwed if the criteria is not matched. 31 | // Valid input formats are: 0xFFFFFFF, 12345 and 1234.1234 32 | function filter_number(value) { 33 | var m = NUMBER_RE(value); 34 | if(m == null) throw OptError('Expected a number representative'); 35 | if(m[1]) { 36 | // The number is in HEX format. Convert into a number, then return it 37 | return parseInt(m[1], 16); 38 | } else { 39 | // The number is in regular- or decimal form. Just run in through 40 | // the float caster. 41 | return parseFloat(m[2] || m[3]); 42 | } 43 | }; 44 | 45 | // Switch argument filter that expects a Date expression. The date string MUST be 46 | // formated as: "yyyy-mm-dd" An exception is throwed if the criteria is not 47 | // matched. An DATE object is returned on success. 48 | function filter_date(value) { 49 | var m = DATE_RE(value); 50 | if(m == null) throw OptError('Expected a date representation in the "yyyy-mm-dd" format.'); 51 | return new Date(parseInt(m[0]), parseInt(m[1]), parseInt(m[2])); 52 | }; 53 | 54 | // Switch argument filter that expects an email address. An exception is throwed 55 | // if the criteria doesn`t match. 56 | function filter_email(value) { 57 | var m = EMAIL_RE(value); 58 | if(m == null) throw OptError('Excpeted an email address.'); 59 | return m[1]; 60 | } 61 | 62 | // Register all predefined filters. This dict is used by each OptionParser 63 | // instance, when parsing arguments. Custom filters can be added to the parser 64 | // instance by calling the "add_filter" -method. 65 | PREDEFINED_FILTERS[DEFAULT_FILTER] = filter_text; 66 | PREDEFINED_FILTERS['TEXT'] = filter_text; 67 | PREDEFINED_FILTERS['NUMBER'] = filter_number; 68 | PREDEFINED_FILTERS['DATE'] = filter_date; 69 | PREDEFINED_FILTERS['EMAIL'] = filter_email; 70 | 71 | // Buildes rules from a switches collection. The switches collection is defined 72 | // when constructing a new OptionParser object. 73 | function build_rules(filters, arr) { 74 | var rules = []; 75 | for(var i=0; i> value means that the switch does 112 | // not take anargument. 113 | function build_rule(filters, short, expr, desc) { 114 | var optional, filter; 115 | var m = expr.match(EXT_RULE_RE); 116 | if(m == null) throw OptError('The switch is not well-formed.'); 117 | var long = m[1] || m[3]; 118 | if(m[2] != undefined) { 119 | // A switch argument is expected. Check if the argument is optional, 120 | // then find a filter that suites. 121 | var optional_match = ARG_OPTIONAL_RE(m[2]); 122 | var filter_name = optional_match === null ? m[2] : optional_match[1]; 123 | optional = optional_match !== null; 124 | filter = filters[filter_name]; 125 | if(filter === undefined) filter = filters[DEFAULT_FILTER]; 126 | } 127 | return { 128 | name: long.substr(2), 129 | short: short, 130 | long: long, 131 | decl: expr, 132 | desc: desc, 133 | optional_arg: optional, 134 | filter: filter 135 | } 136 | } 137 | 138 | // Loop's trough all elements of an array and check if there is valid 139 | // options expression within. An valid option is a token that starts 140 | // double dashes. E.G. --my_option 141 | function contains_expr(arr) { 142 | if(!arr || !arr.length) return false; 143 | var l = arr.length; 144 | while(l-- > 0) if(LONG_SWITCH_RE(arr[l])) return true; 145 | return false; 146 | } 147 | 148 | // Extends destination object with members of source object 149 | function extend(dest, src) { 150 | var result = dest; 151 | for(var n in src) { 152 | result[n] = src[n]; 153 | } 154 | return result; 155 | } 156 | 157 | // Appends spaces to match specified number of chars 158 | function spaces(arg1, arg2) { 159 | var l, builder = []; 160 | if(arg1.constructor === Number) { 161 | l = arg1; 162 | } else { 163 | if(arg1.length == arg2) return arg1; 164 | l = arg2 - arg1.length; 165 | builder.push(arg1); 166 | } 167 | while(l-- > 0) builder.push(' '); 168 | return builder.join(''); 169 | } 170 | 171 | // Create a new Parser object that can be used to parse command line arguments. 172 | // 173 | // 174 | function Parser(rules) { 175 | return new OptionParser(rules); 176 | } 177 | 178 | // Creates an error object with specified error message. 179 | function OptError(msg) { 180 | return new function() { 181 | this.msg = msg; 182 | this.toString = function() { 183 | return this.msg; 184 | } 185 | } 186 | } 187 | 188 | function OptionParser(rules) { 189 | this.banner = 'Usage: [Options]'; 190 | this.options_title = 'Available options:' 191 | this._rules = rules; 192 | this._halt = false; 193 | this.filters = extend({}, PREDEFINED_FILTERS); 194 | this.on_args = {}; 195 | this.on_switches = {}; 196 | this.on_halt = function() {}; 197 | this.default_handler = function() {}; 198 | } 199 | 200 | OptionParser.prototype = { 201 | 202 | // Adds args and switchs handler. 203 | on: function(value, fn) { 204 | if(value.constructor === Function ) { 205 | this.default_handler = value; 206 | } else if(value.constructor === Number) { 207 | this.on_args[value] = fn; 208 | } else { 209 | this.on_switches[value] = fn; 210 | } 211 | }, 212 | 213 | // Adds a custom filter to the parser. It's possible to override the 214 | // default filter by passing the value "_DEFAULT" to the ´´name´´ 215 | // argument. The name of the filter is automatically transformed into 216 | // upper case. 217 | filter: function(name, fn) { 218 | this.filters[name.toUpperCase()] = fn; 219 | }, 220 | 221 | // Parses specified args. Returns remaining arguments. 222 | parse: function(args) { 223 | var result = [], callback; 224 | var rules = build_rules(this.filters, this._rules); 225 | var tokens = args.concat([]); 226 | while((token = tokens.shift()) && this._halt == false) { 227 | if(LONG_SWITCH_RE(token) || SHORT_SWITCH_RE(token)) { 228 | var arg = undefined; 229 | // The token is a long or a short switch. Get the corresponding 230 | // rule, filter and handle it. Pass the switch to the default 231 | // handler if no rule matched. 232 | for(var i = 0; i < rules.length; i++) { 233 | var rule = rules[i]; 234 | if(rule.long == token || rule.short == token) { 235 | if(rule.filter !== undefined) { 236 | arg = tokens.shift(); 237 | if(!LONG_SWITCH_RE(arg) && !SHORT_SWITCH_RE(arg)) { 238 | try { 239 | arg = rule.filter(arg); 240 | } catch(e) { 241 | throw OptError(token + ': ' + e.toString()); 242 | } 243 | } else if(rule.optional_arg) { 244 | tokens.unshift(arg); 245 | } else { 246 | throw OptError('Expected switch argument.'); 247 | } 248 | } 249 | callback = this.on_switches[rule.name]; 250 | if (!callback) callback = this.on_switches['*']; 251 | if(callback) callback.apply(this, [rule.name, arg]); 252 | break; 253 | } 254 | } 255 | if(i == rules.length) this.default_handler.apply(this, [token]); 256 | } else { 257 | // Did not match long or short switch. Parse the token as a 258 | // normal argument. 259 | callback = this.on_args[result.length]; 260 | result.push(token); 261 | if(callback) callback.apply(this, [token]); 262 | } 263 | } 264 | return this._halt ? this.on_halt.apply(this, []) : result; 265 | }, 266 | 267 | // Returns an Array with all defined option rules 268 | options: function() { 269 | return build_rules(this.filters, this._rules); 270 | }, 271 | 272 | // Add an on_halt callback if argument ´´fn´´ is specified. on_switch handlers can 273 | // call instance.halt to abort the argument parsing. This can be useful when 274 | // displaying help or version information. 275 | halt: function(fn) { 276 | this._halt = fn === undefined 277 | if(fn) this.on_halt = fn; 278 | }, 279 | 280 | // Returns a string representation of this OptionParser instance. 281 | toString: function() { 282 | var builder = [this.banner, '', this.options_title], 283 | shorts = false, longest = 0, rule; 284 | var rules = build_rules(this.filters, this._rules); 285 | for(var i = 0; i < rules.length; i++) { 286 | rule = rules[i]; 287 | // Quick-analyze the options. 288 | if(rule.short) shorts = true; 289 | if(rule.decl.length > longest) longest = rule.decl.length; 290 | } 291 | for(var i = 0; i < rules.length; i++) { 292 | var text; 293 | rule = rules[i]; 294 | if(shorts) { 295 | if(rule.short) text = spaces(2) + rule.short + ', '; 296 | else text = spaces(6); 297 | } 298 | text += spaces(rule.decl, longest) + spaces(3); 299 | text += rule.desc; 300 | builder.push(text); 301 | } 302 | return builder.join('\n'); 303 | } 304 | } 305 | 306 | self.VERSION = VERSION; 307 | self.OptionParser = OptionParser; 308 | 309 | })(optparse); -------------------------------------------------------------------------------- /lib/websocket.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var buffer = require('buffer'); 3 | var crypto = require('crypto'); 4 | var events = require('events'); 5 | var http = require('http'); 6 | var net = require('net'); 7 | var urllib = require('url'); 8 | var sys = require('sys'); 9 | 10 | var FRAME_NO = 0; 11 | var FRAME_LO = 1; 12 | var FRAME_HI = 2; 13 | 14 | // Values for readyState as per the W3C spec 15 | var CONNECTING = 0; 16 | var OPEN = 1; 17 | var CLOSING = 2; 18 | var CLOSED = 3; 19 | 20 | var debugLevel = parseInt(process.env.NODE_DEBUG, 16); 21 | var debug = (debugLevel & 0x4) ? 22 | function() { sys.error.apply(this, arguments); } : 23 | function() { }; 24 | 25 | // Generate a Sec-WebSocket-* value 26 | var createSecretKey = function() { 27 | // How many spaces will we be inserting? 28 | var numSpaces = 1 + Math.floor(Math.random() * 12); 29 | assert.ok(1 <= numSpaces && numSpaces <= 12); 30 | 31 | // What is the numerical value of our key? 32 | var keyVal = (Math.floor( 33 | Math.random() * (4294967295 / numSpaces) 34 | ) * numSpaces); 35 | 36 | // Our string starts with a string representation of our key 37 | var s = keyVal.toString(); 38 | 39 | // Insert 'numChars' worth of noise in the character ranges 40 | // [0x21, 0x2f] (14 characters) and [0x3a, 0x7e] (68 characters) 41 | var numChars = 1 + Math.floor(Math.random() * 12); 42 | assert.ok(1 <= numChars && numChars <= 12); 43 | 44 | for (var i = 0; i < numChars; i++) { 45 | var pos = Math.floor(Math.random() * s.length + 1); 46 | 47 | var c = Math.floor(Math.random() * (14 + 68)); 48 | c = (c <= 14) ? 49 | String.fromCharCode(c + 0x21) : 50 | String.fromCharCode((c - 14) + 0x3a); 51 | 52 | s = s.substring(0, pos) + c + s.substring(pos, s.length); 53 | } 54 | 55 | // We shoudln't have any spaces in our value until we insert them 56 | assert.equal(s.indexOf(' '), -1); 57 | 58 | // Insert 'numSpaces' worth of spaces 59 | for (var i = 0; i < numSpaces; i++) { 60 | var pos = Math.floor(Math.random() * (s.length - 1)) + 1; 61 | s = s.substring(0, pos) + ' ' + s.substring(pos, s.length); 62 | } 63 | 64 | assert.notEqual(s.charAt(0), ' '); 65 | assert.notEqual(s.charAt(s.length), ' '); 66 | 67 | return s; 68 | }; 69 | 70 | // Generate a challenge sequence 71 | var createChallenge = function() { 72 | var c = ''; 73 | for (var i = 0; i < 8; i++) { 74 | c += String.fromCharCode(Math.floor(Math.random() * 255)); 75 | } 76 | 77 | return c; 78 | }; 79 | 80 | // Get the value of a secret key string 81 | // 82 | // This strips non-digit values and divides the result by the number of 83 | // spaces found. 84 | var secretKeyValue = function(sk) { 85 | var ns = 0; 86 | var v = 0; 87 | 88 | for (var i = 0; i < sk.length; i++) { 89 | var cc = sk.charCodeAt(i); 90 | 91 | if (cc == 0x20) { 92 | ns++; 93 | } else if (0x30 <= cc && cc <= 0x39) { 94 | v = v * 10 + cc - 0x30; 95 | } 96 | } 97 | 98 | return Math.floor(v / ns); 99 | } 100 | 101 | // Get the to-be-hashed value of a secret key string 102 | // 103 | // This takes the result of secretKeyValue() and encodes it in a big-endian 104 | // byte string 105 | var secretKeyHashValue = function(sk) { 106 | var skv = secretKeyValue(sk); 107 | 108 | var hv = ''; 109 | hv += String.fromCharCode((skv >> 24) & 0xff); 110 | hv += String.fromCharCode((skv >> 16) & 0xff); 111 | hv += String.fromCharCode((skv >> 8) & 0xff); 112 | hv += String.fromCharCode((skv >> 0) & 0xff); 113 | 114 | return hv; 115 | }; 116 | 117 | // Compute the secret key signature based on two secret key strings and some 118 | // handshaking data. 119 | var computeSecretKeySignature = function(s1, s2, hs) { 120 | assert.equal(hs.length, 8); 121 | 122 | var hash = crypto.createHash('md5'); 123 | 124 | hash.update(secretKeyHashValue(s1)); 125 | hash.update(secretKeyHashValue(s2)); 126 | hash.update(hs); 127 | 128 | return hash.digest('binary'); 129 | }; 130 | 131 | // Return a hex representation of the given binary string; used for debugging 132 | var str2hex = function(str) { 133 | var hexChars = [ 134 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 135 | 'a', 'b', 'c', 'd', 'e', 'f' 136 | ]; 137 | 138 | var out = ''; 139 | for (var i = 0; i < str.length; i++) { 140 | var c = str.charCodeAt(i); 141 | out += hexChars[(c & 0xf0) >>> 4]; 142 | out += hexChars[c & 0x0f]; 143 | out += ' '; 144 | } 145 | 146 | return out.trim(); 147 | }; 148 | 149 | // Get the scheme for a URL, undefined if none is found 150 | var getUrlScheme = function(url) { 151 | var i = url.indexOf(':'); 152 | if (i == -1) { 153 | return undefined; 154 | } 155 | 156 | return url.substring(0, i); 157 | }; 158 | 159 | // Set a constant on the given object 160 | var setConstant = function(obj, name, value) { 161 | Object.defineProperty(obj, name, { 162 | get : function() { 163 | return value; 164 | } 165 | }); 166 | }; 167 | 168 | // WebSocket object 169 | // 170 | // This is intended to conform (mostly) to http://dev.w3.org/html5/websockets/ 171 | // 172 | // N.B. Arguments are parsed in the anonymous function at the bottom of the 173 | // constructor. 174 | var WebSocket = function(url, proto, opts) { 175 | events.EventEmitter.call(this); 176 | 177 | // Retain a reference to our object 178 | var self = this; 179 | 180 | // State of the connection 181 | var readyState = CONNECTING; 182 | 183 | // Our underlying net.Stream instance 184 | var stream = undefined; 185 | 186 | opts = opts || { 187 | origin : 'http://www.example.com' 188 | }; 189 | 190 | // Frame parsing functions 191 | // 192 | // These read data from the given buffer starting at the given offset look 193 | // for the end of the current frame. If found, the current frame is emitted 194 | // and the function returns. Only a single frame is processed at a time. 195 | // 196 | // The number of bytes of completed frames read is returned, which the 197 | // caller is to use to advance along its buffer. If 0 is returned, no 198 | // completed frame bytes were found, and the caller should probably enqueue 199 | // the buffer as a continuation of the current message. If a complete frame 200 | // is read, the function is responsible fro resting 'frameType'. 201 | 202 | // Framing data 203 | var frameType = FRAME_NO; 204 | var bufs = []; 205 | var bufsBytes = 0; 206 | 207 | // Frame-parsing functions 208 | var frameFuncs = [ 209 | // FRAME_NO 210 | function(buf, off) { 211 | if (buf[off] & 0x80) { 212 | throw new Error('High-byte frames not yet supported'); 213 | } 214 | 215 | frameType = FRAME_LO; 216 | return 1; 217 | }, 218 | 219 | // FRAME_LO 220 | function(buf, off) { 221 | assert.ok(bufs.length > 0 || bufsBytes == 0); 222 | 223 | debug('frame_lo(' + sys.inspect(buf) + ', ' + off + ')'); 224 | 225 | // Find the first instance of 0xff, our terminating byte 226 | for (var i = off; i < buf.length && buf[i] != 0xff; i++) 227 | ; 228 | 229 | // We didn't find a terminating byte 230 | if (i >= buf.length) { 231 | return 0; 232 | } 233 | 234 | // We found a terminating byte; collect all bytes into a single buffer 235 | // and emit it 236 | var mb = null; 237 | if (bufs.length == 0) { 238 | mb = buf.slice(off, i); 239 | } else { 240 | mb = new buffer.Buffer(bufsBytes + i); 241 | 242 | var mbOff = 0; 243 | bufs.forEach(function(b) { 244 | b.copy(mb, mbOff, 0, b.length); 245 | mbOff += b.length; 246 | }); 247 | 248 | assert.equal(mbOff, bufsBytes); 249 | 250 | // Don't call Buffer.copy() if we're coping 0 bytes. Rather 251 | // than being a no-op, this will trigger a range violation on 252 | // the destination. 253 | if (i > 0) { 254 | buf.copy(mb, mbOff, off, i); 255 | } 256 | 257 | // We consumed all of the buffers that we'd been saving; clear 258 | // things out 259 | bufs = []; 260 | bufsBytes = 0; 261 | } 262 | 263 | process.nextTick(function() { 264 | var b = mb; 265 | return function() { 266 | var m = b.toString('utf8'); 267 | 268 | self.emit('data', b); 269 | self.emit('message', m); // wss compat 270 | 271 | if (self.onmessage) { 272 | self.onmessage({data: m}); 273 | } 274 | }; 275 | }()); 276 | 277 | frameType = FRAME_NO; 278 | return i - off + 1; 279 | }, 280 | 281 | // FRAME_HI 282 | function(buf, off) { 283 | debug('High-byte framing not yet supported'); 284 | 285 | frameType = FRAME_NO; 286 | return buf.length - off; 287 | } 288 | ]; 289 | 290 | // Handle data coming from our socket 291 | var dataListener = function(buf) { 292 | if (buf.length <= 0) { 293 | return; 294 | } 295 | 296 | debug('dataListener(' + sys.inspect(buf) + ')'); 297 | 298 | var off = 0; 299 | var consumed = 0; 300 | 301 | do { 302 | if (frameType < 0 || frameFuncs.length <= frameType) { 303 | throw new Error('Unexpected frame type: ' + frameType); 304 | } 305 | 306 | consumed = frameFuncs[frameType](buf, off); 307 | off += consumed; 308 | } while (consumed > 0 && off < buf.length); 309 | 310 | if (consumed == 0) { 311 | bufs.push(buf.slice(off, buf.length)); 312 | bufsBytes += buf.length - off; 313 | } 314 | }; 315 | 316 | // Handle incoming file descriptors 317 | var fdListener = function(fd) { 318 | self.emit('fd', fd); 319 | }; 320 | 321 | // Handle errors from any source (HTTP client, stream, etc) 322 | var errorListener = function(e) { 323 | process.nextTick(function() { 324 | self.emit('wserror', e); 325 | 326 | if (self.onerror) { 327 | self.onerror(e); 328 | } 329 | }); 330 | }; 331 | 332 | // External API 333 | self.close = function() { 334 | var f = function() { 335 | readyState = CLOSED; 336 | 337 | if (stream) { 338 | stream.end(); 339 | stream.destroy(); 340 | stream = undefined; 341 | } 342 | 343 | process.nextTick(function() { 344 | self.emit('close'); 345 | 346 | if (self.onclose) { 347 | self.onclose(); 348 | } 349 | }); 350 | }; 351 | 352 | switch (readyState) { 353 | case CLOSED: 354 | case CLOSING: 355 | break; 356 | 357 | case CONNECTING: 358 | f(); 359 | break; 360 | 361 | default: 362 | readyState = CLOSING; 363 | 364 | // XXX: Run f() on the next tick so that we conform a little 365 | // closer to the spirit of the API in that the caller 366 | // never sees us transition directly to CLOSED. Instead, we 367 | // just seem to have an infinitely fast closing handshake. 368 | if (stream.write('', 'binary')) { 369 | process.nextTick(f); 370 | } else { 371 | stream.addListener('drain', f); 372 | } 373 | } 374 | }; 375 | 376 | self.send = function(str, fd) { 377 | if (readyState != OPEN) { 378 | throw new Error('Cannot write to non-open WebSocket client'); 379 | } 380 | 381 | stream.write('\x00', 'binary'); 382 | stream.write(str, 'utf8', fd); 383 | stream.write('\xff', 'binary'); 384 | }; 385 | 386 | // wss compat 387 | self.write = self.send; 388 | 389 | setConstant(self, 'url', url); 390 | 391 | Object.defineProperty(self, 'readyState', { 392 | get : function() { 393 | return readyState; 394 | } 395 | }); 396 | 397 | // Connect and perform handshaking with the server 398 | (function() { 399 | // Parse constructor arguments 400 | if (!url) { 401 | throw new Error('Url and must be specified.'); 402 | } 403 | 404 | // Secrets used for handshaking 405 | var key1 = createSecretKey(); 406 | var key2 = createSecretKey(); 407 | var challenge = createChallenge(); 408 | 409 | debug( 410 | 'key1=\'' + str2hex(key1) + '\'; ' + 411 | 'key2=\'' + str2hex(key2) + '\'; ' + 412 | 'challenge=\'' + str2hex(challenge) + '\'' 413 | ); 414 | 415 | var httpHeaders = { 416 | 'Connection' : 'Upgrade', 417 | 'Upgrade' : 'WebSocket', 418 | 'Sec-WebSocket-Key1' : key1, 419 | 'Sec-WebSocket-Key2' : key2 420 | }; 421 | if (opts.origin) { 422 | httpHeaders['Origin'] = opts.origin; 423 | } 424 | 425 | var httpPath = '/'; 426 | 427 | if (proto) { 428 | httpHeaders['Sec-WebSocket-Protocol'] = proto; 429 | } 430 | 431 | // Create the HTTP client that we'll use for handshaking. We'll cannabalize 432 | // its socket via the 'upgrade' event and leave it to rot. 433 | // 434 | // XXX: The ws+unix:// scheme makes use of the implementation detail 435 | // that http.Client passes its constructor arguments through, 436 | // un-inspected to net.Stream.connect(). The latter accepts a 437 | // string as its first argument to connect to a UNIX socket. 438 | var httpClient = undefined; 439 | switch (getUrlScheme(url)) { 440 | case 'ws': 441 | var u = urllib.parse(url); 442 | httpClient = http.createClient(u.port || 80, u.hostname); 443 | httpPath = (u.pathname || '/') + (u.search || ''); 444 | httpHeaders.Host = u.hostname; 445 | break; 446 | 447 | case 'ws+unix': 448 | var sockPath = url.substring('ws+unix://'.length, url.length); 449 | httpClient = http.createClient(sockPath); 450 | httpHeaders.Host = 'localhost'; 451 | break; 452 | 453 | default: 454 | throw new Error('Invalid URL scheme \'' + urlScheme + '\' specified.'); 455 | } 456 | 457 | httpClient.addListener('upgrade', (function() { 458 | var data = undefined; 459 | 460 | return function(req, s, head) { 461 | stream = s; 462 | 463 | stream.addListener('data', function(d) { 464 | if (!data) { 465 | data = d; 466 | } else { 467 | var data2 = new buffer.Buffer(data.length + d.length); 468 | 469 | if (data.length) { 470 | data.copy(data2, 0, 0, data.length); 471 | } 472 | if (data2.length) { 473 | d.copy(data2, data.length, 0, d.length); 474 | } 475 | 476 | data = data2; 477 | } 478 | 479 | if (data.length >= 16) { 480 | var expected = computeSecretKeySignature(key1, key2, challenge); 481 | var actual = data.slice(0, 16).toString('binary'); 482 | 483 | // Handshaking fails; we're donezo 484 | if (actual != expected) { 485 | debug( 486 | 'expected=\'' + str2hex(expected) + '\'; ' + 487 | 'actual=\'' + str2hex(actual) + '\'' 488 | ); 489 | 490 | process.nextTick(function() { 491 | // XXX: Emit 'wserror' here, as 'error' is a reserved word in the 492 | // EventEmitter world, and gets thrown. 493 | self.emit( 494 | 'wserror', 495 | new Error('Invalid handshake from server:' + 496 | 'expected \'' + str2hex(expected) + '\', ' + 497 | 'actual \'' + str2hex(actual) + '\'' 498 | ) 499 | ); 500 | 501 | if (self.onerror) { 502 | self.onerror(); 503 | } 504 | 505 | self.close(); 506 | }); 507 | } 508 | 509 | // Un-register our data handler and add the one to be used 510 | // for the normal, non-handshaking case. If we have extra 511 | // data left over, manually fire off the handler on 512 | // whatever remains. 513 | // 514 | // XXX: This is lame. We should only remove the listeners 515 | // that we added. 516 | httpClient.removeAllListeners('upgrade'); 517 | stream.removeAllListeners('data'); 518 | stream.addListener('data', dataListener); 519 | 520 | // Fire the 'open' event 521 | process.nextTick(function() { 522 | self.emit('open'); 523 | 524 | if (self.onopen) { 525 | self.onopen(); 526 | } 527 | }); 528 | 529 | readyState = OPEN; 530 | 531 | if (data.length > 16) { 532 | stream.emit('data', data.slice(16, data.length)); 533 | } 534 | } 535 | }); 536 | stream.addListener('fd', fdListener); 537 | stream.addListener('error', errorListener); 538 | 539 | stream.emit('data', head); 540 | }; 541 | })()); 542 | httpClient.addListener('error', function(e) { 543 | httpClient.end(); 544 | errorListener(e); 545 | }); 546 | 547 | var httpReq = httpClient.request(httpPath, httpHeaders); 548 | 549 | httpReq.write(challenge, 'binary'); 550 | httpReq.end(); 551 | })(); 552 | }; 553 | sys.inherits(WebSocket, events.EventEmitter); 554 | exports.WebSocket = WebSocket; 555 | 556 | // Add some constants to the WebSocket object 557 | setConstant(WebSocket.prototype, 'CONNECTING', CONNECTING); 558 | setConstant(WebSocket.prototype, 'OPEN', OPEN); 559 | setConstant(WebSocket.prototype, 'CLOSING', CLOSING); 560 | setConstant(WebSocket.prototype, 'CLOSED', CLOSED); 561 | 562 | // vim:ts=4 sw=4 et 563 | -------------------------------------------------------------------------------- /wsbench: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var OptionParser = require('./lib/optparse').OptionParser; 4 | var sys = require('sys'); 5 | var url = require('url'); 6 | var WebSocket = require('./lib/websocket').WebSocket; 7 | var assert = require('assert'); 8 | 9 | var OPTIONS = { 10 | connections : 10, 11 | rate : 0, 12 | session : function(ws) { 13 | var ALPHABET = function() { 14 | var a = ''; 15 | for (i = 0; i < 100; i++) { 16 | a += String.fromCharCode(97 + Math.floor(Math.random() * 26)); 17 | } 18 | 19 | return a; 20 | }(); 21 | 22 | ws.onopen = function() { 23 | for (i = 0; i < OPTIONS.msgs; i++) { 24 | var m = ''; 25 | var len = OPTIONS.msgSize; 26 | var off = Math.floor(Math.random() * ALPHABET.length); 27 | while (len > 0) { 28 | var l = Math.min(len, ALPHABET.length - off) 29 | m += ALPHABET.substr(off, l); 30 | len -= l; 31 | } 32 | 33 | ws.send(m); 34 | } 35 | 36 | ws.close(); 37 | }; 38 | }, 39 | msgs : 0, 40 | msgSize : 32 41 | }; 42 | 43 | var op = new OptionParser([ 44 | ['-c', '--num-conns NUMBER', 45 | 'number of connections to open (default: 10)' 46 | ], 47 | ['-h', '--help', 48 | 'display this help' 49 | ], 50 | ['-m', '--num-msgs NUMBER', 51 | 'number of messages per connection (dfeault: 0)' 52 | ], 53 | ['-p', '--protocol PROTO', 54 | 'set the Web Socket protocol to use (default: empty)' 55 | ], 56 | ['-r', '--rate NUMBER', 57 | 'number of connections per second (default: 0)' 58 | ], 59 | ['-s', '--msg-size NUMBER', 60 | 'size of messages to send, in bytes (default: 32)' 61 | ], 62 | ['-S', '--session FILE', 63 | 'file to use for session logic (default: None)' 64 | ] 65 | ]); 66 | 67 | op.on('num-conns', function(o, v) { 68 | OPTIONS.connections = v; 69 | }); 70 | op.on('help', function() { 71 | console.log(op.toString()); 72 | process.exit(0); 73 | }); 74 | op.on('protocol', function(o, v) { 75 | OPTIONS.protocol = v; 76 | }); 77 | op.on('rate', function(o, v) { 78 | OPTIONS.rate = v; 79 | }); 80 | op.on('session', function(o, v) { 81 | if (v.length > 3 && v.substring(v.length - 3) === '.js') { 82 | v = v.substring(0, v.length - 3); 83 | } 84 | 85 | if (v[0] !== '/') { 86 | v = './' + v; 87 | } 88 | 89 | OPTIONS.session = require(v); 90 | }); 91 | op.on('num-msgs', function(o, v) { 92 | OPTIONS.msgs = v; 93 | }); 94 | op.on('msg-size', function(o, v) { 95 | OPTIONS.msgSize = v; 96 | }); 97 | op.on(2, function(v) { 98 | OPTIONS.url = v; 99 | }); 100 | 101 | op.banner = 'usage: wsbench [options] \n' + 102 | '\n' + 103 | 'Kick off a benchmarking run against the given ws:// URL.\n' + 104 | '\n' + 105 | 'We can execute our workload in one of two ways: serially, wherein each\n' + 106 | 'connection is closed before the next is initiated; or in parallel, wherein\n' + 107 | 'a desired rate is specified and connections initiated to meet this rate,\n' + 108 | 'independent of the state of other connections. Serial execution is the\n' + 109 | 'default, and parallel execution can be specified using the -r \n' + 110 | 'option. Parallel execution is bounded by the total number of connections\n' + 111 | 'to be made, specified by the -c option.' 112 | 113 | op.parse(process.argv); 114 | 115 | if (!OPTIONS.url) { 116 | console.error('wsbench: missing required parameter'); 117 | console.log(''); 118 | console.log(op.toString()); 119 | process.stdout.flush(); 120 | 121 | /* 122 | // XXX: This is lame. For some reason a single long string is getting 123 | op.toString().split('\n').forEach(function(l) { 124 | console.log(l); 125 | }); 126 | */ 127 | process.exit(1); 128 | } 129 | 130 | var wsOpen = 0; 131 | var wsClose = 0; 132 | var wsErr = 0; 133 | 134 | var createWebSocket = function() { 135 | var ws = new WebSocket(OPTIONS.url, OPTIONS.protocol); 136 | OPTIONS.session(ws); 137 | 138 | wsOpen++; 139 | 140 | ws.onerror = function(e) { 141 | wsErr++; 142 | ws.close(); 143 | }; 144 | 145 | ws.onclose = function() { 146 | wsClose++; 147 | }; 148 | 149 | return ws; 150 | }; 151 | 152 | if (!OPTIONS.rate) { 153 | // We have no rate; synchronous 154 | var f = function() { 155 | var ws = createWebSocket(); 156 | 157 | if (!ws.onclose) { 158 | ws.onclose = function() { 159 | if (wsOpen < OPTIONS.connections) { 160 | f(); 161 | } 162 | }; 163 | } 164 | } 165 | 166 | f(); 167 | } else { 168 | var tickInterval = (OPTIONS.rate > 1000) ? 1 : (1000 / OPTIONS.rate); 169 | var connsPerTick = (OPTIONS.rate > 1000) ? (OPTIONS.rate / 1000) : 1; 170 | var isUnlim = (OPTIONS.connections == 0); 171 | 172 | assert.ok(tickInterval >= 1); 173 | assert.ok(connsPerTick >= 1); 174 | 175 | // We have a rate; parallel 176 | for (i = 0; i < 1000 / tickInterval; i++) { 177 | setTimeout((function() { 178 | var rem = 0; 179 | var id = i; 180 | var iid = undefined; 181 | 182 | var f = function() { 183 | for (rem += connsPerTick; 184 | rem >= 1 && (isUnlim || wsOpen < OPTIONS.connections); 185 | rem--) { 186 | createWebSocket(); 187 | } 188 | 189 | if (!isUnlim && 190 | ((wsOpen + (OPTIONS.rate - connsPerTick)) > OPTIONS.connections)) { 191 | if (iid) { 192 | clearInterval(iid); 193 | } 194 | 195 | // If we're the last worker and we're shutting down, fire 196 | // off any remaining requests. We have to do this here 197 | // because we may have some fractions of a request 198 | // lingering in different worker's 'rem' values. 199 | if (id == (1000 / tickInterval) - 1) { 200 | while (wsOpen < OPTIONS.connections) { 201 | createWebSocket(); 202 | } 203 | } 204 | } else if (!iid) { 205 | iid = setInterval(f, 1000); 206 | } 207 | }; 208 | 209 | return f; 210 | })(), i * tickInterval); 211 | } 212 | } 213 | 214 | process.addListener('SIGINT', function() { 215 | process.exit(0); 216 | }); 217 | process.addListener('exit', function() { 218 | console.log('Success rate: ' + 219 | (Math.round(((1000 * (wsOpen - wsErr - (wsOpen - wsClose))) / wsOpen)) / 10) + '% ' + 220 | 'from ' + wsOpen + ' connections'); 221 | }); 222 | 223 | // vim: filetype=javascript 224 | --------------------------------------------------------------------------------