Skip to content
This repository was archived by the owner on Jan 13, 2021. It is now read-only.

Commit 455d696

Browse files
committed
Initial buffered socket readline proposal.
1 parent 9103cb2 commit 455d696

File tree

3 files changed

+159
-13
lines changed

3 files changed

+159
-13
lines changed

hyper/http20/bufsocket.py

Lines changed: 83 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
process.
1212
"""
1313
import select
14-
from .exceptions import ConnectionResetError
14+
from .exceptions import ConnectionResetError, LineTooLongError
1515

1616
class BufferedSocket(object):
1717
"""
@@ -76,6 +76,25 @@ def can_read(self):
7676

7777
return False
7878

79+
def new_buffer(self):
80+
"""
81+
This method moves all the data in the backing buffer to the start of
82+
a new, fresh buffer. This gives the ability to read much more data.
83+
"""
84+
def read_all_from_buffer():
85+
end = self._index + self._bytes_in_buffer
86+
return self._buffer_view[self._index:end]
87+
88+
new_buffer = bytearray(self._buffer_size)
89+
new_buffer_view = memoryview(new_buffer)
90+
new_buffer_view[0:self._bytes_in_buffer] = read_all_from_buffer()
91+
92+
self._index = 0
93+
self._backing_buffer = new_buffer
94+
self._buffer_view = new_buffer_view
95+
96+
return
97+
7998
def recv(self, amt):
8099
"""
81100
Read some data from the socket.
@@ -85,26 +104,16 @@ def recv(self, amt):
85104
bytes. The data *must* be copied out by the caller before the next
86105
call to this function.
87106
"""
88-
def read_all_from_buffer():
89-
end = self._index + self._bytes_in_buffer
90-
return self._buffer_view[self._index:end]
91-
92107
# In this implementation you can never read more than the number of
93108
# bytes in the buffer.
94109
if amt > self._buffer_size:
95110
amt = self._buffer_size
96111

97112
# If the amount of data we've been asked to read is less than the
98113
# remaining space in the buffer, we need to clear out the buffer and
99-
# start over. Copy the data into the new array.
114+
# start over.
100115
if amt > self._remaining_capacity:
101-
new_buffer = bytearray(self._buffer_size)
102-
new_buffer_view = memoryview(new_buffer)
103-
new_buffer_view[0:self._bytes_in_buffer] = read_all_from_buffer()
104-
105-
self._index = 0
106-
self._backing_buffer = new_buffer
107-
self._buffer_view = new_buffer_view
116+
self.new_buffer()
108117

109118
# If there's still some room in the buffer, opportunistically attempt
110119
# to read into it.
@@ -136,5 +145,66 @@ def read_all_from_buffer():
136145

137146
return data
138147

148+
def readline(self):
149+
"""
150+
Read up to a newline from the network and returns it. The implicit
151+
maximum line length is the buffer size of the buffered socket.
152+
153+
:returns: A ``memoryview`` object containing the appropriate number of
154+
bytes. The data *must* be copied out by the caller before the next
155+
call to this function.
156+
"""
157+
# First, check if there's anything in the buffer. This is one of those
158+
# rare circumstances where this will work correctly on all platforms.
159+
index = self._backing_buffer.find(
160+
b'\n',
161+
self._index,
162+
self._index + self._bytes_in_buffer
163+
)
164+
165+
if index != -1:
166+
length = index + 1 - self._index
167+
data = self._buffer_view[self._index:self._index+length]
168+
self._index += length
169+
self._bytes_in_buffer -= length
170+
return data
171+
172+
# In this case, we didn't find a newline in the buffer. To fix that,
173+
# read some data into the buffer. To do our best to satisfy the read,
174+
# we should shunt the data down in the buffer so that it's right at
175+
# the start. We don't bother if we're already at the start of the
176+
# buffer.
177+
if self._index != 0:
178+
self.new_buffer()
179+
180+
while self._bytes_in_buffer < self._buffer_size:
181+
count = self._sck.recv_into(self._buffer_view[self._buffer_end:])
182+
if not count:
183+
raise ConnectionResetError()
184+
185+
# We have some more data. Again, look for a newline in that gap.
186+
first_new_byte = self._buffer_end
187+
self._bytes_in_buffer += count
188+
index = self._backing_buffer.find(
189+
b'\n',
190+
first_new_byte,
191+
first_new_byte + count,
192+
)
193+
194+
if index != -1:
195+
# The length of the buffer is the index into the
196+
# buffer at which we found the newline plus 1, minus the start
197+
# index of the buffer, which really should be zero.
198+
assert not self._index
199+
length = index + 1
200+
data = self._buffer_view[:length]
201+
self._index += length
202+
self._bytes_in_buffer -= length
203+
return data
204+
205+
# If we got here, it means we filled the buffer without ever getting
206+
# a newline. Time to throw an exception.
207+
raise LineTooLongError()
208+
139209
def __getattr__(self, name):
140210
return getattr(self._sck, name)

