Skip to content

Commit 30eccee

Browse files
guymguymlpinca
authored andcommitted
[feature] Add "fragments" as possible value for binaryType (#1018)
1 parent af8f003 commit 30eccee

File tree

8 files changed

+191
-24
lines changed

8 files changed

+191
-24
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
node_modules/
22
coverage/
33
npm-debug.log
4+
.vscode/

doc/ws.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,10 @@ Register an event listener emulating the `EventTarget` interface.
277277
- {String}
278278

279279
A string indicating the type of binary data being transmitted by the connection.
280-
This should be either "nodebuffer" or "arraybuffer". Defaults to "nodebuffer".
280+
This should be either "nodebuffer" or "arraybuffer" or "fragments". Defaults to "nodebuffer".
281+
Type "fragments" will emit the array of fragments as received from the sender,
282+
without copyfull concatenation, which is useful for the performance of binary protocols
283+
transfering large messages with multiple fragments.
281284

282285
### websocket.bufferedAmount
283286

lib/Constants.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33
exports.GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
44
exports.EMPTY_BUFFER = Buffer.alloc(0);
55
exports.NOOP = () => {};
6+
7+
exports.BINARY_TYPES = ['nodebuffer', 'arraybuffer', 'fragments'];

lib/EventTarget.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class MessageEvent extends Event {
2828
/**
2929
* Create a new `MessageEvent`.
3030
*
31-
* @param {(String|Buffer|ArrayBuffer)} data The received data
31+
* @param {(String|Buffer|ArrayBuffer|Buffer[])} data The received data
3232
* @param {Boolean} isBinary Specifies if `data` is binary
3333
* @param {WebSocket} target A reference to the target to which the event was dispatched
3434
*/
@@ -100,9 +100,6 @@ const EventTarget = {
100100
if (typeof listener !== 'function') return;
101101

102102
function onMessage (data, flags) {
103-
if (flags.binary && this.binaryType === 'arraybuffer') {
104-
data = new Uint8Array(data).buffer;
105-
}
106103
listener.call(this, new MessageEvent(data, !!flags.binary, this));
107104
}
108105

lib/Receiver.js

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@ class Receiver {
2828
*
2929
* @param {Object} extensions An object containing the negotiated extensions
3030
* @param {Number} maxPayload The maximum allowed message length
31+
* @param {String} binaryType The type for binary data, see constants.BINARY_TYPES
3132
*/
32-
constructor (extensions, maxPayload) {
33+
constructor (extensions, maxPayload, binaryType) {
3334
this.extensions = extensions || {};
3435
this.maxPayload = maxPayload | 0;
36+
this.binaryType = binaryType || constants.BINARY_TYPES[0];
3537

3638
this.bufferedBytes = 0;
3739
this.buffers = [];
@@ -352,20 +354,28 @@ class Receiver {
352354
*/
353355
dataMessage () {
354356
if (this.fin) {
355-
const buf = this.fragments.length > 1
356-
? Buffer.concat(this.fragments, this.messageLength)
357-
: this.fragments.length === 1
358-
? this.fragments[0]
359-
: constants.EMPTY_BUFFER;
360-
357+
const fragments = this.fragments;
358+
const messageLength = this.messageLength;
361359
this.totalPayloadLength = 0;
362-
this.fragments.length = 0;
360+
this.fragments = [];
363361
this.messageLength = 0;
364362
this.fragmented = 0;
365363

366364
if (this.opcode === 2) {
367-
this.onmessage(buf, { masked: this.masked, binary: true });
365+
var data;
366+
367+
if (this.binaryType === 'nodebuffer') {
368+
data = bufferFromFragments(fragments, messageLength);
369+
} else if (this.binaryType === 'arraybuffer') {
370+
data = new Uint8Array(bufferFromFragments(fragments, messageLength)).buffer;
371+
} else {
372+
data = fragments;
373+
}
374+
375+
this.onmessage(data, { masked: this.masked, binary: true });
368376
} else {
377+
const buf = bufferFromFragments(fragments, messageLength);
378+
369379
if (!isValidUTF8(buf)) {
370380
this.error(new Error('invalid utf8 sequence'), 1007);
371381
return;
@@ -509,4 +519,20 @@ class Receiver {
509519
}
510520
}
511521

522+
/**
523+
* Make a buffer from a list of fragments.
524+
* Optimized for the common case of a single fragment in order to
525+
* avoid copyfull concat and simply return the fragment buffer.
526+
*
527+
* @param {Buffer[]} fragments
528+
* @param {Number} messageLength
529+
* @return {Buffer}
530+
* @private
531+
*/
532+
function bufferFromFragments (fragments, messageLength) {
533+
if (fragments.length === 1) return fragments[0];
534+
if (fragments.length > 1) return Buffer.concat(fragments, messageLength);
535+
return constants.EMPTY_BUFFER;
536+
}
537+
512538
module.exports = Receiver;

lib/WebSocket.js

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ class WebSocket extends EventEmitter {
5353
this.protocol = '';
5454

5555
this._finalize = this.finalize.bind(this);
56-
this._binaryType = 'nodebuffer';
56+
this._binaryType = constants.BINARY_TYPES[0];
5757
this._finalizeCalled = false;
5858
this._closeMessage = null;
5959
this._closeTimer = null;
@@ -98,10 +98,17 @@ class WebSocket extends EventEmitter {
9898
}
9999

100100
set binaryType (type) {
101-
if (type === 'arraybuffer' || type === 'nodebuffer') {
102-
this._binaryType = type;
103-
} else {
104-
throw new SyntaxError('unsupported binaryType: must be either "nodebuffer" or "arraybuffer"');
101+
// silently ignore unsupported types
102+
if (constants.BINARY_TYPES.indexOf(type) < 0) {
103+
return;
104+
}
105+
106+
this._binaryType = type;
107+
108+
// update the receiver if already created,
109+
// if not then it will take the value once created
110+
if (this._receiver) {
111+
this._receiver.binaryType = type;
105112
}
106113
}
107114

@@ -116,7 +123,7 @@ class WebSocket extends EventEmitter {
116123
socket.setTimeout(0);
117124
socket.setNoDelay();
118125

119-
this._receiver = new Receiver(this.extensions, this.maxPayload);
126+
this._receiver = new Receiver(this.extensions, this.maxPayload, this.binaryType);
120127
this._sender = new Sender(socket, this.extensions);
121128
this._ultron = new Ultron(socket);
122129
this._socket = socket;

test/Receiver.test.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const crypto = require('crypto');
55

66
const PerMessageDeflate = require('../lib/PerMessageDeflate');
77
const Receiver = require('../lib/Receiver');
8+
const Sender = require('../lib/Sender');
89
const util = require('./hybi-util');
910

1011
describe('Receiver', function () {
@@ -827,4 +828,85 @@ describe('Receiver', function () {
827828
});
828829
});
829830
});
831+
832+
it('can emit nodebuffer of fragmented binary message', function (done) {
833+
const p = new Receiver();
834+
const frags = [
835+
crypto.randomBytes(7321),
836+
crypto.randomBytes(137),
837+
crypto.randomBytes(285787),
838+
crypto.randomBytes(3)
839+
];
840+
841+
p.binaryType = 'nodebuffer';
842+
p.onmessage = function (data) {
843+
assert.ok(Buffer.isBuffer(data));
844+
assert.ok(data.equals(Buffer.concat(frags)));
845+
done();
846+
};
847+
848+
addBinaryFragments(p, frags);
849+
});
850+
851+
it('can emit arraybuffer of fragmented binary message', function (done) {
852+
const p = new Receiver();
853+
const frags = [
854+
crypto.randomBytes(19221),
855+
crypto.randomBytes(954),
856+
crypto.randomBytes(623987)
857+
];
858+
859+
p.binaryType = 'arraybuffer';
860+
p.onmessage = function (data) {
861+
assert.ok(data instanceof ArrayBuffer);
862+
assert.ok(Buffer.from(data).equals(Buffer.concat(frags)));
863+
done();
864+
};
865+
866+
addBinaryFragments(p, frags);
867+
});
868+
869+
it('can emit fragments of fragmented binary message', function (done) {
870+
const p = new Receiver();
871+
const frags = [
872+
crypto.randomBytes(17),
873+
crypto.randomBytes(419872),
874+
crypto.randomBytes(83),
875+
crypto.randomBytes(9928),
876+
crypto.randomBytes(1)
877+
];
878+
879+
p.binaryType = 'fragments';
880+
p.onmessage = function (data) {
881+
assert.ok(Array.isArray(data));
882+
assert.ok(data.length === frags.length);
883+
for (let i = 0; i < frags.length; ++i) {
884+
assert.ok(Buffer.isBuffer(data[i]));
885+
assert.ok(frags[i].equals(data[i]));
886+
}
887+
done();
888+
};
889+
890+
addBinaryFragments(p, frags);
891+
});
830892
});
893+
894+
/**
895+
* Adds a list of binary fragments to the receiver prefixing each one
896+
* with info byte and length.
897+
*
898+
* @param {Receiver} receiver
899+
* @param {Buffer[]} frags
900+
* @private
901+
*/
902+
function addBinaryFragments (receiver, frags) {
903+
for (let i = 0; i < frags.length; ++i) {
904+
Sender.frame(frags[i], {
905+
opcode: i === 0 ? 2 : 0,
906+
fin: i + 1 === frags.length,
907+
mask: false,
908+
rsv1: false,
909+
readOnly: true
910+
}).forEach(buf => receiver.add(buf));
911+
}
912+
}

test/WebSocket.test.js

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1168,12 +1168,23 @@ describe('WebSocket', function () {
11681168
assert.strictEqual(ws.onopen, listener);
11691169
});
11701170

1171-
it('should throw an error when setting an invalid binary type', function () {
1171+
it('should ignore when setting an invalid binary type', function () {
11721172
const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() });
11731173

1174-
assert.throws(() => {
1175-
ws.binaryType = 'foo';
1176-
}, /^SyntaxError: unsupported binaryType: must be either "nodebuffer" or "arraybuffer"$/);
1174+
ws.binaryType = 'nodebuffer';
1175+
assert.ok(ws.binaryType === 'nodebuffer');
1176+
ws.binaryType = 'foo';
1177+
assert.ok(ws.binaryType === 'nodebuffer');
1178+
ws.binaryType = 'arraybuffer';
1179+
assert.ok(ws.binaryType === 'arraybuffer');
1180+
ws.binaryType = '';
1181+
assert.ok(ws.binaryType === 'arraybuffer');
1182+
ws.binaryType = 'fragments';
1183+
assert.ok(ws.binaryType === 'fragments');
1184+
ws.binaryType = 'buffer';
1185+
assert.ok(ws.binaryType === 'fragments');
1186+
ws.binaryType = 'nodebuffer';
1187+
assert.ok(ws.binaryType === 'nodebuffer');
11771188
});
11781189

11791190
it('should work the same as the EventEmitter api', function (done) {
@@ -1420,6 +1431,44 @@ describe('WebSocket', function () {
14201431
};
14211432
});
14221433
});
1434+
1435+
it('should allow to update binaryType after receiver created', function (done) {
1436+
server.createServer(++port, (srv) => {
1437+
const ws = new WebSocket(`ws://localhost:${port}`);
1438+
1439+
function testType (binaryType, callback) {
1440+
const buf = Buffer.from(binaryType);
1441+
ws.binaryType = binaryType;
1442+
ws.onmessage = (messageEvent) => {
1443+
if (binaryType === 'nodebuffer') {
1444+
assert.ok(Buffer.isBuffer(messageEvent.data));
1445+
assert.ok(messageEvent.data.equals(buf));
1446+
} else if (binaryType === 'arraybuffer') {
1447+
assert.ok(messageEvent.data instanceof ArrayBuffer);
1448+
assert.ok(Buffer.from(messageEvent.data).equals(buf));
1449+
} else if (binaryType === 'fragments') {
1450+
assert.ok(Array.isArray(messageEvent.data));
1451+
assert.ok(messageEvent.data.length === 1);
1452+
assert.ok(Buffer.from(messageEvent.data[0]).equals(buf));
1453+
}
1454+
callback();
1455+
};
1456+
ws.send(buf);
1457+
}
1458+
1459+
ws.onopen =
1460+
() => testType('nodebuffer',
1461+
() => testType('arraybuffer',
1462+
() => testType('fragments',
1463+
() => {
1464+
srv.close(done);
1465+
ws.terminate();
1466+
}
1467+
)
1468+
)
1469+
);
1470+
});
1471+
});
14231472
});
14241473

14251474
describe('ssl', function () {

0 commit comments

Comments
 (0)