Skip to content

Commit ad455cd

Browse files
nirsvstinner
authored andcommitted
bpo-31945: Configurable blocksize in HTTP(S)Connection (#4279)
blocksize was hardcoded to 8192, preventing efficient upload when using file-like body. Add blocksize argument to __init__, so users can configure the blocksize to fit their needs. I tested this uploading data from /dev/zero to a web server dropping the received data, to test the overhead of the HTTPConnection.send() with a file-like object. Here is an example 10g upload with the default buffer size (8192): $ time ~/src/cpython/release/python upload-httplib.py 10 https://localhost:8000/ Uploaded 10.00g in 17.53 seconds (584.00m/s) real 0m17.574s user 0m8.887s sys 0m5.971s Same with 512k blocksize: $ time ~/src/cpython/release/python upload-httplib.py 10 https://localhost:8000/ Uploaded 10.00g in 6.60 seconds (1551.15m/s) real 0m6.641s user 0m3.426s sys 0m2.162s In real world usage the difference will be smaller, depending on the local and remote storage and the network. See https://github.com/nirs/http-bench for more info.
1 parent 30f4fa4 commit ad455cd

File tree

5 files changed

+56
-9
lines changed

5 files changed

+56
-9
lines changed

Doc/library/http.client.rst

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ HTTPS protocols. It is normally not used directly --- the module
3131
The module provides the following classes:
3232

3333

34-
.. class:: HTTPConnection(host, port=None[, timeout], source_address=None)
34+
.. class:: HTTPConnection(host, port=None[, timeout], source_address=None, \
35+
blocksize=8192)
3536

3637
An :class:`HTTPConnection` instance represents one transaction with an HTTP
3738
server. It should be instantiated passing it a host and optional port
@@ -42,6 +43,8 @@ The module provides the following classes:
4243
(if it is not given, the global default timeout setting is used).
4344
The optional *source_address* parameter may be a tuple of a (host, port)
4445
to use as the source address the HTTP connection is made from.
46+
The optional *blocksize* parameter sets the buffer size in bytes for
47+
sending a file-like message body.
4548

4649
For example, the following calls all create instances that connect to the server
4750
at the same host and port::
@@ -58,11 +61,14 @@ The module provides the following classes:
5861
The *strict* parameter was removed. HTTP 0.9-style "Simple Responses" are
5962
not longer supported.
6063

64+
.. versionchanged:: 3.7
65+
*blocksize* parameter was added.
66+
6167

6268
.. class:: HTTPSConnection(host, port=None, key_file=None, \
6369
cert_file=None[, timeout], \
6470
source_address=None, *, context=None, \
65-
check_hostname=None)
71+
check_hostname=None, blocksize=8192)
6672
6773
A subclass of :class:`HTTPConnection` that uses SSL for communication with
6874
secure servers. Default port is ``443``. If *context* is specified, it
@@ -338,6 +344,14 @@ HTTPConnection Objects
338344

339345
Close the connection to the server.
340346

347+
348+
.. attribute:: HTTPConnection.blocksize
349+
350+
Buffer size in bytes for sending a file-like message body.
351+
352+
.. versionadded:: 3.7
353+
354+
341355
As an alternative to using the :meth:`request` method described above, you can
342356
also send your request step by step, by using the four functions below.
343357

Doc/whatsnew/3.7.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,13 @@ README.rst is now included in the list of distutils standard READMEs and
276276
therefore included in source distributions.
277277
(Contributed by Ryan Gonzalez in :issue:`11913`.)
278278

279+
http.client
280+
-----------
281+
282+
Add Configurable *blocksize* to ``HTTPConnection`` and
283+
``HTTPSConnection`` for improved upload throughput.
284+
(Contributed by Nir Soffer in :issue:`31945`.)
285+
279286
http.server
280287
-----------
281288

Lib/http/client.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -825,9 +825,10 @@ def _get_content_length(body, method):
825825
return None
826826

827827
def __init__(self, host, port=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
828-
source_address=None):
828+
source_address=None, blocksize=8192):
829829
self.timeout = timeout
830830
self.source_address = source_address
831+
self.blocksize = blocksize
831832
self.sock = None
832833
self._buffer = []
833834
self.__response = None
@@ -958,15 +959,14 @@ def send(self, data):
958959

959960
if self.debuglevel > 0:
960961
print("send:", repr(data))
961-
blocksize = 8192
962962
if hasattr(data, "read") :
963963
if self.debuglevel > 0:
964964
print("sendIng a read()able")
965965
encode = self._is_textIO(data)
966966
if encode and self.debuglevel > 0:
967967
print("encoding file using iso-8859-1")
968968
while 1:
969-
datablock = data.read(blocksize)
969+
datablock = data.read(self.blocksize)
970970
if not datablock:
971971
break
972972
if encode:
@@ -991,14 +991,13 @@ def _output(self, s):
991991
self._buffer.append(s)
992992

993993
def _read_readable(self, readable):
994-
blocksize = 8192
995994
if self.debuglevel > 0:
996995
print("sendIng a read()able")
997996
encode = self._is_textIO(readable)
998997
if encode and self.debuglevel > 0:
999998
print("encoding file using iso-8859-1")
1000999
while True:
1001-
datablock = readable.read(blocksize)
1000+
datablock = readable.read(self.blocksize)
10021001
if not datablock:
10031002
break
10041003
if encode:
@@ -1353,9 +1352,10 @@ class HTTPSConnection(HTTPConnection):
13531352
def __init__(self, host, port=None, key_file=None, cert_file=None,
13541353
timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
13551354
source_address=None, *, context=None,
1356-
check_hostname=None):
1355+
check_hostname=None, blocksize=8192):
13571356
super(HTTPSConnection, self).__init__(host, port, timeout,
1358-
source_address)
1357+
source_address,
1358+
blocksize=blocksize)
13591359
if (key_file is not None or cert_file is not None or
13601360
check_hostname is not None):
13611361
import warnings

Lib/test/test_httplib.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,29 @@ def body():
756756
conn.request('GET', '/foo', body(), {'Content-Length': '11'})
757757
self.assertEqual(sock.data, expected)
758758

759+
def test_blocksize_request(self):
760+
"""Check that request() respects the configured block size."""
761+
blocksize = 8 # For easy debugging.
762+
conn = client.HTTPConnection('example.com', blocksize=blocksize)
763+
sock = FakeSocket(None)
764+
conn.sock = sock
765+
expected = b"a" * blocksize + b"b"
766+
conn.request("PUT", "/", io.BytesIO(expected), {"Content-Length": "9"})
767+
self.assertEqual(sock.sendall_calls, 3)
768+
body = sock.data.split(b"\r\n\r\n", 1)[1]
769+
self.assertEqual(body, expected)
770+
771+
def test_blocksize_send(self):
772+
"""Check that send() respects the configured block size."""
773+
blocksize = 8 # For easy debugging.
774+
conn = client.HTTPConnection('example.com', blocksize=blocksize)
775+
sock = FakeSocket(None)
776+
conn.sock = sock
777+
expected = b"a" * blocksize + b"b"
778+
conn.send(io.BytesIO(expected))
779+
self.assertEqual(sock.sendall_calls, 2)
780+
self.assertEqual(sock.data, expected)
781+
759782
def test_send_type_error(self):
760783
# See: Issue #12676
761784
conn = client.HTTPConnection('example.com')
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add Configurable *blocksize* to ``HTTPConnection`` and
2+
``HTTPSConnection`` for improved upload throughput. Patch by Nir
3+
Soffer.

0 commit comments

Comments
 (0)