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

Commit 4bb3a89

Browse files
committed
Merge pull request #89 from Lukasa/readline
Initial buffered socket readline proposal.
2 parents 9103cb2 + d018249 commit 4bb3a89

File tree

3 files changed

+188
-13
lines changed

3 files changed

+188
-13
lines changed

hyper/http20/bufsocket.py

Lines changed: 86 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,69 @@ 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+
Note that, unlike recv, this method absolutely *does* block until it
154+
can read the line.
155+
156+
:returns: A ``memoryview`` object containing the appropriate number of
157+
bytes. The data *must* be copied out by the caller before the next
158+
call to this function.
159+
"""
160+
# First, check if there's anything in the buffer. This is one of those
161+
# rare circumstances where this will work correctly on all platforms.
162+
index = self._backing_buffer.find(
163+
b'\n',
164+
self._index,
165+
self._index + self._bytes_in_buffer
166+
)
167+
168+
if index != -1:
169+
length = index + 1 - self._index
170+
data = self._buffer_view[self._index:self._index+length]
171+
self._index += length
172+
self._bytes_in_buffer -= length
173+
return data
174+
175+
# In this case, we didn't find a newline in the buffer. To fix that,
176+
# read some data into the buffer. To do our best to satisfy the read,
177+
# we should shunt the data down in the buffer so that it's right at
178+
# the start. We don't bother if we're already at the start of the
179+
# buffer.
180+
if self._index != 0:
181+
self.new_buffer()
182+
183+
while self._bytes_in_buffer < self._buffer_size:
184+
count = self._sck.recv_into(self._buffer_view[self._buffer_end:])
185+
if not count:
186+
raise ConnectionResetError()
187+
188+
# We have some more data. Again, look for a newline in that gap.
189+
first_new_byte = self._buffer_end
190+
self._bytes_in_buffer += count
191+
index = self._backing_buffer.find(
192+
b'\n',
193+
first_new_byte,
194+
first_new_byte + count,
195+
)
196+
197+
if index != -1:
198+
# The length of the buffer is the index into the
199+
# buffer at which we found the newline plus 1, minus the start
200+
# index of the buffer, which really should be zero.
201+
assert not self._index
202+
length = index + 1
203+
data = self._buffer_view[:length]
204+
self._index += length
205+
self._bytes_in_buffer -= length
206+
return data
207+
208+
# If we got here, it means we filled the buffer without ever getting
209+
# a newline. Time to throw an exception.
210+
raise LineTooLongError()
211+
139212
def __getattr__(self, name):
140213
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: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
66
Test the BufferedSocket implementation in hyper.
77
"""
8+
import pytest
9+
810
import hyper.http20.bufsocket
911
from hyper.http20.bufsocket import BufferedSocket
12+
from hyper.http20.exceptions import ConnectionResetError, LineTooLongError
1013

1114
# Patch the select method in bufsocket to make sure that it always returns
1215
# the dummy socket as readable.
@@ -120,6 +123,90 @@ def test_oversized_read(self, monkeypatch):
120123
d = b.recv(1200).tobytes()
121124
assert d == b'a' * 600
122125

126+
def test_readline_from_buffer(self, monkeypatch):
127+
monkeypatch.setattr(
128+
hyper.http20.bufsocket.select, 'select', dummy_select
129+
)
130+
s = DummySocket()
131+
b = BufferedSocket(s)
132+
133+
one = b'hi there\r\n'
134+
two = b'this is another line\r\n'
135+
three = b'\r\n'
136+
combined = b''.join([one, two, three])
137+
b._buffer_view[0:len(combined)] = combined
138+
b._bytes_in_buffer += len(combined)
139+
140+
assert b.readline().tobytes() == one
141+
assert b.readline().tobytes() == two
142+
assert b.readline().tobytes() == three
143+
144+
def test_readline_from_socket(self, monkeypatch):
145+
monkeypatch.setattr(
146+
hyper.http20.bufsocket.select, 'select', dummy_select
147+
)
148+
s = DummySocket()
149+
b = BufferedSocket(s)
150+
151+
one = b'hi there\r\n'
152+
two = b'this is another line\r\n'
153+
three = b'\r\n'
154+
combined = b''.join([one, two, three])
155+
156+
for i in range(0, len(combined), 5):
157+
s.inbound_packets.append(combined[i:i+5])
158+
159+
assert b.readline().tobytes() == one
160+
assert b.readline().tobytes() == two
161+
assert b.readline().tobytes() == three
162+
163+
def test_readline_both(self, monkeypatch):
164+
monkeypatch.setattr(
165+
hyper.http20.bufsocket.select, 'select', dummy_select
166+
)
167+
s = DummySocket()
168+
b = BufferedSocket(s)
169+
170+
one = b'hi there\r\n'
171+
two = b'this is another line\r\n'
172+
three = b'\r\n'
173+
combined = b''.join([one, two, three])
174+
175+
split_index = int(len(combined) / 2)
176+
177+
b._buffer_view[0:split_index] = combined[0:split_index]
178+
b._bytes_in_buffer += split_index
179+
180+
for i in range(split_index, len(combined), 5):
181+
s.inbound_packets.append(combined[i:i+5])
182+
183+
assert b.readline().tobytes() == one
184+
assert b.readline().tobytes() == two
185+
assert b.readline().tobytes() == three
186+
187+
def test_socket_error_on_readline(self, monkeypatch):
188+
monkeypatch.setattr(
189+
hyper.http20.bufsocket.select, 'select', dummy_select
190+
)
191+
s = DummySocket()
192+
b = BufferedSocket(s)
193+
194+
with pytest.raises(ConnectionResetError):
195+
b.readline()
196+
197+
def test_socket_readline_too_long(self, monkeypatch):
198+
monkeypatch.setattr(
199+
hyper.http20.bufsocket.select, 'select', dummy_select
200+
)
201+
s = DummySocket()
202+
b = BufferedSocket(s)
203+
204+
b._buffer_view[0:b._buffer_size] = b'0' * b._buffer_size
205+
b._bytes_in_buffer = b._buffer_size
206+
207+
with pytest.raises(LineTooLongError):
208+
b.readline()
209+
123210

124211
class DummySocket(object):
125212
def __init__(self):

0 commit comments

Comments
 (0)