Skip to content

Commit af8f003

Browse files
authored
[feature] Make Sender#frameAndSend() a static method (#1016)
1 parent eed0814 commit af8f003

File tree

3 files changed

+109
-141
lines changed

3 files changed

+109
-141
lines changed

bench/sender.benchmark.js

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,17 @@ const opts2 = {
3333
};
3434

3535
const suite = new benchmark.Suite();
36-
var sender = new Sender();
37-
sender._socket = { write () {} };
38-
39-
suite.add('frameAndSend, unmasked (64 B)', () => sender.frameAndSend(data1, opts1));
40-
suite.add('frameAndSend, masked (64 B)', () => sender.frameAndSend(data1, opts2));
41-
suite.add('frameAndSend, unmasked (16 KiB)', () => sender.frameAndSend(data2, opts1));
42-
suite.add('frameAndSend, masked (16 KiB)', () => sender.frameAndSend(data2, opts2));
43-
suite.add('frameAndSend, unmasked (64 KiB)', () => sender.frameAndSend(data3, opts1));
44-
suite.add('frameAndSend, masked (64 KiB)', () => sender.frameAndSend(data3, opts2));
45-
suite.add('frameAndSend, unmasked (200 KiB)', () => sender.frameAndSend(data4, opts1));
46-
suite.add('frameAndSend, masked (200 KiB)', () => sender.frameAndSend(data4, opts2));
47-
suite.add('frameAndSend, unmasked (1 MiB)', () => sender.frameAndSend(data5, opts1));
48-
suite.add('frameAndSend, masked (1 MiB)', () => sender.frameAndSend(data5, opts2));
36+
37+
suite.add('frame, unmasked (64 B)', () => Sender.frame(data1, opts1));
38+
suite.add('frame, masked (64 B)', () => Sender.frame(data1, opts2));
39+
suite.add('frame, unmasked (16 KiB)', () => Sender.frame(data2, opts1));
40+
suite.add('frame, masked (16 KiB)', () => Sender.frame(data2, opts2));
41+
suite.add('frame, unmasked (64 KiB)', () => Sender.frame(data3, opts1));
42+
suite.add('frame, masked (64 KiB)', () => Sender.frame(data3, opts2));
43+
suite.add('frame, unmasked (200 KiB)', () => Sender.frame(data4, opts1));
44+
suite.add('frame, masked (200 KiB)', () => Sender.frame(data4, opts2));
45+
suite.add('frame, unmasked (1 MiB)', () => Sender.frame(data5, opts1));
46+
suite.add('frame, masked (1 MiB)', () => Sender.frame(data5, opts2));
4947

5048
suite.on('cycle', (e) => console.log(e.target.toString()));
5149

lib/Sender.js

Lines changed: 91 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,71 @@ class Sender {
3636
this.onerror = null;
3737
}
3838

39+
/**
40+
* Frames a piece of data according to the HyBi WebSocket protocol.
41+
*
42+
* @param {Buffer} data The data to frame
43+
* @param {Object} options Options object
44+
* @param {Number} options.opcode The opcode
45+
* @param {Boolean} options.readOnly Specifies whether `data` can be modified
46+
* @param {Boolean} options.fin Specifies whether or not to set the FIN bit
47+
* @param {Boolean} options.mask Specifies whether or not to mask `data`
48+
* @param {Boolean} options.rsv1 Specifies whether or not to set the RSV1 bit
49+
* @return {Buffer[]} The framed data as a list of `Buffer` instances
50+
* @public
51+
*/
52+
static frame (data, options) {
53+
const merge = data.length < 1024 || options.mask && options.readOnly;
54+
var offset = options.mask ? 6 : 2;
55+
var payloadLength = data.length;
56+
57+
if (data.length >= 65536) {
58+
offset += 8;
59+
payloadLength = 127;
60+
} else if (data.length > 125) {
61+
offset += 2;
62+
payloadLength = 126;
63+
}
64+
65+
const target = Buffer.allocUnsafe(merge ? data.length + offset : offset);
66+
67+
target[0] = options.fin ? options.opcode | 0x80 : options.opcode;
68+
if (options.rsv1) target[0] |= 0x40;
69+
70+
if (payloadLength === 126) {
71+
target.writeUInt16BE(data.length, 2, true);
72+
} else if (payloadLength === 127) {
73+
target.writeUInt32BE(0, 2, true);
74+
target.writeUInt32BE(data.length, 6, true);
75+
}
76+
77+
if (!options.mask) {
78+
target[1] = payloadLength;
79+
if (merge) {
80+
data.copy(target, offset);
81+
return [target];
82+
}
83+
84+
return [target, data];
85+
}
86+
87+
const mask = crypto.randomBytes(4);
88+
89+
target[1] = payloadLength | 0x80;
90+
target[offset - 4] = mask[0];
91+
target[offset - 3] = mask[1];
92+
target[offset - 2] = mask[2];
93+
target[offset - 1] = mask[3];
94+
95+
if (merge) {
96+
bufferUtil.mask(data, mask, target, offset, data.length);
97+
return [target];
98+
}
99+
100+
bufferUtil.mask(data, mask, data, 0, data.length);
101+
return [target, data];
102+
}
103+
39104
/**
40105
* Sends a close message to the other peer.
41106
*
@@ -71,13 +136,13 @@ class Sender {
71136
* @private
72137
*/
73138
doClose (data, mask, cb) {
74-
this.frameAndSend(data, {
139+
this.sendFrame(Sender.frame(data, {
75140
readOnly: false,
76141
opcode: 0x08,
77142
rsv1: false,
78143
fin: true,
79144
mask
80-
}, cb);
145+
}), cb);
81146
}
82147

