diff --git a/README.md b/README.md index 3b3d3ae47..eed1bfa02 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ var engine = require('engine.io') server.on('connection', function (socket) { socket.send('utf 8 string'); + socket.send(new Buffer([0, 1, 2, 3, 4, 5])); // binary data }); ``` @@ -67,6 +68,23 @@ httpServer.on('request', function (req, res) { ``` +Sending and receiving binary + +```html + + +``` + For more information on the client refer to the [engine-client](http://github.com/learnboost/engine.io-client) repository. @@ -220,7 +238,7 @@ A representation of a client. _Inherits from EventEmitter_. - `message` - Fired when the client sends a message. - **Arguments** - - `String`: Unicode string + - `String` or `Buffer`: Unicode string or Buffer with binary contents - `error` - Fired when an error occurs. - **Arguments** @@ -253,9 +271,10 @@ A representation of a client. _Inherits from EventEmitter_. ##### Methods - `send`: - - Sends a message, performing `message = toString(arguments[0])`. + - Sends a message, performing `message = toString(arguments[0])` unless + sending binary data, which is sent as is. - **Parameters** - - `String`: a string or any object implementing `toString()`, with outgoing data + - `String` | `Buffer` | `ArrayBuffer` | `ArrayBufferView`: a string or any object implementing `toString()`, with outgoing data, or a Buffer or ArrayBuffer with binary data. Also any ArrayBufferView can be sent as is. - `Function`: optional, a callback executed when the message gets flushed out by the transport - **Returns** `Socket` for chaining - `close` diff --git a/lib/server.js b/lib/server.js index 82511e2b5..84607f2ec 100644 --- a/lib/server.js +++ b/lib/server.js @@ -205,6 +205,11 @@ Server.prototype.handshake = function(transport, req){ try { var transport = new transports[transport](req); + if (req.query && req.query.b64) { + transport.supportsBinary = false; + } else { + transport.supportsBinary = true; + } } catch (e) { sendErrorMessage(req.res, Server.errors.BAD_REQUEST); @@ -282,6 +287,11 @@ Server.prototype.onWebSocket = function(req, socket){ } else { debug('upgrading existing transport'); var transport = new transports[req.query.transport](req); + if (req.query && req.query.b64) { + transport.supportsBinary = false; + } else { + transport.supportsBinary = true; + } this.clients[id].maybeUpgrade(transport); } } else { diff --git a/lib/transports/polling-xhr.js b/lib/transports/polling-xhr.js index dde63aa98..8bc1d50fa 100644 --- a/lib/transports/polling-xhr.js +++ b/lib/transports/polling-xhr.js @@ -5,6 +5,7 @@ var Polling = require('./polling'); var Transport = require('../transport'); +var debug = require('debug')('engine:polling-xhr'); /** * Module exports. @@ -36,9 +37,15 @@ XHR.prototype.__proto__ = Polling.prototype; XHR.prototype.doWrite = function(data){ // explicit UTF-8 is required for pages not served under utf + var isString = typeof data == 'string'; + var contentType = isString + ? 'text/plain; charset=UTF-8' + : 'application/octet-stream'; + var contentLength = '' + (isString ? Buffer.byteLength(data) : data.length); + var headers = { - 'Content-Type': 'text/plain; charset=UTF-8', - 'Content-Length': Buffer.byteLength(data) + 'Content-Type': contentType, + 'Content-Length': contentLength }; // prevent XSS warnings on IE diff --git a/lib/transports/polling.js b/lib/transports/polling.js index 7b40bb9bc..72d435319 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -118,14 +118,16 @@ Polling.prototype.onDataRequest = function (req, res) { this.onError('data request overlap from client'); res.writeHead(500); } else { + var isBinary = 'application/octet-stream' == req.headers['content-type']; + this.dataReq = req; this.dataRes = res; - var chunks = '' + var chunks = isBinary ? new Buffer(0) : '' , self = this function cleanup () { - chunks = ''; + chunks = isBinary ? new Buffer(0) : ''; req.removeListener('data', onData); req.removeListener('end', onEnd); req.removeListener('close', onClose); @@ -138,7 +140,11 @@ Polling.prototype.onDataRequest = function (req, res) { }; function onData (data) { - chunks += data; + if (typeof data == 'string') { + chunks += data; + } else { + chunks = Buffer.concat([chunks, data]); + } }; function onEnd () { @@ -157,7 +163,7 @@ Polling.prototype.onDataRequest = function (req, res) { req.on('close', onClose); req.on('data', onData); req.on('end', onEnd); - req.setEncoding('utf8'); + if (!isBinary) { req.setEncoding('utf8'); } } }; @@ -171,7 +177,7 @@ Polling.prototype.onDataRequest = function (req, res) { Polling.prototype.onData = function (data) { debug('received "%s"', data); var self = this; - parser.decodePayload(data, function(packet){ + var callback = function(packet) { if ('close' == packet.type) { debug('got xhr close packet'); self.onClose(); @@ -179,7 +185,9 @@ Polling.prototype.onData = function (data) { } self.onPacket(packet); - }); + }; + + parser.decodePayload(data, callback); }; /** @@ -197,7 +205,10 @@ Polling.prototype.send = function (packets) { this.shouldClose = null; } - this.write(parser.encodePayload(packets)); + var self = this; + parser.encodePayload(packets, this.supportsBinary, function(data) { + self.write(data); + }); }; /** diff --git a/lib/transports/websocket.js b/lib/transports/websocket.js index 5582c58fc..59b8eb7b1 100644 --- a/lib/transports/websocket.js +++ b/lib/transports/websocket.js @@ -83,15 +83,16 @@ WebSocket.prototype.onData = function (data) { */ WebSocket.prototype.send = function (packets) { + var self = this; for (var i = 0, l = packets.length; i < l; i++) { - var data = parser.encodePacket(packets[i]); - debug('writing "%s"', data); - this.writable = false; - var self = this; - this.socket.send(data, function (err){ - if (err) return self.onError('write error', err.stack); - self.writable = true; - self.emit('drain'); + parser.encodePacket(packets[i], this.supportsBinary, function(data) { + debug('writing "%s"', data); + self.writable = false; + self.socket.send(data, function (err){ + if (err) return self.onError('write error', err.stack); + self.writable = true; + self.emit('drain'); + }); }); } }; diff --git a/test/jsonp.js b/test/jsonp.js index 28ef65364..42e3935b7 100644 --- a/test/jsonp.js +++ b/test/jsonp.js @@ -115,6 +115,27 @@ describe('JSONP', function () { }); }); }); + + it('should arrive from server to client and back with binary data (pollingJSONP)', function(done) { + var binaryData = new Buffer(5); + for (var i = 0; i < 5; i++) binaryData[i] = i; + var engine = listen( { allowUpgrades: false, transports: ['polling'] }, function (port) { + var socket = new eioc.Socket('ws://localhost:' + port, { transports: ['polling'], forceJSONP: true, upgrade: false}); + engine.on('connection', function (conn) { + conn.on('message', function (msg) { + conn.send(msg); + }); + }); + + socket.on('open', function() { + socket.send(binaryData); + socket.on('message', function (msg) { + for (var i = 0; i < msg.length; i++) expect(msg[i]).to.be(i); + done(); + }); + }); + }); + }); }); describe('close', function () { diff --git a/test/server.js b/test/server.js index f5b81b247..7593b91af 100644 --- a/test/server.js +++ b/test/server.js @@ -63,7 +63,7 @@ describe('server', function () { it('should send the io cookie', function (done) { var engine = listen(function (port) { request.get('http://localhost:%d/engine.io/default/'.s(port)) - .query({ transport: 'polling' }) + .query({ transport: 'polling', b64: 1 }) .end(function (res) { // hack-obtain sid var sid = res.text.match(/"sid":"([^"]+)"/)[1]; @@ -76,7 +76,7 @@ describe('server', function () { it('should send the io cookie custom name', function (done) { var engine = listen({ cookie: 'woot' }, function (port) { request.get('http://localhost:%d/engine.io/default/'.s(port)) - .query({ transport: 'polling' }) + .query({ transport: 'polling', b64: 1 }) .end(function (res) { var sid = res.text.match(/"sid":"([^"]+)"/)[1]; expect(res.headers['set-cookie'][0]).to.be('woot=' + sid); @@ -833,6 +833,196 @@ describe('server', function () { }); }); + it('should arrive when binary data is sent as Int8Array (ws)', function (done) { + var binaryData = new Int8Array(5); + for (var i = 0; i < binaryData.length; i++) { + binaryData[i] = i; + } + + var opts = { allowUpgrades: false, transports: ['websocket'] }; + var engine = listen(opts, function(port) { + var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] }) + + engine.on('connection', function (conn) { + conn.send(binaryData); + }); + + socket.on('open', function () { + socket.on('message', function(msg) { + for (var i = 0; i < binaryData.length; i++) { + var num = msg.readInt8(i); + expect(num).to.be(i); + } + done(); + }); + }); + }); + }); + + it('should arrive when binary data is sent as Int32Array (ws)', function (done) { + var binaryData = new Int32Array(5); + for (var i = 0; i < binaryData.length; i++) { + binaryData[i] = (i + 100) * 9823; + } + + var opts = { allowUpgrades: false, transports: ['websocket'] }; + var engine = listen(opts, function(port) { + var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] }) + + engine.on('connection', function (conn) { + conn.send(binaryData); + }); + + socket.on('open', function () { + socket.on('message', function(msg) { + for (var i = 0, ii = 0; i < binaryData.length; i += 4, ii++) { + var num = msg.readInt32LE(i); + expect(num).to.be((ii + 100) * 9823); + } + done(); + }); + }); + }); + }); + + it('should arrive when binary data is sent as Int32Array, given as ArrayBuffer(ws)', function (done) { + var binaryData = new Int32Array(5); + for (var i = 0; i < binaryData.length; i++) { + binaryData[i] = (i + 100) * 9823; + } + + var opts = { allowUpgrades: false, transports: ['websocket'] }; + var engine = listen(opts, function(port) { + var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] }) + + engine.on('connection', function (conn) { + conn.send(binaryData.buffer); + }); + + socket.on('open', function () { + socket.on('message', function(msg) { + for (var i = 0, ii = 0; i < binaryData.length; i += 4, ii++) { + var num = msg.readInt32LE(i); + expect(num).to.be((ii + 100) * 9823); + } + done(); + }); + }); + }); + }); + + it('should arrive when binary data is sent as Buffer (ws)', function (done) { + var binaryData = Buffer(5); + for (var i = 0; i < binaryData.length; i++) { + binaryData.writeInt8(i, i); + } + + var opts = { allowUpgrades: false, transports: ['websocket'] }; + var engine = listen(opts, function(port) { + var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] }); + + engine.on('connection', function (conn) { + conn.send(binaryData); + }); + + socket.on('open', function () { + socket.on('message', function(msg) { + for (var i = 0; i < binaryData.length; i++) { + var num = msg.readInt8(i); + expect(num).to.be(i); + } + done(); + }); + }); + }); + }); + + it('should arrive when binary data sent as Buffer (polling)', function (done) { + var binaryData = Buffer(5); + for (var i = 0; i < binaryData.length; i++) { + binaryData.writeInt8(i, i); + } + + var opts = { allowUpgrades: false, transports: ['polling'] }; + var engine = listen(opts, function(port) { + var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['polling'] }); + + engine.on('connection', function (conn) { + conn.send(binaryData); + }); + + socket.on('open', function() { + socket.on('message', function(msg) { + for (var i = 0; i < binaryData.length; i++) { + var num = msg.readInt8(i); + expect(num).to.be(i); + } + + done(); + }); + }); + }); + }); + + it('should arrive as ArrayBuffer if requested when binary data sent as Buffer (ws)', function (done) { + var binaryData = Buffer(5); + for (var i = 0; i < binaryData.length; i++) { + binaryData.writeInt8(i, i); + } + + var opts = { allowUpgrades: false, transports: ['websocket'] }; + var engine = listen(opts, function(port) { + var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] }); + socket.binaryType = 'arraybuffer'; + + engine.on('connection', function (conn) { + conn.send(binaryData); + }); + + socket.on('open', function() { + socket.on('message', function(msg) { + expect(msg instanceof ArrayBuffer).to.be(true); + var intArray = new Int8Array(msg); + for (var i = 0; i < binaryData.length; i++) { + expect(intArray[i]).to.be(i); + } + + done(); + }); + }); + }); + }); + + it('should arrive as ArrayBuffer if requested when binary data sent as Buffer (polling)', function (done) { + var binaryData = Buffer(5); + for (var i = 0; i < binaryData.length; i++) { + binaryData.writeInt8(i, i); + } + + var opts = { allowUpgrades: false, transports: ['polling'] }; + var engine = listen(opts, function(port) { + var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['polling'] }); + socket.binaryType = 'arraybuffer'; + + engine.on('connection', function (conn) { + conn.send(binaryData); + }); + + socket.on('open', function() { + socket.on('message', function(msg) { + expect(msg instanceof ArrayBuffer).to.be(true); + var intArray = new Int8Array(msg); + for (var i = 0; i < binaryData.length; i++) { + expect(intArray[i]).to.be(i); + } + + done(); + }); + }); + }); + }); + + it('should trigger a flush/drain event', function(done){ var engine = listen({ allowUpgrades: false }, function(port){ engine.on('connection', function(socket){