Skip to content

Commit fbb59fb

Browse files
wtnclaude
authored andcommitted
Optimize frame reading.
Co-authored-by: Claude <noreply@anthropic.com>
1 parent c6446ac commit fbb59fb

6 files changed

Lines changed: 109 additions & 30 deletions

File tree

lib/protocol/websocket/connection.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,11 @@ def write(message, **options)
281281

282282
# The default implementation for reading a message buffer. This is used by the {#reader} interface.
283283
def unpack_frames(frames)
284-
frames.map(&:unpack).join("")
284+
if frames.size == 1
285+
frames[0].unpack
286+
else
287+
frames.map(&:unpack).join("")
288+
end
285289
end
286290

287291
# Read a message from the connection. If an error occurs while reading the message, the connection will be closed.

lib/protocol/websocket/frame.rb

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -173,15 +173,15 @@ def apply(connection)
173173
# @returns [Array] A tuple of `[finished, flags, opcode]`.
174174
# @raises [ProtocolError] If the opcode is a reserved non-control or control opcode.
175175
def self.parse_header(buffer)
176-
byte = buffer.unpack("C").first
176+
byte = buffer.getbyte(0)
177177

178178
finished = (byte & 0b1000_0000 != 0)
179179
flags = (byte & 0b0111_0000) >> 4
180180
opcode = byte & 0b0000_1111
181181

182-
if (0x3 .. 0x7).include?(opcode)
182+
if opcode >= 0x3 && opcode <= 0x7
183183
raise ProtocolError, "Non-control opcode = #{opcode} is reserved!"
184-
elsif (0xB .. 0xF).include?(opcode)
184+
elsif opcode >= 0xB
185185
raise ProtocolError, "Control opcode = #{opcode} is reserved!"
186186
end
187187

@@ -197,12 +197,14 @@ def self.parse_header(buffer)
197197
# @returns [Frame] The fully read and populated frame.
198198
# @raises [ProtocolError] If the frame violates protocol constraints.
199199
# @raises [EOFError] If the stream ends unexpectedly.
200-
def self.read(finished, flags, opcode, stream, maximum_frame_size)
201-
buffer = stream.read(1) or raise EOFError, "Could not read header!"
202-
byte = buffer.unpack("C").first
200+
def self.read(finished, flags, opcode, stream, maximum_frame_size, second_byte = nil)
201+
unless second_byte
202+
buffer = stream.read(1) or raise EOFError, "Could not read header!"
203+
second_byte = buffer.getbyte(0)
204+
end
203205

204-
mask = (byte & 0b1000_0000 != 0)
205-
length = byte & 0b0111_1111
206+
mask = (second_byte & 0b1000_0000 != 0)
207+
length = second_byte & 0b0111_1111
206208

207209
if opcode & 0x8 != 0
208210
if length > 125
@@ -213,21 +215,31 @@ def self.read(finished, flags, opcode, stream, maximum_frame_size)
213215
end
214216

215217
if length == 126
216-
buffer = stream.read(2) or raise EOFError, "Could not read length!"
217-
length = buffer.unpack("n").first
218+
if mask
219+
buffer = stream.read(6) or raise EOFError, "Could not read length and mask!"
220+
length = buffer.unpack1("n")
221+
mask = buffer.byteslice(2, 4)
222+
else
223+
buffer = stream.read(2) or raise EOFError, "Could not read length!"
224+
length = buffer.unpack1("n")
225+
end
218226
elsif length == 127
219-
buffer = stream.read(8) or raise EOFError, "Could not read length!"
220-
length = buffer.unpack("Q>").first
227+
if mask
228+
buffer = stream.read(12) or raise EOFError, "Could not read length and mask!"
229+
length = buffer.unpack1("Q>")
230+
mask = buffer.byteslice(8, 4)
231+
else
232+
buffer = stream.read(8) or raise EOFError, "Could not read length!"
233+
length = buffer.unpack1("Q>")
234+
end
235+
elsif mask
236+
mask = stream.read(4) or raise EOFError, "Could not read mask!"
221237
end
222238

223239
if length > maximum_frame_size
224240
raise ProtocolError, "Invalid payload length: #{length} > #{maximum_frame_size}!"
225241
end
226242

227-
if mask
228-
mask = stream.read(4) or raise EOFError, "Could not read mask!"
229-
end
230-
231243
payload = stream.read(length) or raise EOFError, "Could not read payload!"
232244

233245
if payload.bytesize != length