83148
/**
@@ -117,13 +182,13 @@ class Sender {
117182
* @private
118183
*/
119184
doPing (data, mask, readOnly) {
120-
this.frameAndSend(data, {
185+
this.sendFrame(Sender.frame(data, {
121186
opcode: 0x09,
122187
rsv1: false,
123188
fin: true,
124189
readOnly,
125190
mask
126-
});
191+
}));
127192
}
128193

129194
/**
@@ -163,13 +228,13 @@ class Sender {
163228
* @private
164229
*/
165230
doPong (data, mask, readOnly) {
166-
this.frameAndSend(data, {
231+
this.sendFrame(Sender.frame(data, {
167232
opcode: 0x0a,
168233
rsv1: false,
169234
fin: true,
170235
readOnly,
171236
mask
172-
});
237+
}));
173238
}
174239

175240
/**
@@ -229,13 +294,13 @@ class Sender {
229294
this.dispatch(data, opts, cb);
230295
}
231296
} else {
232-
this.frameAndSend(data, {
297+
this.sendFrame(Sender.frame(data, {
233298
mask: options.mask,
234299
fin: options.fin,
235300
rsv1: false,
236301
readOnly,
237302
opcode
238-
}, cb);
303+
}), cb);
239304
}
240305
}
241306

@@ -255,7 +320,7 @@ class Sender {
255320
*/
256321
dispatch (data, options, cb) {
257322
if (!options.compress) {
258-
this.frameAndSend(data, options, cb);
323+
this.sendFrame(Sender.frame(data, options), cb);
259324
return;
260325
}
261326

@@ -268,74 +333,12 @@ class Sender {
268333
}
269334

270335
options.readOnly = false;
271-
this.frameAndSend(buf, options, cb);
336+
this.sendFrame(Sender.frame(buf, options), cb);
272337
this.deflating = false;
273338
this.dequeue();
274339
});
275340
}
276341

277-
/**
278-
* Frames and sends a piece of data according to the HyBi WebSocket protocol.
279-
*
280-
* @param {Buffer} data The data to send
281-
* @param {Object} options Options object
282-
* @param {Number} options.opcode The opcode
283-
* @param {Boolean} options.readOnly Specifies whether `data` can be modified
284-
* @param {Boolean} options.fin Specifies whether or not to set the FIN bit
285-
* @param {Boolean} options.mask Specifies whether or not to mask `data`
286-
* @param {Boolean} options.rsv1 Specifies whether or not to set the RSV1 bit
287-
* @param {Function} cb Callback
288-
* @private
289-
*/
290-
frameAndSend (data, options, cb) {
291-
const mergeBuffers = data.length < 1024 || options.mask && options.readOnly;
292-
var dataOffset = options.mask ? 6 : 2;
293-
var payloadLength = data.length;
294-
295-
if (data.length >= 65536) {
296-
dataOffset += 8;
297-
payloadLength = 127;
298-
} else if (data.length > 125) {
299-
dataOffset += 2;
300-
payloadLength = 126;
301-
}
302-
303-
const outputBuffer = Buffer.allocUnsafe(
304-
mergeBuffers ? data.length + dataOffset : dataOffset
305-
);
306-
307-
outputBuffer[0] = options.fin ? options.opcode | 0x80 : options.opcode;
308-
if (options.rsv1) outputBuffer[0] |= 0x40;
309-
310-
if (payloadLength === 126) {
311-
outputBuffer.writeUInt16BE(data.length, 2, true);
312-
} else if (payloadLength === 127) {
313-
outputBuffer.writeUInt32BE(0, 2, true);
314-
outputBuffer.writeUInt32BE(data.length, 6, true);
315-
}
316-
317-
if (options.mask) {
318-
const mask = getRandomMask();
319-
320-
outputBuffer[1] = payloadLength | 0x80;
321-
outputBuffer[dataOffset - 4] = mask[0];
322-
outputBuffer[dataOffset - 3] = mask[1];
323-
outputBuffer[dataOffset - 2] = mask[2];
324-
outputBuffer[dataOffset - 1] = mask[3];
325-
326-
if (mergeBuffers) {
327-
bufferUtil.mask(data, mask, outputBuffer, dataOffset, data.length);
328-
} else {
329-
bufferUtil.mask(data, mask, data, 0, data.length);
330-
}
331-
} else {
332-
outputBuffer[1] = payloadLength;
333-
if (mergeBuffers) data.copy(outputBuffer, dataOffset);
334-
}
335-
336-
sendFramedData(this, outputBuffer, mergeBuffers ? null : data, cb);
337-
}
338-
339342
/**
340343
* Executes queued send operations.
341344
*
@@ -360,6 +363,22 @@ class Sender {
360363
this.bufferedBytes += params[1].length;
361364
this.queue.push(params);
362365
}
366+
367+
/**
368+
* Sends a frame.
369+
*
370+
* @param {Buffer[]} list The frame to send
371+
* @param {Function} cb Callback
372+
* @private
373+
*/
374+
sendFrame (list, cb) {
375+
if (list.length === 2) {
376+
this._socket.write(list[0]);
377+
this._socket.write(list[1], cb);
378+
} else {
379+
this._socket.write(list[0], cb);
380+
}
381+
}
363382
}
364383

