Skip to content

Commit 5165d67

Browse files
authored
feat(websocket): only consume necessary bytes (nodejs#1812)
1 parent e94093a commit 5165d67

File tree

3 files changed

+67
-43
lines changed

3 files changed

+67
-43
lines changed

lib/websocket/constants.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,14 @@ const parserStates = {
3838
READ_DATA: 4
3939
}
4040

41+
const emptyBuffer = Buffer.allocUnsafe(0)
42+
4143
module.exports = {
4244
uid,
4345
staticPropertyDescriptors,
4446
states,
4547
opcodes,
4648
maxUnsigned16Bit,
47-
parserStates
49+
parserStates,
50+
emptyBuffer
4851
}

lib/websocket/receiver.js

Lines changed: 61 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@
22

33
const { Writable } = require('stream')
44
const diagnosticsChannel = require('diagnostics_channel')
5-
const { parserStates, opcodes, states } = require('./constants')
5+
const { parserStates, opcodes, states, emptyBuffer } = require('./constants')
66
const { kReadyState, kSentClose, kResponse, kReceivedClose } = require('./symbols')
77
const { isValidStatusCode, failWebsocketConnection, websocketMessageReceived } = require('./util')
88
const { WebsocketFrameSend } = require('./frame')
99

10+
// This code was influenced by ws released under the MIT license.
11+
// Copyright (c) 2011 Einar Otto Stangvik <[email protected]>
12+
// Copyright (c) 2013 Arnout Kazemier and contributors
13+
// Copyright (c) 2016 Luigi Pinca and contributors
14+
1015
const channels = {}
1116
channels.ping = diagnosticsChannel.channel('undici:websocket:ping')
1217
channels.pong = diagnosticsChannel.channel('undici:websocket:pong')
@@ -49,7 +54,7 @@ class ByteParser extends Writable {
4954
return callback()
5055
}
5156

52-
const buffer = Buffer.concat(this.#buffers, this.#byteOffset)
57+
const buffer = this.consume(2)
5358

5459
this.#info.fin = (buffer[0] & 0x80) !== 0
5560
this.#info.opcode = buffer[0] & 0x0F
@@ -96,7 +101,7 @@ class ByteParser extends Writable {
96101
return
97102
}
98103

99-
const body = buffer.subarray(2, payloadLength + 2)
104+
const body = this.consume(payloadLength)
100105

101106
this.#info.closeInfo = this.parseCloseBody(false, body)
102107

@@ -125,9 +130,6 @@ class ByteParser extends Writable {
125130
this.ws[kReadyState] = states.CLOSING
126131
this.ws[kReceivedClose] = true
127132

128-
this.#buffers = [buffer.subarray(2 + payloadLength)]
129-
this.#byteOffset -= 2 + payloadLength
130-
131133
this.end()
132134

133135
return
@@ -137,8 +139,9 @@ class ByteParser extends Writable {
137139
// A Pong frame sent in response to a Ping frame must have identical
138140
// "Application data"
139141

142+
const body = this.consume(payloadLength)
143+
140144
if (!this.ws[kReceivedClose]) {
141-
const body = payloadLength === 0 ? undefined : buffer.subarray(2, payloadLength + 2)
142145
const frame = new WebsocketFrameSend(body)
143146

144147
this.ws[kResponse].socket.write(frame.createFrame(opcodes.PONG))
@@ -150,8 +153,6 @@ class ByteParser extends Writable {
150153
}
151154
}
152155

153-
this.#buffers = [buffer.subarray(2 + payloadLength)]
154-
this.#byteOffset -= 2 + payloadLength
155156
this.#state = parserStates.INFO
156157

157158
if (this.#byteOffset > 0) {
@@ -165,72 +166,50 @@ class ByteParser extends Writable {
165166
// unidirectional heartbeat. A response to an unsolicited Pong frame is
166167
// not expected.
167168

169+
const body = this.consume(payloadLength)
170+
168171
if (channels.pong.hasSubscribers) {
169172
channels.pong.publish({
170-
payload: buffer.subarray(2, payloadLength + 2)
173+
payload: body
171174
})
172175
}
173176

174-
this.#buffers = [buffer.subarray(2 + payloadLength)]
175-
this.#byteOffset -= 2 + payloadLength
176-
177177
if (this.#byteOffset > 0) {
178178
return this.run(callback)
179179
} else {
180180
callback()
181181
return
182182
}
183183
}
184-
185-
// TODO: handle control frames here. Since they are unfragmented, and can
186-
// be sent in the middle of other frames, we shouldn't parse them as normal.
187-
188-
this.#buffers = [buffer.subarray(2)]
189-
this.#byteOffset -= 2
190184
} else if (this.#state === parserStates.PAYLOADLENGTH_16) {
191185
if (this.#byteOffset < 2) {
192186
return callback()
193187
}
194188

195-
const buffer = Buffer.concat(this.#buffers, this.#byteOffset)
189+
const buffer = this.consume(2)
196190

197-
// TODO: optimize this
198-
this.#info.payloadLength = buffer.subarray(0, 2).readUInt16BE(0)
191+
this.#info.payloadLength = buffer.readUInt16BE(0)
199192
this.#state = parserStates.READ_DATA
200-
201-
this.#buffers = [buffer.subarray(2)]
202-
this.#byteOffset -= 2
203193
} else if (this.#state === parserStates.PAYLOADLENGTH_64) {
204194
if (this.#byteOffset < 8) {
205195
return callback()
206196
}
207197

208-
const buffer = Buffer.concat(this.#buffers, this.#byteOffset)
198+
const buffer = this.consume(8)
209199

210200
// TODO: optimize this
211-
this.#info.payloadLength = buffer.subarray(0, 8).readBigUint64BE(0)
201+
this.#info.payloadLength = buffer.readBigUint64BE(0)
212202
this.#state = parserStates.READ_DATA
213-
214-
this.#buffers = [buffer.subarray(8)]
215-
this.#byteOffset -= 8
216203
} else if (this.#state === parserStates.READ_DATA) {
217204
if (this.#byteOffset < this.#info.payloadLength) {
218205
// If there is still more data in this chunk that needs to be read
219206
return callback()
220207
} else if (this.#byteOffset >= this.#info.payloadLength) {
221208
// If the server sent multiple frames in a single chunk
222-
const buffer = Buffer.concat(this.#buffers, this.#byteOffset)
223209

224-
const body = buffer.subarray(0, this.#info.payloadLength)
225-
this.#fragments.push(body)
210+
const body = this.consume(this.#info.payloadLength)
226211

227-
if (this.#byteOffset > this.#info.payloadLength) {
228-
this.#buffers = [buffer.subarray(body.length)]
229-
this.#byteOffset -= body.length
230-
} else {
231-
this.#buffers.length = 0
232-
this.#byteOffset = 0
233-
}
212+
this.#fragments.push(body)
234213

235214
// If the frame is unfragmented, or a fragmented frame was terminated,
236215
// a message was received
@@ -254,6 +233,48 @@ class ByteParser extends Writable {
254233
}
255234
}
256235

236+
/**
237+
* Take n bytes from the buffered Buffers
238+
* @param {number} n
239+
* @returns {Buffer|null}
240+
*/
241+
consume (n) {
242+
if (n > this.#byteOffset) {
243+
return null
244+
} else if (n === 0) {
245+
return emptyBuffer
246+
}
247+
248+
if (this.#buffers[0].length === n) {
249+
this.#byteOffset -= this.#buffers[0].length
250+
return this.#buffers.shift()
251+
}
252+
253+
const buffer = Buffer.allocUnsafe(n)
254+
let offset = 0
255+
256+
while (offset !== n) {
257+
const next = this.#buffers[0]
258+
const { length } = next
259+
260+
if (length + offset === n) {
261+
buffer.set(this.#buffers.shift(), offset)
262+
break
263+
} else if (length + offset > n) {
264+
buffer.set(next.subarray(0, n - offset), offset)
265+
this.#buffers[0] = next.subarray(n - offset)
266+
break
267+
} else {
268+
buffer.set(this.#buffers.shift(), offset)
269+
offset += next.length
270+
}
271+
}
272+
273+
this.#byteOffset -= n
274+
275+
return buffer
276+
}
277+
257278
parseCloseBody (onlyCode, data) {
258279
// https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5
259280
/** @type {number|undefined} */

lib/websocket/websocket.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
const { webidl } = require('../fetch/webidl')
44
const { DOMException } = require('../fetch/constants')
55
const { URLSerializer } = require('../fetch/dataURL')
6-
const { staticPropertyDescriptors, states, opcodes } = require('./constants')
6+
const { staticPropertyDescriptors, states, opcodes, emptyBuffer } = require('./constants')
77
const {
88
kWebSocketURL,
99
kReadyState,
@@ -207,7 +207,7 @@ class WebSocket extends EventTarget {
207207
// the body MAY contain UTF-8-encoded data with value /reason/
208208
frame.frameData.write(reason, 2, 'utf-8')
209209
} else {
210-
frame.frameData = Buffer.alloc(0)
210+
frame.frameData = emptyBuffer
211211
}
212212

213213
/** @type {import('stream').Duplex} */

0 commit comments

Comments
 (0)