hyper/http20/exceptions.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,21 @@
55
66
This defines exceptions used in the HTTP/2 portion of hyper.
77
"""
8+
class SocketError(Exception):
9+
"""
10+
An error occurred during socket operation.
11+
"""
12+
pass
13+
14+
15+
class LineTooLongError(Exception):
16+
"""
17+
An attempt to read a line from a socket failed because no newline was
18+
found.
19+
"""
20+
pass
21+
22+
823
class HTTP20Error(Exception):
924
"""
1025
The base class for all of ``hyper``'s HTTP/2-related exceptions.

test/test_socket.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,67 @@ def test_oversized_read(self, monkeypatch):
120120
d = b.recv(1200).tobytes()
121121
assert d == b'a' * 600
122122

123+
def test_readline_from_buffer(self, monkeypatch):
124+
monkeypatch.setattr(
125+
hyper.http20.bufsocket.select, 'select', dummy_select
126+
)
127+
s = DummySocket()
128+
b = BufferedSocket(s)
129+
130+
one = b'hi there\r\n'
131+
two = b'this is another line\r\n'
132+
three = b'\r\n'
133+
combined = b''.join([one, two, three])
134+
b._buffer_view[0:len(combined)] = combined
135+
b._bytes_in_buffer += len(combined)
136+
137+
assert b.readline().tobytes() == one
138+
assert b.readline().tobytes() == two
139+
assert b.readline().tobytes() == three
140+
141+
def test_readline_from_socket(self, monkeypatch):
142+
monkeypatch.setattr(
143+
hyper.http20.bufsocket.select, 'select', dummy_select
144+
)
145+
s = DummySocket()
146+
b = BufferedSocket(s)
147+
148+
one = b'hi there\r\n'
149+
two = b'this is another line\r\n'
150+
three = b'\r\n'
151+
combined = b''.join([one, two, three])
152+
153+
for i in range(0, len(combined), 5):
154+
s.inbound_packets.append(combined[i:i+5])
155+
156+
assert b.readline().tobytes() == one
157+
assert b.readline().tobytes() == two
158+
assert b.readline().tobytes() == three
159+
160+
def test_readline_both(self, monkeypatch):
161+
monkeypatch.setattr(
162+
hyper.http20.bufsocket.select, 'select', dummy_select
163+
)
164+
s = DummySocket()
165+
b = BufferedSocket(s)
166+
167+
one = b'hi there\r\n'
168+
two = b'this is another line\r\n'
169+
three = b'\r\n'
170+
combined = b''.join([one, two, three])
171+
172+
split_index = int(len(combined) / 2)
173+
174+
b._buffer_view[0:split_index] = combined[0:split_index]
175+
b._bytes_in_buffer += split_index
176+
177+
for i in range(split_index, len(combined), 5):
178+
s.inbound_packets.append(combined[i:i+5])
179+
180+
assert b.readline().tobytes() == one
181+
assert b.readline().tobytes() == two
182+
assert b.readline().tobytes() == three
183+
123184

124185
class DummySocket(object):
125186
def __init__(self):

0 commit comments

Comments
 (0)