365384
module.exports = Sender;
@@ -380,31 +399,3 @@ function viewToBuffer (view) {
380399

381400
return buf;
382401
}
383-
384-
/**
385-
* Generates a random mask.
386-
*
387-
* @return {Buffer} The mask
388-
* @private
389-
*/
390-
function getRandomMask () {
391-
return crypto.randomBytes(4);
392-
}
393-
394-
/**
395-
* Sends a frame.
396-
*
397-
* @param {Sender} sender Sender instance
398-
* @param {Buffer} outputBuffer The data to send
399-
* @param {Buffer} data Additional data to send if frame is split into two buffers
400-
* @param {Function} cb Callback
401-
* @private
402-
*/
403-
function sendFramedData (sender, outputBuffer, data, cb) {
404-
if (data) {
405-
sender._socket.write(outputBuffer);
406-
sender._socket.write(data, cb);
407-
} else {
408-
sender._socket.write(outputBuffer, cb);
409-
}
410-
}

test/Sender.test.js

Lines changed: 7 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,11 @@ const PerMessageDeflate = require('../lib/PerMessageDeflate');
66
const Sender = require('../lib/Sender');
77

88
describe('Sender', function () {
9-
describe('#frameAndSend', function () {
10-
it('does not modify a masked binary buffer', function () {
11-
const sender = new Sender({ write: () => {} });
9+
describe('.frame', function () {
10+
it('does not mutate the input buffer if data is `readOnly`', function () {
1211
const buf = Buffer.from([1, 2, 3, 4, 5]);
1312

14-
sender.frameAndSend(buf, {
13+
Sender.frame(buf, {
1514
readOnly: true,
1615
rsv1: false,
1716
mask: true,
@@ -22,36 +21,16 @@ describe('Sender', function () {
2221
assert.ok(buf.equals(Buffer.from([1, 2, 3, 4, 5])));
2322
});
2423

25-
it('does not modify a masked text buffer', function () {
26-
const sender = new Sender({ write: () => {} });
27-
const text = Buffer.from('hi there');
28-
29-
sender.frameAndSend(text, {
30-
readOnly: true,
31-
rsv1: false,
32-
mask: true,
33-
opcode: 1,
34-
fin: true
35-
});
36-
37-
assert.ok(text.equals(Buffer.from('hi there')));
38-
});
39-
40-
it('sets RSV1 bit if compressed', function (done) {
41-
const sender = new Sender({
42-
write: (data) => {
43-
assert.strictEqual(data[0] & 0x40, 0x40);
44-
done();
45-
}
46-
});
47-
48-
sender.frameAndSend(Buffer.from('hi'), {
24+
it('sets RSV1 bit if compressed', function () {
25+
const list = Sender.frame(Buffer.from('hi'), {
4926
readOnly: false,
5027
mask: false,
5128
rsv1: true,
5229
opcode: 1,
5330
fin: true
5431
});
32+
33+
assert.strictEqual(list[0][0] & 0x40, 0x40);
5534
});
5635
});
5736

0 commit comments

Comments
 (0)