Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 915d141

Browse files
committed
fix issue #17552: add socket.sendfile() method allowing to send a file over a socket by using high-performance os.sendfile() on UNIX. Patch by Giampaolo Rodola'·
1 parent b398d33 commit 915d141

9 files changed

Lines changed: 483 additions & 2 deletions

File tree

Doc/library/os.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,6 +1092,10 @@ or `the MSDN <http://msdn.microsoft.com/en-us/library/z0kc8e3z.aspx>`_ on Window
10921092

10931093
Availability: Unix.
10941094

1095+
.. note::
1096+
1097+
For a higher-level version of this see :mod:`socket.socket.sendfile`.
1098+
10951099
.. versionadded:: 3.3
10961100

10971101

Doc/library/socket.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,6 +1148,21 @@ to sockets.
11481148

11491149
.. versionadded:: 3.3
11501150

1151+
.. method:: socket.sendfile(file, offset=0, count=None)
1152+
1153+
Send a file until EOF is reached by using high-performance
1154+
:mod:`os.sendfile` and return the total number of bytes which were sent.
1155+
*file* must be a regular file object opened in binary mode. If
1156+
:mod:`os.sendfile` is not available (e.g. Windows) or *file* is not a
1157+
regular file :meth:`send` will be used instead. *offset* tells from where to
1158+
start reading the file. If specified, *count* is the total number of bytes
1159+
to transmit as opposed to sending the file until EOF is reached. File
1160+
position is updated on return or also in case of error in which case
1161+
:meth:`file.tell() <io.IOBase.tell>` can be used to figure out the number of
1162+
bytes which were sent. The socket must be of :const:`SOCK_STREAM` type. Non-
1163+
blocking sockets are not supported.
1164+
1165+
.. versionadded:: 3.5
11511166

11521167
.. method:: socket.set_inheritable(inheritable)
11531168

Doc/library/ssl.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,9 @@ SSL sockets provide the following methods of :ref:`socket-objects`:
789789
(but passing a non-zero ``flags`` argument is not allowed)
790790
- :meth:`~socket.socket.send()`, :meth:`~socket.socket.sendall()` (with
791791
the same limitation)
792+
- :meth:`~socket.socket.sendfile()` (but :mod:`os.sendfile` will be used
793+
for plain-text sockets only, else :meth:`~socket.socket.send()` will be used)
794+
.. versionadded:: 3.5
792795
- :meth:`~socket.socket.shutdown()`
793796

794797
However, since the SSL (and TLS) protocol has its own framing atop

Doc/whatsnew/3.5.rst

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,18 @@ signal
181181

