Skip to content

Commit 7be17b5

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

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
@@ -147,27 +147,29 @@ def apply(connection)
147147
end
148148

149149
def self.parse_header(buffer)
150-
byte = buffer.unpack("C").first
150+
byte = buffer.getbyte(0)
151151

152152
finished = (byte & 0b1000_0000 != 0)
153153
flags = (byte & 0b0111_0000) >> 4
154154
opcode = byte & 0b0000_1111
155155

156-
if (0x3 .. 0x7).include?(opcode)
156+
if opcode >= 0x3 && opcode <= 0x7
157157
raise ProtocolError, "Non-control opcode = #{opcode} is reserved!"
158-
elsif (0xB .. 0xF).include?(opcode)
158+
elsif opcode >= 0xB
159159
raise ProtocolError, "Control opcode = #{opcode} is reserved!"
160160
end
161161

162162
return finished, flags, opcode
163163
end
164164

165-
def self.read(finished, flags, opcode, stream, maximum_frame_size)
166-
buffer = stream.read(1) or raise EOFError, "Could not read header!"
167-
byte = buffer.unpack("C").first
165+
def self.read(finished, flags, opcode, stream, maximum_frame_size, second_byte = nil)
166+
unless second_byte
167+
buffer = stream.read(1) or raise EOFError, "Could not read header!"
168+
second_byte = buffer.getbyte(0)
169+
end
168170

169-
mask = (byte & 0b1000_0000 != 0)
170-
length = byte & 0b0111_1111
171+
mask = (second_byte & 0b1000_0000 != 0)
172+
length = second_byte & 0b0111_1111
171173

172174
if opcode & 0x8 != 0
173175
if length > 125
@@ -178,21 +180,31 @@ def self.read(finished, flags, opcode, stream, maximum_frame_size)
178180
end
179181

180182
if length == 126
181-
buffer = stream.read(2) or raise EOFError, "Could not read length!"
182-
length = buffer.unpack("n").first
183+
if mask
184+
buffer = stream.read(6) or raise EOFError, "Could not read length and mask!"
185+
length = buffer.unpack1("n")
186+
mask = buffer.byteslice(2, 4)
187+
else
188+
buffer = stream.read(2) or raise EOFError, "Could not read length!"
189+
length = buffer.unpack1("n")
190+
end
183191
elsif length == 127
184-
buffer = stream.read(8) or raise EOFError, "Could not read length!"
185-
length = buffer.unpack("Q>").first
192+
if mask
193+
buffer = stream.read(12) or raise EOFError, "Could not read length and mask!"
194+
length = buffer.unpack1("Q>")
195+
mask = buffer.byteslice(8, 4)
196+
else
197+
buffer = stream.read(8) or raise EOFError, "Could not read length!"
198+
length = buffer.unpack1("Q>")
199+
end
200+
elsif mask
201+
mask = stream.read(4) or raise EOFError, "Could not read mask!"
186202
end
187203

188204
if length > maximum_frame_size
189205
raise ProtocolError, "Invalid payload length: #{length} > #{maximum_frame_size}!"
190206
end
191207

192-
if mask
193-
mask = stream.read(4) or raise EOFError, "Could not read mask!"
194-
end
195-
196208
payload = stream.read(length) or raise EOFError, "Could not read payload!"
197209

198210
if payload.bytesize != length

lib/protocol/websocket/framer.rb

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

53-
# Read the frame:
5468
klass = @frames[opcode] || Frame
55-
frame = klass.read(finished, flags, opcode, @stream, maximum_frame_size)
69+
frame = klass.read(finished, flags, opcode, @stream, maximum_frame_size, buffer.getbyte(1))
5670

5771
return frame
5872
end
@@ -61,15 +75,6 @@ def read_frame(maximum_frame_size = MAXIMUM_ALLOWED_FRAME_SIZE)
6175
def write_frame(frame)
6276
frame.write(@stream)
6377
end
64-
65-
# Read the header of the frame.
66-
def read_header
67-
if buffer = @stream.read(1) and buffer.bytesize == 1
68-
return Frame.parse_header(buffer)
69-
end
70-
71-
raise EOFError, "Could not read frame header!"
72-
end
7378
end
7479
end
7580
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)