lib/protocol/websocket/framer.rb

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,26 @@ def flush
5050
# Read a frame from the underlying stream.
5151
# @returns [Frame] the frame read from the stream.
5252
def read_frame(maximum_frame_size = MAXIMUM_ALLOWED_FRAME_SIZE)
53-
# Read the header:
54-
finished, flags, opcode = read_header
53+
buffer = @stream.read(2)
54+
55+
unless buffer and buffer.bytesize == 2
56+
raise EOFError, "Could not read frame header!"
57+
end
58+
59+
first_byte = buffer.getbyte(0)
60+
61+
finished = (first_byte & 0b1000_0000 != 0)
62+
flags = (first_byte & 0b0111_0000) >> 4
63+
opcode = first_byte & 0b0000_1111
64+
65+
if opcode >= 0x3 && opcode <= 0x7
66+
raise ProtocolError, "Non-control opcode = #{opcode} is reserved!"
67+
elsif opcode >= 0xB
68+
raise ProtocolError, "Control opcode = #{opcode} is reserved!"
69+
end
5570

56-
# Read the frame:
5771
klass = @frames[opcode] || Frame
58-
frame = klass.read(finished, flags, opcode, @stream, maximum_frame_size)
72+
frame = klass.read(finished, flags, opcode, @stream, maximum_frame_size, buffer.getbyte(1))
5973

6074
return frame
6175
end
@@ -64,15 +78,6 @@ def read_frame(maximum_frame_size = MAXIMUM_ALLOWED_FRAME_SIZE)
6478
def write_frame(frame)
6579
frame.write(@stream)
6680
end
67-
68-
# Read the header of the frame.
69-
def read_header
70-
if buffer = @stream.read(1) and buffer.bytesize == 1
71-
return Frame.parse_header(buffer)
72-
end
73-
74-
raise EOFError, "Could not read frame header!"
75-
end
7681
end
7782
end
7883
end

test/protocol/websocket/connection.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,37 @@
301301
end
302302
end
303303

304+
with "masked frames with extended lengths" do
305+
let(:connection) {subject.new(server)}
306+
307+
it "can handle a masked medium message (length=126 encoding)" do
308+
thread = Thread.new do
309+
frame = Protocol::WebSocket::TextFrame.new(true, mask: true)
310+
frame.pack("a" * 200)
311+
client.write_frame(frame)
312+
end
313+
314+
message = connection.read
315+
expect(message.size).to be == 200
316+
expect(message).to be == ("a" * 200)
317+
318+
thread.join
319+
end
320+
321+
it "can handle a masked large message (length=127 encoding)" do
322+
thread = Thread.new do
323+
frame = Protocol::WebSocket::TextFrame.new(true, mask: true)
324+
frame.pack("a" * 70_000)
325+
client.write_frame(frame)
326+
end
327+
328+
message = connection.read
329+
expect(message.size).to be == 70_000
330+
331+
thread.join
332+
end
333+
end
334+
304335
with "invalid unicode text message in 3 fragments" do
305336
let(:payload1) {"\xce\xba\xe1\xbd\xb9\xcf\x83\xce\xbc\xce\xb5".b}
306337
let(:payload2) {"\xf4\x90\x80\x80".b}

test/protocol/websocket/frame.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,15 @@
7575
subject.read(false, 0, 0, stream, 128)
7676
end.to raise_exception(EOFError, message: be =~ /Incorrect payload length: \d+ != \d+!/)
7777
end
78+
79+
it "accepts a pre-read second byte" do
80+
stream = StringIO.new("Hello")
81+
second_byte = 0x05
82+
83+
frame = subject.read(true, 0, 0x1, stream, 128, second_byte)
84+
expect(frame.payload).to be == "Hello"
85+
expect(frame.mask).to be == false
86+
end
7887
end
7988

8089
with ".write" do

test/protocol/websocket/framer.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,23 @@
1515
framer.read_frame
1616
end.to raise_exception(EOFError, message: be =~ /Could not read frame header/)
1717
end
18+
19+
it "rejects reserved non-control opcodes" do
20+
stream.string = "\x83\x00"
21+
stream.rewind
22+
23+
expect do
24+
framer.read_frame
25+
end.to raise_exception(Protocol::WebSocket::ProtocolError, message: be =~ /Non-control opcode.*reserved/)
26+
end
27+
28+
it "rejects reserved control opcodes" do
29+
stream.string = "\x8B\x00"
30+
stream.rewind
31+
32+
expect do
33+
framer.read_frame
34+
end.to raise_exception(Protocol::WebSocket::ProtocolError, message: be =~ /Control opcode.*reserved/)
35+
end
1836
end
1937
end

0 commit comments

Comments
 (0)