182182
* Different constants of :mod:`signal` module are now enumeration values using
183183
the :mod:`enum` module. This allows meaningful names to be printed during
184-
debugging, instead of integer “magic numbers”. (contribute by Giampaolo
184+
debugging, instead of integer “magic numbers”. (contributed by Giampaolo
185185
Rodola' in :issue:`21076`)
186186

187+
socket
188+
------
189+
190+
* New :meth:`socket.socket.sendfile` method allows to send a file over a socket
191+
by using high-performance :func:`os.sendfile` function on UNIX resulting in
192+
uploads being from 2x to 3x faster than when using plain
193+
:meth:`socket.socket.send`.
194+
(contributed by Giampaolo Rodola' in :issue:`17552`)
195+
187196
xmlrpc
188197
------
189198

Lib/socket.py

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
import _socket
4848
from _socket import *
4949

50-
import os, sys, io
50+
import os, sys, io, selectors
5151
from enum import IntEnum
5252

5353
try:
@@ -109,6 +109,9 @@ def _intenum_converter(value, enum_klass):
109109
__all__.append("errorTab")
110110

111111

112+
class _GiveupOnSendfile(Exception): pass
113+
114+
112115
class socket(_socket.socket):
113116

114117
"""A subclass of _socket.socket adding the makefile() method."""
@@ -233,6 +236,149 @@ def makefile(self, mode="r", buffering=None, *,
233236
text.mode = mode
234237
return text
235238

239+
if hasattr(os, 'sendfile'):
240+
241+
def _sendfile_use_sendfile(self, file, offset=0, count=None):
242+
self._check_sendfile_params(file, offset, count)
243+
sockno = self.fileno()
244+
try:
245+
fileno = file.fileno()
246+
except (AttributeError, io.UnsupportedOperation) as err:
247+
raise _GiveupOnSendfile(err) # not a regular file
248+
try:
249+
fsize = os.fstat(fileno).st_size
250+
except OSError:
251+
raise _GiveupOnSendfile(err) # not a regular file
252+
if not fsize:
253+
return 0 # empty file
254+
blocksize = fsize if not count else count
255+
256+
timeout = self.gettimeout()
257+
if timeout == 0:
258+
raise ValueError("non-blocking sockets are not supported")
259+
# poll/select have the advantage of not requiring any
260+
# extra file descriptor, contrarily to epoll/kqueue
261+
# (also, they require a single syscall).
262+
if hasattr(selectors, 'PollSelector'):
263+
selector = selectors.PollSelector()
264+
else:
265+
selector = selectors.SelectSelector()
266+
selector.register(sockno, selectors.EVENT_WRITE)
267+
268+
total_sent = 0
269+
# localize variable access to minimize overhead
270+
selector_select = selector.select
271+
os_sendfile = os.sendfile
272+
try:
273+
while True:
274+
if timeout and not selector_select(timeout):
275+
raise _socket.timeout('timed out')
276+
if count:
277+
blocksize = count - total_sent
278+
if blocksize <= 0:
279+
break
280+
try:
281+
sent = os_sendfile(sockno, fileno, offset, blocksize)
282+
except BlockingIOError:
283+
if not timeout:
284+
# Block until the socket is ready to send some
285+
# data; avoids hogging CPU resources.
286+
selector_select()
287+
continue
288+
except OSError as err:
289+
if total_sent == 0:
290+
# We can get here for different reasons, the main
291+
# one being 'file' is not a regular mmap(2)-like
292+
# file, in which case we'll fall back on using
293+
# plain send().
294+
raise _GiveupOnSendfile(err)
295+
raise err from None
296+
else:
297+
if sent == 0:
298+
break # EOF
299+
offset += sent
300+
total_sent += sent
301+
return total_sent
302+
finally:
303+
if total_sent > 0 and hasattr(file, 'seek'):
304+
file.seek(offset)
305+
else:
306+
def _sendfile_use_sendfile(self, file, offset=0, count=None):
307+
raise _GiveupOnSendfile(
308+
"os.sendfile() not available on this platform")
309+
310+
def _sendfile_use_send(self, file, offset=0, count=None):
311+
self._check_sendfile_params(file, offset, count)
312+
if self.gettimeout() == 0:
313+
raise ValueError("non-blocking sockets are not supported")
314+
if offset:
315+
file.seek(offset)
316+
blocksize = min(count, 8192) if count else 8192
317+
total_sent = 0
318+
# localize variable access to minimize overhead
319+
file_read = file.read
320+
sock_send = self.send
321+
try:
322+
while True:
323+
if count:
324+
blocksize = min(count - total_sent, blocksize)
325+
if blocksize <= 0:
326+
break
327+
data = memoryview(file_read(blocksize))
328+
if not data:
329+
break # EOF
330+
while True:
331+
try:
332+
sent = sock_send(data)
333+
except BlockingIOError:
334+
continue
335+
else:
336+
total_sent += sent
337+
if sent < len(data):
338+
data = data[sent:]
339+
else:
340+
break
341+
return total_sent
342+
finally:
343+
if total_sent > 0 and hasattr(file, 'seek'):
344+
file.seek(offset + total_sent)
345+
346+
def _check_sendfile_params(self, file, offset, count):
347+
if 'b' not in getattr(file, 'mode', 'b'):
348+
raise ValueError("file should be opened in binary mode")
349+
if not self.type & SOCK_STREAM:
350+
raise ValueError("only SOCK_STREAM type sockets are supported")
351+
if count is not None:
352+
if not isinstance(count, int):
353+
raise TypeError(
354+
"count must be a positive integer (got {!r})".format(count))
355+
if count <= 0:
356+
raise ValueError(
357+
"count must be a positive integer (got {!r})".format(count))
358+
359+
def sendfile(self, file, offset=0, count=None):
360+
"""sendfile(file[, offset[, count]]) -> sent
361+
362+
Send a file until EOF is reached by using high-performance
363+
os.sendfile() and return the total number of bytes which
364+
were sent.
365+
*file* must be a regular file object opened in binary mode.
366+
If os.sendfile() is not available (e.g. Windows) or file is
367+
not a regular file socket.send() will be used instead.
368+
*offset* tells from where to start reading the file.
369+
If specified, *count* is the total number of bytes to transmit
370+
as opposed to sending the file until EOF is reached.
371+
File position is updated on return or also in case of error in
372+
which case file.tell() can be used to figure out the number of
373+
bytes which were sent.
374+
The socket must be of SOCK_STREAM type.
375+
Non-blocking sockets are not supported.
376+
"""
377+
try:
378+
return self._sendfile_use_sendfile(file, offset, count)
379+
except _GiveupOnSendfile:
380+
return self._sendfile_use_send(file, offset, count)
381+
236382
def _decref_socketios(self):
237383
if self._io_refs > 0:
238384
self._io_refs -= 1

Lib/ssl.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,16 @@ def sendall(self, data, flags=0):
700700
else:
701701
return socket.sendall(self, data, flags)
702702

703+
def sendfile(self, file, offset=0, count=None):
704+
"""Send a file, possibly by using os.sendfile() if this is a
705+
clear-text socket. Return the total number of bytes sent.
706+
"""
707+
if self._sslobj is None:
708+
# os.sendfile() works with plain sockets only
709+
return super().sendfile(file, offset, count)
710+
else:
711+
return self._sendfile_use_send(file, offset, count)
712+
703713
def recv(self, buflen=1024, flags=0):
704714
self._checkClosed()
705715
if self._sslobj:

0 commit comments

Comments
 (0)