|
47 | 47 | import _socket |
48 | 48 | from _socket import * |
49 | 49 |
|
50 | | -import os, sys, io |
| 50 | +import os, sys, io, selectors |
51 | 51 | from enum import IntEnum |
52 | 52 |
|
53 | 53 | try: |
@@ -109,6 +109,9 @@ def _intenum_converter(value, enum_klass): |
109 | 109 | __all__.append("errorTab") |
110 | 110 |
|
111 | 111 |
|
| 112 | +class _GiveupOnSendfile(Exception): pass |
| 113 | + |
| 114 | + |
112 | 115 | class socket(_socket.socket): |
113 | 116 |
|
114 | 117 | """A subclass of _socket.socket adding the makefile() method.""" |
@@ -233,6 +236,149 @@ def makefile(self, mode="r", buffering=None, *, |
233 | 236 | text.mode = mode |
234 | 237 | return text |
235 | 238 |
|
| 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 | + |
236 | 382 | def _decref_socketios(self): |
237 | 383 | if self._io_refs > 0: |
238 | 384 | self._io_refs -= 1 |
|
0 commit comments